diff --git a/src/common/api/indexer/hooks.ts b/src/common/api/indexer/hooks.ts index 9825e02b..aa89abd4 100644 --- a/src/common/api/indexer/hooks.ts +++ b/src/common/api/indexer/hooks.ts @@ -1,6 +1,5 @@ import type { AxiosResponse } from "axios"; -import { envConfig } from "@/common/_config/production.env-config"; import { NOOP_STRING } from "@/common/constants"; import { isAccountId, isEthereumAddress } from "@/common/lib"; import { diff --git a/src/common/api/indexer/sync.ts b/src/common/api/indexer/sync.ts index 95218373..82195f13 100644 --- a/src/common/api/indexer/sync.ts +++ b/src/common/api/indexer/sync.ts @@ -23,7 +23,6 @@ export const syncApi = { } const result = await response.json(); - console.log("Campaign synced:", result); return { success: true, message: result.message }; } catch (error) { console.warn("Failed to sync campaign:", error); @@ -65,4 +64,142 @@ export const syncApi = { return { success: false, message: String(error) }; } }, + + /** + * Sync an account profile and recalculate donation stats + * @param accountId - The NEAR account ID + */ + async account(accountId: string): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/accounts/${accountId}/sync`, { + method: "POST", + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + console.warn("Failed to sync account:", 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 account:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync a list after creation or update + * @param listId - The on-chain list ID + */ + async list(listId: number | string): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/sync`, { + method: "POST", + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + console.warn("Failed to sync list:", 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 list:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync all registrations for a list + * @param listId - The on-chain list ID + */ + async listRegistrations( + listId: number | string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch( + `${SYNC_API_BASE_URL}/api/v1/lists/${listId}/registrations/sync`, + { + method: "POST", + }, + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + console.warn("Failed to sync list registrations:", 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 list registrations:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync a single registration for a list + * @param listId - The on-chain list ID + * @param registrantId - The registrant account ID + */ + async listRegistration( + listId: number | string, + registrantId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch( + `${SYNC_API_BASE_URL}/api/v1/lists/${listId}/registrations/${registrantId}/sync`, + { + method: "POST", + }, + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + console.warn("Failed to sync list registration:", 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 list registration:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync a direct donation after it's made + * @param txHash - Transaction hash from the donation + * @param senderId - Account ID of the donor + */ + async directDonation( + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/donations/sync`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tx_hash: txHash, sender_id: senderId }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + console.warn("Failed to sync direct donation:", 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 direct donation:", error); + return { success: false, message: String(error) }; + } + }, }; diff --git a/src/common/contracts/core/donation/client.ts b/src/common/contracts/core/donation/client.ts index 9aa648b8..2424c0fc 100644 --- a/src/common/contracts/core/donation/client.ts +++ b/src/common/contracts/core/donation/client.ts @@ -1,5 +1,5 @@ import { DONATION_CONTRACT_ACCOUNT_ID } from "@/common/_config"; -import { contractApi } from "@/common/blockchains/near-protocol/client"; +import { contractApi, walletApi } from "@/common/blockchains/near-protocol/client"; import { FULL_TGAS } from "@/common/constants"; import type { IndivisibleUnits } from "@/common/types"; @@ -10,6 +10,11 @@ import { DirectDonationConfig, } from "./interfaces"; +export type DirectDonateResult = { + donation: DirectDonation; + txHash: string | null; +}; + const donationContractApi = contractApi({ contractId: DONATION_CONTRACT_ACCOUNT_ID, }); @@ -41,25 +46,145 @@ export const get_donations_for_donor = (args: { donor_id: string }) => args, }); -export const donate = (args: DirectDonationArgs, depositAmountYocto: IndivisibleUnits) => - donationContractApi.call("donate", { +export const donate = async ( + args: DirectDonationArgs, + depositAmountYocto: IndivisibleUnits, +): Promise => { + const wallet = await walletApi.ensureWallet(); + const signerId = walletApi.accountId; + + if (!signerId) { + throw new Error("Wallet is not signed in."); + } + + const { actionCreators } = await import("@near-js/transactions"); + const { providers } = await import("near-api-js"); + + const action = actionCreators.functionCall( + "donate", args, - deposit: depositAmountYocto, - gas: FULL_TGAS, - callbackUrl: window.location.href, - }); + BigInt(FULL_TGAS), + BigInt(depositAmountYocto), + ); + + let outcome: any; + const walletAny = wallet as any; + + if ("signAndSendTransaction" in walletAny) { + outcome = await walletAny.signAndSendTransaction({ + signerId, + receiverId: DONATION_CONTRACT_ACCOUNT_ID, + actions: [action], + }); + } else if ("signAndSendTransactions" in walletAny) { + const results = await walletAny.signAndSendTransactions({ + transactions: [ + { + receiverId: DONATION_CONTRACT_ACCOUNT_ID, + actions: [action], + }, + ], + }); + + outcome = Array.isArray(results) ? results[0] : results; + } else { + throw new Error("Wallet does not support transaction signing"); + } + + const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null; + const donation = providers.getTransactionLastResult(outcome) as DirectDonation; + + return { donation, txHash }; +}; + +export type DirectBatchDonateResult = { + donations: DirectDonation[]; + txHash: string | null; +}; -export const donateBatch = (txInputs: DirectBatchDonationItem[]) => - donationContractApi.callMultiple( - txInputs.map(({ amountYoctoNear, ...txInput }) => ({ - method: "donate", - deposit: amountYoctoNear, - gas: FULL_TGAS, +export const donateBatch = async ( + txInputs: DirectBatchDonationItem[], +): Promise => { + const wallet = await walletApi.ensureWallet(); + const signerId = walletApi.accountId; - ...txInput, - })), + if (!signerId) { + throw new Error("Wallet is not signed in."); + } + + const { actionCreators } = await import("@near-js/transactions"); + const { providers } = await import("near-api-js"); + + // Create actions for each donation + const actions = txInputs.map(({ amountYoctoNear, args }) => + actionCreators.functionCall("donate", args, BigInt(FULL_TGAS), BigInt(amountYoctoNear)), ); + let outcome: any; + const walletAny = wallet as any; + + if ("signAndSendTransaction" in walletAny) { + // Single transaction with multiple actions + outcome = await walletAny.signAndSendTransaction({ + signerId, + receiverId: DONATION_CONTRACT_ACCOUNT_ID, + actions, + }); + } else if ("signAndSendTransactions" in walletAny) { + // For wallets that only support signAndSendTransactions + const results = await walletAny.signAndSendTransactions({ + transactions: [ + { + receiverId: DONATION_CONTRACT_ACCOUNT_ID, + actions, + }, + ], + }); + + outcome = Array.isArray(results) ? results[0] : results; + } else { + throw new Error("Wallet does not support transaction signing"); + } + + const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null; + + // Parse all donations from the outcome + const donations: DirectDonation[] = []; + + if (outcome?.receipts_outcome) { + for (const receipt of outcome.receipts_outcome) { + const successValue = receipt?.outcome?.status?.SuccessValue; + + if (successValue) { + try { + const parsed = JSON.parse(atob(successValue)); + + if (parsed && "recipient_id" in parsed && "donor_id" in parsed) { + donations.push(parsed as DirectDonation); + } + } catch { + // Not valid JSON, skip + } + } + } + } + + // Fallback: try to get last result + if (donations.length === 0) { + try { + const lastResult = providers.getTransactionLastResult(outcome); + + if (lastResult && typeof lastResult === "object" && "recipient_id" in lastResult) { + donations.push(lastResult as DirectDonation); + } + } catch { + // Ignore + } + } + + return { donations, txHash }; +}; + export const storage_deposit = (depositAmountYocto: IndivisibleUnits) => donationContractApi.call<{}, IndivisibleUnits>("storage_deposit", { deposit: depositAmountYocto, diff --git a/src/common/contracts/core/donation/index.ts b/src/common/contracts/core/donation/index.ts index 1c14b0c3..b32380be 100644 --- a/src/common/contracts/core/donation/index.ts +++ b/src/common/contracts/core/donation/index.ts @@ -3,5 +3,6 @@ import * as donationContractHooks from "./hooks"; export type * from "./hooks"; export * from "./interfaces"; +export type { DirectDonateResult, DirectBatchDonateResult } from "./client"; export { donationContractClient, donationContractHooks }; diff --git a/src/entities/list/components/ListDetails.tsx b/src/entities/list/components/ListDetails.tsx index dbe08bf4..89783a06 100644 --- a/src/entities/list/components/ListDetails.tsx +++ b/src/entities/list/components/ListDetails.tsx @@ -8,7 +8,7 @@ import { FaHeart, FaRegHeart } from "react-icons/fa"; import { prop } from "remeda"; import { PLATFORM_NAME } from "@/common/_config"; -import { List } from "@/common/api/indexer"; +import { List, syncApi } from "@/common/api/indexer"; import { PLATFORM_TWITTER_ACCOUNT_ID } from "@/common/constants"; import { listsContractClient } from "@/common/contracts/core/lists"; import { truncate } from "@/common/lib"; @@ -77,9 +77,11 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet } = useListForm(); const applyToListModal = (note: string) => { + const onChainListId = parseInt(listDetails?.on_chain_id as any); + listsContractClient .register_batch({ - list_id: parseInt(listDetails?.on_chain_id as any) as any, + list_id: onChainListId as any, notes: note, registrations: [ { @@ -95,7 +97,12 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet }, ], }) - .then((data) => { + .then(async (data) => { + // Sync registration to indexer + if (viewer.accountId) { + await syncApi.listRegistration(onChainListId, viewer.accountId).catch(() => {}); + } + setIsApplicationSuccessful(true); }) .catch((error) => console.error("Error applying to list:", error)); diff --git a/src/entities/list/components/ListFormDetails.tsx b/src/entities/list/components/ListFormDetails.tsx index 873e3bb8..4b6330a8 100644 --- a/src/entities/list/components/ListFormDetails.tsx +++ b/src/entities/list/components/ListFormDetails.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/router"; import { SubmitHandler, useForm } from "react-hook-form"; import { prop } from "remeda"; +import { syncApi } from "@/common/api/indexer"; import { IPFS_NEAR_SOCIAL_URL } from "@/common/constants"; import { RegistrationStatus, @@ -159,7 +160,10 @@ export const ListFormDetails: React.FC = ({ listId, isDuplicate = list_id: listId, image_cover_url: coverImage || undefined, }) - .then((updatedData) => { + .then(async (updatedData) => { + // Sync list to indexer after update + await syncApi.list(listId).catch(() => {}); + setListCreateSuccess({ open: true, type: "UPDATE_LIST", @@ -184,16 +188,22 @@ export const ListFormDetails: React.FC = ({ listId, isDuplicate = })), image_cover_url: coverImage, }) - .then((dataToReturn) => { + .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, }); }) - .catch((error) => { - console.error("Error creating list:", error); - }); + .catch(() => {}); } }; diff --git a/src/entities/list/hooks/useListForm.ts b/src/entities/list/hooks/useListForm.ts index 3177ccf7..45529401 100644 --- a/src/entities/list/hooks/useListForm.ts +++ b/src/entities/list/hooks/useListForm.ts @@ -5,6 +5,7 @@ import { useRouter } from "next/router"; import { prop } from "remeda"; import { LISTS_CONTRACT_ACCOUNT_ID } from "@/common/_config"; +import { syncApi } from "@/common/api/indexer"; import { contractApi } from "@/common/blockchains/near-protocol/client"; import { listsContractClient } from "@/common/contracts/core/lists"; import { floatToYoctoNear } from "@/common/lib"; @@ -51,9 +52,11 @@ export const useListForm = () => { }; 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, registrations: registrants.map((data: string) => ({ registrant_id: data, status: "Approved", @@ -62,7 +65,10 @@ export const useListForm = () => { notes: "", })), }) - .then(() => { + .then(async () => { + // Sync registrations to indexer + await syncApi.listRegistrations(listId).catch(() => {}); + setFinishModal({ open: true, type: ListFormModalType.BATCH_REGISTER }); }) .catch((error) => console.error(error)); @@ -76,6 +82,7 @@ export const useListForm = () => { const handleUnRegisterAccount = (registrants: AccountGroupItem[]) => { if (!id) return; + const listId = Number(id); const allTransactions: any = []; registrants.map((registrant: AccountGroupItem) => { @@ -83,7 +90,7 @@ export const useListForm = () => { buildTransaction("unregister", { receiverId: LISTS_CONTRACT_ACCOUNT_ID, args: { - list_id: Number(id), + list_id: listId, registration_id: Number(registrant.registrationId), }, deposit: floatToYoctoNear(0.015), @@ -96,7 +103,10 @@ export const useListForm = () => { contractId: LISTS_CONTRACT_ACCOUNT_ID, }) .callMultiple(allTransactions) - .then((_res) => { + .then(async (_res) => { + // Sync registrations to indexer after unregister + await syncApi.listRegistrations(listId).catch(() => {}); + dispatch.listEditor.updateListModalState({ header: "Account(s) Deleted From List Successfully", description, @@ -108,13 +118,17 @@ export const useListForm = () => { const handleRemoveAdmin = (accounts: AccountGroupItem[]) => { const accountIds = accounts.map(prop("accountId")); + const listId = Number(id); listsContractClient .remove_admins_from_list({ - list_id: Number(id), + list_id: listId, admins: accountIds, }) - .then(() => { + .then(async () => { + // Sync list to indexer after admin removal + await syncApi.list(listId).catch(() => {}); + setFinishModal({ open: true, type: ListFormModalType.REMOVE_ADMINS }); }) .catch((error) => { @@ -130,13 +144,17 @@ export const useListForm = () => { const handleSaveAdminsSettings = (admins: AccountId[]) => { if (!id) return; + const listId = Number(id); listsContractClient .add_admins_to_list({ - list_id: Number(id), + list_id: listId, admins, }) - .then(() => { + .then(async () => { + // Sync list to indexer after admin addition + await syncApi.list(listId).catch(() => {}); + setFinishModal({ open: true, type: ListFormModalType.ADD_ADMINS }); }) .catch((error) => { @@ -161,14 +179,18 @@ export const useListForm = () => { const handleTransferOwner = () => { if (transferAccountError && !transferAccountField) return; if (!id) return; // Ensure id is available + const listId = parseInt(id as string); listsContractClient .transfer_list_ownership({ - list_id: parseInt(id as string), + list_id: listId, new_owner_id: transferAccountField, }) - .then((data) => { + .then(async (data) => { if (data) { + // Sync list to indexer after ownership transfer + await syncApi.list(listId).catch(() => {}); + setFinishModal({ open: true, type: ListFormModalType.TRANSFER_OWNER, diff --git a/src/entities/list/models/effects.ts b/src/entities/list/models/effects.ts index 2b3aad98..8be192fa 100644 --- a/src/entities/list/models/effects.ts +++ b/src/entities/list/models/effects.ts @@ -1,6 +1,6 @@ import { ExecutionStatusBasic } from "near-api-js/lib/providers/provider"; -import { List } from "@/common/api/indexer"; +import { syncApi } from "@/common/api/indexer"; import { nearRpc, walletApi } from "@/common/blockchains/near-protocol/client"; import { AppDispatcher } from "@/store"; @@ -11,7 +11,7 @@ export const effects = (dispatch: AppDispatcher) => ({ const { accountId: owner_account_id } = walletApi; if (owner_account_id) { - nearRpc.txStatus(transactionHash, owner_account_id).then((response) => { + nearRpc.txStatus(transactionHash, owner_account_id).then(async (response) => { const method = response.transaction?.actions[0]?.FunctionCall?.method_name; const { status } = response.receipts_outcome.at(method === "donate" ? 6 : 0)?.outcome ?? {}; let type: ListFormModalType = ListFormModalType.NONE; @@ -52,6 +52,16 @@ export const effects = (dispatch: AppDispatcher) => ({ break; } + case "register_batch": { + type = ListFormModalType.BATCH_REGISTER; + break; + } + + case "unregister": { + type = ListFormModalType.UNREGISTER; + break; + } + default: { type = ListFormModalType.NONE; break; @@ -70,19 +80,47 @@ export const effects = (dispatch: AppDispatcher) => ({ } } else if (typeof status?.SuccessValue === "string") { try { + const rawData = + type === ListFormModalType.DELETE_LIST + ? undefined + : JSON.parse(atob(status.SuccessValue)); + + // Handle both array and object responses + const parsedData = Array.isArray(rawData) ? rawData[0] : rawData; + + // Sync list to indexer after successful transaction + if (parsedData?.id && type !== ListFormModalType.DELETE_LIST) { + await syncApi.list(parsedData.id).catch(() => {}); + } + + // Sync registrations for registration-related operations + if ( + type === ListFormModalType.BATCH_REGISTER || + type === ListFormModalType.UNREGISTER + ) { + const args = response.transaction?.actions[0]?.FunctionCall?.args; + + if (args) { + try { + const decodedArgs = JSON.parse(atob(args)); + + if (decodedArgs?.list_id) { + await syncApi.listRegistrations(decodedArgs.list_id).catch(() => {}); + } + } catch { + // Ignore parse errors + } + } + } + dispatch.listEditor.deploymentSuccess({ - data: - type === ListFormModalType.DELETE_LIST - ? undefined - : (JSON.parse(atob(status.SuccessValue)) as List), + data: parsedData, type, ...(type === ListFormModalType.TRANSFER_OWNER && { accountId: JSON.parse(atob(status.SuccessValue)) as string, }), }); - } catch (error) { - console.error("Error parsing JSON:", error); - // Handle the error appropriately, e.g., dispatch an error action or show a notification + } catch { throw "Unable to Update List: Invalid JSON input"; } } else { diff --git a/src/features/donation/hooks/redirects.ts b/src/features/donation/hooks/redirects.ts index 67058de8..34526f48 100644 --- a/src/features/donation/hooks/redirects.ts +++ b/src/features/donation/hooks/redirects.ts @@ -28,7 +28,7 @@ export const useDonationSuccessWalletRedirect = () => { : undefined); const isTransactionOutcomeDetected = - transactionHash && Boolean(recipientAccountId ?? potAccountId); + transactionHash && Boolean(recipientAccountId ?? potAccountId ?? listId); useEffect(() => { if (isTransactionOutcomeDetected && !donationModal.visible) { diff --git a/src/features/donation/models/effects/group-list-donation.ts b/src/features/donation/models/effects/group-list-donation.ts index f9d7fb73..c663bf4d 100644 --- a/src/features/donation/models/effects/group-list-donation.ts +++ b/src/features/donation/models/effects/group-list-donation.ts @@ -1,5 +1,7 @@ -import type { InformativeSuccessfulExecutionOutcome } from "@/common/blockchains/near-protocol"; +import { syncApi } from "@/common/api/indexer"; +import { walletApi } from "@/common/blockchains/near-protocol/client"; import { + type DirectBatchDonateResult, type DirectBatchDonationItem, type DirectDonation, donationContractClient, @@ -14,7 +16,7 @@ type GroupListDonationMulticallInputs = Pick< "groupAllocationStrategy" | "groupAllocationPlan" | "referrerAccountId" | "bypassProtocolFee" > & {}; -export const groupListDonationMulticall = ({ +export const groupListDonationMulticall = async ({ groupAllocationStrategy, groupAllocationPlan = [], referrerAccountId, @@ -23,47 +25,37 @@ export const groupListDonationMulticall = ({ const isDistributionManual = groupAllocationStrategy === DonationGroupAllocationStrategyEnum.manual; - return donationContractClient - .donateBatch( - groupAllocationPlan.reduce( - (txs, { account_id, amount: donationAmount = 0 }) => - isDistributionManual && donationAmount === 0 - ? txs - : txs.concat([ - { - args: { - recipient_id: account_id, - referrer_id: referrerAccountId, - bypass_protocol_fee: bypassProtocolFee, - }, - - amountYoctoNear: floatToYoctoNear(donationAmount), - }, - ]), - - [] as DirectBatchDonationItem[], - ), - ) - .then((finalExecutionOutcomes) => { - const receipts: DirectDonation[] = - finalExecutionOutcomes?.reduce( - (acc, { status }) => { - const decodedReceipt = atob( - (status as InformativeSuccessfulExecutionOutcome["status"]).SuccessValue, - ); - - try { - return [...acc, JSON.parse(decodedReceipt) as DirectDonation]; - } catch { - return acc; - } - }, - - [] as DirectDonation[], - ) ?? []; - - if (receipts.length > 0) { - return receipts; - } else throw new Error("Unable to determine transaction execution status."); - }); + const txInputs = groupAllocationPlan.reduce( + (txs, { account_id, amount: donationAmount = 0 }) => + isDistributionManual && donationAmount === 0 + ? txs + : txs.concat([ + { + args: { + recipient_id: account_id, + referrer_id: referrerAccountId, + bypass_protocol_fee: bypassProtocolFee, + }, + amountYoctoNear: floatToYoctoNear(donationAmount), + }, + ]), + [] as DirectBatchDonationItem[], + ); + + const result: DirectBatchDonateResult = await donationContractClient.donateBatch(txInputs); + + // Sync donations to indexer + if (result.txHash && result.donations.length > 0) { + const senderId = walletApi.accountId; + + if (senderId) { + await syncApi.directDonation(result.txHash, senderId).catch(() => {}); + } + } + + if (result.donations.length > 0) { + return result.donations; + } else { + throw new Error("Unable to determine transaction execution status."); + } }; diff --git a/src/features/donation/models/effects/index.ts b/src/features/donation/models/effects/index.ts index 6a22b72a..b59fd6ca 100644 --- a/src/features/donation/models/effects/index.ts +++ b/src/features/donation/models/effects/index.ts @@ -96,7 +96,16 @@ export const effects = (dispatch: AppDispatcher) => ({ floatToYoctoNear(amount), ) - .then(dispatch.donation.success) + .then(async (result) => { + // Sync direct donation to indexer for popup wallets + if (result.txHash && result.donation) { + await syncApi + .directDonation(result.txHash, result.donation.donor_id) + .catch(() => {}); + } + + dispatch.donation.success(result.donation); + }) .catch((error) => { onError(error); dispatch.donation.failure(error); @@ -215,16 +224,44 @@ export const effects = (dispatch: AppDispatcher) => ({ }, handleOutcome: async (transactionHash: string): Promise => { - // TODO: Use nearRps.txStatus for each tx hash & handle batch tx outcome - const { accountId: sender_account_id } = walletApi; if (sender_account_id) { const { data } = await getTransactionStatus({ tx_hash: transactionHash, sender_account_id }); + const receiptsOutcome = data?.result?.receipts_outcome || []; + + // Parse all direct donations from receipts (handles both single and batch donations) + const donations: DirectDonation[] = []; + + for (const receipt of receiptsOutcome) { + const successValue = receipt?.outcome?.status?.SuccessValue; + + if (successValue) { + try { + const parsed = JSON.parse(atob(successValue)); + + // Check if it's a direct donation (has recipient_id, no campaign_id) + if (parsed && "recipient_id" in parsed && !("campaign_id" in parsed)) { + donations.push(parsed as DirectDonation); + } + } catch { + // Not valid JSON, skip + } + } + } + + // Sync all direct donations to indexer + if (donations.length > 0) { + await syncApi.directDonation(transactionHash, sender_account_id).catch(() => {}); + } - const donationData = JSON.parse( - atob(data?.result?.receipts_outcome[3].outcome.status.SuccessValue), - ) as DirectDonation | CampaignDonation | PotDonation; + // 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")); dispatch.donation.success(donationData); } else { diff --git a/src/features/profile-configuration/models/effects.ts b/src/features/profile-configuration/models/effects.ts index 79cd5658..d4f6bf2a 100644 --- a/src/features/profile-configuration/models/effects.ts +++ b/src/features/profile-configuration/models/effects.ts @@ -10,6 +10,7 @@ import { SOCIAL_DB_CONTRACT_ACCOUNT_ID, SOCIAL_PLATFORM_NAME, } from "@/common/_config"; +import { syncApi } from "@/common/api/indexer"; import { nearProtocolClient } from "@/common/blockchains/near-protocol"; import { FIFTY_TGAS, @@ -102,7 +103,12 @@ export const save = async ({ return nearProtocolClient .contractApi() .callMultiple(directTransactions, callbackUrl) - .then(() => ({ success: true, error: null })) + .then(async () => { + // Sync account to indexer after profile save + await syncApi.account(accountId).catch(() => {}); + + return { success: true, error: null }; + }) .catch((err) => { console.error(err);