Skip to content

Commit ef7121d

Browse files
authored
Merge pull request #579 from PotLock/feature/sync-accounts-lists
Feature/sync accounts lists
2 parents a7ae79f + 64f4b3a commit ef7121d

File tree

12 files changed

+472
-98
lines changed

12 files changed

+472
-98
lines changed

src/common/api/indexer/hooks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { AxiosResponse } from "axios";
22

3-
import { envConfig } from "@/common/_config/production.env-config";
43
import { NOOP_STRING } from "@/common/constants";
54
import { isAccountId, isEthereumAddress } from "@/common/lib";
65
import {

src/common/api/indexer/sync.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export const syncApi = {
2323
}
2424

2525
const result = await response.json();
26-
console.log("Campaign synced:", result);
2726
return { success: true, message: result.message };
2827
} catch (error) {
2928
console.warn("Failed to sync campaign:", error);
@@ -65,4 +64,142 @@ export const syncApi = {
6564
return { success: false, message: String(error) };
6665
}
6766
},
67+
68+
/**
69+
* Sync an account profile and recalculate donation stats
70+
* @param accountId - The NEAR account ID
71+
*/
72+
async account(accountId: string): Promise<{ success: boolean; message?: string }> {
73+
try {
74+
const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/accounts/${accountId}/sync`, {
75+
method: "POST",
76+
});
77+
78+
if (!response.ok) {
79+
const error = await response.json().catch(() => ({}));
80+
console.warn("Failed to sync account:", error);
81+
return { success: false, message: error?.error || "Sync failed" };
82+
}
83+
84+
const result = await response.json();
85+
return { success: true, message: result.message };
86+
} catch (error) {
87+
console.warn("Failed to sync account:", error);
88+
return { success: false, message: String(error) };
89+
}
90+
},
91+
92+
/**
93+
* Sync a list after creation or update
94+
* @param listId - The on-chain list ID
95+
*/
96+
async list(listId: number | string): Promise<{ success: boolean; message?: string }> {
97+
try {
98+
const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/sync`, {
99+
method: "POST",
100+
});
101+
102+
if (!response.ok) {
103+
const error = await response.json().catch(() => ({}));
104+
console.warn("Failed to sync list:", error);
105+
return { success: false, message: error?.error || "Sync failed" };
106+
}
107+
108+
const result = await response.json();
109+
return { success: true, message: result.message };
110+
} catch (error) {
111+
console.warn("Failed to sync list:", error);
112+
return { success: false, message: String(error) };
113+
}
114+
},
115+
116+
/**
117+
* Sync all registrations for a list
118+
* @param listId - The on-chain list ID
119+
*/
120+
async listRegistrations(
121+
listId: number | string,
122+
): Promise<{ success: boolean; message?: string }> {
123+
try {
124+
const response = await fetch(
125+
`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/registrations/sync`,
126+
{
127+
method: "POST",
128+
},
129+
);
130+
131+
if (!response.ok) {
132+
const error = await response.json().catch(() => ({}));
133+
console.warn("Failed to sync list registrations:", error);
134+
return { success: false, message: error?.error || "Sync failed" };
135+
}
136+
137+
const result = await response.json();
138+
return { success: true, message: result.message };
139+
} catch (error) {
140+
console.warn("Failed to sync list registrations:", error);
141+
return { success: false, message: String(error) };
142+
}
143+
},
144+
145+
/**
146+
* Sync a single registration for a list
147+
* @param listId - The on-chain list ID
148+
* @param registrantId - The registrant account ID
149+
*/
150+
async listRegistration(
151+
listId: number | string,
152+
registrantId: string,
153+
): Promise<{ success: boolean; message?: string }> {
154+
try {
155+
const response = await fetch(
156+
`${SYNC_API_BASE_URL}/api/v1/lists/${listId}/registrations/${registrantId}/sync`,
157+
{
158+
method: "POST",
159+
},
160+
);
161+
162+
if (!response.ok) {
163+
const error = await response.json().catch(() => ({}));
164+
console.warn("Failed to sync list registration:", error);
165+
return { success: false, message: error?.error || "Sync failed" };
166+
}
167+
168+
const result = await response.json();
169+
return { success: true, message: result.message };
170+
} catch (error) {
171+
console.warn("Failed to sync list registration:", error);
172+
return { success: false, message: String(error) };
173+
}
174+
},
175+
176+
/**
177+
* Sync a direct donation after it's made
178+
* @param txHash - Transaction hash from the donation
179+
* @param senderId - Account ID of the donor
180+
*/
181+
async directDonation(
182+
txHash: string,
183+
senderId: string,
184+
): Promise<{ success: boolean; message?: string }> {
185+
try {
186+
const response = await fetch(`${SYNC_API_BASE_URL}/api/v1/donations/sync`, {
187+
method: "POST",
188+
headers: { "Content-Type": "application/json" },
189+
body: JSON.stringify({ tx_hash: txHash, sender_id: senderId }),
190+
});
191+
192+
if (!response.ok) {
193+
const error = await response.json().catch(() => ({}));
194+
console.warn("Failed to sync direct donation:", error);
195+
return { success: false, message: error?.error || "Sync failed" };
196+
}
197+
198+
const result = await response.json();
199+
return { success: true, message: result.message };
200+
} catch (error) {
201+
console.warn("Failed to sync direct donation:", error);
202+
return { success: false, message: String(error) };
203+
}
204+
},
68205
};

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

Lines changed: 140 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DONATION_CONTRACT_ACCOUNT_ID } from "@/common/_config";
2-
import { contractApi } from "@/common/blockchains/near-protocol/client";
2+
import { contractApi, walletApi } from "@/common/blockchains/near-protocol/client";
33
import { FULL_TGAS } from "@/common/constants";
44
import type { IndivisibleUnits } from "@/common/types";
55

@@ -10,6 +10,11 @@ import {
1010
DirectDonationConfig,
1111
} from "./interfaces";
1212

13+
export type DirectDonateResult = {
14+
donation: DirectDonation;
15+
txHash: string | null;
16+
};
17+
1318
const donationContractApi = contractApi({
1419
contractId: DONATION_CONTRACT_ACCOUNT_ID,
1520
});
@@ -41,25 +46,145 @@ export const get_donations_for_donor = (args: { donor_id: string }) =>
4146
args,
4247
});
4348

44-
export const donate = (args: DirectDonationArgs, depositAmountYocto: IndivisibleUnits) =>
45-
donationContractApi.call<typeof args, DirectDonation>("donate", {
49+
export const donate = async (
50+
args: DirectDonationArgs,
51+
depositAmountYocto: IndivisibleUnits,
52+
): Promise<DirectDonateResult> => {
53+
const wallet = await walletApi.ensureWallet();
54+
const signerId = walletApi.accountId;
55+
56+
if (!signerId) {
57+
throw new Error("Wallet is not signed in.");
58+
}
59+
60+
const { actionCreators } = await import("@near-js/transactions");
61+
const { providers } = await import("near-api-js");
62+
63+
const action = actionCreators.functionCall(
64+
"donate",
4665
args,
47-
deposit: depositAmountYocto,
48-
gas: FULL_TGAS,
49-
callbackUrl: window.location.href,
50-
});
66+
BigInt(FULL_TGAS),
67+
BigInt(depositAmountYocto),
68+
);
69+
70+
let outcome: any;
71+
const walletAny = wallet as any;
72+
73+
if ("signAndSendTransaction" in walletAny) {
74+
outcome = await walletAny.signAndSendTransaction({
75+
signerId,
76+
receiverId: DONATION_CONTRACT_ACCOUNT_ID,
77+
actions: [action],
78+
});
79+
} else if ("signAndSendTransactions" in walletAny) {
80+
const results = await walletAny.signAndSendTransactions({
81+
transactions: [
82+
{
83+
receiverId: DONATION_CONTRACT_ACCOUNT_ID,
84+
actions: [action],
85+
},
86+
],
87+
});
88+
89+
outcome = Array.isArray(results) ? results[0] : results;
90+
} else {
91+
throw new Error("Wallet does not support transaction signing");
92+
}
93+
94+
const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null;
95+
const donation = providers.getTransactionLastResult(outcome) as DirectDonation;
96+
97+
return { donation, txHash };
98+
};
99+
100+
export type DirectBatchDonateResult = {
101+
donations: DirectDonation[];
102+
txHash: string | null;
103+
};
51104

52-
export const donateBatch = (txInputs: DirectBatchDonationItem[]) =>
53-
donationContractApi.callMultiple<DirectDonationArgs>(
54-
txInputs.map(({ amountYoctoNear, ...txInput }) => ({
55-
method: "donate",
56-
deposit: amountYoctoNear,
57-
gas: FULL_TGAS,
105+
export const donateBatch = async (
106+
txInputs: DirectBatchDonationItem[],
107+
): Promise<DirectBatchDonateResult> => {
108+
const wallet = await walletApi.ensureWallet();
109+
const signerId = walletApi.accountId;
58110

59-
...txInput,
60-
})),
111+
if (!signerId) {
112+
throw new Error("Wallet is not signed in.");
113+
}
114+
115+
const { actionCreators } = await import("@near-js/transactions");
116+
const { providers } = await import("near-api-js");
117+
118+
// Create actions for each donation
119+
const actions = txInputs.map(({ amountYoctoNear, args }) =>
120+
actionCreators.functionCall("donate", args, BigInt(FULL_TGAS), BigInt(amountYoctoNear)),
61121
);
62122

123+
let outcome: any;
124+
const walletAny = wallet as any;
125+
126+
if ("signAndSendTransaction" in walletAny) {
127+
// Single transaction with multiple actions
128+
outcome = await walletAny.signAndSendTransaction({
129+
signerId,
130+
receiverId: DONATION_CONTRACT_ACCOUNT_ID,
131+
actions,
132+
});
133+
} else if ("signAndSendTransactions" in walletAny) {
134+
// For wallets that only support signAndSendTransactions
135+
const results = await walletAny.signAndSendTransactions({
136+
transactions: [
137+
{
138+
receiverId: DONATION_CONTRACT_ACCOUNT_ID,
139+
actions,
140+
},
141+
],
142+
});
143+
144+
outcome = Array.isArray(results) ? results[0] : results;
145+
} else {
146+
throw new Error("Wallet does not support transaction signing");
147+
}
148+
149+
const txHash = outcome?.transaction?.hash || outcome?.transaction_outcome?.id || null;
150+
151+
// Parse all donations from the outcome
152+
const donations: DirectDonation[] = [];
153+
154+
if (outcome?.receipts_outcome) {
155+
for (const receipt of outcome.receipts_outcome) {
156+
const successValue = receipt?.outcome?.status?.SuccessValue;
157+
158+
if (successValue) {
159+
try {
160+
const parsed = JSON.parse(atob(successValue));
161+
162+
if (parsed && "recipient_id" in parsed && "donor_id" in parsed) {
163+
donations.push(parsed as DirectDonation);
164+
}
165+
} catch {
166+
// Not valid JSON, skip
167+
}
168+
}
169+
}
170+
}
171+
172+
// Fallback: try to get last result
173+
if (donations.length === 0) {
174+
try {
175+
const lastResult = providers.getTransactionLastResult(outcome);
176+
177+
if (lastResult && typeof lastResult === "object" && "recipient_id" in lastResult) {
178+
donations.push(lastResult as DirectDonation);
179+
}
180+
} catch {
181+
// Ignore
182+
}
183+
}
184+
185+
return { donations, txHash };
186+
};
187+
63188
export const storage_deposit = (depositAmountYocto: IndivisibleUnits) =>
64189
donationContractApi.call<{}, IndivisibleUnits>("storage_deposit", {
65190
deposit: depositAmountYocto,

src/common/contracts/core/donation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import * as donationContractHooks from "./hooks";
33

44
export type * from "./hooks";
55
export * from "./interfaces";
6+
export type { DirectDonateResult, DirectBatchDonateResult } from "./client";
67

78
export { donationContractClient, donationContractHooks };

src/entities/list/components/ListDetails.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FaHeart, FaRegHeart } from "react-icons/fa";
88
import { prop } from "remeda";
99

1010
import { PLATFORM_NAME } from "@/common/_config";
11-
import { List } from "@/common/api/indexer";
11+
import { List, syncApi } from "@/common/api/indexer";
1212
import { PLATFORM_TWITTER_ACCOUNT_ID } from "@/common/constants";
1313
import { listsContractClient } from "@/common/contracts/core/lists";
1414
import { truncate } from "@/common/lib";
@@ -77,9 +77,11 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet
7777
} = useListForm();
7878

7979
const applyToListModal = (note: string) => {
80+
const onChainListId = parseInt(listDetails?.on_chain_id as any);
81+
8082
listsContractClient
8183
.register_batch({
82-
list_id: parseInt(listDetails?.on_chain_id as any) as any,
84+
list_id: onChainListId as any,
8385
notes: note,
8486
registrations: [
8587
{
@@ -95,7 +97,12 @@ export const ListDetails = ({ admins, listId, listDetails, savedUsers }: ListDet
9597
},
9698
],
9799
})
98-
.then((data) => {
100+
.then(async (data) => {
101+
// Sync registration to indexer
102+
if (viewer.accountId) {
103+
await syncApi.listRegistration(onChainListId, viewer.accountId).catch(() => {});
104+
}
105+
99106
setIsApplicationSuccessful(true);
100107
})
101108
.catch((error) => console.error("Error applying to list:", error));

0 commit comments

Comments
 (0)