diff --git a/package-lock.json b/package-lock.json index d3435db..1e6349e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "did-resolver": "4.1.0", "eslint": "^8.13.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", diff --git a/package.json b/package.json index 44b9ab8..1610194 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "tsc-alias": "^1.8.8", "tsconfig-paths": "^3.14.2", "typechain": "^8.1.1", - "typescript": "^4.3.5" + "typescript": "^4.3.5", + "did-resolver": "4.1.0" } } diff --git a/src/index.ts b/src/index.ts index f738d8b..e6d8fb3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * as auth from '@lib/auth/auth'; export * as resolver from '@lib/state/resolver'; +export { default as VidosResolver } from '@lib/state/vidosResolver'; export * as protocol from '@lib/types-sdk'; export { core } from '@0xpolygonid/js-sdk'; diff --git a/src/state/vidosResolver.ts b/src/state/vidosResolver.ts new file mode 100644 index 0000000..1335cbf --- /dev/null +++ b/src/state/vidosResolver.ts @@ -0,0 +1,133 @@ +/* eslint-disable prettier/prettier */ +import { Id } from '@iden3/js-iden3-core'; +import { type IStateResolver, type ResolvedState, isGenesisStateId } from './resolver'; +import type { DIDDocument, DIDResolutionResult, VerificationMethod } from 'did-resolver'; + +/** + * Extended DID resolution result that includes additional information about Polygon ID resolution. + */ +type PolygonDidResolutionResult = DIDResolutionResult & { + didDocument: DIDDocument & { + verificationMethod: (VerificationMethod & { + info: { + id: string; + state: string; + replacedByState: string; + createdAtTimestamp: string; + replacedAtTimestamp: string; + createdAtBlock: string; + replacedAtBlock: string; + }; + global: { + root: string; + replacedByRoot: string; + createdAtTimestamp: string; + replacedAtTimestamp: string; + createdAtBlock: string; + replacedAtBlock: string; + }; + })[]; + }; +}; + +/** + * Implementation of {@link IStateResolver} that uses Vidos resolver service to resolve states. + * It can serve as drop-in replacement for EthStateResolver. + */ +export default class VidosResolver implements IStateResolver { + constructor(private readonly resolverUrl: string, private readonly apiKey: string, private readonly network: 'main' | 'mumbai' | 'amoy' = 'main') {} + + // Note: implementation closely resembles EthStateResolver because Vidos resolver internally uses the same contract. + // The only difference is the usage of regular HTTP requests instead of web3 calls. + + async rootResolve(state: bigint): Promise { + const stateHex = state.toString(16); + + const zeroAddress = '11111111111111111111'; // 1 is 0 in base58 + const did = `did:polygonid:polygon:${this.network}:${zeroAddress}?gist=${stateHex}`; + + const response = await fetch(`${this.resolverUrl}/${encodeURIComponent(did)}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${this.apiKey}` + } + }); + const result = (await response.json()) as PolygonDidResolutionResult; + if (result.didResolutionMetadata.error) { + throw new Error(`error resolving DID: ${result.didResolutionMetadata.error}`); + } + + const globalInfo = result.didDocument.verificationMethod[0].global; + if (globalInfo == null) throw new Error('gist info not found'); + + if (globalInfo.root !== stateHex) { + throw new Error('gist info contains invalid state'); + } + + if (globalInfo.replacedByRoot !== '0') { + if (globalInfo.replacedAtTimestamp === '0') { + throw new Error('state was replaced, but replaced time unknown'); + } + return { + latest: false, + state: state, + transitionTimestamp: globalInfo.replacedAtTimestamp, + genesis: false + }; + } + + return { + latest: true, + state: state, + transitionTimestamp: 0, + genesis: false + }; + } + + async resolve(id: bigint, state: bigint): Promise { + const iden3Id = Id.fromBigInt(id); + const stateHex = state.toString(16); + + const did = `did:polygonid:polygon:${this.network}:${iden3Id.string()}`; + + const didWithState = `${did}?state=${stateHex}`; + const response = await fetch(`${this.resolverUrl}/${encodeURIComponent(didWithState)}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${this.apiKey}` + } + }); + const result = (await response.json()) as PolygonDidResolutionResult; + if (result.didResolutionMetadata.error) { + throw new Error(`error resolving DID: ${result.didResolutionMetadata.error}`); + } + + const isGenesis = isGenesisStateId(id, state); + + const stateInfo = result.didDocument.verificationMethod[0].info; + if (stateInfo == null) throw new Error('state info not found'); + + if (stateInfo.id !== did) { + throw new Error(`state was recorded for another identity`); + } + + if (stateInfo.state !== stateHex) { + if (stateInfo.replacedAtTimestamp === '0') { + throw new Error(`no information about state transition`); + } + return { + latest: false, + genesis: false, + state: state, + transitionTimestamp: Number.parseInt(stateInfo.replacedAtTimestamp) + }; + } + + return { + latest: stateInfo.replacedAtTimestamp === '0', + genesis: isGenesis, + state, + transitionTimestamp: Number.parseInt(stateInfo.replacedAtTimestamp) + }; + } +}