Skip to content

Contract: Normalize session expiry units at input boundaries#336

Closed
Abolax123 wants to merge 7 commits into
ancore-org:mainfrom
Abolax123:feat/213-session-expiry-normalization
Closed

Contract: Normalize session expiry units at input boundaries#336
Abolax123 wants to merge 7 commits into
ancore-org:mainfrom
Abolax123:feat/213-session-expiry-normalization

Conversation

@Abolax123
Copy link
Copy Markdown
Contributor

@Abolax123 Abolax123 commented Apr 23, 2026

Summary

  • define explicit session expiry unit rules in the account contract:
    • values < 1_000_000_000_000 are treated as seconds
    • values >= 1_000_000_000_000 are treated as milliseconds and converted to seconds
  • normalize expiry timestamps once at add_session_key and refresh_session_key_ttl entry points
  • update refresh path to normalize legacy millisecond-stored values into canonical seconds

Tests

  • add test_add_session_key_normalizes_milliseconds_expiry_to_seconds
  • add test_add_session_key_keeps_seconds_expiry_unchanged
  • add test_refresh_session_key_ttl_normalizes_legacy_milliseconds_expiry

Security impact

  • removes ambiguous unit handling from internal TTL helper logic
  • enforces a clear input contract and canonical storage representation

Closes #213

Summary by CodeRabbit

  • New Features

    • Enhanced wallet unlock experience with real-time password validation feedback.
  • Bug Fixes

    • Fixed session key expiry timestamp handling to ensure consistent storage and proper TTL management in wallet operations.
  • Tests

    • Added test coverage for invalid password scenarios and session key expiry normalization.
  • Documentation

    • Updated documentation describing session key storage behavior and TTL bump policies for wallet operations.

Cover high session-key storage growth behavior and validate instance TTL threshold policy across read/write paths, plus document expected limits and rejection behavior for extreme inputs.

Made-with: Cursor
Verify storage unlock results before transitioning to authenticated state, keep locked state on invalid credentials, and add regression tests for lock-manager and router invalid-password paths.

Made-with: Cursor
Add explicit coverage that non-owner upgrade attempts are rejected and that owner-authorized upgrades still succeed with version increment behavior preserved.

Made-with: Cursor
Define an explicit seconds-vs-milliseconds normalization rule, normalize expiry once in add/refresh paths, and add regression tests for second and millisecond inputs including legacy refresh normalization.

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@Abolax123 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 56 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f92f59f-807f-4b38-afd7-abc2e1833ce4

📥 Commits

Reviewing files that changed from the base of the PR and between d56017f and 02fe4c6.

📒 Files selected for processing (9)
  • apps/extension-wallet/src/errors/error-handler.ts
  • apps/extension-wallet/src/router/AuthGuard.tsx
  • apps/extension-wallet/src/router/__tests__/router.test.tsx
  • apps/extension-wallet/src/security/__tests__/lock-manager.test.ts
  • apps/web-dashboard/src/components/AccountSelector.tsx
  • apps/web-dashboard/src/components/MobileNav.tsx
  • apps/web-dashboard/src/components/__tests__/TransactionList.test.tsx
  • contracts/account/README.md
  • contracts/account/src/lib.rs
📝 Walkthrough

Walkthrough

This PR transitions wallet unlock from synchronous to async, integrating SecureStorageManager for credential validation with inline error feedback, updates LockManager to handle Promise<boolean> results, and normalizes session-key expiry timestamps in the smart contract with expanded test coverage.

Changes

Cohort / File(s) Summary
Wallet Unlock Flow Integration
apps/extension-wallet/src/router/AuthGuard.tsx, apps/extension-wallet/src/router/index.tsx, apps/extension-wallet/src/router/__tests__/router.test.tsx
Converts unlockWallet() to async with password parameter delegating to SecureStorageManager.unlock(), returns Promise<boolean>, includes new mocks for storage unlock/lock behavior, and adds UI error state for invalid password feedback.
Lock Manager Updates
apps/extension-wallet/src/security/lock-manager.ts, apps/extension-wallet/src/security/__tests__/lock-manager.test.ts
Refactors unlock(password) to expect Promise<boolean> from storage manager, throws on false result, includes new test validating locked state when unlock resolves to false.
Session Key Expiry Normalization
contracts/account/src/lib.rs
Adds explicit normalization of expires_at timestamps to canonical seconds in add_session_key and refresh_session_key_ttl, replaces auto-detection with dedicated normalize_expiry_timestamp, extends test coverage for seconds/milliseconds handling and TTL bump policies.
Documentation
contracts/account/README.md
Documents regression guardrails for Issue #260 including session-key persistence, SessionKeyNotFound error handling, and TTL bumping behavior on mutating vs. read-only entry points.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as UnlockScreen
    participant AuthCtx as AuthGuard (Context)
    participant StorageMgr as SecureStorageManager
    participant Storage as Encrypted Storage

    User->>UI: Enter password & submit
    UI->>UI: Clear prior error
    UI->>AuthCtx: await unlockWallet(password)
    AuthCtx->>StorageMgr: unlock(password)
    StorageMgr->>Storage: Validate & decrypt
    alt Valid password
        Storage-->>StorageMgr: Success (true)
        StorageMgr-->>AuthCtx: Promise<true>
        AuthCtx->>AuthCtx: Set isUnlocked=true, hasOnboarded=true
        AuthCtx-->>UI: Promise<true>
        UI->>UI: Navigate to Home
    else Invalid password
        Storage-->>StorageMgr: Fail (false)
        StorageMgr-->>AuthCtx: Promise<false>
        AuthCtx->>AuthCtx: Set isUnlocked=false
        AuthCtx-->>UI: Promise<false>
        UI->>UI: Display error: "Invalid password"
        UI->>UI: Remain on Unlock screen
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A hop, a skip, through async tricks!
Passwords locked in crypto-sticks,
Timestamps normalized, session-keys aligned,
Storage and secrets forever intertwined! ✨🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes significant out-of-scope changes unrelated to issue #213: extension wallet unlock flow refactoring (AuthGuard, router, lock-manager) and unrelated regression tests (storage growth, TTL policy, upgrade authorization) documented in commit messages. Move extension wallet changes and unrelated regression tests to separate PRs focused on their respective issues to maintain clear change scope and reviewability.
✅ 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 accurately describes the main contract change: session expiry unit normalization at input boundaries, which is the core focus of the PR and linked issue #213.
Linked Issues check ✅ Passed All requirements from linked issue #213 are met: explicit seconds-vs-milliseconds contract rule defined (< 1B = seconds, >= 1B = milliseconds), normalization at add_session_key and refresh_session_key_ttl entry points, and tests added for both units and legacy refresh behavior.
Docstring Coverage ✅ Passed Docstring coverage is 80.95% which is sufficient. The required threshold is 80.00%.

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

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

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

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 13 minutes and 56 seconds.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
contracts/account/src/lib.rs (1)

268-271: ⚠️ Potential issue | 🟠 Major

session_key_added event still publishes the raw, non-normalized expires_at.

The session key is stored with normalized_expires_at at line 264, but the event payload at line 270 uses the original caller-provided expires_at. As a result, indexers/SDK consumers subscribing to session_key_added will observe a millisecond timestamp (e.g. 1_735_689_600_000) while get_session_key() returns the seconds value (1_735_689_600). That breaks the "canonical seconds representation" property the PR is trying to establish and silently corrupts off-chain analytics that rely on the documented event schema in README.md (line 185) which declares expires_at as the expiration timestamp.

🐛 Proposed fix
         // Emit session_key_added event
         env.events()
-            .publish((events::session_key_added(&env),), (public_key, expires_at));
+            .publish(
+                (events::session_key_added(&env),),
+                (public_key, normalized_expires_at),
+            );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/account/src/lib.rs` around lines 268 - 271, The event publish uses
the caller-provided expires_at instead of the stored normalized_expires_at;
change the env.events().publish call that emits events::session_key_added to
pass normalized_expires_at (the value saved with the session key) rather than
the original expires_at so subscribers and get_session_key() see the same
canonical seconds timestamp; update the tuple arguments to (public_key,
normalized_expires_at) where events::session_key_added is invoked.
🧹 Nitpick comments (7)
apps/extension-wallet/src/router/index.tsx (2)

260-269: No pending guard around the async unlock — allows duplicate submissions.

handleUnlock awaits unlockWallet(password) but the submit button stays enabled while in flight. If the underlying storage.unlock does any non-trivial work (KDF, async decryption), the user can press Enter or click Unlock multiple times and trigger overlapping unlock attempts. This is mostly cosmetic, but with strong KDF parameters it can stack expensive operations and obscure error feedback.

♻️ Suggested fix: track a pending flag
   const [password, setPassword] = React.useState('');
   const [error, setError] = React.useState<string | null>(null);
+  const [pending, setPending] = React.useState(false);
   const from = (location.state as { from?: string } | null)?.from ?? '/home';

   async function handleUnlock(event: React.FormEvent<HTMLFormElement>) {
     event.preventDefault();
+    if (pending) return;
     setError(null);
-    const didUnlock = await unlockWallet(password);
-    if (!didUnlock) {
-      setError('Invalid password. Please try again.');
-      return;
+    setPending(true);
+    try {
+      const didUnlock = await unlockWallet(password);
+      if (!didUnlock) {
+        setError('Invalid password. Please try again.');
+        return;
+      }
+      navigate(from, { replace: true });
+    } finally {
+      setPending(false);
     }
-    navigate(from, { replace: true });
   }

And on the submit button:

-          <PrimaryButton disabled={!password.trim()} type="submit">
-            Unlock
+          <PrimaryButton disabled={!password.trim() || pending} type="submit">
+            {pending ? 'Unlocking…' : 'Unlock'}
           </PrimaryButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/extension-wallet/src/router/index.tsx` around lines 260 - 269, The
handleUnlock function allows concurrent submissions; add an isPending (or
isUnlocking) React state and guard to prevent duplicate submits: in handleUnlock
(and related unlockWallet call) return early if isPending is true, set isPending
= true immediately before awaiting unlockWallet(password), and set isPending =
false in a finally block so it always resets; also use isPending to disable the
submit button (and optionally the form) so the UI cannot trigger overlapping
unlock attempts and to provide proper feedback while unlockWallet runs.

282-288: Stale error stays visible while user retypes.

Once error is set, it remains rendered until the next submit. Clearing it on input change avoids the awkward "still showing Invalid password while I type the right one" state.

♻️ Suggested fix
             <input
               className="mt-2 w-full rounded-xl border border-border bg-background px-3 py-3 text-sm outline-none transition focus:border-primary"
-              onChange={(event) => setPassword(event.target.value)}
+              onChange={(event) => {
+                setPassword(event.target.value);
+                if (error) setError(null);
+              }}
               placeholder="Enter your password"
               type="password"
               value={password}
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/extension-wallet/src/router/index.tsx` around lines 282 - 288, The
password input currently calls setPassword onChange but leaves the existing
error visible; update the onChange handler for the password input (the element
using value={password} and setPassword) to also clear the form error (call
setError('') or setError(null)/setError(undefined) depending on how error state
is typed) before or after setPassword so the "Invalid password" message is
removed as the user types.
apps/extension-wallet/src/router/AuthGuard.tsx (2)

110-116: Silently swallowing unlock errors hurts diagnosability.

catch {} collapses every failure mode (wrong password, decryption error, storage adapter throw, async I/O failure) into the same "invalid password" UI in UnlockScreen. For non-credential errors this is misleading and you lose any signal in the console.

Suggest at minimum logging at warn so operational issues (corrupt storage, adapter failure) are surfaced during debugging:

♻️ Proposed change
-        } catch {
+        } catch (err) {
+          // eslint-disable-next-line no-console
+          console.warn('[auth] storage.unlock threw', err);
           setAuthState((current) => ({
             ...current,
             isUnlocked: false,
           }));
           return false;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/extension-wallet/src/router/AuthGuard.tsx` around lines 110 - 116, The
catch block in AuthGuard.tsx that calls setAuthState(...) is swallowing all
errors silently; change the anonymous catch to capture the error (e.g., catch
(err)) and log it at warn level before setting isUnlocked:false so operational
issues are visible; update the catch in the unlock flow where setAuthState is
used (reference: setAuthState in AuthGuard.tsx / UnlockScreen unlock handling)
to call your existing logger.warn or console.warn with a clear message plus the
error object, preserving the current state update and return false behavior.

30-37: Module-level singleton — acceptable, but consider lifecycle.

The lazy module-level storageManager works fine in the extension runtime (single JS context) and is correctly intercepted by the test mock. Two minor caveats worth documenting or rethinking:

  1. The instance survives React unmounts and HMR reloads — if you ever introduce per-account or per-profile storage adapters, this singleton becomes a bug.
  2. Tests that want to fully reset SDK state across test cases will need vi.resetModules() in addition to mockReset(), since the singleton holds the same MockSecureStorageManager instance for the file's lifetime. Today this is fine because state lives on mockUnlock / mockLock, but it's a subtle coupling.

If a per-component instance is acceptable, React.useState(() => getStorageManager())[0] (or just calling the getter inline) makes the lifecycle explicit and the useMemo on line 65 unnecessary.

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

In `@apps/extension-wallet/src/router/AuthGuard.tsx` around lines 30 - 37, The
module-level singleton storageManager/getStorageManager pattern persists across
React unmounts and HMR and can leak across tests; either document these
lifecycle caveats and note tests must call vi.resetModules() to fully reset the
singleton, or switch to a per-component lifecycle by replacing the global
reliance with a component-local instance (e.g., using React.useState(() =>
getStorageManager())[0] or calling getStorageManager() inline) and remove the
now-unnecessary useMemo — update references to SecureStorageManager and
createStorageAdapter accordingly so each component controls the storage manager
lifecycle.
apps/extension-wallet/src/router/__tests__/router.test.tsx (1)

119-134: Test correctly validates the failure path.

Drives the actual unlockWallet → storage.unlock → false path, asserts the a11y role="alert" element contains the expected copy, and verifies the user remains on /unlock (heading present, /home heading absent). One small consideration: it doesn't assert that mockUnlock was actually called with 'wrong-password', which would catch a regression where the password isn't forwarded into the SDK. Optional add:

expect(mockUnlock).toHaveBeenCalledWith('wrong-password');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/extension-wallet/src/router/__tests__/router.test.tsx` around lines 119
- 134, Add an assertion to ensure the unlock flow actually received the typed
password: after the user click in the failing unlock test, add
expect(mockUnlock).toHaveBeenCalledWith('wrong-password'); so the test verifies
mockUnlock (the mocked storage.unlock/unlockWallet path) was invoked with the
entered password and prevents regressions where the password isn't forwarded.
contracts/account/README.md (1)

101-116: Document the new expiry unit contract for add_session_key/refresh_session_key_ttl.

This PR introduces a public input contract (values < 1_000_000_000_000 are seconds, >= are milliseconds and converted to seconds) and a canonical seconds storage representation, but the README's Session Keys section still only declares expires_at: u64 with no mention of unit handling. SDK and frontend integrators reading this README will not learn about the threshold or the fact that the emitted/stored expires_at may differ from the value they passed in. Consider adding a short subsection (next to the Session Keys interface) describing:

  • the seconds-vs-milliseconds threshold,
  • that values are normalized to seconds at the entry points,
  • that refresh_session_key_ttl will rewrite legacy millisecond-stored entries to seconds.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/account/README.md` around lines 101 - 116, Update the "Session
Keys" README section to document the new expiry-unit contract: explain that
add_session_key and refresh_session_key_ttl accept a public input where values <
1_000_000_000_000 are treated as seconds and values >= 1_000_000_000_000 are
treated as milliseconds (and converted to seconds), state that expiry values are
normalized to canonical seconds at the entry points
(add_session_key/refresh_session_key_ttl) so the stored/emitted
SessionKey.expires_at may differ from the original input, and note that
refresh_session_key_ttl will migrate legacy millisecond-stored entries into
seconds; reference the function names add_session_key, refresh_session_key_ttl,
get_session_key and the SessionKey storage representation so integrators know
where to look.
contracts/account/src/lib.rs (1)

1322-1371: Cast on INSTANCE_BUMP_AMOUNT is redundant; otherwise the test reads well.

INSTANCE_BUMP_AMOUNT is already u32 (line 115) and env.ledger().sequence() returns u32, so INSTANCE_BUMP_AMOUNT as u32 at line 1344 is a no-op cast. Minor cleanup.

Also worth double-checking: env.storage().instance().get_ttl() is only callable from inside the contract context in some soroban-sdk versions; if you see panics here in CI under different SDK versions, you may need to wrap the read in env.as_contract(&contract_id, || ...).

Cleanup
-        env.ledger().set_sequence_number(
-            env.ledger()
-                .sequence()
-                .saturating_add(INSTANCE_BUMP_AMOUNT as u32),
-        );
+        env.ledger().set_sequence_number(
+            env.ledger().sequence().saturating_add(INSTANCE_BUMP_AMOUNT),
+        );

Please confirm that env.storage().instance().get_ttl() is callable from outside as_contract in the soroban-sdk version pinned in Cargo.toml for this crate.

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

In `@contracts/account/src/lib.rs` around lines 1322 - 1371, Remove the redundant
cast on INSTANCE_BUMP_AMOUNT in the ledger sequence bump (replace
INSTANCE_BUMP_AMOUNT as u32 with INSTANCE_BUMP_AMOUNT) since
INSTANCE_BUMP_AMOUNT is already u32 and env.ledger().sequence() returns u32;
also verify the pinned soroban-sdk version in Cargo.toml to ensure
env.storage().instance().get_ttl() is allowed outside contract context, and if
CI shows panics, wrap the get_ttl() reads in env.as_contract(&contract_id, || {
... }) around the ttl calls (references: INSTANCE_BUMP_AMOUNT,
env.ledger().sequence(), env.storage().instance().get_ttl(),
env.as_contract(&contract_id, || ...)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/account/src/lib.rs`:
- Around line 397-402: The calculation for ledgers_to_live in the
ledgers_to_live block is inconsistent with the DAY_IN_LEDGERS constant:
DAY_IN_LEDGERS is computed using 5 seconds-per-ledger but the code uses
(expires_at - current_timestamp) / 4 and comments “4 seconds-per-ledger”; fix by
making the seconds-per-ledger assumption consistent—either change the divisor to
5 in the ledgers_to_live computation or adjust DAY_IN_LEDGERS to use 4; update
the comment accordingly so ledgers_to_live, DAY_IN_LEDGERS, and their comments
all use the same seconds-per-ledger value (referencing DAY_IN_LEDGERS,
ledgers_to_live, expires_at, and current_timestamp).
- Around line 366-378: The expiry check in execute() can be bypassed for legacy
millisecond timestamps; normalize expiry before any comparison by calling
Self::normalize_expiry_timestamp(...) on the loaded session (or normalize inside
Self::get_session_key) so that env.ledger().timestamp() is compared to canonical
seconds; update execute() to use the normalized value (and persist the
normalized session if changed) or add a one-time migration during
initialize/upgrade to scan DataKey::SessionKey entries and rewrite expires_at to
normalized seconds to prevent legacy keys from bypassing expiry checks.
- Around line 1246-1279: The tests
test_upgrade_owner_path_succeeds_and_increments_version and
test_upgrade_non_owner_is_rejected_and_version_unchanged call
client.try_upgrade(&wasm_hash) with a synthetic BytesN hash that was never
uploaded, so first upload a real WASM blob via
env.deployer().upload_contract_wasm(...) and use the returned hash for
try_upgrade instead of BytesN::from_array; capture the Result from
AncoreAccountClient::try_upgrade, assert it is Ok for the owner test (and Err
for the non-owner test), and then assert the version changes (or remains)
accordingly; update both tests to replace the synthetic hash creation and
ignored result (let _ =) with the upload call and proper result assertions.

---

Outside diff comments:
In `@contracts/account/src/lib.rs`:
- Around line 268-271: The event publish uses the caller-provided expires_at
instead of the stored normalized_expires_at; change the env.events().publish
call that emits events::session_key_added to pass normalized_expires_at (the
value saved with the session key) rather than the original expires_at so
subscribers and get_session_key() see the same canonical seconds timestamp;
update the tuple arguments to (public_key, normalized_expires_at) where
events::session_key_added is invoked.

---

Nitpick comments:
In `@apps/extension-wallet/src/router/__tests__/router.test.tsx`:
- Around line 119-134: Add an assertion to ensure the unlock flow actually
received the typed password: after the user click in the failing unlock test,
add expect(mockUnlock).toHaveBeenCalledWith('wrong-password'); so the test
verifies mockUnlock (the mocked storage.unlock/unlockWallet path) was invoked
with the entered password and prevents regressions where the password isn't
forwarded.

In `@apps/extension-wallet/src/router/AuthGuard.tsx`:
- Around line 110-116: The catch block in AuthGuard.tsx that calls
setAuthState(...) is swallowing all errors silently; change the anonymous catch
to capture the error (e.g., catch (err)) and log it at warn level before setting
isUnlocked:false so operational issues are visible; update the catch in the
unlock flow where setAuthState is used (reference: setAuthState in AuthGuard.tsx
/ UnlockScreen unlock handling) to call your existing logger.warn or
console.warn with a clear message plus the error object, preserving the current
state update and return false behavior.
- Around line 30-37: The module-level singleton storageManager/getStorageManager
pattern persists across React unmounts and HMR and can leak across tests; either
document these lifecycle caveats and note tests must call vi.resetModules() to
fully reset the singleton, or switch to a per-component lifecycle by replacing
the global reliance with a component-local instance (e.g., using
React.useState(() => getStorageManager())[0] or calling getStorageManager()
inline) and remove the now-unnecessary useMemo — update references to
SecureStorageManager and createStorageAdapter accordingly so each component
controls the storage manager lifecycle.

In `@apps/extension-wallet/src/router/index.tsx`:
- Around line 260-269: The handleUnlock function allows concurrent submissions;
add an isPending (or isUnlocking) React state and guard to prevent duplicate
submits: in handleUnlock (and related unlockWallet call) return early if
isPending is true, set isPending = true immediately before awaiting
unlockWallet(password), and set isPending = false in a finally block so it
always resets; also use isPending to disable the submit button (and optionally
the form) so the UI cannot trigger overlapping unlock attempts and to provide
proper feedback while unlockWallet runs.
- Around line 282-288: The password input currently calls setPassword onChange
but leaves the existing error visible; update the onChange handler for the
password input (the element using value={password} and setPassword) to also
clear the form error (call setError('') or setError(null)/setError(undefined)
depending on how error state is typed) before or after setPassword so the
"Invalid password" message is removed as the user types.

In `@contracts/account/README.md`:
- Around line 101-116: Update the "Session Keys" README section to document the
new expiry-unit contract: explain that add_session_key and
refresh_session_key_ttl accept a public input where values < 1_000_000_000_000
are treated as seconds and values >= 1_000_000_000_000 are treated as
milliseconds (and converted to seconds), state that expiry values are normalized
to canonical seconds at the entry points
(add_session_key/refresh_session_key_ttl) so the stored/emitted
SessionKey.expires_at may differ from the original input, and note that
refresh_session_key_ttl will migrate legacy millisecond-stored entries into
seconds; reference the function names add_session_key, refresh_session_key_ttl,
get_session_key and the SessionKey storage representation so integrators know
where to look.

In `@contracts/account/src/lib.rs`:
- Around line 1322-1371: Remove the redundant cast on INSTANCE_BUMP_AMOUNT in
the ledger sequence bump (replace INSTANCE_BUMP_AMOUNT as u32 with
INSTANCE_BUMP_AMOUNT) since INSTANCE_BUMP_AMOUNT is already u32 and
env.ledger().sequence() returns u32; also verify the pinned soroban-sdk version
in Cargo.toml to ensure env.storage().instance().get_ttl() is allowed outside
contract context, and if CI shows panics, wrap the get_ttl() reads in
env.as_contract(&contract_id, || { ... }) around the ttl calls (references:
INSTANCE_BUMP_AMOUNT, env.ledger().sequence(),
env.storage().instance().get_ttl(), env.as_contract(&contract_id, || ...)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ee54c2d-19cb-475c-b89a-48ea43f61e71

📥 Commits

Reviewing files that changed from the base of the PR and between de7f311 and d56017f.

📒 Files selected for processing (7)
  • apps/extension-wallet/src/router/AuthGuard.tsx
  • apps/extension-wallet/src/router/__tests__/router.test.tsx
  • apps/extension-wallet/src/router/index.tsx
  • apps/extension-wallet/src/security/__tests__/lock-manager.test.ts
  • apps/extension-wallet/src/security/lock-manager.ts
  • contracts/account/README.md
  • contracts/account/src/lib.rs

Comment thread contracts/account/src/lib.rs Outdated
Comment on lines +366 to +378
let mut session_key = Self::get_session_key(env.clone(), public_key.clone())
.ok_or(ContractError::SessionKeyNotFound)?;

Self::extend_session_key_ttl(&env, &public_key, session_key.expires_at);
// Normalize legacy values here so refresh always operates on canonical seconds.
let normalized_expires_at = Self::normalize_expiry_timestamp(session_key.expires_at);
if session_key.expires_at != normalized_expires_at {
session_key.expires_at = normalized_expires_at;
env.storage()
.persistent()
.set(&DataKey::SessionKey(public_key.clone()), &session_key);
}

Self::extend_session_key_ttl(&env, &public_key, normalized_expires_at);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd contracts/account/src && wc -l lib.rs

Repository: ancore-org/ancore

Length of output: 72


🏁 Script executed:

cd contracts/account/src && head -n 400 lib.rs | tail -n 50

Repository: ancore-org/ancore

Length of output: 2065


🏁 Script executed:

cd contracts/account/src && sed -n '190,210p' lib.rs

Repository: ancore-org/ancore

Length of output: 1074


🏁 Script executed:

cd contracts/account/src && sed -n '80,150p' lib.rs

Repository: ancore-org/ancore

Length of output: 2080


🏁 Script executed:

cd contracts/account/src && sed -n '1,50p' lib.rs

Repository: ancore-org/ancore

Length of output: 1714


🏁 Script executed:

cd contracts/account/src && rg "upgrade|migrate" -n

Repository: ancore-org/ancore

Length of output: 1220


🏁 Script executed:

cd contracts/account/src && rg "add_session_key|create_session_key" -A 15 -n

Repository: ancore-org/ancore

Length of output: 14194


🏁 Script executed:

cd contracts/account/src && sed -n '250,350p' lib.rs

Repository: ancore-org/ancore

Length of output: 3204


🏁 Script executed:

cd contracts/account/src && git log --oneline --all -- lib.rs | head -20

Repository: ancore-org/ancore

Length of output: 1472


🏁 Script executed:

cd contracts/account/src && git show HEAD:lib.rs | grep -A 20 "normalize_expiry_timestamp" || echo "Not in HEAD"

Repository: ancore-org/ancore

Length of output: 233


🏁 Script executed:

cd contracts && find . -type f \( -name "*.md" -o -name "MIGRATION*" -o -name "UPGRADE*" \) | head -10

Repository: ancore-org/ancore

Length of output: 92


🏁 Script executed:

cd contracts/account/src && sed -n '865,900p' lib.rs

Repository: ancore-org/ancore

Length of output: 1503


Session key expiry check at execute() (line 203) should normalize before comparison to prevent bypass for legacy keys.

The conditional rewrite and TTL extension are correct, but the vulnerability is real: if any legacy millisecond-stored session keys exist in persistent storage (from before normalization was added), execute() will bypass expiry checks. The comparison env.ledger().timestamp() >= session.expires_at treats millisecond values (1.7 × 10¹²) as effectively never-expiring against seconds ( 1.7 × 10⁹).

New keys are safe because add_session_key() normalizes on write (line 254), and refresh handles individual keys, but this leaves an unprotected read path. Consider:

  • Normalize in get_session_key() or directly in execute()'s expiry check, or
  • Add a one-time migration on contract upgrade/initialize to scan and fix any legacy keys in storage.

The test test_refresh_session_key_ttl_normalizes_legacy_milliseconds_expiry (line 865) indicates developers are already aware of this scenario.

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

In `@contracts/account/src/lib.rs` around lines 366 - 378, The expiry check in
execute() can be bypassed for legacy millisecond timestamps; normalize expiry
before any comparison by calling Self::normalize_expiry_timestamp(...) on the
loaded session (or normalize inside Self::get_session_key) so that
env.ledger().timestamp() is compared to canonical seconds; update execute() to
use the normalized value (and persist the normalized session if changed) or add
a one-time migration during initialize/upgrade to scan DataKey::SessionKey
entries and rewrite expires_at to normalized seconds to prevent legacy keys from
bypassing expiry checks.

Comment on lines +397 to 402
let ledgers_to_live = if expires_at > current_timestamp {
// Using 4 seconds-per-ledger + 1 day buffer to guarantee it outlives expiry
((expires_at_secs - current_timestamp) / 4) as u32 + DAY_IN_LEDGERS
((expires_at - current_timestamp) / 4) as u32 + DAY_IN_LEDGERS
} else {
DAY_IN_LEDGERS // 1 day default buffer
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent seconds-per-ledger assumption with DAY_IN_LEDGERS.

DAY_IN_LEDGERS (line 114) is computed assuming 5 seconds per ledger, but the new comment and arithmetic here assume 4 seconds per ledger ((expires_at - current_timestamp) / 4). Either both should use 5 (correct on Stellar mainnet/testnet) or both should use 4. The current mix overestimates ledgers_to_live by ~25% relative to the DAY_IN_LEDGERS baseline used for the threshold and the default branch — fine as a buffer, but the comment is misleading and the math should be self-consistent.

🛠️ Suggested fix
-        let ledgers_to_live = if expires_at > current_timestamp {
-            // Using 4 seconds-per-ledger + 1 day buffer to guarantee it outlives expiry
-            ((expires_at - current_timestamp) / 4) as u32 + DAY_IN_LEDGERS
+        let ledgers_to_live = if expires_at > current_timestamp {
+            // 5 seconds-per-ledger (matches DAY_IN_LEDGERS) + 1 day buffer
+            ((expires_at - current_timestamp) / 5) as u32 + DAY_IN_LEDGERS
         } else {
             DAY_IN_LEDGERS // 1 day default buffer
         };
📝 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
let ledgers_to_live = if expires_at > current_timestamp {
// Using 4 seconds-per-ledger + 1 day buffer to guarantee it outlives expiry
((expires_at_secs - current_timestamp) / 4) as u32 + DAY_IN_LEDGERS
((expires_at - current_timestamp) / 4) as u32 + DAY_IN_LEDGERS
} else {
DAY_IN_LEDGERS // 1 day default buffer
};
let ledgers_to_live = if expires_at > current_timestamp {
// 5 seconds-per-ledger (matches DAY_IN_LEDGERS) + 1 day buffer
((expires_at - current_timestamp) / 5) as u32 + DAY_IN_LEDGERS
} else {
DAY_IN_LEDGERS // 1 day default buffer
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/account/src/lib.rs` around lines 397 - 402, The calculation for
ledgers_to_live in the ledgers_to_live block is inconsistent with the
DAY_IN_LEDGERS constant: DAY_IN_LEDGERS is computed using 5 seconds-per-ledger
but the code uses (expires_at - current_timestamp) / 4 and comments “4
seconds-per-ledger”; fix by making the seconds-per-ledger assumption
consistent—either change the divisor to 5 in the ledgers_to_live computation or
adjust DAY_IN_LEDGERS to use 4; update the comment accordingly so
ledgers_to_live, DAY_IN_LEDGERS, and their comments all use the same
seconds-per-ledger value (referencing DAY_IN_LEDGERS, ledgers_to_live,
expires_at, and current_timestamp).

Comment on lines +1246 to +1279
#[test]
fn test_upgrade_owner_path_succeeds_and_increments_version() {
let env = Env::default();
let contract_id = env.register_contract(None, AncoreAccount);
let client = AncoreAccountClient::new(&env, &contract_id);

let owner = Address::generate(&env);
client.initialize(&owner);
assert_eq!(client.get_version(), 1);

env.mock_all_auths();
let wasm_hash = BytesN::from_array(&env, &[42u8; 32]);
let _ = client.try_upgrade(&wasm_hash);

// Owner-authorized upgrade should increment version.
assert_eq!(client.get_version(), 2);
}

#[test]
fn test_upgrade_non_owner_is_rejected_and_version_unchanged() {
let env = Env::default();
let contract_id = env.register_contract(None, AncoreAccount);
let client = AncoreAccountClient::new(&env, &contract_id);

let owner = Address::generate(&env);
client.initialize(&owner);
assert_eq!(client.get_version(), 1);

// No owner auth is provided; require_auth() should reject this upgrade attempt.
let wasm_hash = BytesN::from_array(&env, &[7u8; 32]);
let result = client.try_upgrade(&wasm_hash);
assert!(result.is_err());
assert_eq!(client.get_version(), 1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how the test env handles update_current_contract_wasm with arbitrary hashes
# and confirm the test behavior the author intended.
rg -nP --type=rust -C3 'update_current_contract_wasm'
fd -t f 'Cargo.toml' contracts | xargs rg -nP 'soroban-sdk\s*=' -C1

Repository: ancore-org/ancore

Length of output: 883


🌐 Web query:

soroban-sdk testutils update_current_contract_wasm behavior with non-installed hash

💡 Result:

The soroban-sdk::Env::deployer.update_current_contract_wasm(wasm_hash) method, available only with the "testutils" feature enabled, replaces the executable (WASM) of the currently executing contract with the WASM identified by the provided wasm_hash (a BytesN<32>). This is a test utility for simulating contract upgrades during unit tests. Key behavior: - The WASM blob identified by wasm_hash must already be present in the ledger, typically uploaded via env.deployer.upload_contract_wasm(wasm_bytes) which returns the hash. - If a non-installed (non-existent) hash is provided, the host function invocation fails with a HostError. Specifically, the Soroban host will return an error indicating the WASM code cannot be found (likely Error(ContractInstance, InvalidExecutable) or similar, based on analogous deployment behaviors where missing WASM causes simulation failures). - The update is deferred: It does not take effect immediately during the current invocation. The new executable is applied only after the current transaction successfully completes. - In test environments (Env::default or similar), this allows testing upgrade logic without deploying to a real network. - Usage example from docs and code samples: let env = Env::default; let new_wasm_hash = env.deployer.upload_contract_wasm(&new_wasm_bytes); env.deployer.update_current_contract_wasm(new_wasm_hash); // Update applies post-invocation; test subsequent calls use new WASM. For non-test environments, contract upgrades follow the standard Stellar/Soroban deployment flow (upload WASM, then deploy/update instance via transactions), not this testutils method. This is grounded in the official Rust SDK documentation for soroban_sdk::deploy::Deployer (latest version), which explicitly states the prerequisite, and consistent with Soroban host error patterns for missing WASM references during simulations/invocations.

Citations:


🏁 Script executed:

# Examine the try_upgrade implementation and its error handling
cd contracts/account/src && sed -n '290,320p' lib.rs

Repository: ancore-org/ancore

Length of output: 1001


🏁 Script executed:

# Search for the complete upgrade function implementation including error handling
rg -nP --type=rust -A 25 'fn upgrade|fn try_upgrade' contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 1634


🏁 Script executed:

# Check if there's any WASM upload or mocking in the test file
rg -nP --type=rust -B 10 'test_upgrade_owner_path_succeeds' contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 444


🏁 Script executed:

# Find try_upgrade implementation
rg -nP --type=rust -A 15 'fn try_upgrade|#\[contract\]' contracts/account/src/lib.rs | head -60

Repository: ancore-org/ancore

Length of output: 674


🏁 Script executed:

# Look for contract methods/exports and the client generated code
rg -nP --type=rust '#\[contractimpl\]' -A 5 contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 363


🏁 Script executed:

# Check if any WASM is actually uploaded before the test
rg -nP --type=rust -B 20 'test_upgrade_owner_path_succeeds' contracts/account/src/lib.rs | tail -30

Repository: ancore-org/ancore

Length of output: 821


🏁 Script executed:

# Get the full test implementation
rg -nP --type=rust -A 20 'test_upgrade_owner_path_succeeds_and_increments_version' contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 970


🏁 Script executed:

# Find where AncoreAccountClient is generated/defined and understand try_upgrade
rg -nP --type=rust 'AncoreAccountClient|try_upgrade' contracts/account/src/lib.rs | head -20

Repository: ancore-org/ancore

Length of output: 1480


🏁 Script executed:

# Check if there's a separate test module file
find contracts/account -name '*test*' -o -name '*spec*'

Repository: ancore-org/ancore

Length of output: 2970


🏁 Script executed:

# Check if there are any test snapshots for the upgrade test (may indicate test passed)
ls -la contracts/account/test_snapshots/test/ | grep -i upgrade

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Look for how soroban-sdk handles update_current_contract_wasm errors
rg -nP 'update_current_contract_wasm' contracts/account/src/lib.rs -A 3 -B 3

Repository: ancore-org/ancore

Length of output: 316


🏁 Script executed:

# Check if there's documentation or error handling around deployer calls
rg -nP --type=rust 'deployer\(\)' contracts/account/src/lib.rs -B 2 -A 2

Repository: ancore-org/ancore

Length of output: 232


🏁 Script executed:

# Check if there are CI/test configurations that show test results
find . -name '*.yml' -o -name '*.yaml' | xargs grep -l 'test' 2>/dev/null | head -5

Repository: ancore-org/ancore

Length of output: 169


🏁 Script executed:

# Look for any WASM upload or preparation in the test or module
rg -nP --type=rust 'upload_contract_wasm|deployer' contracts/account/src/lib.rs | head -20

Repository: ancore-org/ancore

Length of output: 87


🏁 Script executed:

# Check the actual behavior by looking at Soroban documentation or examples in the repo
rg -nP 'env.deployer' contracts/ -A 2 | head -40

Repository: ancore-org/ancore

Length of output: 249


🏁 Script executed:

# Check CI workflow to understand if tests pass
cat .github/workflows/ci.yml | head -100

Repository: ancore-org/ancore

Length of output: 2437


🏁 Script executed:

# Look for test invocation and any WASM preparation
rg -nP 'cargo test|test_upgrade' .github/workflows/ci.yml -B 2 -A 2

Repository: ancore-org/ancore

Length of output: 185


🏁 Script executed:

# Search for any error handling or Result types in upgrade context
rg -nP --type=rust 'Result.*upgrade|try_upgrade' contracts/account/src/lib.rs -B 3 -A 3

Repository: ancore-org/ancore

Length of output: 673


🏁 Script executed:

# Check if update_current_contract_wasm is documented or wrapped differently
rg -nP --type=rust 'pub fn update_current_contract_wasm' --max-count 5

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Look at the generated client to understand try_upgrade behavior
rg -nP '#\[derive.*Client\]|impl.*Client' contracts/account/src/lib.rs -B 2 -A 5

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Search for documentation or examples of similar upgrade patterns in soroban
web_search
soroban-sdk deploy.Deployer update_current_contract_wasm test environment behavior invalid hash

Repository: ancore-org/ancore

Length of output: 161


🏁 Script executed:

# Search for upload_contract_wasm usage in the repository to understand the pattern
rg -nP 'upload_contract_wasm' contracts/

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Look for other test patterns with deployer to understand how upgrade tests should work
rg -nP --type=rust 'deployer\(\)\.upload|deployer\(\)\.update' contracts/ -B 2 -A 2

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Check if there's any documentation or patterns in soroban examples
find . -name '*.md' -o -name '*.rs' | xargs grep -l 'upgrade.*wasm\|deployer.*update' 2>/dev/null | head -5

Repository: ancore-org/ancore

Length of output: 121


🏁 Script executed:

# Check the README for any documentation about upgrade testing
cat contracts/account/README.md | grep -A 10 -B 10 -i upgrade

Repository: ancore-org/ancore

Length of output: 3384


🏁 Script executed:

# Look at error handling around deployer calls - check if try_ prefix wraps errors
rg -nP --type=rust 'fn try_' contracts/account/src/lib.rs | head -10

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Check how soroban SDK generated clients wrap contract methods
rg -nP --type=rust 'try_.*Result' contracts/account/src/lib.rs -B 2 -A 2

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Check what happens when try_upgrade is called - look at the full error handling flow
rg -nP --type=rust -B 5 -A 10 'fn try_upgrade' contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Search for the generated client methods and how soroban SDK handles try_ prefixes
rg -nP --type=rust 'pub.*fn try_' contracts/account/src/lib.rs -A 5

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Try to find any soroban examples or similar patterns in the repo
find . -name '*.rs' | xargs grep -l 'register_contract.*AncoreAccount' | xargs cat | grep -A 30 'test_upgrade'

Repository: ancore-org/ancore

Length of output: 1898


🏁 Script executed:

# Check if there are any comments or documentation about how soroban testutils handles hashes
rg -nP --type=rust 'mock.*wasm|test.*wasm|update_current_contract_wasm' contracts/account/src/lib.rs -B 3 -A 3

Repository: ancore-org/ancore

Length of output: 316


🏁 Script executed:

# Look for any integration test or example that shows proper upgrade testing with uploaded WASM
find . -name '*.rs' | xargs grep -l 'upload_contract_wasm.*update_current_contract_wasm' 2>/dev/null

Repository: ancore-org/ancore

Length of output: 45


🏁 Script executed:

# Check the soroban SDK version and see if there's any changelog mentioning testutils behavior
cat contracts/Cargo.toml | grep -A 5 -B 5 'soroban-sdk'

Repository: ancore-org/ancore

Length of output: 236


🏁 Script executed:

# Try to understand soroban testutils behavior by checking if there are comments or examples
rg -nP --type=rust 'Env::default|register_contract' contracts/account/src/lib.rs -B 2 -A 2 | head -30

Repository: ancore-org/ancore

Length of output: 1171


🏁 Script executed:

# Look for any documentation about transaction atomicity/rollback in soroban
find . -name '*.md' | xargs grep -i 'rollback\|atomic\|transaction' 2>/dev/null | head -20

Repository: ancore-org/ancore

Length of output: 1564


🏁 Script executed:

# Check if the tests actually run and what the error would be
# by looking for test output or snapshots
cat contracts/account/test_snapshots/test/test_upgrade_owner_path_succeeds_and_increments_version.1.json 2>/dev/null || echo "No snapshot found"

Repository: ancore-org/ancore

Length of output: 78


🏁 Script executed:

# Check if snapshot exists and also verify by looking at the test file structure
ls -la contracts/account/test_snapshots/test/ | wc -l
# Compare with actual tests
rg -nP '#\[test\]' contracts/account/src/lib.rs | wc -l

Repository: ancore-org/ancore

Length of output: 66


🏁 Script executed:

# Check if try_upgrade is a soroban-sdk generated method by looking at the module structure
rg -nP --type=rust 'mod.*test|#\[cfg.*test\]' contracts/account/src/lib.rs -B 2 -A 2 | head -40

Repository: ancore-org/ancore

Length of output: 168


🏁 Script executed:

# Verify the exact behavior: search for how update_current_contract_wasm works in tests
rg -nP --type=rust -B 10 'env.mock_all_auths.*upgrade|upgrade.*env.mock' contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Check if upgrade tests are excluded or skipped
rg -nP --type=rust '#\[ignore\]|#\[skip\]' contracts/account/src/lib.rs -B 3 | grep -A 3 upgrade

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Look at the actual test output by checking if there's a test report or CI artifacts
find . -name '*.json' -path '*/test*' | head -20

Repository: ancore-org/ancore

Length of output: 1817


🏁 Script executed:

# Check if soroban testutils really doesn't validate the hash by examining testutils behavior documentation in soroban SDK
# Let's look at the Cargo.lock to see exact versions
grep -A 5 'soroban-sdk' contracts/Cargo.lock | head -20

Repository: ancore-org/ancore

Length of output: 114


🏁 Script executed:

# Critical: Check if upgrade test snapshots exist - list all snapshot files
find contracts/account/test_snapshots -name '*upgrade*'

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# Count snapshots vs tests to see if upgrade tests are missing
echo "Total test snapshots:" && find contracts/account/test_snapshots/test -name '*.json' | wc -l
echo "Upgrade test snapshots:" && find contracts/account/test_snapshots/test -name '*upgrade*' | wc -l
echo "All tests in lib.rs:" && rg -c '#\[test\]' contracts/account/src/lib.rs

Repository: ancore-org/ancore

Length of output: 135


🏁 Script executed:

# Look at what the snapshots directory contains
ls -1 contracts/account/test_snapshots/test/ | grep -i upgrade

Repository: ancore-org/ancore

Length of output: 43


🏁 Script executed:

# List all test names from lib.rs
rg -nP '#\[test\]' contracts/account/src/lib.rs -A 1 | grep 'fn test'

Repository: ancore-org/ancore

Length of output: 1799


🏁 Script executed:

# List all snapshot files to see what tests they cover
ls -1 contracts/account/test_snapshots/test/ | sed 's/\.1\.json//'

Repository: ancore-org/ancore

Length of output: 1414


🏁 Script executed:

# Compare the two lists to see which tests are missing snapshots
echo "=== Tests in lib.rs ===" && rg -nP '#\[test\]' contracts/account/src/lib.rs -A 1 | grep 'fn test' | sed 's/.*fn //' | sed 's/().*//' | sort
echo "=== Tests with snapshots ===" && ls -1 contracts/account/test_snapshots/test/ | sed 's/\.1\.json//' | sort

Repository: ancore-org/ancore

Length of output: 2761


Address missing WASM upload in upgrade tests.

The test calls client.try_upgrade(&wasm_hash) with a synthetic hash [42u8; 32] that is never uploaded. According to soroban-sdk documentation, update_current_contract_wasm() requires the WASM blob to be pre-installed in the ledger via env.deployer().upload_contract_wasm(...). Providing a non-existent hash causes a HostError.

The test only verifies version bookkeeping (that it increments from 1 to 2) but doesn't actually confirm the WASM upgrade succeeds. The result of try_upgrade is ignored (let _ = ...), so any failure is silently discarded. Additionally, both upgrade tests lack snapshots in test_snapshots/test/, suggesting they may not be running successfully in CI.

To make this a faithful upgrade test, upload real WASM before calling upgrade:

let new_wasm_bytes = vec![/* valid WASM bytecode */];
let wasm_hash = env.deployer().upload_contract_wasm(&new_wasm_bytes);
let result = client.try_upgrade(&wasm_hash);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/account/src/lib.rs` around lines 1246 - 1279, The tests
test_upgrade_owner_path_succeeds_and_increments_version and
test_upgrade_non_owner_is_rejected_and_version_unchanged call
client.try_upgrade(&wasm_hash) with a synthetic BytesN hash that was never
uploaded, so first upload a real WASM blob via
env.deployer().upload_contract_wasm(...) and use the returned hash for
try_upgrade instead of BytesN::from_array; capture the Result from
AncoreAccountClient::try_upgrade, assert it is Ok for the owner test (and Err
for the non-owner test), and then assert the version changes (or remains)
accordingly; update both tests to replace the synthetic hash creation and
ignored result (let _ =) with the upload call and proper result assertions.

@wheval
Copy link
Copy Markdown
Contributor

wheval commented Apr 25, 2026

@Abolax123 resolvev conflicts

Wittig18 and others added 3 commits May 2, 2026 17:59
Resolve conflicts in extension-wallet AuthGuard (storage + verifier + unlockError), router unlock UI, router tests, lock manager, and account contract session key paths. Align test storage mock types and unblock ESLint no-undef/no-empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Use global TextEncoder/TextDecoder and WebCrypto-only paths so Vite can build the extension wallet. Replace Buffer-based hex/base64 helpers with small UTF-8/binary routines. Stellar verify keeps type-only Buffer cast for upstream typings.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wheval wheval closed this May 28, 2026
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.

Contract: Normalize session expiry units (seconds vs milliseconds)

4 participants