UNPKG

@iden3/js-iden3-auth

Version:

iden3-auth implementation in JavaScript

174 lines (173 loc) 7.12 kB
import { Id } from '@iden3/js-iden3-core'; import { ethers } from 'ethers'; import { createInMemoryCache } from '@0xpolygonid/js-sdk'; import { Abi__factory } from '../state/types/ethers-contracts/index.js'; import { CONSTANTS } from '../constants.js'; import { isRootDoesNotExistError, isStateDoesNotExistError } from '@0xpolygonid/js-sdk'; /** * Ethereum-based state resolver that resolves identity states and GIST roots * from a smart contract deployed on an Ethereum-compatible blockchain. * * This resolver caches results with different TTL values: * - Latest states/roots: shorter TTL since they can transition to historical * - Historical states/roots: longer TTL since they are immutable once replaced */ export class EthStateResolver { /** * Creates a new EthStateResolver instance * @param rpcUrl - The RPC URL for the Ethereum-compatible blockchain * @param contractAddress - The address of the state contract * @param options - Optional configuration for caching and provider setup */ constructor(rpcUrl, contractAddress, options) { const url = new URL(rpcUrl); const ethersProvider = new ethers.providers.JsonRpcProvider({ skipFetchSetup: options?.skipFetchSetup ?? false, url: url.href, user: url.username || undefined, password: url.password || undefined }); this._contract = Abi__factory.connect(contractAddress, ethersProvider); // Store cache options for later use this._stateCacheOptions = { notReplacedTtl: options?.stateCacheOptions?.notReplacedTtl ?? CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY / 2, replacedTtl: options?.stateCacheOptions?.replacedTtl ?? CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY, maxSize: options?.stateCacheOptions?.maxSize ?? CONSTANTS.DEFAULT_CACHE_MAX_SIZE }; this._rootCacheOptions = { replacedTtl: options?.rootCacheOptions?.replacedTtl ?? CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY, notReplacedTtl: options?.rootCacheOptions?.notReplacedTtl ?? CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY / 2, maxSize: options?.rootCacheOptions?.maxSize ?? CONSTANTS.DEFAULT_CACHE_MAX_SIZE }; // Initialize cache instances this._stateResolveCache = options?.stateCacheOptions?.cache ?? createInMemoryCache({ maxSize: this._stateCacheOptions.maxSize, ttl: this._stateCacheOptions.replacedTtl }); this._rootResolveCache = options?.rootCacheOptions?.cache ?? createInMemoryCache({ maxSize: this._rootCacheOptions.maxSize, ttl: this._rootCacheOptions.replacedTtl }); } getCacheKey(id, state) { return `${id.toString()}-${state.toString()}`; } getRootCacheKey(root) { return root.toString(); } async resolve(id, state) { const cacheKey = this.getCacheKey(id, state); // Check cache first const cachedResult = await this._stateResolveCache?.get(cacheKey); if (cachedResult) { return cachedResult; } // Perform the actual resolution const result = await this.performResolve(id, state); // Cache the result with appropriate TTL based on whether it's latest or historical const ttl = result.transitionTimestamp === 0 ? this._stateCacheOptions.notReplacedTtl : this._stateCacheOptions.replacedTtl; await this._stateResolveCache?.set(cacheKey, result, ttl); return result; } async performResolve(id, state) { // check if id is genesis const isGenesis = isGenesisStateId(id, state); let contractState; try { contractState = await this._contract.getStateInfoByIdAndState(id, state); } catch (e) { if (isStateDoesNotExistError(e)) { if (isGenesis) { return { latest: true, genesis: isGenesis, state, transitionTimestamp: 0 }; } throw new Error('State is not genesis and not registered in the smart contract'); } throw e; } if (!contractState.id.eq(id)) { throw new Error(`state was recorded for another identity`); } if (!contractState.state.eq(state)) { if (contractState.replacedAtTimestamp.eq(0n)) { throw new Error(`no information about state transition`); } return { latest: false, genesis: false, state, transitionTimestamp: contractState.replacedAtTimestamp.toNumber() }; } return { latest: contractState.replacedAtTimestamp.isZero(), genesis: isGenesis, state, transitionTimestamp: contractState.replacedAtTimestamp.toNumber() }; } async rootResolve(root) { const cacheKey = this.getRootCacheKey(root); // Check cache first const cachedResult = await this._rootResolveCache?.get(cacheKey); if (cachedResult) { return cachedResult; } // Perform the actual root resolution const result = await this.performRootResolve(root); // Cache the result with appropriate TTL based on whether it's latest or historical const ttl = result.latest ? this._rootCacheOptions.notReplacedTtl : this._rootCacheOptions.replacedTtl; await this._rootResolveCache?.set(cacheKey, result, ttl); return result; } async performRootResolve(root) { let globalStateInfo; try { globalStateInfo = await this._contract.getGISTRootInfo(root); } catch (e) { if (isRootDoesNotExistError(e)) { throw new Error('GIST root does not exist in the smart contract'); } throw e; } if (!globalStateInfo.root.eq(root)) { throw new Error(`gist info contains invalid state`); } if (!globalStateInfo.replacedByRoot.eq(0n)) { if (globalStateInfo.replacedAtTimestamp.eq(0n)) { throw new Error(`state was replaced, but replaced time unknown`); } return { latest: false, state: root, transitionTimestamp: globalStateInfo.replacedAtTimestamp.toString(), genesis: false }; } return { latest: true, state: root, transitionTimestamp: 0, genesis: false }; } } export function isGenesisStateId(id, state) { const userID = Id.fromBigInt(id); const identifier = Id.idGenesisFromIdenState(userID.type(), state); return userID.equal(identifier); }