diff --git a/src/common/api/indexer/sync.ts b/src/common/api/indexer/sync.ts index a9afdb1f..29961525 100644 --- a/src/common/api/indexer/sync.ts +++ b/src/common/api/indexer/sync.ts @@ -69,6 +69,111 @@ export const syncApi = { } }, + /** + * Sync a campaign deletion after the owner deletes it on-chain + * @param campaignId - The on-chain campaign ID + * @param txHash - Transaction hash from the delete transaction + * @param senderId - Account ID of the campaign owner who deleted it + */ + async campaignDelete( + campaignId: number | string, + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch( + `${CAMPAIGNS_SYNC_API_BASE_URL}/api/v1/campaigns/${campaignId}/delete/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 campaign deletion:", 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 campaign deletion:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync campaign donation refunds after process_refunds_batch is executed + * @param campaignId - The on-chain campaign ID + * @param txHash - Transaction hash from the refund transaction + * @param senderId - Account ID of the sender who triggered refunds + */ + async campaignRefund( + campaignId: number | string, + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch( + `${CAMPAIGNS_SYNC_API_BASE_URL}/api/v1/campaigns/${campaignId}/refunds/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 campaign refunds:", 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 campaign refunds:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync campaign donation unescrow after process_escrowed_donations_batch is executed + * @param campaignId - The on-chain campaign ID + * @param txHash - Transaction hash from the unescrow transaction + * @param senderId - Account ID of the sender who triggered unescrow + */ + async campaignUnescrow( + campaignId: number | string, + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch( + `${CAMPAIGNS_SYNC_API_BASE_URL}/api/v1/campaigns/${campaignId}/unescrow/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 campaign unescrow:", 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 campaign unescrow:", error); + return { success: false, message: String(error) }; + } + }, + /** * Sync an account profile and recalculate donation stats * @param accountId - The NEAR account ID diff --git a/src/common/contracts/core/campaigns/client.ts b/src/common/contracts/core/campaigns/client.ts index 1350f3f5..f9ab89e7 100644 --- a/src/common/contracts/core/campaigns/client.ts +++ b/src/common/contracts/core/campaigns/client.ts @@ -86,17 +86,71 @@ export const create_campaign = ({ args }: CreateCampaignParams) => { } }; -export const process_escrowed_donations_batch = ({ args }: { args: { campaign_id: CampaignId } }) => - contractApi.call("process_escrowed_donations_batch", { - args, - gas: FULL_TGAS, - }); +export type TxHashResult = { + txHash: string | null; +}; + +const callWithTxHash = async ( + method: string, + args: Record, + deposit?: string, +): Promise => { + const { walletApi } = await import("@/common/blockchains/near-protocol/client"); + const wallet = await walletApi.ensureWallet(); + const signerId = walletApi.accountId; -export const process_refunds_batch = ({ args }: { args: { campaign_id: CampaignId } }) => - contractApi.call("process_refunds_batch", { + if (!signerId) { + throw new Error("Wallet is not signed in."); + } + + const { actionCreators } = await import("@near-js/transactions"); + + const action = actionCreators.functionCall( + method, args, - gas: FULL_TGAS, - }); + BigInt(FULL_TGAS), + BigInt(deposit ?? "0"), + ); + + let outcome: any; + const walletAny = wallet as any; + + if ("signAndSendTransaction" in walletAny) { + outcome = await walletAny.signAndSendTransaction({ + signerId, + receiverId: CAMPAIGNS_CONTRACT_ACCOUNT_ID, + actions: [action], + }); + } else if ("signAndSendTransactions" in walletAny) { + const results = await walletAny.signAndSendTransactions({ + transactions: [ + { + receiverId: CAMPAIGNS_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; + return { txHash }; +}; + +export const process_escrowed_donations_batch = ({ + args, +}: { + args: { campaign_id: CampaignId }; +}): Promise => callWithTxHash("process_escrowed_donations_batch", args); + +export const process_refunds_batch = ({ + args, +}: { + args: { campaign_id: CampaignId }; +}): Promise => callWithTxHash("process_refunds_batch", args); export type UpdateCampaignParams = { args: CampaignInputs & { campaign_id: CampaignId } }; @@ -109,12 +163,8 @@ export const update_campaign = ({ args }: UpdateCampaignParams) => export type DeleteCampaignParams = { args: { campaign_id: CampaignId } }; -export const delete_campaign = ({ args }: DeleteCampaignParams) => - contractApi.call("delete_campaign", { - args, - deposit: floatToYoctoNear(0.021), - gas: FULL_TGAS, - }); +export const delete_campaign = ({ args }: DeleteCampaignParams): Promise => + callWithTxHash("delete_campaign", args, floatToYoctoNear(0.021)); export type DonateResult = { donation: CampaignDonation; diff --git a/src/entities/campaign/hooks/forms.ts b/src/entities/campaign/hooks/forms.ts index 76305a2f..a16ed401 100644 --- a/src/entities/campaign/hooks/forms.ts +++ b/src/entities/campaign/hooks/forms.ts @@ -181,7 +181,14 @@ export const useCampaignForm = ({ campaignId, ftId, onUpdateSuccess }: CampaignF const handleDeleteCampaign = async () => { if (!isNewCampaign) { try { - await campaignsContractClient.delete_campaign({ args: { campaign_id: campaignId } }); + const { txHash } = await campaignsContractClient.delete_campaign({ + args: { campaign_id: campaignId }, + }); + + // Sync deletion to indexer database + if (txHash && viewer.accountId) { + await syncApi.campaignDelete(campaignId, txHash, viewer.accountId).catch(console.warn); + } dispatch.campaignEditor.updateCampaignModalState({ header: "Campaign Deleted Successfully", @@ -207,7 +214,14 @@ export const useCampaignForm = ({ campaignId, ftId, onUpdateSuccess }: CampaignF .process_escrowed_donations_batch({ args: { campaign_id: campaignId }, }) - .then(() => { + .then(async ({ txHash }) => { + // Sync unescrow to indexer database + if (txHash && viewer.accountId) { + await syncApi + .campaignUnescrow(campaignId, txHash, viewer.accountId) + .catch(console.warn); + } + return toast({ title: "Successfully processed escrowed donations", }); @@ -229,7 +243,12 @@ export const useCampaignForm = ({ campaignId, ftId, onUpdateSuccess }: CampaignF .process_refunds_batch({ args: { campaign_id: campaignId }, }) - .then(() => { + .then(async ({ txHash }) => { + // Sync refunds to indexer database + if (txHash && viewer.accountId) { + await syncApi.campaignRefund(campaignId, txHash, viewer.accountId).catch(console.warn); + } + return toast({ title: "Successfully processed donation refunds", });