Repository navigation
Add ClaimSDK transaction-submitted callback #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3813c7d
bff870d
e12fbb1
6c37cae
ce781ab
ece1a37
23bb57c
b151da1
878954f
f3b7d9f
744e984
a552d32
4bbae54
28f5ce9
d75d22c
0376129
40543fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,4 +44,5 @@ e2e.log | |
| .bounties | ||
| codex-resume | ||
| .codex | ||
| .npmrc | ||
| .npmrc | ||
| .agents/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| export type TransactionSubmittedCallback = ( | ||
| hash: `0x${string}`, | ||
| ) => void | Promise<void> | ||
|
|
||
| /** | ||
| * Invokes a submitted-transaction callback without awaiting it (so a slow or | ||
| * hanging callback cannot block confirmation) and logs callback failures without | ||
| * interrupting the transaction flow. | ||
| * @param hash - The submitted transaction hash. | ||
| * @param onSubmitted - Optional callback to notify after transaction broadcast. | ||
| * @param label - Log message prefix used when the callback throws. | ||
| */ | ||
| export function safeInvokeSubmittedCallback( | ||
| hash: `0x${string}`, | ||
| onSubmitted?: TransactionSubmittedCallback, | ||
| label = "Transaction submitted callback", | ||
| ): void { | ||
| try { | ||
| Promise.resolve(onSubmitted?.(hash)).catch((error) => { | ||
| console.warn(`${label} failed`, error) | ||
| }) | ||
| } catch (error) { | ||
| console.warn(`${label} failed`, error) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" | ||
| import type { PublicClient, TransactionReceipt, WalletClient } from "viem" | ||
| import { ClaimSDK } from "../src/sdks/viem-claim-sdk" | ||
| import { SupportedChains } from "../src/constants" | ||
|
|
||
| vi.mock("viem/actions", () => ({ | ||
| waitForTransactionReceipt: vi.fn(async (_client, { hash }) => ({ | ||
| transactionHash: hash, | ||
| })), | ||
| })) | ||
|
|
||
| const MOCK_ACCOUNT = "0x1111111111111111111111111111111111111111" | ||
| const MOCK_HASH = "0xdeadbeef" as `0x${string}` | ||
|
|
||
| type MockPublicClient = Pick<PublicClient, "simulateContract"> | ||
| type MockWalletClient = Pick< | ||
| WalletClient, | ||
| "account" | "chain" | "getAddresses" | "writeContract" | ||
| > | ||
|
|
||
| describe("ClaimSDK claim callbacks", () => { | ||
| let publicClient: MockPublicClient | ||
| let walletClient: MockWalletClient | ||
| let sdk: ClaimSDK | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers() | ||
| vi.restoreAllMocks() | ||
| }) | ||
|
|
||
| beforeEach(() => { | ||
| publicClient = { | ||
| simulateContract: vi.fn().mockResolvedValue({ request: {} }), | ||
| } | ||
|
|
||
| walletClient = { | ||
| account: { address: MOCK_ACCOUNT }, | ||
| chain: { id: SupportedChains.CELO }, | ||
| getAddresses: vi.fn().mockResolvedValue([MOCK_ACCOUNT]), | ||
| writeContract: vi.fn().mockResolvedValue(MOCK_HASH), | ||
| } | ||
|
|
||
| sdk = new ClaimSDK({ | ||
| account: MOCK_ACCOUNT, | ||
| publicClient: publicClient as PublicClient, | ||
| walletClient: walletClient as WalletClient, | ||
| identitySDK: { | ||
| getWhitelistedRoot: vi.fn().mockResolvedValue({ | ||
| isWhitelisted: true, | ||
| root: MOCK_ACCOUNT, | ||
| }), | ||
| } as any, | ||
| env: "development", | ||
| }) | ||
|
|
||
| vi.spyOn(sdk, "checkEntitlement").mockResolvedValue({ | ||
| amount: 1n, | ||
| altClaimAvailable: false, | ||
| altChainId: null, | ||
| altAmount: null, | ||
| }) | ||
| vi.spyOn(sdk, "checkBalanceWithRetry").mockResolvedValue(true) | ||
| }) | ||
|
|
||
| it("calls onClaimSubmitted once after claim submission and before confirmation", async () => { | ||
| const events: string[] = [] | ||
| let confirmReceipt: (receipt: TransactionReceipt) => void = () => {} | ||
| const receiptPromise = new Promise<TransactionReceipt>((resolve) => { | ||
| confirmReceipt = resolve | ||
| }) | ||
| const onClaimSubmitted = vi.fn(() => { | ||
| events.push("submitted") | ||
| }) | ||
| vi.spyOn(sdk, "submitAndWait").mockImplementation( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test mocks out submitAndWait and invokes the callback itself, so the real callback-before-confirmation ordering inside ClaimSDK.submitAndWait is never exercised. A regression there wouldn't fail the suite.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
| async (_params, onSubmitted) => { | ||
| await onSubmitted?.(MOCK_HASH) | ||
| events.push("waiting") | ||
| return receiptPromise | ||
| }, | ||
| ) | ||
|
|
||
| const claimPromise = sdk | ||
| .claim(undefined, onClaimSubmitted) | ||
| .then((receipt) => { | ||
| events.push("confirmed") | ||
| return receipt as TransactionReceipt | ||
| }) | ||
|
|
||
| await vi.waitFor(() => { | ||
| expect(onClaimSubmitted).toHaveBeenCalledWith(MOCK_HASH) | ||
| }) | ||
|
|
||
| expect(onClaimSubmitted).toHaveBeenCalledTimes(1) | ||
| expect(events).toEqual(["submitted", "waiting"]) | ||
|
|
||
| confirmReceipt({ transactionHash: MOCK_HASH } as TransactionReceipt) | ||
|
|
||
| const receipt = await claimPromise | ||
| expect(receipt.transactionHash).toBe(MOCK_HASH) | ||
| expect(events).toEqual(["submitted", "waiting", "confirmed"]) | ||
| }) | ||
|
|
||
| it("invokes the callback inside submitAndWait before confirmation without blocking on it", async () => { | ||
| vi.useFakeTimers() | ||
| const { waitForTransactionReceipt } = await import("viem/actions") | ||
| const onSubmitted = vi.fn(() => new Promise<void>(() => {})) | ||
|
|
||
| const promise = sdk.submitAndWait({} as any, onSubmitted) | ||
|
|
||
| await vi.waitFor(() => { | ||
| expect(onSubmitted).toHaveBeenCalledWith(MOCK_HASH) | ||
| }) | ||
| expect(waitForTransactionReceipt).not.toHaveBeenCalled() | ||
|
|
||
| await vi.advanceTimersByTimeAsync(5000) | ||
| await expect(promise).resolves.toMatchObject({ | ||
| transactionHash: MOCK_HASH, | ||
| }) | ||
| expect(onSubmitted).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it("swallows onClaimSubmitted errors so claim still resolves", async () => { | ||
| vi.useFakeTimers() | ||
| const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) | ||
| const onClaimSubmitted = vi | ||
| .fn() | ||
| .mockRejectedValue(new Error("ui callback failed")) | ||
|
|
||
| const claimPromise = sdk.claim(undefined, onClaimSubmitted) | ||
|
|
||
| await vi.waitFor(() => { | ||
| expect(onClaimSubmitted).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| await vi.advanceTimersByTimeAsync(5000) | ||
|
|
||
| await expect(claimPromise).resolves.toMatchObject({ | ||
| transactionHash: MOCK_HASH, | ||
| }) | ||
| expect(warnSpy).toHaveBeenCalledWith( | ||
| "[ClaimSDK] onClaimSubmitted callback failed", | ||
| expect.any(Error), | ||
| ) | ||
| }) | ||
|
|
||
| it("supports onConfirmFaucetTx and onClaimSubmitted callbacks", async () => { | ||
| vi.useFakeTimers() | ||
| const onConfirmFaucetTx = vi.fn() | ||
| const onClaimSubmitted = vi.fn() | ||
|
|
||
| const claimPromise = sdk.claim(onConfirmFaucetTx, onClaimSubmitted) | ||
|
|
||
| await vi.waitFor(() => { | ||
| expect(onClaimSubmitted).toHaveBeenCalledWith(MOCK_HASH) | ||
| }) | ||
|
|
||
| expect(sdk.checkBalanceWithRetry).toHaveBeenCalledWith(onConfirmFaucetTx) | ||
|
|
||
| await vi.advanceTimersByTimeAsync(5000) | ||
| await claimPromise | ||
| }) | ||
|
|
||
| it("preserves the positional txConfirm callback signature", async () => { | ||
| vi.useFakeTimers() | ||
| const txConfirm = vi.fn() | ||
|
|
||
| const claimPromise = sdk.claim(txConfirm) | ||
|
|
||
| await vi.waitFor(() => { | ||
| expect(sdk.checkBalanceWithRetry).toHaveBeenCalledWith(txConfirm) | ||
| }) | ||
|
|
||
| await vi.advanceTimersByTimeAsync(5000) | ||
| await claimPromise | ||
| }) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.