From a8bca6c91c1387c2c19ea889fa7953c997f8fdaf Mon Sep 17 00:00:00 2001 From: aunali8812 Date: Sun, 22 Feb 2026 23:44:19 +0500 Subject: [PATCH] Added lists deletion, upvotes and remove upvotes sync apis --- src/common/api/indexer/sync.ts | 99 ++++++++++++++++++++ src/common/contracts/core/lists/client.ts | 80 ++++++++++++---- src/entities/list/components/ListCard.tsx | 23 ++++- src/entities/list/components/ListDetails.tsx | 18 +++- src/entities/list/hooks/useListForm.ts | 9 +- 5 files changed, 204 insertions(+), 25 deletions(-) diff --git a/src/common/api/indexer/sync.ts b/src/common/api/indexer/sync.ts index 29961525..820f0fbb 100644 --- a/src/common/api/indexer/sync.ts +++ b/src/common/api/indexer/sync.ts @@ -174,6 +174,105 @@ export const syncApi = { } }, + /** + * Sync a list deletion after the owner deletes it on-chain + * @param listId - The on-chain list ID + * @param txHash - Transaction hash from the delete transaction + * @param senderId - Account ID of the list owner who deleted it + */ + async listDelete( + listId: number | string, + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/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 list 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 list deletion:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync a list upvote after the user upvotes on-chain + * @param listId - The on-chain list ID + * @param txHash - Transaction hash from the upvote transaction + * @param senderId - Account ID of the user who upvoted + */ + async listUpvote( + listId: number | string, + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/upvote/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 list upvote:", 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 upvote:", error); + return { success: false, message: String(error) }; + } + }, + + /** + * Sync a list remove-upvote after the user removes their upvote on-chain + * @param listId - The on-chain list ID + * @param txHash - Transaction hash from the remove-upvote transaction + * @param senderId - Account ID of the user who removed their upvote + */ + async listRemoveUpvote( + listId: number | string, + txHash: string, + senderId: string, + ): Promise<{ success: boolean; message?: string }> { + try { + const response = await fetch( + `${SYNC_API_BASE_URL}/api/v1/lists/${listId}/remove-upvote/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 list remove-upvote:", 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 remove-upvote:", 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/lists/client.ts b/src/common/contracts/core/lists/client.ts index e6856e8d..2d7dfd4a 100644 --- a/src/common/contracts/core/lists/client.ts +++ b/src/common/contracts/core/lists/client.ts @@ -1,6 +1,6 @@ import { LISTS_CONTRACT_ACCOUNT_ID } from "@/common/_config"; import { contractApi as createContractApi } from "@/common/blockchains/near-protocol/client"; -import { PUBLIC_GOODS_REGISTRY_LIST_ID } from "@/common/constants"; +import { FULL_TGAS, PUBLIC_GOODS_REGISTRY_LIST_ID } from "@/common/constants"; import { floatToYoctoNear } from "@/common/lib"; import { AccountId } from "@/common/types"; @@ -17,6 +17,60 @@ const contractApi = createContractApi({ contractId: LISTS_CONTRACT_ACCOUNT_ID, }); +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; + + if (!signerId) { + throw new Error("Wallet is not signed in."); + } + + const { actionCreators } = await import("@near-js/transactions"); + + const action = actionCreators.functionCall( + method, + args, + BigInt(FULL_TGAS), + BigInt(deposit ?? "0"), + ); + + let outcome: any; + const walletAny = wallet as any; + + if ("signAndSendTransaction" in walletAny) { + outcome = await walletAny.signAndSendTransaction({ + signerId, + receiverId: LISTS_CONTRACT_ACCOUNT_ID, + actions: [action], + }); + } else if ("signAndSendTransactions" in walletAny) { + const results = await walletAny.signAndSendTransactions({ + transactions: [ + { + receiverId: LISTS_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 get_lists = () => contractApi.view<{}, List[]>("get_lists"); export const create_list = ({ @@ -107,19 +161,11 @@ export const update_registered_project = (args: UpdateRegistration) => args, }); -export const delete_list = (args: { list_id: number }) => - contractApi.call("delete_list", { - args, - deposit: floatToYoctoNear(0.01), - gas: "300000000000000", - }); +export const delete_list = (args: { list_id: number }): Promise => + callWithTxHash("delete_list", args, floatToYoctoNear(0.01)); -export const upvote = (args: { list_id: number }) => - contractApi.call("upvote", { - args, - deposit: floatToYoctoNear(0.01), - gas: "300000000000000", - }); +export const upvote = (args: { list_id: number }): Promise => + callWithTxHash("upvote", args, floatToYoctoNear(0.01)); export const add_admins_to_list = (args: { list_id: number; admins: Array }) => contractApi.call("owner_add_admins", { @@ -142,12 +188,8 @@ export const transfer_list_ownership = (args: { list_id: number; new_owner_id: s gas: "300000000000000", }); -export const remove_upvote = (args: { list_id: number }) => - contractApi.call("remove_upvote", { - args, - deposit: floatToYoctoNear(0.01), - gas: "300000000000000", - }); +export const remove_upvote = (args: { list_id: number }): Promise => + callWithTxHash("remove_upvote", args, floatToYoctoNear(0.01)); export const get_list_for_owner = (args: { owner_id: string }) => contractApi.view("get_lists_for_owner", { args }); diff --git a/src/entities/list/components/ListCard.tsx b/src/entities/list/components/ListCard.tsx index 31537edf..c0e110b6 100644 --- a/src/entities/list/components/ListCard.tsx +++ b/src/entities/list/components/ListCard.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import { useRouter } from "next/router"; import { FaHeart } from "react-icons/fa"; +import { syncApi } from "@/common/api/indexer"; import { listsContractClient } from "@/common/contracts/core/lists"; import { truncate } from "@/common/lib"; import { LazyImage } from "@/common/ui/layout/components/LazyImage"; @@ -42,14 +43,32 @@ export const ListCard = ({ e.stopPropagation(); if (isUpvoted) { - listsContractClient.remove_upvote({ list_id: dataForList?.on_chain_id }); + listsContractClient + .remove_upvote({ list_id: dataForList?.on_chain_id }) + .then(async ({ txHash }) => { + if (txHash && viewer.accountId) { + await syncApi + .listRemoveUpvote(dataForList?.on_chain_id, txHash, viewer.accountId) + .catch(() => {}); + } + }) + .catch((error) => console.error("Error removing upvote:", error)); dispatch.listEditor.handleListToast({ name: truncate(dataForList?.name ?? "", 15), type: ListFormModalType.DOWNVOTE, }); } else { - listsContractClient.upvote({ list_id: dataForList?.on_chain_id }); + listsContractClient + .upvote({ list_id: dataForList?.on_chain_id }) + .then(async ({ txHash }) => { + if (txHash && viewer.accountId) { + await syncApi + .listUpvote(dataForList?.on_chain_id, txHash, viewer.accountId) + .catch(() => {}); + } + }) + .catch((error) => console.error("Error upvoting:", error)); dispatch.listEditor.handleListToast({ name: truncate(dataForList?.name ?? "", 15), diff --git a/src/entities/list/components/ListDetails.tsx b/src/entities/list/components/ListDetails.tsx index 89783a06..7cf59858 100644 --- a/src/entities/list/components/ListDetails.tsx +++ b/src/entities/list/components/ListDetails.tsx @@ -132,10 +132,17 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet admins.includes(viewer.accountId ?? "") || listDetails.owner?.id === viewer.accountId; const handleUpvote = () => { + const onChainId = Number(listDetails.on_chain_id); + if (isUpvoted) { listsContractClient - .remove_upvote({ list_id: Number(listDetails.on_chain_id) }) - .catch((error) => console.error("Error upvoting:", error)); + .remove_upvote({ list_id: onChainId }) + .then(async ({ txHash }) => { + if (txHash && viewer.accountId) { + await syncApi.listRemoveUpvote(onChainId, txHash, viewer.accountId).catch(() => {}); + } + }) + .catch((error) => console.error("Error removing upvote:", error)); dispatch.listEditor.handleListToast({ name: truncate(listDetails?.name ?? "", 15), @@ -143,7 +150,12 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet }); } else { listsContractClient - .upvote({ list_id: Number(listDetails.on_chain_id) }) + .upvote({ list_id: onChainId }) + .then(async ({ txHash }) => { + if (txHash && viewer.accountId) { + await syncApi.listUpvote(onChainId, txHash, viewer.accountId).catch(() => {}); + } + }) .catch((error) => console.error("Error upvoting:", error)); dispatch.listEditor.handleListToast({ diff --git a/src/entities/list/hooks/useListForm.ts b/src/entities/list/hooks/useListForm.ts index 45529401..1ffe01e5 100644 --- a/src/entities/list/hooks/useListForm.ts +++ b/src/entities/list/hooks/useListForm.ts @@ -10,6 +10,7 @@ import { contractApi } from "@/common/blockchains/near-protocol/client"; import { listsContractClient } from "@/common/contracts/core/lists"; import { floatToYoctoNear } from "@/common/lib"; import { AccountId } from "@/common/types"; +import { useWalletUserSession } from "@/common/wallet"; import { AccountGroupItem, validateAccountId } from "@/entities/_shared/account"; import { useDispatch } from "@/store/hooks"; @@ -18,6 +19,7 @@ import { ListFormModalType } from "../types"; export const useListForm = () => { const { push, query } = useRouter(); const dispatch = useDispatch(); + const viewer = useWalletUserSession(); const [transferAccountField, setTransferAccountField] = useState(""); const [transferAccountError, setTransferAccountError] = useState(""); @@ -37,7 +39,12 @@ export const useListForm = () => { listsContractClient .delete_list({ list_id: id }) - .then(() => { + .then(async ({ txHash }) => { + // Sync deletion to indexer + if (txHash && viewer.accountId) { + await syncApi.listDelete(id, txHash, viewer.accountId).catch(() => {}); + } + push("/lists"); }) .catch((error) => {