From 05a7f4a179b1b03a362d28051d96c510f947ca30 Mon Sep 17 00:00:00 2001 From: mansur-codes Date: Sat, 29 Aug 2026 07:23:38 +0100 Subject: [PATCH 1/2] fix: reconcile optimistic store on feed refetch to drop stale filtered/confirmed entries (#1203) --- apps/web/src/app/feed/page.tsx | 10 ++- apps/web/src/lib/optimisticStore.test.ts | 95 ++++++++++++++++++++++++ apps/web/src/lib/optimisticStore.ts | 61 ++++++++++++++- 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/lib/optimisticStore.test.ts diff --git a/apps/web/src/app/feed/page.tsx b/apps/web/src/app/feed/page.tsx index 5def28d7..270be3c8 100644 --- a/apps/web/src/app/feed/page.tsx +++ b/apps/web/src/app/feed/page.tsx @@ -181,6 +181,9 @@ export default function FeedPage() { const newPosts = append ? [...posts, ...fetchedPosts] : fetchedPosts; persistFeed({ posts: newPosts, cursor: cursorParam, hasMore: data.has_more ?? false }); + + // Reconcile optimistic state against the fresh server response (#1203) + OptimisticStore.reconcileFeed(currentUserAddress, newPosts); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { @@ -188,7 +191,7 @@ export default function FeedPage() { setLoadingMore(false); } }, - [posts, persistFeed] + [posts, persistFeed, currentUserAddress] ); const fetchFollowingFeed = useCallback( @@ -217,6 +220,8 @@ export default function FeedPage() { setFollowsNobody(true); setLoading(false); setLoadingMore(false); + // No visible posts — prune all optimistic entries (#1203) + OptimisticStore.reconcileFeed(currentUserAddress, []); return; } setFollowsNobody(false); @@ -253,6 +258,9 @@ export default function FeedPage() { cursor: cursorParam, hasMore: startIdx + paginated.length < allFetchedPosts.length, }); + + // Reconcile optimistic state against the fresh server response (#1203) + OptimisticStore.reconcileFeed(currentUserAddress, newPosts); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { diff --git a/apps/web/src/lib/optimisticStore.test.ts b/apps/web/src/lib/optimisticStore.test.ts new file mode 100644 index 00000000..580c8dc7 --- /dev/null +++ b/apps/web/src/lib/optimisticStore.test.ts @@ -0,0 +1,95 @@ +import { OptimisticStore } from "./optimisticStore"; + +/** + * Unit tests for OptimisticStore.reconcileFeed — issue #1203. + * + * The reconcileFeed method prunes optimistic like/tip state that is stale + * after a feed refetch (e.g. when the active filter changes). + */ +describe("OptimisticStore.reconcileFeed (#1203)", () => { + afterEach(() => { + // Clean up any leftover state between tests. + // We call reconcileFeed with no posts to clear like entries, and + // manually clear remaining entries via the public API. + OptimisticStore.reconcileFeed("__cleanup__", []); + }); + + it("prunes an optimistic like entry when the post is filtered out of the visible set", () => { + const user = "GUSER1"; + const key = `${user}:42`; + + OptimisticStore.setLikeState(key, { isLiked: true, likeCount: 5 }); + expect(OptimisticStore.getLikeState(key)).toEqual({ isLiked: true, likeCount: 5 }); + + // Refetch returns posts that do NOT include post 42 + OptimisticStore.reconcileFeed(user, [{ id: 10 }, { id: 20 }]); + + expect(OptimisticStore.getLikeState(key)).toBeUndefined(); + }); + + it("drops the optimistic like entry for a server-confirmed post (server wins via initialState fallback)", () => { + const user = "GUSER2"; + const key = `${user}:7`; + + OptimisticStore.setLikeState(key, { isLiked: true, likeCount: 10 }); + expect(OptimisticStore.getLikeState(key)).toBeDefined(); + + // Refetch returns post 7 with server-confirmed data — + // the optimistic entry is deleted so the component falls back to + // initialState which reflects the server truth. + OptimisticStore.reconcileFeed(user, [{ id: 7 }, { id: 8 }]); + + expect(OptimisticStore.getLikeState(key)).toBeUndefined(); + }); + + it("prunes optimistic tip state for a post absent from the visible set", () => { + OptimisticStore.setTipState("99", { tipTotal: 500 }); + expect(OptimisticStore.getTipState("99")).toEqual({ tipTotal: 500 }); + + OptimisticStore.reconcileFeed("GUSER3", [{ id: 1 }]); + + expect(OptimisticStore.getTipState("99")).toBeUndefined(); + }); + + it("prunes optimistic tip state for a post present in the visible set (server wins)", () => { + OptimisticStore.setTipState("5", { tipTotal: 100 }); + expect(OptimisticStore.getTipState("5")).toBeDefined(); + + OptimisticStore.reconcileFeed("GUSER4", [{ id: 5 }]); + + expect(OptimisticStore.getTipState("5")).toBeUndefined(); + }); + + it("does NOT prune optimistic like entries belonging to a different user", () => { + const otherUser = "GOTHER"; + const currentUser = "GCURRENT"; + const otherKey = `${otherUser}:42`; + + OptimisticStore.setLikeState(otherKey, { isLiked: true, likeCount: 3 }); + + // Reconcile as currentUser — otherUser's entry should survive + OptimisticStore.reconcileFeed(currentUser, [{ id: 42 }]); + + expect(OptimisticStore.getLikeState(otherKey)).toEqual({ isLiked: true, likeCount: 3 }); + + // Cleanup + OptimisticStore.reconcileFeed(otherUser, []); + }); + + it("notifies subscribers when entries are pruned", () => { + const listener = jest.fn(); + const unsubscribe = OptimisticStore.subscribe(listener); + + const user = "GNOTIFY"; + OptimisticStore.setLikeState(`${user}:1`, { isLiked: true, likeCount: 1 }); + + // Reset the call count after the setLikeState notification + listener.mockClear(); + + OptimisticStore.reconcileFeed(user, []); + + expect(listener).toHaveBeenCalledTimes(1); + + unsubscribe(); + }); +}); diff --git a/apps/web/src/lib/optimisticStore.ts b/apps/web/src/lib/optimisticStore.ts index b9bac6e1..25415f02 100644 --- a/apps/web/src/lib/optimisticStore.ts +++ b/apps/web/src/lib/optimisticStore.ts @@ -86,6 +86,62 @@ export const OptimisticStore = { return tipStateMap.get(key); }, + clearLikeState(key: string) { + likeStateMap.delete(key); + notify(); + }, + + clearTipState(key: string) { + tipStateMap.delete(key); + notify(); + }, + + /** + * Reconcile optimistic state against a fresh server response. + * + * Resolution rules: + * 1. For each likeStateMap entry whose postId IS in visiblePosts, + * delete it — the component will fall back to `initialState` which + * reflects the server-confirmed value (server wins). + * 2. For each likeStateMap entry whose postId is NOT in visiblePosts, + * delete it — the post has been filtered out and the optimistic + * entry should not persist. + * 3. Entries belonging to a different userAddress are left untouched. + * 4. Same rules apply for tipStateMap (keyed by postId string). + */ + reconcileFeed(userAddress: string | null, visiblePosts: Array<{ id: string | number }>) { + const visibleIds = new Set(visiblePosts.map((p) => String(p.id))); + let changed = false; + + // Reconcile like state — keys are `${userAddress}:${postId}` + if (userAddress) { + for (const key of [...likeStateMap.keys()]) { + const separatorIdx = key.indexOf(":"); + if (separatorIdx === -1) continue; + + const keyUser = key.slice(0, separatorIdx); + if (keyUser !== userAddress) continue; + + // Post is either present (server wins) or absent (filtered out) — either way, drop it + likeStateMap.delete(key); + changed = true; + } + } + + // Reconcile tip state — keys are postId strings + for (const key of [...tipStateMap.keys()]) { + // Only prune entries for posts we have a definitive answer about. + // If the post is in the visible set, server truth is now available + // via initialState. If absent, the optimistic entry is stale. + tipStateMap.delete(key); + changed = true; + } + + if (changed) { + notify(); + } + }, + // Legacy API for FollowList.tsx subscribe, isFollowing(targetAddress: string): boolean { @@ -152,10 +208,7 @@ export function useOptimisticLike( * Returns the optimistic tip state if one exists, otherwise falls back * to `initialState`. */ -export function useOptimisticTip( - postId: string | bigint, - initialState: TipState -): TipState { +export function useOptimisticTip(postId: string | bigint, initialState: TipState): TipState { const key = String(postId); const optimistic = useSyncExternalStore( From 45336f400fca4482b0b5b645e0be821e4be41ffe Mon Sep 17 00:00:00 2001 From: mansur-codes Date: Sat, 29 Aug 2026 07:46:01 +0100 Subject: [PATCH 2/2] Ci issues fix --- apps/web/jest.config.js | 1 + apps/web/jest.setup.ts | 4 + .../sdk/src/__tests__/events-drift.test.ts | 9 +- packages/sdk/src/__tests__/health.test.ts | 35 ++++--- packages/sdk/src/__tests__/retry.test.ts | 4 +- packages/sdk/src/__tests__/write.test.ts | 97 +++++++++++++++++-- packages/sdk/src/client.ts | 18 ++-- packages/sdk/src/events/types.ts | 75 +++++++++----- 8 files changed, 176 insertions(+), 67 deletions(-) diff --git a/apps/web/jest.config.js b/apps/web/jest.config.js index 53f8f875..35b0919a 100644 --- a/apps/web/jest.config.js +++ b/apps/web/jest.config.js @@ -11,6 +11,7 @@ module.exports = { "^linkora-sdk$": "/../../packages/sdk/src/index.ts", "^@linkora/types/src/(.*)$": "/../../packages/types/src/$1", "^@linkora/types$": "/../../packages/types/src/index.ts", + "^(\\.\\.?/.*)\\.js$": "$1", }, transform: { "^.+\\.(ts|tsx)$": [ diff --git a/apps/web/jest.setup.ts b/apps/web/jest.setup.ts index c85d98b0..0cbf03bd 100644 --- a/apps/web/jest.setup.ts +++ b/apps/web/jest.setup.ts @@ -1,6 +1,10 @@ // Jest setup file for DOM and React testing utilities import "@testing-library/jest-dom"; import { toHaveNoViolations } from "jest-axe"; +import { TextEncoder, TextDecoder } from "util"; + +(globalThis as any).TextEncoder = (globalThis as any).TextEncoder || TextEncoder; +(globalThis as any).TextDecoder = (globalThis as any).TextDecoder || TextDecoder; // Extend Jest matchers with jest-axe expect.extend(toHaveNoViolations); diff --git a/packages/sdk/src/__tests__/events-drift.test.ts b/packages/sdk/src/__tests__/events-drift.test.ts index 5aba2382..a2445656 100644 --- a/packages/sdk/src/__tests__/events-drift.test.ts +++ b/packages/sdk/src/__tests__/events-drift.test.ts @@ -1,14 +1,9 @@ import { parseContractEvent } from "../events/types.js"; -import { RawLinkoraEvent } from "../generated/events.js"; import { parseRawContractEvent } from "../generated/events.js"; describe("Event type drift", () => { it("dummy runtime check to satisfy jest", () => { - // We would ideally want to dynamically check that `parseContractEvent` supports every event. - // However, TypeScript doesn't let us iterate over union variants at runtime. - // Instead, the fact that `events/types.ts` now uses `Omit` ensures - // that the fields themselves cannot drift. - // This is the core fix to the "Duplicate/XOR'd kept events.ts types vs generated/events.ts" issue. - expect(true).toBe(true); + expect(typeof parseContractEvent).toBe("function"); + expect(typeof parseRawContractEvent).toBe("function"); }); }); diff --git a/packages/sdk/src/__tests__/health.test.ts b/packages/sdk/src/__tests__/health.test.ts index e6075561..85752194 100644 --- a/packages/sdk/src/__tests__/health.test.ts +++ b/packages/sdk/src/__tests__/health.test.ts @@ -325,43 +325,50 @@ describe("ConnectionHealthMonitor", () => { describe("jitter and backoff (Issue 1265)", () => { it("adds jitter to initial and subsequent checks", async () => { const setTimeoutSpy = jest.spyOn(global, "setTimeout"); - - const monitor = new ConnectionHealthMonitor("https://rpc.example.com", { intervalMs: 50, backoffMs: 20 }); + + const monitor = new ConnectionHealthMonitor("https://rpc.example.com", { + intervalMs: 50, + backoffMs: 20, + }); monitor.start(); - + expect(setTimeoutSpy).toHaveBeenCalled(); - const firstCallDelay = setTimeoutSpy.mock.calls[setTimeoutSpy.mock.calls.length - 1][1] as number; + const firstCallDelay = setTimeoutSpy.mock.calls[ + setTimeoutSpy.mock.calls.length - 1 + ][1] as number; expect(firstCallDelay).toBeGreaterThanOrEqual(0); expect(firstCallDelay).toBeLessThanOrEqual(20); // up to this.backoffMs - + monitor.stop(); setTimeoutSpy.mockRestore(); }); it("stops probing when max backoff is reached and can be resumed", async () => { - let callCount = 0; mockGetLatestLedger.mockImplementation(() => { return Promise.reject(new Error("down")); }); - const monitor = new ConnectionHealthMonitor("https://rpc.example.com", { intervalMs: 10, backoffMs: 10, maxBackoffMs: 10 }); + const monitor = new ConnectionHealthMonitor("https://rpc.example.com", { + intervalMs: 10, + backoffMs: 10, + maxBackoffMs: 10, + }); monitor.start(); - + // Wait for a few backoff cycles await new Promise((r) => setTimeout(r, 100)); - + const checksAfterStop = mockGetLatestLedger.mock.calls.length; - + // Wait another 100ms to ensure no further checks occur await new Promise((r) => setTimeout(r, 100)); expect(mockGetLatestLedger.mock.calls.length).toBe(checksAfterStop); - + // Manual resume should restart it monitor.resume(); await new Promise((r) => setTimeout(r, 100)); expect(mockGetLatestLedger.mock.calls.length).toBeGreaterThan(checksAfterStop); - + monitor.stop(); }); }); - -}); \ No newline at end of file +}); diff --git a/packages/sdk/src/__tests__/retry.test.ts b/packages/sdk/src/__tests__/retry.test.ts index d1c90fff..c3d81927 100644 --- a/packages/sdk/src/__tests__/retry.test.ts +++ b/packages/sdk/src/__tests__/retry.test.ts @@ -174,7 +174,7 @@ describe("withRetry", () => { }) ).rejects.toThrow("still failing"); expect(fn).toHaveBeenCalledTimes(3); - expect(attempts.at(-1)?.reason).toBe("exhausted"); + expect(attempts[attempts.length - 1]?.reason).toBe("exhausted"); }); it("honors a Retry-After delay over the computed backoff", async () => { @@ -211,6 +211,6 @@ describe("withRetry", () => { // Two failures trip the breaker; it never reaches maxAttempts. expect(fn).toHaveBeenCalledTimes(2); expect(cb.isOpen).toBe(true); - expect(attempts.at(-1)?.reason).toBe("circuit-open"); + expect(attempts[attempts.length - 1]?.reason).toBe("circuit-open"); }); }); diff --git a/packages/sdk/src/__tests__/write.test.ts b/packages/sdk/src/__tests__/write.test.ts index 3d1ebf8a..8d88c8be 100644 --- a/packages/sdk/src/__tests__/write.test.ts +++ b/packages/sdk/src/__tests__/write.test.ts @@ -271,10 +271,12 @@ describe("prepare*Tx methods (Submittable)", () => { const val = (v: unknown) => expect.objectContaining({ _val: v }); it("prepareCreatePostTx fetches sequence and uses prepareTransaction", async () => { - jest.spyOn(client as any, 'getAccountForTx').mockResolvedValue(new (require("@stellar/stellar-base").Account)("GAUTHOR", "100")); - jest.spyOn(client, 'prepareTransaction').mockResolvedValue({ - toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }) - } as any); + jest + .spyOn(client as unknown as { getAccountForTx: jest.Mock }, "getAccountForTx") + .mockResolvedValue({ _accountId: "GAUTHOR", sequence: "100" }); + jest.spyOn(client, "prepareTransaction").mockResolvedValue({ + toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }), + } as unknown as ReturnType); const result = await client.prepareCreatePostTx("GAUTHOR", "hello"); expect(result).toBe("PREPARED_XDR"); @@ -287,10 +289,12 @@ describe("prepare*Tx methods (Submittable)", () => { }); it("prepareFollowTx fetches sequence and uses prepareTransaction", async () => { - jest.spyOn(client as any, 'getAccountForTx').mockResolvedValue(new (require("@stellar/stellar-base").Account)("GA", "100")); - jest.spyOn(client, 'prepareTransaction').mockResolvedValue({ - toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }) - } as any); + jest + .spyOn(client as unknown as { getAccountForTx: jest.Mock }, "getAccountForTx") + .mockResolvedValue({ _accountId: "GA", sequence: "100" }); + jest.spyOn(client, "prepareTransaction").mockResolvedValue({ + toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }), + } as unknown as ReturnType); const result = await client.prepareFollowTx("GA", "GB"); expect(result).toBe("PREPARED_XDR"); @@ -301,5 +305,80 @@ describe("prepare*Tx methods (Submittable)", () => { addr("GB") ); }); -}); + it("prepareUnfollowTx fetches sequence and uses prepareTransaction", async () => { + jest + .spyOn(client as unknown as { getAccountForTx: jest.Mock }, "getAccountForTx") + .mockResolvedValue({ _accountId: "GA", sequence: "100" }); + jest.spyOn(client, "prepareTransaction").mockResolvedValue({ + toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }), + } as unknown as ReturnType); + + const result = await client.prepareUnfollowTx("GA", "GB"); + expect(result).toBe("PREPARED_XDR"); + expect(client.prepareTransaction).toHaveBeenCalledWith( + "unfollow", + expect.objectContaining({ _accountId: "GA" }), + addr("GA"), + addr("GB") + ); + }); + + it("prepareLikePostTx fetches sequence and encodes postId as u64", async () => { + jest + .spyOn(client as unknown as { getAccountForTx: jest.Mock }, "getAccountForTx") + .mockResolvedValue({ _accountId: "GUSER", sequence: "100" }); + jest.spyOn(client, "prepareTransaction").mockResolvedValue({ + toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }), + } as unknown as ReturnType); + + const result = await client.prepareLikePostTx("GUSER", 42); + expect(result).toBe("PREPARED_XDR"); + expect(client.prepareTransaction).toHaveBeenCalledWith( + "like_post", + expect.objectContaining({ _accountId: "GUSER" }), + addr("GUSER"), + val(42) + ); + }); + + it("prepareTipTx fetches sequence and encodes arguments correctly", async () => { + jest + .spyOn(client as unknown as { getAccountForTx: jest.Mock }, "getAccountForTx") + .mockResolvedValue({ _accountId: "GTIPPER", sequence: "100" }); + jest.spyOn(client, "prepareTransaction").mockResolvedValue({ + toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }), + } as unknown as ReturnType); + + const result = await client.prepareTipTx("GTIPPER", 42, "GTOKEN", 1000n); + expect(result).toBe("PREPARED_XDR"); + expect(client.prepareTransaction).toHaveBeenCalledWith( + "tip", + expect.objectContaining({ _accountId: "GTIPPER" }), + addr("GTIPPER"), + val(42), + addr("GTOKEN"), + val(1000n) + ); + }); + + it("preparePoolDepositTx fetches sequence and encodes poolId as symbol", async () => { + jest + .spyOn(client as unknown as { getAccountForTx: jest.Mock }, "getAccountForTx") + .mockResolvedValue({ _accountId: "GDEPOSITOR", sequence: "100" }); + jest.spyOn(client, "prepareTransaction").mockResolvedValue({ + toEnvelope: () => ({ toXDR: () => "PREPARED_XDR" }), + } as unknown as ReturnType); + + const result = await client.preparePoolDepositTx("GDEPOSITOR", "pool1", "GTOKEN", 500n); + expect(result).toBe("PREPARED_XDR"); + expect(client.prepareTransaction).toHaveBeenCalledWith( + "pool_deposit", + expect.objectContaining({ _accountId: "GDEPOSITOR" }), + addr("GDEPOSITOR"), + val("pool1"), + addr("GTOKEN"), + val(500n) + ); + }); +}); diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 4f7a3583..43f02aae 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -101,6 +101,12 @@ function scvString(value: string): xdr.ScVal { function scvU32(value: number): xdr.ScVal { return nativeToScVal(value, { type: "u32" }); } +function scvU64(value: number | bigint): xdr.ScVal { + return nativeToScVal(value, { type: "u64" }); +} +function scvSymbol(value: string): xdr.ScVal { + return nativeToScVal(value, { type: "symbol" }); +} function scvI128(value: number | bigint): xdr.ScVal { return nativeToScVal(value, { type: "i128" }); } @@ -1097,11 +1103,7 @@ export class LinkoraClient extends GeneratedLinkoraClient { * @param horizonUrl Optional Horizon URL to use. Defaults based on the network passphrase. * @returns The base64-encoded transaction envelope XDR ready for wallet signing. */ - async prepareCreatePostTx( - author: string, - content: string, - horizonUrl?: string - ): Promise { + async prepareCreatePostTx(author: string, content: string, horizonUrl?: string): Promise { ensureAddress(author, "author"); ensureNonEmptyString(content, "content"); const sourceAccount = await this.getAccountForTx(author, horizonUrl); @@ -1160,11 +1162,7 @@ export class LinkoraClient extends GeneratedLinkoraClient { * @param horizonUrl Optional Horizon URL to use. Defaults based on the network passphrase. * @returns The base64-encoded transaction envelope XDR ready for wallet signing. */ - async prepareFollowTx( - follower: string, - followee: string, - horizonUrl?: string - ): Promise { + async prepareFollowTx(follower: string, followee: string, horizonUrl?: string): Promise { ensureAddress(follower, "follower"); ensureAddress(followee, "followee"); const sourceAccount = await this.getAccountForTx(follower, horizonUrl); diff --git a/packages/sdk/src/events/types.ts b/packages/sdk/src/events/types.ts index 68cee8cd..d07f6936 100644 --- a/packages/sdk/src/events/types.ts +++ b/packages/sdk/src/events/types.ts @@ -39,45 +39,60 @@ export interface RentPaidEvent extends BaseLinkoraEvent { extended_to_ledger: number; } -export type ProfileSetEvent = BaseLinkoraEvent & Omit & { type: "profile_set" }; +export type ProfileSetEvent = BaseLinkoraEvent & + Omit & { type: "profile_set" }; export type FollowEvent = BaseLinkoraEvent & Omit & { type: "follow" }; -export type UnfollowEvent = BaseLinkoraEvent & Omit & { type: "unfollow" }; +export type UnfollowEvent = BaseLinkoraEvent & + Omit & { type: "unfollow" }; export type BlockEvent = BaseLinkoraEvent & Omit & { type: "block" }; export type UnblockEvent = BaseLinkoraEvent & Omit & { type: "unblock" }; -export type PostCreatedEvent = BaseLinkoraEvent & Omit & { type: "post_created" }; +export type PostCreatedEvent = BaseLinkoraEvent & + Omit & { type: "post_created" }; export type TipEvent = BaseLinkoraEvent & Omit & { type: "tip" }; -export type PoolDepositEvent = BaseLinkoraEvent & Omit & { type: "pool_deposit" }; +export type PoolDepositEvent = BaseLinkoraEvent & + Omit & { type: "pool_deposit" }; -export type PoolWithdrawEvent = BaseLinkoraEvent & Omit & { type: "pool_withdraw" }; +export type PoolWithdrawEvent = BaseLinkoraEvent & + Omit & { type: "pool_withdraw" }; -export type PoolCreatedEvent = BaseLinkoraEvent & Omit & { type: "pool_created" }; +export type PoolCreatedEvent = BaseLinkoraEvent & + Omit & { type: "pool_created" }; export type LikePostEvent = BaseLinkoraEvent & Omit & { type: "like" }; -export type ContractUpgradedEvent = BaseLinkoraEvent & Omit & { type: "contract_upgraded" }; +export type ContractUpgradedEvent = BaseLinkoraEvent & + Omit & { type: "contract_upgraded" }; -export type PostDeletedEvent = BaseLinkoraEvent & Omit & { type: "post_deleted" }; +export type PostDeletedEvent = BaseLinkoraEvent & + Omit & { type: "post_deleted" }; -export type ProposalCreatedEvent = BaseLinkoraEvent & Omit & { type: "proposal_created" }; +export type ProposalCreatedEvent = BaseLinkoraEvent & + Omit & { type: "proposal_created" }; -export type ProposalSignedEvent = BaseLinkoraEvent & Omit & { type: "proposal_signed" }; +export type ProposalSignedEvent = BaseLinkoraEvent & + Omit & { type: "proposal_signed" }; -export type ProposalExecutedEvent = BaseLinkoraEvent & Omit & { type: "proposal_executed" }; +export type ProposalExecutedEvent = BaseLinkoraEvent & + Omit & { type: "proposal_executed" }; -export type PoolAdminAddedEvent = BaseLinkoraEvent & Omit & { type: "pool_admin_added" }; +export type PoolAdminAddedEvent = BaseLinkoraEvent & + Omit & { type: "pool_admin_added" }; -export type PoolAdminRemovedEvent = BaseLinkoraEvent & Omit & { type: "pool_admin_removed" }; +export type PoolAdminRemovedEvent = BaseLinkoraEvent & + Omit & { type: "pool_admin_removed" }; -export type PoolThresholdUpdatedEvent = BaseLinkoraEvent & Omit & { type: "pool_threshold_updated" }; +export type PoolThresholdUpdatedEvent = BaseLinkoraEvent & + Omit & { type: "pool_threshold_updated" }; -export type DmKeyPublishedEvent = BaseLinkoraEvent & Omit & { type: "dm_key_published" }; +export type DmKeyPublishedEvent = BaseLinkoraEvent & + Omit & { type: "dm_key_published" }; export interface CredentialRootUpdatedEvent extends BaseLinkoraEvent { type: "credential_root_updated"; @@ -91,19 +106,25 @@ export interface CredentialVerifiedEvent extends BaseLinkoraEvent { nullifier: string; } -export type FeeUpdatedEvent = BaseLinkoraEvent & Omit & { type: "fee_updated" }; +export type FeeUpdatedEvent = BaseLinkoraEvent & + Omit & { type: "fee_updated" }; -export type TreasuryUpdatedEvent = BaseLinkoraEvent & Omit & { type: "treasury_updated" }; +export type TreasuryUpdatedEvent = BaseLinkoraEvent & + Omit & { type: "treasury_updated" }; -export type GovProposalCreatedEvent = BaseLinkoraEvent & Omit & { type: "gov_proposal_created" }; +export type GovProposalCreatedEvent = BaseLinkoraEvent & + Omit & { type: "gov_proposal_created" }; export type GovVoteEvent = BaseLinkoraEvent & Omit & { type: "gov_vote" }; -export type GovProposalExecutedEvent = BaseLinkoraEvent & Omit & { type: "gov_proposal_executed" }; +export type GovProposalExecutedEvent = BaseLinkoraEvent & + Omit & { type: "gov_proposal_executed" }; -export type GovProposalVetoedEvent = BaseLinkoraEvent & Omit & { type: "gov_proposal_vetoed" }; +export type GovProposalVetoedEvent = BaseLinkoraEvent & + Omit & { type: "gov_proposal_vetoed" }; -export type EmergencyBypassEvent = BaseLinkoraEvent & Omit & { type: "emergency_bypass" }; +export type EmergencyBypassEvent = BaseLinkoraEvent & + Omit & { type: "emergency_bypass" }; export interface AttestationVerifiedEvent extends BaseLinkoraEvent { type: "attestation_verified"; @@ -443,7 +464,11 @@ export function parseContractEvent(raw: SorobanEvent): LinkoraEvent | null { meta: eventMeta, }; case "contract_upgraded": - return { type: eventType, new_wasm_hash: payload.new_wasm_hash as Uint8Array, meta: eventMeta }; + return { + type: eventType, + new_wasm_hash: payload.new_wasm_hash as Uint8Array, + meta: eventMeta, + }; case "proposal_created": return { type: eventType, @@ -573,7 +598,7 @@ export function parseContractEvent(raw: SorobanEvent): LinkoraEvent | null { case "post_reported": return { type: eventType, - post_id: big(payload.post_id), + post_id: num(payload.post_id), reporter: str(payload.reporter), stake_amount: big(payload.stake_amount), meta: eventMeta, @@ -581,14 +606,14 @@ export function parseContractEvent(raw: SorobanEvent): LinkoraEvent | null { case "post_removed_by_moderation": return { type: eventType, - post_id: big(payload.post_id), + post_id: num(payload.post_id), reporter: str(payload.reporter), meta: eventMeta, }; case "report_dismissed": return { type: eventType, - post_id: big(payload.post_id), + post_id: num(payload.post_id), reporter: str(payload.reporter), meta: eventMeta, };