Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/common/api/indexer/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 65 additions & 15 deletions src/common/contracts/core/campaigns/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
deposit?: string,
): Promise<TxHashResult> => {
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<TxHashResult> => callWithTxHash("process_escrowed_donations_batch", args);

export const process_refunds_batch = ({
args,
}: {
args: { campaign_id: CampaignId };
}): Promise<TxHashResult> => callWithTxHash("process_refunds_batch", args);

export type UpdateCampaignParams = { args: CampaignInputs & { campaign_id: CampaignId } };

Expand All @@ -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<DeleteCampaignParams["args"], void>("delete_campaign", {
args,
deposit: floatToYoctoNear(0.021),
gas: FULL_TGAS,
});
export const delete_campaign = ({ args }: DeleteCampaignParams): Promise<TxHashResult> =>
callWithTxHash("delete_campaign", args, floatToYoctoNear(0.021));

export type DonateResult = {
donation: CampaignDonation;
Expand Down
25 changes: 22 additions & 3 deletions src/entities/campaign/hooks/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
});
Expand All @@ -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",
});
Expand Down
Loading