Skip to content

Commit 1043a07

Browse files
authored
Merge pull request #597 from PotLock/staging
Staging
2 parents afbe26a + 2f7f8ab commit 1043a07

File tree

5 files changed

+204
-25
lines changed

5 files changed

+204
-25
lines changed

src/common/api/indexer/sync.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,105 @@ export const syncApi = {
174174
}
175175
},
176176

177+
/**
178+
* Sync a list deletion after the owner deletes it on-chain
179+
* @param listId - The on-chain list ID
180+
* @param txHash - Transaction hash from the delete transaction
181+
* @param senderId - Account ID of the list owner who deleted it
182+
*/
183+
async listDelete(
184+
listId: number | string,
185+
txHash: string,
186+
senderId: string,
187+
): Promise<{ success: boolean; message?: string }> {
188+
try {
189+
const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/delete/sync`, {
190+
method: "POST",
191+
headers: { "Content-Type": "application/json" },
192+
body: JSON.stringify({ tx_hash: txHash, sender_id: senderId }),
193+
});
194+
195+
if (!response.ok) {
196+
const error = await response.json().catch(() => ({}));
197+
console.warn("Failed to sync list deletion:", error);
198+
return { success: false, message: error?.error || "Sync failed" };
199+
}
200+
201+
const result = await response.json();
202+
return { success: true, message: result.message };
203+
} catch (error) {
204+
console.warn("Failed to sync list deletion:", error);
205+
return { success: false, message: String(error) };
206+
}
207+
},
208+
209+
/**
210+
* Sync a list upvote after the user upvotes on-chain
211+
* @param listId - The on-chain list ID
212+
* @param txHash - Transaction hash from the upvote transaction
213+
* @param senderId - Account ID of the user who upvoted
214+
*/
215+
async listUpvote(
216+
listId: number | string,
217+
txHash: string,
218+
senderId: string,
219+
): Promise<{ success: boolean; message?: string }> {
220+
try {
221+
const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/upvote/sync`, {
222+
method: "POST",
223+
headers: { "Content-Type": "application/json" },
224+
body: JSON.stringify({ tx_hash: txHash, sender_id: senderId }),
225+
});
226+
227+
if (!response.ok) {
228+
const error = await response.json().catch(() => ({}));
229+
console.warn("Failed to sync list upvote:", error);
230+
return { success: false, message: error?.error || "Sync failed" };
231+
}
232+
233+
const result = await response.json();
234+
return { success: true, message: result.message };
235+
} catch (error) {
236+
console.warn("Failed to sync list upvote:", error);
237+
return { success: false, message: String(error) };
238+
}
239+
},
240+
241+
/**
242+
* Sync a list remove-upvote after the user removes their upvote on-chain
243+
* @param listId - The on-chain list ID
244+
* @param txHash - Transaction hash from the remove-upvote transaction
245+
* @param senderId - Account ID of the user who removed their upvote
246+
*/
247+
async listRemoveUpvote(
248+
listId: number | string,
249+
txHash: string,
250+
senderId: string,
251+
): Promise<{ success: boolean; message?: string }> {
252+
try {
253+
const response = await fetch(
254+
`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/remove-upvote/sync`,
255+
{
256+
method: "POST",
257+
headers: { "Content-Type": "application/json" },
258+
body: JSON.stringify({ tx_hash: txHash, sender_id: senderId }),
259+
},
260+
);
261+
262+
if (!response.ok) {
263+
const error = await response.json().catch(() => ({}));
264+
console.warn("Failed to sync list remove-upvote:", error);
265+
return { success: false, message: error?.error || "Sync failed" };
266+
}
267+
268+
const result = await response.json();
269+
return { success: true, message: result.message };
270+
} catch (error) {
271+
console.warn("Failed to sync list remove-upvote:", error);
272+
return { success: false, message: String(error) };
273+
}
274+
},
275+
177276
/**
178277
* Sync an account profile and recalculate donation stats
179278
* @param accountId - The NEAR account ID

src/common/contracts/core/lists/client.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LISTS_CONTRACT_ACCOUNT_ID } from "@/common/_config";
22
import { contractApi as createContractApi } from "@/common/blockchains/near-protocol/client";
3-
import { PUBLIC_GOODS_REGISTRY_LIST_ID } from "@/common/constants";
3+
import { FULL_TGAS, PUBLIC_GOODS_REGISTRY_LIST_ID } from "@/common/constants";
44
import { floatToYoctoNear } from "@/common/lib";
55
import { AccountId } from "@/common/types";
66

@@ -17,6 +17,60 @@ const contractApi = createContractApi({
1717
contractId: LISTS_CONTRACT_ACCOUNT_ID,
1818
});
1919

20+
export type TxHashResult = {
21+
txHash: string | null;
22+
};
23+
24+
const callWithTxHash = async (
25+
method: string,
26+
args: Record<string, unknown>,
27+
deposit?: string,
28+
): Promise<TxHashResult> => {
29+
const { walletApi } = await import("@/common/blockchains/near-protocol/client");
30+
const wallet = await walletApi.ensureWallet();
31+
const signerId = walletApi.accountId;
32+
33+
if (!signerId) {
34+
throw new Error("Wallet is not signed in.");
35+
}
36+
37+
const { actionCreators } = await import("@near-js/transactions");
38+
39+
const action = actionCreators.functionCall(
40+
method,
41+
args,
42+
BigInt(FULL_TGAS),
43+
BigInt(deposit ?? "0"),
44+
);
45+
46+
let outcome: any;
47+
const walletAny = wallet as any;
48+
49+
if ("signAndSendTransaction" in walletAny) {
50+
outcome = await walletAny.signAndSendTransaction({
51+
signerId,
52+
receiverId: LISTS_CONTRACT_ACCOUNT_ID,
53+
actions: [action],
54+
});
55+
} else if ("signAndSendTransactions" in walletAny) {
56+
const results = await walletAny.signAndSendTransactions({
57+
transactions: [
58+
{
59+
receiverId: LISTS_CONTRACT_ACCOUNT_ID,
60+
actions: [action],
61+
},
62+
],
63+
});
64+
65+
outcome = Array.isArray(results) ? results[0] : results;
66+
} else {
67+
throw new Error("Wallet does not support transaction signing");
68+
}
69+
70+
const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null;
71+
return { txHash };
72+
};
73+
2074
export const get_lists = () => contractApi.view<{}, List[]>("get_lists");
2175

2276
export const create_list = ({
@@ -107,19 +161,11 @@ export const update_registered_project = (args: UpdateRegistration) =>
107161
args,
108162
});
109163

110-
export const delete_list = (args: { list_id: number }) =>
111-
contractApi.call<typeof args, List>("delete_list", {
112-
args,
113-
deposit: floatToYoctoNear(0.01),
114-
gas: "300000000000000",
115-
});
164+
export const delete_list = (args: { list_id: number }): Promise<TxHashResult> =>
165+
callWithTxHash("delete_list", args, floatToYoctoNear(0.01));
116166

117-
export const upvote = (args: { list_id: number }) =>
118-
contractApi.call<typeof args, List>("upvote", {
119-
args,
120-
deposit: floatToYoctoNear(0.01),
121-
gas: "300000000000000",
122-
});
167+
export const upvote = (args: { list_id: number }): Promise<TxHashResult> =>
168+
callWithTxHash("upvote", args, floatToYoctoNear(0.01));
123169

124170
export const add_admins_to_list = (args: { list_id: number; admins: Array<string> }) =>
125171
contractApi.call<typeof args, List>("owner_add_admins", {
@@ -142,12 +188,8 @@ export const transfer_list_ownership = (args: { list_id: number; new_owner_id: s
142188
gas: "300000000000000",
143189
});
144190

145-
export const remove_upvote = (args: { list_id: number }) =>
146-
contractApi.call<typeof args, List>("remove_upvote", {
147-
args,
148-
deposit: floatToYoctoNear(0.01),
149-
gas: "300000000000000",
150-
});
191+
export const remove_upvote = (args: { list_id: number }): Promise<TxHashResult> =>
192+
callWithTxHash("remove_upvote", args, floatToYoctoNear(0.01));
151193

152194
export const get_list_for_owner = (args: { owner_id: string }) =>
153195
contractApi.view<typeof args, List>("get_lists_for_owner", { args });

src/entities/list/components/ListCard.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Image from "next/image";
44
import { useRouter } from "next/router";
55
import { FaHeart } from "react-icons/fa";
66

7+
import { syncApi } from "@/common/api/indexer";
78
import { listsContractClient } from "@/common/contracts/core/lists";
89
import { truncate } from "@/common/lib";
910
import { LazyImage } from "@/common/ui/layout/components/LazyImage";
@@ -42,14 +43,32 @@ export const ListCard = ({
4243
e.stopPropagation();
4344

4445
if (isUpvoted) {
45-
listsContractClient.remove_upvote({ list_id: dataForList?.on_chain_id });
46+
listsContractClient
47+
.remove_upvote({ list_id: dataForList?.on_chain_id })
48+
.then(async ({ txHash }) => {
49+
if (txHash && viewer.accountId) {
50+
await syncApi
51+
.listRemoveUpvote(dataForList?.on_chain_id, txHash, viewer.accountId)
52+
.catch(() => {});
53+
}
54+
})
55+
.catch((error) => console.error("Error removing upvote:", error));
4656

4757
dispatch.listEditor.handleListToast({
4858
name: truncate(dataForList?.name ?? "", 15),
4959
type: ListFormModalType.DOWNVOTE,
5060
});
5161
} else {
52-
listsContractClient.upvote({ list_id: dataForList?.on_chain_id });
62+
listsContractClient
63+
.upvote({ list_id: dataForList?.on_chain_id })
64+
.then(async ({ txHash }) => {
65+
if (txHash && viewer.accountId) {
66+
await syncApi
67+
.listUpvote(dataForList?.on_chain_id, txHash, viewer.accountId)
68+
.catch(() => {});
69+
}
70+
})
71+
.catch((error) => console.error("Error upvoting:", error));
5372

5473
dispatch.listEditor.handleListToast({
5574
name: truncate(dataForList?.name ?? "", 15),

src/entities/list/components/ListDetails.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,30 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet
132132
admins.includes(viewer.accountId ?? "") || listDetails.owner?.id === viewer.accountId;
133133

134134
const handleUpvote = () => {
135+
const onChainId = Number(listDetails.on_chain_id);
136+
135137
if (isUpvoted) {
136138
listsContractClient
137-
.remove_upvote({ list_id: Number(listDetails.on_chain_id) })
138-
.catch((error) => console.error("Error upvoting:", error));
139+
.remove_upvote({ list_id: onChainId })
140+
.then(async ({ txHash }) => {
141+
if (txHash && viewer.accountId) {
142+
await syncApi.listRemoveUpvote(onChainId, txHash, viewer.accountId).catch(() => {});
143+
}
144+
})
145+
.catch((error) => console.error("Error removing upvote:", error));
139146

140147
dispatch.listEditor.handleListToast({
141148
name: truncate(listDetails?.name ?? "", 15),
142149
type: ListFormModalType.DOWNVOTE,
143150
});
144151
} else {
145152
listsContractClient
146-
.upvote({ list_id: Number(listDetails.on_chain_id) })
153+
.upvote({ list_id: onChainId })
154+
.then(async ({ txHash }) => {
155+
if (txHash && viewer.accountId) {
156+
await syncApi.listUpvote(onChainId, txHash, viewer.accountId).catch(() => {});
157+
}
158+
})
147159
.catch((error) => console.error("Error upvoting:", error));
148160

149161
dispatch.listEditor.handleListToast({

src/entities/list/hooks/useListForm.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { contractApi } from "@/common/blockchains/near-protocol/client";
1010
import { listsContractClient } from "@/common/contracts/core/lists";
1111
import { floatToYoctoNear } from "@/common/lib";
1212
import { AccountId } from "@/common/types";
13+
import { useWalletUserSession } from "@/common/wallet";
1314
import { AccountGroupItem, validateAccountId } from "@/entities/_shared/account";
1415
import { useDispatch } from "@/store/hooks";
1516

@@ -18,6 +19,7 @@ import { ListFormModalType } from "../types";
1819
export const useListForm = () => {
1920
const { push, query } = useRouter();
2021
const dispatch = useDispatch();
22+
const viewer = useWalletUserSession();
2123
const [transferAccountField, setTransferAccountField] = useState<string>("");
2224
const [transferAccountError, setTransferAccountError] = useState<string | undefined>("");
2325

@@ -37,7 +39,12 @@ export const useListForm = () => {
3739

3840
listsContractClient
3941
.delete_list({ list_id: id })
40-
.then(() => {
42+
.then(async ({ txHash }) => {
43+
// Sync deletion to indexer
44+
if (txHash && viewer.accountId) {
45+
await syncApi.listDelete(id, txHash, viewer.accountId).catch(() => {});
46+
}
47+
4148
push("/lists");
4249
})
4350
.catch((error) => {

0 commit comments

Comments
 (0)