-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathwriter.ts
More file actions
96 lines (86 loc) · 3.05 KB
/
writer.ts
File metadata and controls
96 lines (86 loc) · 3.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import * as Sentry from "@sentry/nextjs";
import { createWalletClient } from "viem";
import { nonceManager, privateKeyToAccount } from "viem/accounts";
import { celo } from "viem/chains";
import { celoTransport } from "~/config/viem.config.server";
import { env } from "~/env";
import { redis } from "~/utils/cache/kv";
// Factory function — TypeScript infers the specific WalletClient<celoTransport, celo, PrivateKeyAccount>
// type from the actual createWalletClient(...) call, preserving account/chain narrowing.
function createWriterInstances() {
const { WRITER_PRIVATE_KEY } = env;
const account = privateKeyToAccount(
WRITER_PRIVATE_KEY as `0x${string}`,
{ nonceManager }
);
const client = createWalletClient({
account,
chain: celo,
transport: celoTransport,
});
return { account, client };
}
// Singleton pattern (same as src/server/db/index.ts) to share nonce manager
// state across sequential transactions within a single serverless invocation.
type WriterInstances = ReturnType<typeof createWriterInstances>;
const globalForWriter = globalThis as unknown as {
writerInstances: WriterInstances | undefined;
};
function getWriterInstances() {
if (!globalForWriter.writerInstances) {
globalForWriter.writerInstances = createWriterInstances();
}
return globalForWriter.writerInstances;
}
export function getWriterAccount() {
return getWriterInstances().account;
}
export function getWriterWalletClient() {
return getWriterInstances().client;
}
// Distributed lock for serializing writer transactions across serverless instances.
// Uses Upstash Redis SET NX EX for acquisition and Lua script for safe release.
const WRITER_LOCK_KEY = "writer:tx:lock";
const LOCK_TTL_SECONDS = 15;
const LOCK_RETRY_DELAY_MS = 200;
const LOCK_MAX_RETRIES = 25; // 25 x 200ms = 5s max wait
async function acquireWriterLock(): Promise<string> {
const lockId = crypto.randomUUID();
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
const acquired = await redis.set(WRITER_LOCK_KEY, lockId, {
nx: true,
ex: LOCK_TTL_SECONDS,
});
if (acquired === "OK") return lockId;
await new Promise((r) => setTimeout(r, LOCK_RETRY_DELAY_MS));
}
throw new Error("Failed to acquire writer lock after timeout");
}
async function releaseWriterLock(lockId: string): Promise<void> {
// Only release if we still own the lock (atomic Lua script)
await redis.eval(
`if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`,
[WRITER_LOCK_KEY],
[lockId]
);
}
/**
* Execute a function while holding the distributed writer lock.
* Ensures only one serverless instance sends writer transactions at a time.
*/
export async function withWriterLock<T>(fn: () => Promise<T>): Promise<T> {
const lockId = await acquireWriterLock();
try {
return await fn();
} catch (error) {
Sentry.addBreadcrumb({
category: "writer",
message: "Writer lock transaction failed",
data: { lockId },
level: "error",
});
throw error;
} finally {
await releaseWriterLock(lockId);
}
}