Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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.
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 | 🟠 MajorFire-and-forget promise chain — errors surface as unhandled rejections.
nearRpc.txStatus(...).then(async ...)is neitherreturned norawaited, sohandleListContractActionsresolves immediately. Anythrowinside 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 addingawaitcalls inside the callback (lines 93, 108) makes the async control flow more significant.Consider
returning orawaiting 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 theawaitserialization.Both
syncApi.list()(line 93) andsyncApi.listRegistrations()(line 108) areawaited 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_registrationswould only hit the list sync, not registrations).src/common/contracts/core/donation/client.ts (1)
70-71: Heavy reliance onanycasts bypasses wallet type safety.
wallet as anyandlet outcome: anyremove 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.listRegistrationisawaited beforesetIsApplicationSuccessful(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 withoutawaitso 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:parseIntwithout a radix and potentialNaNpropagation.
parseInt(listDetails?.on_chain_id as any)omits the radix and will produceNaNifon_chain_idisundefined/null. TheNaNwould 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 intoallTransactionsbut never returns a value. This is flagged by Biome'suseIterableCallbackReturnrule. Use.forEach()for clarity and to satisfy the linter.♻️ Proposed fix
- registrants.map((registrant: AccountGroupItem) => { + registrants.forEach((registrant: AccountGroupItem) => { allTransactions.push(
55-55: Inconsistentidparsing across handlers.The
idfrom the router query is parsed differently across handlers:parseInt(id as any)(lines 55, 182) vsNumber(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 usingparseInt.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
awaitonsyncApi.directDonationdelays 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(() => {}); }
| const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null; | ||
| const donation = providers.getTransactionLastResult(outcome) as DirectDonation; | ||
|
|
||
| return { donation, txHash }; |
There was a problem hiding this comment.
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.
| const actions = txInputs.map(({ amountYoctoNear, args }) => | ||
| actionCreators.functionCall("donate", args, BigInt(FULL_TGAS), BigInt(amountYoctoNear)), | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "FULL_TGAS\s*=" --type=ts -B2 -A2 | head -50Repository: 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 -30Repository: 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 -A2Repository: 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 -60Repository: 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.
| 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.
| .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, |
There was a problem hiding this comment.
🧩 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 -100Repository: 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 -200Repository: 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 2Repository: 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(() => {}); |
There was a problem hiding this comment.
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.
| .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.
| 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, |
There was a problem hiding this comment.
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.
| const isTransactionOutcomeDetected = | ||
| transactionHash && Boolean(recipientAccountId ?? potAccountId); | ||
| transactionHash && Boolean(recipientAccountId ?? potAccountId ?? listId); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/features/donation/hooks/redirects.ts | head -60Repository: PotLock/potlock-nextjs-app
Length of output: 2415
🏁 Script executed:
rg "donateToCampaign" --type=ts --type=tsx -B2 -A2Repository: 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 -A3Repository: PotLock/potlock-nextjs-app
Length of output: 97
🏁 Script executed:
rg "donateToCampaign" --type ts -B2 -A2Repository: 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 -20Repository: PotLock/potlock-nextjs-app
Length of output: 240
🏁 Script executed:
# Search for where query parameters are set for campaign donations
rg "donateToCampaign" -B3 -A3Repository: 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 -50Repository: 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.
| // 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")); |
There was a problem hiding this comment.
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.
| // 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.
Summary by CodeRabbit
New Features
Chores