Skip to content

Feature/sync accounts lists#579

Merged
Ebube111 merged 2 commits intostagingfrom
feature/sync-accounts-lists
Feb 9, 2026
Merged

Feature/sync accounts lists#579
Ebube111 merged 2 commits intostagingfrom
feature/sync-accounts-lists

Conversation

@aunali8812
Copy link
Collaborator

@aunali8812 aunali8812 commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • Added wallet-based signing for direct donations with improved transaction tracking
    • Implemented automatic synchronization of donation, list, registration, and account data with indexer
  • Chores

    • Removed unused imports and debug logging

@aunali8812 aunali8812 requested a review from Ebube111 as a code owner February 9, 2026 08:51
@vercel
Copy link

vercel bot commented Feb 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
potlock-next-app Ready Ready Preview, Comment Feb 9, 2026 8:51am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Walkthrough

This PR integrates wallet-based direct donations and implements indexer synchronization across operations. The donation client becomes async with wallet signing support, producing DirectDonateResult types. New sync endpoints are added for accounts, lists, registrations, and donations. Various list and profile operations now trigger post-operation indexer syncs with error swallowing.

Changes

Cohort / File(s) Summary
Indexer Sync API
src/common/api/indexer/hooks.ts, src/common/api/indexer/sync.ts
Removed unused import from hooks. Added five new sync endpoints: account, list, listRegistrations, listRegistration, and directDonation, each with POST handlers and consistent error handling returning {success, message}. Removed debug console.log from campaign sync.
Donation Client Wallet Integration
src/common/contracts/core/donation/client.ts, src/common/contracts/core/donation/index.ts
Made donate and donateBatch async with wallet signing support. Added DirectDonateResult and DirectBatchDonateResult types. Implemented txHash extraction, receipt parsing, and wallet capability detection for signAndSendTransaction(s). Re-exported new types through index barrel.
List Operations with Indexer Sync
src/entities/list/components/ListDetails.tsx, src/entities/list/components/ListFormDetails.tsx, src/entities/list/hooks/useListForm.ts, src/entities/list/models/effects.ts
Added post-operation indexer syncs across list creation, updates, registrations, and admin changes. Standardized listId extraction and async promise chains. Enhanced effects to recognize register_batch and unregister operations, decode FunctionCall args, and sync registrations. Improved error handling by simplifying catch blocks.
Donation Effects with Indexer Sync
src/features/donation/models/effects/group-list-donation.ts, src/features/donation/models/effects/index.ts, src/features/donation/hooks/redirects.ts
Made groupListDonationMulticall async, using DirectBatchDonateResult and adding directDonation indexer sync. Enhanced single-donation handling with indexer sync. Improved receipt parsing in handleOutcome to accumulate DirectDonation objects and sync all parsed donations. Extended transaction detection to include listId.
Profile Configuration Sync
src/features/profile-configuration/models/effects.ts
Added asyncchronous indexer sync via syncApi.account after successful non-DAO profile saves, swallowing any sync errors.

Sequence Diagram

sequenceDiagram
    participant User
    participant Wallet
    participant DonationClient
    participant NearRPC
    participant Indexer
    
    User->>Wallet: Initiate donation
    DonationClient->>Wallet: Request sign & send transaction
    Wallet->>NearRPC: signAndSendTransaction(s)
    NearRPC-->>Wallet: Transaction hash + receipts
    Wallet-->>DonationClient: txHash, donation results
    DonationClient->>DonationClient: Extract & parse donations<br/>(from receipts)
    DonationClient-->>User: Return DirectDonateResult<br/>(donations + txHash)
    DonationClient->>Indexer: Sync donation<br/>directDonation(txHash, senderId)
    Indexer-->>DonationClient: Sync acknowledged<br/>(error swallowed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • Ebube111
  • carina-akaia

Poem

🐰 Hops through wallets, signs with glee,
Donations flow directly free,
Indexer syncs each op with care,
Lists and profiles everywhere!
From chain to data, seamless flight,
Our rabbit code burns ever bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feature/sync accounts lists' is partially related to the changeset. While the PR adds sync functionality, the title is overly generic and uses slash notation typical of branch names rather than a clear, descriptive commit message format. Rephrase the title to be more descriptive and specific, such as 'Add indexer sync endpoints for accounts, lists, and donations' or 'Implement sync API integration across list and donation operations'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/sync-accounts-lists

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
src/entities/list/models/effects.ts (1)

14-129: ⚠️ Potential issue | 🟠 Major

Fire-and-forget promise chain — errors surface as unhandled rejections.

nearRpc.txStatus(...).then(async ...) is neither returned nor awaited, so handleListContractActions resolves immediately. Any throw inside the .then() callback (lines 74, 78, 124, 127) becomes an unhandled promise rejection rather than propagating to the caller. This was likely pre-existing, but adding await calls inside the callback (lines 93, 108) makes the async control flow more significant.

Consider returning or awaiting the chain so errors propagate:

Proposed fix
-      nearRpc.txStatus(transactionHash, owner_account_id).then(async (response) => {
+      return nearRpc.txStatus(transactionHash, owner_account_id).then(async (response) => {
🤖 Fix all issues with AI agents
In `@src/common/contracts/core/donation/client.ts`:
- Around line 94-97: providers.getTransactionLastResult(outcome) is being
unsafely cast to DirectDonation; replace the blind "as DirectDonation" on the
donation variable with a runtime validation and null-handling similar to
donateBatch's duck-typing: call providers.getTransactionLastResult(outcome),
check the result is non-null and has the expected DirectDonation fields (e.g.,
properties used elsewhere or the DirectDonation discriminants), and only then
assign/return it as donation; if validation fails, return donation as
null/undefined or throw a clear error and include txHash in the return to keep
callers safe.
- Around line 119-121: The code currently assigns BigInt(FULL_TGAS) to every
action created in txInputs.map (via actionCreators.functionCall), which causes
total gas to exceed the per-transaction 300 TGas limit when multiple donations
are batched and sent by signAndSendTransaction / signAndSendTransactions; fix by
computing a per-action gas budget from FULL_TGAS (use BigInt division:
perActionGas = BigInt(FULL_TGAS) / BigInt(txInputs.length)) and assign that to
each functionCall, distributing any remainder across the first few actions so
the sum of all action gas <= BigInt(FULL_TGAS); ensure the same logic is applied
for both code paths that call signAndSendTransaction and signAndSendTransactions
so each transaction respects the 300 TGas limit.

In `@src/entities/list/components/ListFormDetails.tsx`:
- Line 206: The empty .catch(() => {}) after the create_list call swallows
errors; replace it with a handler that mirrors the update_list branch: capture
the error from create_list, log it (e.g., via the same logger used around
update_list), set submission state back to not-submitting, and surface a
user-facing error (set form errors or open the error modal/notification used
elsewhere in ListFormDetails) so the user gets feedback and no success modal is
shown. Ensure you reference and update the create_list promise chain in
ListFormDetails.tsx and follow the same error-handling flow used for
update_list.
- Around line 191-203: dataToReturn is always a List[] so remove the redundant
Array.isArray(dataToReturn) check and explicitly handle the empty-array case:
set listData = dataToReturn.length ? dataToReturn[0] : undefined, then only call
await syncApi.list(createdListId) when createdListId is defined (same as now)
and consider setting an error or no-op path when dataToReturn is empty before
calling setListCreateSuccess; update references to
dataToReturn/listData/createdListId accordingly to avoid relying on
Array.isArray.

In `@src/entities/list/hooks/useListForm.ts`:
- Around line 54-59: handleRegisterBatch and handleRemoveAdmin lack a guard for
undefined query id, so parseInt/Number may produce NaN and be sent to the
contract; add an early return check (e.g. if (!id) return;) at the top of both
handleRegisterBatch and handleRemoveAdmin before calling parseInt(id) or
Number(id), mirroring the existing guards used in handleUnRegisterAccount and
handleSaveAdminsSettings, so listsContractClient.register_batch /
listsContractClient.remove_admin never receive NaN list_id.

In `@src/features/donation/hooks/redirects.ts`:
- Around line 30-31: The detection condition in redirects.ts uses
isTransactionOutcomeDetected = transactionHash && Boolean(recipientAccountId ??
potAccountId ?? listId) and therefore misses campaign donations; update that
expression to include campaignId (i.e., use recipientAccountId ?? potAccountId
?? listId ?? campaignId) so transactionHash combined with any of
recipientAccountId, potAccountId, listId, or campaignId triggers outcome
detection, ensuring behavior matches the user-flow.ts query-parameter logic.

In `@src/features/donation/models/effects/index.ts`:
- Around line 258-264: The fallback path decodes and JSON.parses
atob(receiptsOutcome[3]?.outcome?.status?.SuccessValue || "null") which can
produce garbled bytes and throw a SyntaxError; modify the logic in the block
that computes donationData (the code using donations and receiptsOutcome) to
first check whether receiptsOutcome[3]?.outcome?.status?.SuccessValue is defined
and non-empty before calling atob/JSON.parse, and if it is missing or invalid,
skip decoding/parsing and return the appropriate empty value (e.g., donations
when present or null/[]), or wrap the decoding/parsing in a try/catch to safely
handle parse errors and fallback to a safe default; update the code paths that
reference donationData accordingly (search for donations, receiptsOutcome, and
the handleOutcome flow) so no unguarded atob/JSON.parse is executed.
🧹 Nitpick comments (8)
src/entities/list/models/effects.ts (1)

83-114: Sync logic is well-structured, but note the await serialization.

Both syncApi.list() (line 93) and syncApi.listRegistrations() (line 108) are awaited sequentially. Since these are independent sync operations (list sync doesn't depend on registration sync), they could run concurrently. However, given errors are swallowed via .catch(() => {}) and the sync calls are fire-and-forget side effects, the current approach is acceptable — just slightly slower for operations that need both syncs (e.g., create_list_with_registrations would only hit the list sync, not registrations).

src/common/contracts/core/donation/client.ts (1)

70-71: Heavy reliance on any casts bypasses wallet type safety.

wallet as any and let outcome: any remove all type checking for the wallet interaction and transaction result. This is understandable for wallet adapter compatibility, but consider defining a minimal interface for the expected wallet methods and outcome shape to get at least partial type safety.

Also applies to: 123-124

src/common/api/indexer/sync.ts (1)

68-204: Significant boilerplate across all sync methods — consider a shared helper.

All five new methods (account, list, listRegistrations, listRegistration, directDonation) share identical error-handling logic. A small helper would eliminate ~100 lines of duplication:

Example helper
async function syncPost(
  path: string,
  label: string,
  body?: Record<string, unknown>,
): Promise<{ success: boolean; message?: string }> {
  try {
    const response = await fetch(`${SYNC_API_BASE_URL}${path}`, {
      method: "POST",
      ...(body && {
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      }),
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      console.warn(`Failed to sync ${label}:`, error);
      return { success: false, message: error?.error || "Sync failed" };
    }

    const result = await response.json();
    return { success: true, message: result.message };
  } catch (error) {
    console.warn(`Failed to sync ${label}:`, error);
    return { success: false, message: String(error) };
  }
}

Then each method becomes a one-liner, e.g.:

async account(accountId: string) {
  return syncPost(`/api/v1/accounts/${accountId}/sync`, "account");
},
src/entities/list/components/ListDetails.tsx (2)

100-106: Awaiting the sync call delays the success state update.

Because syncApi.listRegistration is awaited before setIsApplicationSuccessful(true), the user sees the success state only after the sync roundtrip completes. Since the sync is a non-critical side-effect, consider firing it without await so the UI updates immediately:

♻️ Fire-and-forget the sync call
      .then(async (data) => {
        // Sync registration to indexer
        if (viewer.accountId) {
-          await syncApi.listRegistration(onChainListId, viewer.accountId).catch(() => {});
+          syncApi.listRegistration(onChainListId, viewer.accountId).catch(() => {});
        }

        setIsApplicationSuccessful(true);
      })

79-84: parseInt without a radix and potential NaN propagation.

parseInt(listDetails?.on_chain_id as any) omits the radix and will produce NaN if on_chain_id is undefined/null. The NaN would silently flow into the contract call and the sync endpoint URL. Consider adding the radix and a guard:

🛡️ Proposed fix
  const applyToListModal = (note: string) => {
-    const onChainListId = parseInt(listDetails?.on_chain_id as any);
+    const onChainListId = parseInt(listDetails?.on_chain_id as any, 10);
+    if (Number.isNaN(onChainListId)) {
+      console.error("Invalid on_chain_id:", listDetails?.on_chain_id);
+      return;
+    }
src/entities/list/hooks/useListForm.ts (2)

86-100: Use .forEach() instead of .map() for side-effect-only iteration.

The .map() callback on line 88 pushes into allTransactions but never returns a value. This is flagged by Biome's useIterableCallbackReturn rule. Use .forEach() for clarity and to satisfy the linter.

♻️ Proposed fix
-    registrants.map((registrant: AccountGroupItem) => {
+    registrants.forEach((registrant: AccountGroupItem) => {
       allTransactions.push(

55-55: Inconsistent id parsing across handlers.

The id from the router query is parsed differently across handlers: parseInt(id as any) (lines 55, 182) vs Number(id) (lines 85, 121, 147). Both work for integer strings, but the inconsistency reduces readability. Consider standardizing on one approach (e.g., Number(id)) and always including the radix if using parseInt.

Also applies to: 85-85, 121-121, 147-147, 182-182

src/features/donation/models/effects/group-list-donation.ts (1)

48-53: Consider fire-and-forget for the sync call here too.

Same observation as other files: the await on syncApi.directDonation delays the return of donation results to the caller. If the sync endpoint is slow or unresponsive, this adds latency to the donation flow completion. Since the error is already swallowed, there's no functional dependency on the result.

♻️ Fire-and-forget
    if (senderId) {
-      await syncApi.directDonation(result.txHash, senderId).catch(() => {});
+      syncApi.directDonation(result.txHash, senderId).catch(() => {});
    }

Comment on lines +94 to +97
const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null;
const donation = providers.getTransactionLastResult(outcome) as DirectDonation;

return { donation, txHash };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unsafe cast of transaction result to DirectDonation.

providers.getTransactionLastResult(outcome) can return null, undefined, or an unexpected shape. The as DirectDonation cast silently accepts anything, so callers may receive null as donation and later fail. Consider adding a validation check (e.g., duck-typing like donateBatch does at line 162) or at minimum handling a null result.

🤖 Prompt for AI Agents
In `@src/common/contracts/core/donation/client.ts` around lines 94 - 97,
providers.getTransactionLastResult(outcome) is being unsafely cast to
DirectDonation; replace the blind "as DirectDonation" on the donation variable
with a runtime validation and null-handling similar to donateBatch's
duck-typing: call providers.getTransactionLastResult(outcome), check the result
is non-null and has the expected DirectDonation fields (e.g., properties used
elsewhere or the DirectDonation discriminants), and only then assign/return it
as donation; if validation fails, return donation as null/undefined or throw a
clear error and include txHash in the return to keep callers safe.

Comment on lines +119 to 121
const actions = txInputs.map(({ amountYoctoNear, args }) =>
actionCreators.functionCall("donate", args, BigInt(FULL_TGAS), BigInt(amountYoctoNear)),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "FULL_TGAS\s*=" --type=ts -B2 -A2 | head -50

Repository: PotLock/potlock-nextjs-app

Length of output: 318


🏁 Script executed:

# Also check where FULL_TGAS is imported from
rg -n "FULL_TGAS" src/common/contracts/core/donation/client.ts -B5 -A5 | head -30

Repository: PotLock/potlock-nextjs-app

Length of output: 1049


🏁 Script executed:

# Check the entire donateBatch function context around lines 119-121
cat -n src/common/contracts/core/donation/client.ts | sed -n '105,140p'

Repository: PotLock/potlock-nextjs-app

Length of output: 1505


🏁 Script executed:

# Find calls to donateBatch
rg -n "donateBatch" --type=ts -B2 -A2

Repository: PotLock/potlock-nextjs-app

Length of output: 1703


🏁 Script executed:

# Check if the signAndSendTransactions fallback path (line 133-140) has the same gas issue
cat -n src/common/contracts/core/donation/client.ts | sed -n '133,150p'

Repository: PotLock/potlock-nextjs-app

Length of output: 740


🏁 Script executed:

# Check if there's any validation on txInputs.length before calling donateBatch
rg -n "DirectBatchDonationItem" --type=ts -B3 -A3 | head -60

Repository: PotLock/potlock-nextjs-app

Length of output: 4047


Potential transaction gas overflow in batch donations.

Each action is allocated BigInt(FULL_TGAS) (300 TGas). On NEAR, the total gas for all actions in a single transaction cannot exceed 300 TGas. A batch with 2+ donations sent via signAndSendTransaction (lines 126-132) or signAndSendTransactions (lines 133-144) will exceed the per-transaction gas limit and fail.

Divide the gas budget across actions:

Proposed fix
   const actions = txInputs.map(({ amountYoctoNear, args }) =>
     actionCreators.functionCall("donate", args, BigInt(FULL_TGAS) / BigInt(txInputs.length), BigInt(amountYoctoNear)),
   );
📝 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
const actions = txInputs.map(({ amountYoctoNear, args }) =>
actionCreators.functionCall("donate", args, BigInt(FULL_TGAS), BigInt(amountYoctoNear)),
);
const actions = txInputs.map(({ amountYoctoNear, args }) =>
actionCreators.functionCall("donate", args, BigInt(FULL_TGAS) / BigInt(txInputs.length), BigInt(amountYoctoNear)),
);
🤖 Prompt for AI Agents
In `@src/common/contracts/core/donation/client.ts` around lines 119 - 121, The
code currently assigns BigInt(FULL_TGAS) to every action created in txInputs.map
(via actionCreators.functionCall), which causes total gas to exceed the
per-transaction 300 TGas limit when multiple donations are batched and sent by
signAndSendTransaction / signAndSendTransactions; fix by computing a per-action
gas budget from FULL_TGAS (use BigInt division: perActionGas = BigInt(FULL_TGAS)
/ BigInt(txInputs.length)) and assign that to each functionCall, distributing
any remainder across the first few actions so the sum of all action gas <=
BigInt(FULL_TGAS); ensure the same logic is applied for both code paths that
call signAndSendTransaction and signAndSendTransactions so each transaction
respects the 300 TGas limit.

Comment on lines +191 to +203
.then(async (dataToReturn) => {
// Sync list to indexer after creation
const listData = Array.isArray(dataToReturn) ? dataToReturn[0] : dataToReturn;
const createdListId = listData?.id;

if (createdListId) {
await syncApi.list(createdListId).catch(() => {});
}

setListCreateSuccess({
open: true,
type: "CREATE_LIST",
data: dataToReturn,
data: listData,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for create_list method definition and usages
rg -n 'create_list' --type=ts --type=tsx -C 3 -g '!node_modules/**' | head -100

# Also search for API type definitions
rg -n 'create_list.*return|create_list.*\(.*\).*\|' --type=ts --type=tsx -C 5 -g '!node_modules/**' | head -100

Repository: PotLock/potlock-nextjs-app

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Look for create_list in services/API files
fd -e ts -e tsx . -path '*/api*' -o -path '*/service*' | xargs rg -l 'create_list' 2>/dev/null

# Also check the actual file to understand context better
cat -n src/entities/list/components/ListFormDetails.tsx | sed -n '180,210p'

Repository: PotLock/potlock-nextjs-app

Length of output: 1520


🏁 Script executed:

#!/bin/bash
# Read the client.ts file to find create_list definition
cat -n src/common/contracts/core/lists/client.ts | head -200

Repository: PotLock/potlock-nextjs-app

Length of output: 6961


🏁 Script executed:

#!/bin/bash
# Search for create_list in the client file with context
rg -n 'create_list' src/common/contracts/core/lists/client.ts -A 10 -B 2

Repository: PotLock/potlock-nextjs-app

Length of output: 884


Remove the redundant Array.isArray() check and handle the empty array case.

The create_list function has an explicit return type of List[], so dataToReturn is always an array. The conditional check on line 193 is unnecessary. However, if the array is empty, dataToReturn[0] will be undefined, causing createdListId to be undefined and the sync to silently skip. Consider either validating the array is non-empty or adjusting the sync logic.

🤖 Prompt for AI Agents
In `@src/entities/list/components/ListFormDetails.tsx` around lines 191 - 203,
dataToReturn is always a List[] so remove the redundant
Array.isArray(dataToReturn) check and explicitly handle the empty-array case:
set listData = dataToReturn.length ? dataToReturn[0] : undefined, then only call
await syncApi.list(createdListId) when createdListId is defined (same as now)
and consider setting an error or no-op path when dataToReturn is empty before
calling setListCreateSuccess; update references to
dataToReturn/listData/createdListId accordingly to avoid relying on
Array.isArray.

.catch((error) => {
console.error("Error creating list:", error);
});
.catch(() => {});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Empty .catch(() => {}) silently swallows create_list errors.

If create_list fails, the user receives no feedback and no error is logged. The update_list branch correctly logs the error (line 173-175), so this looks like an oversight. The user will be stuck with no success modal and no error indication.

🐛 Proposed fix
-        .catch(() => {});
+        .catch((error) => {
+          console.error("Error creating list:", error);
+        });
📝 Committable suggestion

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

Suggested change
.catch(() => {});
.catch((error) => {
console.error("Error creating list:", error);
});
🤖 Prompt for AI Agents
In `@src/entities/list/components/ListFormDetails.tsx` at line 206, The empty
.catch(() => {}) after the create_list call swallows errors; replace it with a
handler that mirrors the update_list branch: capture the error from create_list,
log it (e.g., via the same logger used around update_list), set submission state
back to not-submitting, and surface a user-facing error (set form errors or open
the error modal/notification used elsewhere in ListFormDetails) so the user gets
feedback and no success modal is shown. Ensure you reference and update the
create_list promise chain in ListFormDetails.tsx and follow the same
error-handling flow used for update_list.

Comment on lines 54 to +59
const handleRegisterBatch = (registrants: string[]) => {
const listId = parseInt(id as any);

listsContractClient
.register_batch({
list_id: parseInt(id as any) as any,
list_id: listId as any,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing guard for undefined id in handleRegisterBatch and handleRemoveAdmin.

handleUnRegisterAccount (line 84) and handleSaveAdminsSettings (line 146) guard with if (!id) return;, but handleRegisterBatch and handleRemoveAdmin do not. If id (from query.id) is undefined, parseInt(undefined) / Number(undefined) yields NaN, which will be sent to the contract call and sync API.

🛡️ Proposed fix
  const handleRegisterBatch = (registrants: string[]) => {
+    if (!id) return;
     const listId = parseInt(id as any);
  const handleRemoveAdmin = (accounts: AccountGroupItem[]) => {
     const accountIds = accounts.map(prop("accountId"));
+    if (!id) return;
     const listId = Number(id);

Also applies to: 119-125

🤖 Prompt for AI Agents
In `@src/entities/list/hooks/useListForm.ts` around lines 54 - 59,
handleRegisterBatch and handleRemoveAdmin lack a guard for undefined query id,
so parseInt/Number may produce NaN and be sent to the contract; add an early
return check (e.g. if (!id) return;) at the top of both handleRegisterBatch and
handleRemoveAdmin before calling parseInt(id) or Number(id), mirroring the
existing guards used in handleUnRegisterAccount and handleSaveAdminsSettings, so
listsContractClient.register_batch / listsContractClient.remove_admin never
receive NaN list_id.

Comment on lines 30 to +31
const isTransactionOutcomeDetected =
transactionHash && Boolean(recipientAccountId ?? potAccountId);
transactionHash && Boolean(recipientAccountId ?? potAccountId ?? listId);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/features/donation/hooks/redirects.ts | head -60

Repository: PotLock/potlock-nextjs-app

Length of output: 2415


🏁 Script executed:

rg "donateToCampaign" --type=ts --type=tsx -B2 -A2

Repository: PotLock/potlock-nextjs-app

Length of output: 97


🏁 Script executed:

# Search for where redirects occur after campaign donations to see if campaignId is ever alone
rg "donateToCampaign.*transactionHash|transactionHash.*donateToCampaign" --type=ts --type=tsx -B3 -A3

Repository: PotLock/potlock-nextjs-app

Length of output: 97


🏁 Script executed:

rg "donateToCampaign" --type ts -B2 -A2

Repository: PotLock/potlock-nextjs-app

Length of output: 1993


🏁 Script executed:

# Look for where redirects happen with campaign donations - search in donation-related files
fd "donation" -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: PotLock/potlock-nextjs-app

Length of output: 240


🏁 Script executed:

# Search for where query parameters are set for campaign donations
rg "donateToCampaign" -B3 -A3

Repository: PotLock/potlock-nextjs-app

Length of output: 2787


🏁 Script executed:

cat -n src/features/donation/hooks/user-flow.ts | grep -A 15 "listId.*in props"

Repository: PotLock/potlock-nextjs-app

Length of output: 583


🏁 Script executed:

# Check if there are any other places where donateToCampaign is set alongside donateTo or donateToPot
rg "donateToCampaign" -B 5 -A 5 | grep -E "(donateTo|donateToPot|donateToCampaign)" | head -50

Repository: PotLock/potlock-nextjs-app

Length of output: 1595


🏁 Script executed:

# Let's examine the full user-flow.ts to understand the complete flow and confirm the if-else chain
cat -n src/features/donation/hooks/user-flow.ts | grep -B 10 -A 10 "accountId.*in props"

Repository: PotLock/potlock-nextjs-app

Length of output: 950


Add campaignId to the transaction outcome detection condition.

The if-else chain in user-flow.ts (lines 26–34) ensures that only one query parameter is set: donateTo, donateToPot, donateToList, or donateToCampaign. This means campaign donations set donateToCampaign exclusively without any of the other parameters. However, the isTransactionOutcomeDetected check in redirects.ts (lines 30–31) only includes the first three, causing campaign donations to skip outcome detection even though the modal displays campaignId (line 40).

Update the condition to:

const isTransactionOutcomeDetected =
  transactionHash && Boolean(recipientAccountId ?? potAccountId ?? listId ?? campaignId);
🤖 Prompt for AI Agents
In `@src/features/donation/hooks/redirects.ts` around lines 30 - 31, The detection
condition in redirects.ts uses isTransactionOutcomeDetected = transactionHash &&
Boolean(recipientAccountId ?? potAccountId ?? listId) and therefore misses
campaign donations; update that expression to include campaignId (i.e., use
recipientAccountId ?? potAccountId ?? listId ?? campaignId) so transactionHash
combined with any of recipientAccountId, potAccountId, listId, or campaignId
triggers outcome detection, ensuring behavior matches the user-flow.ts
query-parameter logic.

Comment on lines +258 to +264
// Return first donation for single donations, or array for batch
const donationData =
donations.length === 1
? donations[0]
: donations.length > 0
? donations
: JSON.parse(atob(receiptsOutcome[3]?.outcome?.status?.SuccessValue || "null"));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

atob("null") in the fallback path will throw a SyntaxError.

When receiptsOutcome[3]?.outcome?.status?.SuccessValue is undefined, the expression evaluates to atob("null"), which decodes to garbled bytes (not the string "null"). JSON.parse then throws a SyntaxError — and this line isn't inside a try/catch, so it propagates as an unhandled error from handleOutcome.

I understand this is temporary code (to be replaced with nearRpc.txStatus), but this crash path is reachable whenever the new receipt-parsing logic finds zero donations and the old fallback index doesn't exist.

Minimal defensive fix
       const donationData =
         donations.length === 1
           ? donations[0]
           : donations.length > 0
             ? donations
-            : JSON.parse(atob(receiptsOutcome[3]?.outcome?.status?.SuccessValue || "null"));
+            : (() => {
+                const sv = receiptsOutcome[3]?.outcome?.status?.SuccessValue;
+                return sv ? JSON.parse(atob(sv)) : null;
+              })();
📝 Committable suggestion

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

Suggested change
// Return first donation for single donations, or array for batch
const donationData =
donations.length === 1
? donations[0]
: donations.length > 0
? donations
: JSON.parse(atob(receiptsOutcome[3]?.outcome?.status?.SuccessValue || "null"));
// Return first donation for single donations, or array for batch
const donationData =
donations.length === 1
? donations[0]
: donations.length > 0
? donations
: (() => {
const sv = receiptsOutcome[3]?.outcome?.status?.SuccessValue;
return sv ? JSON.parse(atob(sv)) : null;
})();
🤖 Prompt for AI Agents
In `@src/features/donation/models/effects/index.ts` around lines 258 - 264, The
fallback path decodes and JSON.parses
atob(receiptsOutcome[3]?.outcome?.status?.SuccessValue || "null") which can
produce garbled bytes and throw a SyntaxError; modify the logic in the block
that computes donationData (the code using donations and receiptsOutcome) to
first check whether receiptsOutcome[3]?.outcome?.status?.SuccessValue is defined
and non-empty before calling atob/JSON.parse, and if it is missing or invalid,
skip decoding/parsing and return the appropriate empty value (e.g., donations
when present or null/[]), or wrap the decoding/parsing in a try/catch to safely
handle parse errors and fallback to a safe default; update the code paths that
reference donationData accordingly (search for donations, receiptsOutcome, and
the handleOutcome flow) so no unguarded atob/JSON.parse is executed.

@Ebube111 Ebube111 merged commit ef7121d into staging Feb 9, 2026
3 of 4 checks passed
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.

2 participants