📄 src/web3/client/EvmClient.ts
D-OPEN SOVEREIGN
1import { type Address, createPublicClient, http, type PublicClient } from 'viem';
2import {
3 type BouncerStorageReadAPI,
4 type ContractAddresses,
5 type FactoryReadAPI,
6 type ProjectManagerStorageReadAPI,
7 type ReadOnlyDomainAPI,
8 type ScientistStorageReadAPI,
9 type Project,
10 type ProjectVersion,
11 type ArchivistStorageReadAPI,
12 type AddressRegistryReadAPI,
13 type StripeDonation,
14 type SubscriptionManagerReadAPI,
15 type LibraryRegistryStorageReadAPI,
16 type Collection,
17 type PublicLibraryStorageReadAPI,
18 type PublicLibrary,
19} from './types';
20import { addressLoader } from '../registry/loader';
21import { DEFAULT_CHAIN_ID, getChain, toViemChain, type EvmChainConfig } from '../network/chains';
22import { pythAbi } from '../contracts/pythAbi';
23import { addressRegistryAbi } from '../contracts/addressRegistryAbi';
24import { bytes4ToString, stringToBytes4 } from '../utils/strings';
25import { normalizeError } from '../utils/errors';
26
27// Shared by BackupStorage.loadActions(Since) and Factory.load/loadUsername(Since) —
28// all four return the same (compressedActions, indexes, stringIndexes) triplet shape.
29const toActionsTuple = (result: any): [bigint[], bigint[], string[]] => [
30 (result?.[0] || []).map((x: any) => BigInt(x)),
31 (result?.[1] || []).map((x: any) => BigInt(x)),
32 (result?.[2] || []).map(String),
33];
34
35const toStripeDonation = (d: any): StripeDonation => ({
36 stripePaymentId: String(d.stripePaymentId ?? d[0]),
37 amountUSD: BigInt(d.amountUSD ?? d[1]),
38 username: String(d.username ?? d[2]),
39 donor: String(d.donor ?? d[3]),
40 country: String(d.country ?? d[4]),
41 language: String(d.language ?? d[5]),
42 timestamp: Number(d.timestamp ?? d[6]),
43 chainId: Number(d.chainId ?? d[7]),
44});
45
46interface ReadOptions<T> {
47 contractKey: keyof ContractAddresses;
48 contractName: string;
49 functionName: string;
50 args?: readonly unknown[];
51 transform?: (result: any) => T;
52 whenAddressMissing?: T;
53}
54
55export class EvmClient implements ReadOnlyDomainAPI {
56 public readonly ready: Promise<void>;
57 public readonly chainId: number;
58 public readonly chain;
59 public readonly publicClient: PublicClient;
60 public readonly environment: 'production' | 'staging';
61 public readonly config: EvmChainConfig;
62 private _contractAddresses: Record<string, string> = {};
63
64 addressRegistry: AddressRegistryReadAPI;
65 archivistStorage: ArchivistStorageReadAPI;
66 bouncerStorage: BouncerStorageReadAPI;
67 scientistStorage: ScientistStorageReadAPI;
68 factory: FactoryReadAPI;
69 projectManagerStorage: ProjectManagerStorageReadAPI;
70 subscriptionManager: SubscriptionManagerReadAPI;
71 libraryRegistryStorage: LibraryRegistryStorageReadAPI;
72 publicLibraryStorage: PublicLibraryStorageReadAPI;
73
74 public async getUsdPrice(): Promise<number> {
75 const { price } = this.config;
76
77 if (price.stable) {
78 return 1.0;
79 }
80
81 const { pythOracle, usdPriceId } = price;
82 if (!pythOracle || !usdPriceId) {
83 throw new Error(`Pyth config missing for chain ${this.chainId}`);
84 }
85
86 const fetchFromHermes = async () => {
87 const res = await fetch(`https://hermes.pyth.network/v2/updates/price/latest?ids[]=${usdPriceId}`);
88 if (!res.ok) throw new Error(`Hermes price fetch failed: ${res.status}`);
89 const data = await res.json();
90 const parsedPrice = data.parsed?.[0]?.price;
91 if (!parsedPrice) throw new Error(`No price data from Hermes for ${usdPriceId}`);
92 return Number(parsedPrice.price) * Math.pow(10, Number(parsedPrice.expo));
93 };
94
95 if (!this.config.main) {
96 return fetchFromHermes();
97 }
98
99 try {
100 const result: any = await this.publicClient.readContract({
101 address: pythOracle as Address,
102 abi: pythAbi,
103 functionName: 'getPriceUnsafe',
104 args: [usdPriceId as Address],
105 });
106 return Number(result.price) * Math.pow(10, Number(result.expo));
107 } catch (e) {
108 console.warn(`[EvmClient] On-chain Pyth unavailable for chain ${this.chainId}, falling back to Hermes`);
109 return fetchFromHermes();
110 }
111 }
112
113 constructor(chainId: number, environment: 'production' | 'staging' = 'production') {
114 this.chainId = chainId;
115 this.environment = environment;
116
117 const chainConfig = getChain(chainId);
118 this.config = chainConfig;
119 const isDeployed = environment === 'staging' ? chainConfig.stagingDeployed : chainConfig.deployed;
120 if (!isDeployed) {
121 throw new Error(`Chain EVM ${chainId} is not deployed for environment ${environment}`);
122 }
123
124 this.chain = toViemChain(chainConfig);
125
126 this.publicClient = createPublicClient({
127 chain: this.chain,
128 transport: http(chainConfig.rpc),
129 });
130
131 this.addressRegistry = this.createAddressRegistryReadAPI();
132 this.bouncerStorage = this.createBouncerStorageReadAPI();
133 this.scientistStorage = this.createScientistStorageReadAPI();
134 this.archivistStorage = this.createArchivistReadApi();
135 this.factory = this.createFactoryReadAPI();
136 this.projectManagerStorage = this.createProjectManagerStorageReadAPI();
137 this.subscriptionManager = this.createSubscriptionManagerReadAPI();
138 this.libraryRegistryStorage = this.createLibraryRegistryStorageReadAPI();
139 this.publicLibraryStorage = this.createPublicLibraryStorageReadAPI();
140
141 this._contractAddresses = { ...(chainConfig.addresses || {}) } as Record<string, string>;
142
143 this.ready = new Promise(async (resolve, reject) => {
144 try {
145 console.log(`[EvmClient] Initializing registry for chain ${this.chainId} (env: ${this.environment})...`);
146 const staleAddresses = await addressLoader.Initialize(
147 this.chainId,
148 this.addressRegistry,
149 this.activeRegistryAddress,
150 true,
151 this.environment
152 );
153 Object.assign(this._contractAddresses, staleAddresses);
154 const freshAddresses = await addressLoader.loadAllKeys(this.chainId, this.environment);
155 Object.assign(this._contractAddresses, freshAddresses);
156 // loadManifest() (called by loadAllKeys) kicks off ABI fetching
157 // fire-and-forget so address resolution isn't blocked on it — but
158 // `ready` must not resolve until ABIs are actually usable, or read()
159 // ends up handing viem a null ABI (see requireAbiOrThrow below).
160 await addressLoader.waitForAbis(this.chainId, this.environment);
161 console.log(`[EvmClient] Registry ready for ${this.chainId}. Addresses:`, this._contractAddresses);
162 resolve();
163 } catch (e) {
164 console.error(`[EvmClient] Failed to initialize registry for ${this.chainId}`, e);
165 reject(e);
166 }
167 });
168 }
169
170 private get activeRegistryAddress(): string {
171 return (this.environment === 'staging' && this.config.addresses?.stagingAddressRegistry
172 ? this.config.addresses.stagingAddressRegistry
173 : this.config.addresses?.addressRegistry) ?? '';
174 }
175
176 private addresses(): ContractAddresses {
177 return this._contractAddresses as unknown as ContractAddresses;
178 }
179
180 // Shared read path for every registry-fetched contract (§4.2a): resolves the
181 // address, fetches the ABI via addressLoader (never a static import), reads,
182 // and normalizes errors. Collapses what used to be ~15 lines of repeated
183 // boilerplate per method (await ready / resolve address / try readContract /
184 // catch normalizeError) into one call. Not used by createAddressRegistryReadAPI
185 // (bootstrap exception, §4.2a — its own ABI is static and it must not await
186 // `ready`) or by methods with genuinely different failure semantics
187 // (`hasAccount` swallows errors and returns false instead of throwing).
188 // Mirrors EvmWriteClient.requireAbi (§4.2a) — throws an actionable error
189 // instead of letting a null ABI reach viem's readContract, which would
190 // otherwise fail deep inside viem with an opaque error (or silently
191 // misbehave) rather than pointing at the real cause: ABIs not yet synced.
192 private requireAbi(contractName: string) {
193 const abi = addressLoader.getAbi(this.chainId, contractName, this.environment);
194 if (!abi) {
195 throw new Error(
196 `[EvmClient] ABI for '${contractName}' on chain ${this.chainId} is not loaded. ` +
197 `The registry ABI sync may have failed or not completed. Reads are blocked until it is ready.`
198 );
199 }
200 return abi;
201 }
202
203 private async read<T = any>(opts: ReadOptions<T>): Promise<T> {
204 await this.ready;
205 const address = this.addresses()[opts.contractKey] as Address | undefined;
206 if (!address) {
207 if (opts.whenAddressMissing !== undefined) return opts.whenAddressMissing;
208 throw new Error(`[EvmClient] ${String(opts.contractKey)} address missing for chain ${this.chainId}`);
209 }
210 try {
211 const result = await this.publicClient.readContract({
212 address,
213 abi: this.requireAbi(opts.contractName),
214 functionName: opts.functionName,
215 args: opts.args ?? [],
216 });
217 return opts.transform ? opts.transform(result) : (result as T);
218 } catch (error) {
219 throw normalizeError(error);
220 }
221 }
222
223 // Bootstrap exception (§4.2a): uses the statically-imported addressRegistryAbi, not
224 // addressLoader.getAbi(). addressLoader.Initialize() below calls into this API to fetch
225 // every other contract's address/ABI — the registry's own ABI can't come from itself.
226 // Must not await `this.ready` either, for the same reason — this is why it doesn't
227 // go through the shared `read()` helper above.
228 private createAddressRegistryReadAPI(): AddressRegistryReadAPI {
229 return {
230 getLatestContract: async (name: string) => {
231 const address = this.activeRegistryAddress as Address;
232 if (!address) throw new Error(`[EvmClient] addressRegistry address missing for chain ${this.chainId}`);
233 const result: readonly [address: string, abiUrl: string] = (await this.publicClient.readContract({
234 address,
235 abi: addressRegistryAbi,
236 functionName: 'getLatestContract',
237 args: [name],
238 })) as any;
239 return [result[0], result[1]] as [string, string];
240 },
241 getContractHistory: async (name: string) => {
242 const address = this.activeRegistryAddress as Address;
243 if (!address) throw new Error(`[EvmClient] addressRegistry address missing for chain ${this.chainId}`);
244 const result = await this.publicClient.readContract({
245 address,
246 abi: addressRegistryAbi,
247 functionName: 'getContractHistory',
248 args: [name],
249 });
250 return result as any;
251 },
252 getAllContractNames: async () => {
253 const address = this.activeRegistryAddress as Address;
254 if (!address) throw new Error(`[EvmClient] addressRegistry address missing for chain ${this.chainId}`);
255 const result = await this.publicClient.readContract({
256 address,
257 abi: addressRegistryAbi,
258 functionName: 'getAllContractNames',
259 args: [],
260 });
261 return result as any;
262 },
263 getLatestData: async (key: string) => {
264 const address = this.activeRegistryAddress as Address;
265 if (!address) throw new Error(`[EvmClient] addressRegistry address missing for chain ${this.chainId}`);
266 const result = await this.publicClient.readContract({
267 address,
268 abi: addressRegistryAbi,
269 functionName: 'getLatestData',
270 args: [key],
271 });
272 return String(result);
273 },
274 getAllDataKeys: async () => {
275 const address = this.activeRegistryAddress as Address;
276 if (!address) throw new Error(`[EvmClient] addressRegistry address missing for chain ${this.chainId}`);
277 const result = await this.publicClient.readContract({
278 address,
279 abi: addressRegistryAbi,
280 functionName: 'getAllDataKeys',
281 args: [],
282 });
283 return result as any;
284 },
285 admin: async () => {
286 const address = this.activeRegistryAddress as Address;
287 if (!address) throw new Error(`[EvmClient] addressRegistry address missing for chain ${this.chainId}`);
288 const result = await this.publicClient.readContract({
289 address,
290 abi: addressRegistryAbi,
291 functionName: 'admin',
292 args: [],
293 });
294 return String(result);
295 },
296 getLastUpdateInfo: async (name: string) => {
297 const address = this.activeRegistryAddress as Address;
298 if (!address) return null;
299
300 try {
301 const logs = await this.publicClient.getLogs({
302 address,
303 event: {
304 type: 'event',
305 name: 'ContractAdded',
306 inputs: [
307 { type: 'string', name: 'name', indexed: true },
308 { type: 'address', name: 'contractAddress', indexed: true },
309 { type: 'string', name: 'abiUrl' },
310 { type: 'uint256', name: 'version' },
311 ],
312 },
313 args: { name },
314 fromBlock: 0n,
315 });
316
317 if (logs.length === 0) return null;
318
319 const lastLog = logs[logs.length - 1];
320 const block = await this.publicClient.getBlock({ blockNumber: lastLog.blockNumber! });
321
322 return {
323 timestamp: Number(block.timestamp),
324 blockNumber: lastLog.blockNumber!,
325 transactionHash: lastLog.transactionHash!,
326 };
327 } catch (e) {
328 console.error(`[EvmClient] Failed to get last update info for ${name}:`, e);
329 return null;
330 }
331 },
332 };
333 }
334
335 private createArchivistReadApi(): ArchivistStorageReadAPI {
336 const contractKey = 'backupStorage' as const;
337 const contractName = 'BackupStorage';
338 return {
339 userLevel: (userId) => this.read({ contractKey, contractName, functionName: 'userLevel', args: [userId], transform: Number }),
340 isAuthorized: (userAddress) => this.read({ contractKey, contractName, functionName: 'isAuthorized', args: [userAddress], transform: Boolean }),
341 nbWrites: (userId) => this.read({ contractKey, contractName, functionName: 'nbWrites', args: [userId], transform: Number }),
342 loadActions: (userId) => this.read({ contractKey, contractName, functionName: 'loadActions', args: [userId], transform: toActionsTuple }),
343 loadActionsSince: (userId, index) =>
344 this.read({ contractKey, contractName, functionName: 'loadActionsSince', args: [userId, index], transform: toActionsTuple }),
345 };
346 }
347
348 private createProjectManagerStorageReadAPI(): ProjectManagerStorageReadAPI {
349 return {
350 nbProjects: async (): Promise<number> => {
351 await this.ready;
352 const factoryAddress = this.addresses().factory;
353 if (!factoryAddress) {
354 console.error('[EvmClient] Factory address missing in config!');
355 return 0;
356 }
357 try {
358 return await this.read({ contractKey: 'factory', contractName: 'Factory', functionName: 'nbProjects', transform: Number });
359 } catch (error) {
360 console.error('[EvmClient] nbProjects failed:', error);
361 throw error;
362 }
363 },
364 balanceOfAddress: (account) =>
365 this.read({
366 contractKey: 'projectManagerStorage',
367 contractName: 'ProjectManagerStorage',
368 functionName: 'getUserBalances',
369 args: [account],
370 transform: (result: any) => ({
371 total: Number(result[0]),
372 donations: Number(result[1]),
373 unspent: Number(result[2]),
374 }),
375 }),
376 loadProject: (projectId): Promise<Project> =>
377 this.read({
378 contractKey: 'factory',
379 contractName: 'Factory',
380 functionName: 'getProject',
381 args: [projectId],
382 transform: (result: any): Project => ({
383 id: projectId,
384 balance: Number(result[1]),
385 versions: result[2].map((version: any) => {
386 version.publicationDate = new Date(Number(version.publicationDate) * 1000);
387 version.maximumFunding = Number(version.maximumFunding);
388 version.minimumFundingBeforeStartWork = Number(version.minimumFundingBeforeStartWork);
389 version.tags = version.tags.map(Number);
390 version.status = Number(version.status);
391 return version;
392 }),
393 availableLanguages: result[3].map((lang: string) => bytes4ToString(lang)),
394 }),
395 }),
396 addressDonationsPerProject: (account) =>
397 this.read({
398 contractKey: 'factory',
399 contractName: 'Factory',
400 functionName: 'addressDonationsPerProject',
401 args: [account],
402 transform: (result: any[]) => result.map(Number),
403 }),
404 getProjectSupportedLanguages: (projectId) =>
405 this.read({
406 contractKey: 'factory',
407 contractName: 'Factory',
408 functionName: 'getProjectSupportedLanguages',
409 args: [projectId],
410 transform: (result: any[]) => result.map(String),
411 }),
412 // NOTE: interface declares Promise<number> but this actually returns number[]
413 // (pre-existing mismatch, previously masked by an `any`-typed intermediate —
414 // preserved as-is here rather than silently changed).
415 projectVotingBalances: (projectId) =>
416 this.read<any>({
417 contractKey: 'factory',
418 contractName: 'Factory',
419 functionName: 'getProjectAllocatedBudget',
420 args: [projectId],
421 transform: (result: any[]) => result.map(Number),
422 }),
423 getWeeklyDonations: (weekNum): Promise<StripeDonation[]> =>
424 this.read({
425 contractKey: 'projectManagerStorage',
426 contractName: 'ProjectManagerStorage',
427 functionName: 'getWeeklyDonations',
428 args: [BigInt(weekNum)],
429 transform: (result: any[]) => result.map(toStripeDonation),
430 }),
431 getDonationsRange: (fromWeek, toWeek): Promise<StripeDonation[]> =>
432 this.read({
433 contractKey: 'projectManagerStorage',
434 contractName: 'ProjectManagerStorage',
435 functionName: 'getDonationsRange',
436 args: [BigInt(fromWeek), BigInt(toWeek)],
437 transform: (result: any[]) => result.map(toStripeDonation),
438 }),
439 isAuthorized: (userAddress) =>
440 this.read({
441 contractKey: 'projectManagerStorage',
442 contractName: 'ProjectManagerStorage',
443 functionName: 'isAuthorized',
444 args: [userAddress],
445 transform: Boolean,
446 }),
447 admin: () =>
448 this.read({
449 contractKey: 'projectManagerStorage',
450 contractName: 'ProjectManagerStorage',
451 functionName: 'admin',
452 transform: String,
453 }),
454 getNbLanguages: (projectId) =>
455 this.read({
456 contractKey: 'projectManagerStorage',
457 contractName: 'ProjectManagerStorage',
458 functionName: 'getNbLanguages',
459 args: [projectId],
460 transform: Number,
461 }),
462 getLanguageAtIndex: (projectId, index) =>
463 this.read({
464 contractKey: 'projectManagerStorage',
465 contractName: 'ProjectManagerStorage',
466 functionName: 'getLanguageAtIndex',
467 args: [projectId, index],
468 transform: (result: string) => bytes4ToString(result),
469 }),
470 getNbVersions: (projectId, lang) =>
471 this.read({
472 contractKey: 'projectManagerStorage',
473 contractName: 'ProjectManagerStorage',
474 functionName: 'getNbVersions',
475 args: [projectId, stringToBytes4(lang)],
476 transform: Number,
477 }),
478 getVersion: (projectId, lang, versionNumber) =>
479 this.read({
480 contractKey: 'projectManagerStorage',
481 contractName: 'ProjectManagerStorage',
482 functionName: 'getVersion',
483 args: [projectId, stringToBytes4(lang), versionNumber],
484 transform: (result: any): ProjectVersion => ({
485 uri: result.uri,
486 publicationDate: new Date(Number(result.publicationDate) * 1000),
487 name: result.name,
488 headline: result.headline,
489 descriptionMarkdown: result.descriptionMarkdown,
490 arweaveAddressLogo: result.arweaveAddressLogo,
491 arweaveAddressBanner: result.arweaveAddressBanner,
492 minimumFundingBeforeStartWork: Number(result.minimumFundingBeforeStartWork),
493 maximumFunding: Number(result.maximumFunding),
494 tags: result.tags.map(Number),
495 status: Number(result.status),
496 complexity: Number(result.complexity),
497 }),
498 }),
499 getVotingBalance: (projectId, lang) =>
500 this.read<bigint>({
501 contractKey: 'projectManagerStorage',
502 contractName: 'ProjectManagerStorage',
503 functionName: 'getVotingBalance',
504 args: [projectId, stringToBytes4(lang)],
505 }),
506 getUserBalances: (address) =>
507 this.read({
508 contractKey: 'projectManagerStorage',
509 contractName: 'ProjectManagerStorage',
510 functionName: 'getUserBalances',
511 args: [address],
512 transform: (result: any): [bigint, bigint, bigint] => [BigInt(result[0]), BigInt(result[1]), BigInt(result[2])],
513 }),
514 };
515 }
516
517 private createBouncerStorageReadAPI(): BouncerStorageReadAPI {
518 const contractKey = 'bouncerStorage' as const;
519 const contractName = 'BouncerStorage';
520 return {
521 hasAccount: async (userAddress: string) => {
522 await this.ready;
523 const address = this.addresses().bouncerStorage as Address | undefined;
524 if (!address) return false;
525 try {
526 const abi = addressLoader.getAbi(this.chainId, contractName, this.environment);
527 if (!abi) return false;
528 const result = await this.publicClient.readContract({ address, abi, functionName: 'hasAccount', args: [userAddress] });
529 return Boolean(result);
530 } catch (error) {
531 console.error(`[EvmClient] hasAccount FAILED for ${userAddress} (returning false):`, error);
532 return false;
533 }
534 },
535 userInfos: (userAddress) =>
536 this.read({
537 contractKey,
538 contractName,
539 functionName: 'userInfos',
540 args: [userAddress],
541 transform: (result: any): [string, string, string, number] => [
542 String((result.username ?? result[0]) || ''),
543 String((result.country ?? result[1]) || ''),
544 String((result.language ?? result[2]) || ''),
545 Number((result.newUserId ?? result[3]) || 0),
546 ],
547 }),
548 userData: (userId) =>
549 this.read({
550 contractKey,
551 contractName,
552 functionName: 'userData',
553 args: [userId],
554 transform: (result: any): [number, string, string, string, string] => [
555 Number(result.id ?? result[0]),
556 String(result.addr ?? result[1]),
557 String(result.username ?? result[2]),
558 String(result.language ?? result[3]),
559 String(result.country ?? result[4]),
560 ],
561 }),
562 userIdOf: (userAddress) => this.read({ contractKey, contractName, functionName: 'userIdOf', args: [userAddress], transform: Number }),
563 userIdToUsername: (userId) =>
564 this.read({ contractKey, contractName, functionName: 'userIdToUsername', args: [BigInt(userId)], transform: String }),
565 userAddr: (userId) => this.read({ contractKey, contractName, functionName: 'userAddr', args: [BigInt(userId)], transform: String }),
566 getUsernameByAddress: (userAddress) =>
567 this.read({ contractKey, contractName, functionName: 'getUsernameByAddress', args: [userAddress], transform: String }),
568 getAccountCountByCountryLanguage: (country, language) =>
569 this.read({
570 contractKey,
571 contractName,
572 functionName: 'getAccountCountByCountryLanguage',
573 args: [country, language],
574 transform: Number,
575 }),
576 nbAccounts: () => this.read({ contractKey, contractName, functionName: 'nbAccounts', transform: Number }),
577 getAllLocationCounts: () =>
578 this.read({
579 contractKey,
580 contractName,
581 functionName: 'getAllLocationCounts',
582 transform: (result: any[]) =>
583 result.map((data) => ({
584 country: String(data.country ?? data[0]),
585 language: String(data.language ?? data[1]),
586 count: Number(data.count ?? data[2]),
587 })),
588 }),
589 getActiveLocationCount: () => this.read({ contractKey, contractName, functionName: 'getActiveLocationCount', transform: Number }),
590 identityStats: () =>
591 this.read({
592 contractKey,
593 contractName,
594 functionName: 'identityStats',
595 transform: (result: any): [number, number] => [Number(result[0]), Number(result[1])],
596 }),
597 isAuthorized: (userAddress) => this.read({ contractKey, contractName, functionName: 'isAuthorized', args: [userAddress], transform: Boolean }),
598 getLatestRegisteredUser: async () => {
599 try {
600 const count = await this.bouncerStorage.nbAccounts();
601 if (count === 0) return null;
602 return await this.bouncerStorage.userAddr(count);
603 } catch (error) {
604 console.error('[EvmClient] getLatestRegisteredUser FAILED:', error);
605 return null;
606 }
607 },
608 getTokenName: () => this.read({ contractKey, contractName, functionName: 'getTokenName', transform: String }),
609 usernameToUserId: (username) => this.read({ contractKey, contractName, functionName: 'usernameToUserId', args: [username], transform: Number }),
610 };
611 }
612
613 private createScientistStorageReadAPI(): ScientistStorageReadAPI {
614 const contractKey = 'scientistStorage' as const;
615 const contractName = 'ScientistStorage';
616 return {
617 getDeploymentDate: () =>
618 this.read({
619 contractKey,
620 contractName,
621 functionName: 'getDeploymentDate',
622 transform: (result: any): [number, number] => [Number(result[0]), Number(result[1])],
623 }),
624 getLastMonthId: () => this.read({ contractKey, contractName, functionName: 'getLastMonthId', transform: Number }),
625 getStatsRaw: (action, objectType, monthId) =>
626 this.read({
627 contractKey,
628 contractName,
629 functionName: 'getStatsRaw',
630 args: [BigInt(action), BigInt(objectType), BigInt(monthId)],
631 transform: (result: any): [bigint[], number[]] => [
632 (result[0] as bigint[]).map((v) => BigInt(v)),
633 (result[1] as any[]).map((v) => Number(v)),
634 ],
635 }),
636 isAuthorized: (userAddress) => this.read({ contractKey, contractName, functionName: 'isAuthorized', args: [userAddress], transform: Boolean }),
637 };
638 }
639
640 private createFactoryReadAPI(): FactoryReadAPI {
641 const contractKey = 'factory' as const;
642 const contractName = 'Factory';
643 return {
644 nbWritesByUsername: (username) =>
645 this.read({ contractKey, contractName, functionName: 'nbWritesByUsername', args: [username], transform: Number }),
646 loadUsername: (username) =>
647 this.read({ contractKey, contractName, functionName: 'loadUsername', args: [username], transform: toActionsTuple }),
648 loadUsernameSince: (username, index) =>
649 this.read({ contractKey, contractName, functionName: 'loadUsernameSince', args: [username, index], transform: toActionsTuple }),
650 load: () => this.read({ contractKey, contractName, functionName: 'load', transform: toActionsTuple }),
651 isAuthorized: (userAddress) => this.read({ contractKey, contractName, functionName: 'isAuthorized', args: [userAddress], transform: Boolean }),
652 owner: () => this.read({ contractKey, contractName, functionName: 'owner', transform: String }),
653 oracleSigner: () => this.read({ contractKey, contractName, functionName: 'oracleSigner', transform: String }),
654 bouncerStorage: () => this.read({ contractKey, contractName, functionName: 'bouncerStorage', transform: String }),
655 scientistStorage: () => this.read({ contractKey, contractName, functionName: 'scientistStorage', transform: String }),
656 backupStorage: () => this.read({ contractKey, contractName, functionName: 'backupStorage', transform: String }),
657 projectManagerStorage: () => this.read({ contractKey, contractName, functionName: 'projectManagerStorage', transform: String }),
658 usedPaymentIntents: (paymentIntentId) =>
659 this.read({ contractKey, contractName, functionName: 'usedPaymentIntents', args: [paymentIntentId], transform: Boolean }),
660 };
661 }
662
663 private createSubscriptionManagerReadAPI(): SubscriptionManagerReadAPI {
664 const contractKey = 'subscriptionManager' as const;
665 const contractName = 'SubscriptionManager';
666 return {
667 getSubscriptions: (userId): Promise<number[]> =>
668 this.read({
669 contractKey,
670 contractName,
671 functionName: 'getSubscriptions',
672 args: [userId],
673 transform: (result: readonly number[]) => [...result],
674 whenAddressMissing: [],
675 }),
676 getSubscriptionCount: (userId): Promise<bigint> =>
677 this.read({ contractKey, contractName, functionName: 'getSubscriptionCount', args: [userId], transform: BigInt, whenAddressMissing: 0n }),
678 isSubscribed: (subscriber, target): Promise<boolean> =>
679 this.read({
680 contractKey,
681 contractName,
682 functionName: 'isSubscribed',
683 args: [subscriber, target],
684 transform: Boolean,
685 whenAddressMissing: false,
686 }),
687 scoreOf: (userId): Promise<bigint> =>
688 this.read({ contractKey, contractName, functionName: 'scoreOf', args: [userId], transform: BigInt, whenAddressMissing: 0n }),
689 };
690 }
691
692 private createLibraryRegistryStorageReadAPI(): LibraryRegistryStorageReadAPI {
693 const contractKey = 'libraryRegistryStorage' as const;
694 const contractName = 'LibraryRegistryStorage';
695 return {
696 getUnverifiedCollectionIds: () =>
697 this.read({
698 contractKey,
699 contractName,
700 functionName: 'getUnverifiedCollectionIds',
701 transform: (result: readonly string[]) => [...result],
702 }),
703 getVerifiedCollectionIds: () =>
704 this.read({
705 contractKey,
706 contractName,
707 functionName: 'getVerifiedCollectionIds',
708 transform: (result: readonly string[]) => [...result],
709 }),
710 getCollection: (id) =>
711 this.read({
712 contractKey,
713 contractName,
714 functionName: 'getCollection',
715 args: [id],
716 transform: (result: any): Collection => ({
717 id: String(result.id),
718 slug: String(result.slug),
719 manifestUrl: String(result.manifestUrl),
720 importerUsername: String(result.importerUsername),
721 owner: String(result.owner),
722 verified: Boolean(result.verified),
723 fileCount: Number(result.fileCount),
724 libraryType: Number(result.libraryType),
725 publicLibraryId: Number(result.publicLibraryId),
726 country: String(result.country),
727 language: String(result.language),
728 isPublic: Boolean(result.isPublic),
729 createdAt: Number(result.createdAt),
730 updatedAt: Number(result.updatedAt),
731 initialBookCount: Number(result.initialBookCount),
732 dSafeBookCount: Number(result.dSafeBookCount),
733 importStatus: Number(result.importStatus),
734 published: Boolean(result.published),
735 }),
736 }),
737 };
738 }
739
740 private createPublicLibraryStorageReadAPI(): PublicLibraryStorageReadAPI {
741 const contractKey = 'publicLibraryStorage' as const;
742 const contractName = 'PublicLibraryStorage';
743 return {
744 libraryExists: (id) =>
745 this.read({
746 contractKey,
747 contractName,
748 functionName: 'libraryExists',
749 args: [id],
750 transform: Boolean,
751 }),
752 getPublicLibrary: (id) =>
753 this.read({
754 contractKey,
755 contractName,
756 functionName: 'getPublicLibrary',
757 args: [id],
758 transform: (result: any): PublicLibrary => ({
759 id: Number(result.id),
760 name: String(result.name),
761 physicalAddress: String(result.physicalAddress),
762 country: String(result.country),
763 language: String(result.language),
764 ownership: Number(result.ownership),
765 website: String(result.website),
766 preferredFormat: Number(result.preferredFormat),
767 createdAt: Number(result.createdAt),
768 updatedAt: Number(result.updatedAt),
769 }),
770 }),
771 nbLibraries: () =>
772 this.read({
773 contractKey,
774 contractName,
775 functionName: 'nbLibraries',
776 transform: Number,
777 }),
778 };
779 }
780
781 public async getRegistryStatus(): Promise<any> {
782 await this.ready;
783 return addressLoader.getLoadingStatus(this.chainId);
784 }
785}
786
787export function createEvmClient(
788 chainId: number = DEFAULT_CHAIN_ID,
789 environment: 'production' | 'staging' = 'production'
790): EvmClient {
791 return new EvmClient(chainId, environment);
792}
793