📄 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