@iden3/js-iden3-auth
Version:
iden3-auth implementation in JavaScript
179 lines (178 loc) • 7.47 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isGenesisStateId = exports.EthStateResolver = void 0;
const js_iden3_core_1 = require("@iden3/js-iden3-core");
const ethers_1 = require("ethers");
const cache_1 = require("../cache");
const ethers_contracts_1 = require("../state/types/ethers-contracts");
const constants_1 = require("../constants");
const js_sdk_1 = require("@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
*/
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_1.ethers.providers.JsonRpcProvider({
skipFetchSetup: options?.skipFetchSetup ?? false,
url: url.href,
user: url.username || undefined,
password: url.password || undefined
});
this._contract = ethers_contracts_1.Abi__factory.connect(contractAddress, ethersProvider);
// Store cache options for later use
this._stateCacheOptions = {
notReplacedTtl: options?.stateCacheOptions?.notReplacedTtl ?? constants_1.CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY / 2,
replacedTtl: options?.stateCacheOptions?.replacedTtl ?? constants_1.CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY,
maxSize: options?.stateCacheOptions?.maxSize ?? constants_1.CONSTANTS.DEFAULT_CACHE_MAX_SIZE
};
this._rootCacheOptions = {
replacedTtl: options?.rootCacheOptions?.replacedTtl ?? constants_1.CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY,
notReplacedTtl: options?.rootCacheOptions?.notReplacedTtl ?? constants_1.CONSTANTS.ACCEPTED_STATE_TRANSITION_DELAY / 2,
maxSize: options?.rootCacheOptions?.maxSize ?? constants_1.CONSTANTS.DEFAULT_CACHE_MAX_SIZE
};
// Initialize cache instances
this._stateResolveCache =
options?.stateCacheOptions?.cache ??
(0, cache_1.createInMemoryCache)({
maxSize: this._stateCacheOptions.maxSize,
ttl: this._stateCacheOptions.replacedTtl
});
this._rootResolveCache =
options?.rootCacheOptions?.cache ??
(0, cache_1.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 ((0, js_sdk_1.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 ((0, js_sdk_1.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
};
}
}
exports.EthStateResolver = EthStateResolver;
function isGenesisStateId(id, state) {
const userID = js_iden3_core_1.Id.fromBigInt(id);
const identifier = js_iden3_core_1.Id.idGenesisFromIdenState(userID.type(), state);
return userID.equal(identifier);
}
exports.isGenesisStateId = isGenesisStateId;