@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
368 lines (335 loc) • 12.9 kB
text/typescript
import { CircuitId } from '../../circuits/models';
import { IProofService } from '../../proof/proof-service';
import { defaultAcceptProfile, PROTOCOL_MESSAGE_TYPE, ProtocolVersion } from '../constants';
import {
AcceptProfile,
BasicMessage,
IPackageManager,
ZeroKnowledgeInvokeResponse,
ZeroKnowledgeProofResponse
} from '../types';
import { ContractInvokeRequest, ContractInvokeResponse } from '../types/protocol/contract-request';
import { DID, ChainIds, getUnixTimestamp, BytesHelper } from '@iden3/js-iden3-core';
import { FunctionSignatures, IOnChainZKPVerifier } from '../../storage';
import { Signer } from 'ethers';
import { processProofAuth, processZeroKnowledgeProofRequests, verifyExpiresTime } from './common';
import {
AbstractMessageHandler,
BasicHandlerOptions,
IProtocolMessageHandler
} from './message-handler';
import { parseAcceptProfile } from '../utils';
import { hexToBytes } from '../../utils';
/**
* Interface that allows the processing of the contract request
*
* @beta
* @interface IContractRequestHandler
*/
export interface IContractRequestHandler {
/**
* unpacks contract invoke request
* @beta
* @param {Uint8Array} request - raw byte message
* @returns `Promise<ContractInvokeRequest>`
*/
parseContractInvokeRequest(request: Uint8Array): Promise<ContractInvokeRequest>;
/**
* handle contract invoke request
* @beta
* @param {did} did - sender DID
* @param {Uint8Array} request - raw byte message
* @param {ContractInvokeHandlerOptions} opts - handler options
* @returns {Map<string, ZeroKnowledgeProofResponse>}` - map of transaction hash - ZeroKnowledgeProofResponse
*/
handleContractInvokeRequest(
did: DID,
request: Uint8Array,
opts?: ContractInvokeHandlerOptions
): Promise<Map<string, ZeroKnowledgeProofResponse>>;
}
/** ContractInvokeHandlerOptions represents contract invoke handler options */
export type ContractInvokeHandlerOptions = BasicHandlerOptions & {
ethSigner: Signer;
challenge?: bigint;
};
export type ContractMessageHandlerOptions = BasicHandlerOptions & {
senderDid: DID;
ethSigner: Signer;
challenge?: bigint;
};
/**
*
* Allows to process ContractInvokeRequest protocol message
*
* @beta
* @class ContractRequestHandler
* @implements implements IContractRequestHandler interface
*/
export class ContractRequestHandler
extends AbstractMessageHandler
implements IContractRequestHandler, IProtocolMessageHandler
{
private readonly _supportedCircuits = [
CircuitId.AuthV2,
CircuitId.AtomicQueryMTPV2OnChain,
CircuitId.AtomicQuerySigV2OnChain,
CircuitId.AtomicQueryV3OnChain,
// Now we support off-chain circuits on-chain
// TODO: We need to create validators for them
CircuitId.AuthV2,
CircuitId.AtomicQueryV3,
CircuitId.LinkedMultiQuery10
];
/**
* Creates an instance of ContractRequestHandler.
* @param {IPackageManager} _packerMgr - package manager to unpack message envelope
* @param {IProofService} _proofService - proof service to verify zk proofs
* @param {IOnChainZKPVerifier} _zkpVerifier - zkp verifier to submit response
* @param {IOnChainVerifierMultiQuery} _verifierMultiQuery - verifier multi-query to submit response
*
*/
constructor(
private readonly _packerMgr: IPackageManager,
private readonly _proofService: IProofService,
private readonly _zkpVerifier: IOnChainZKPVerifier
) {
super();
}
async handle(
message: BasicMessage,
ctx: ContractMessageHandlerOptions
): Promise<BasicMessage | null> {
switch (message.type) {
case PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE: {
const ciMessage = message as ContractInvokeRequest;
const txHashResponsesMap = await this.handleContractInvoke(ciMessage, ctx);
return this.createContractInvokeResponse(ciMessage, txHashResponsesMap);
}
default:
return super.handle(message, ctx);
}
}
private async handleContractInvoke(
message: ContractInvokeRequest,
ctx: ContractMessageHandlerOptions
): Promise<Map<string, ZeroKnowledgeInvokeResponse>> {
if (message.type !== PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE) {
throw new Error('Invalid message type for contract invoke request');
}
const { senderDid: did, ethSigner, challenge } = ctx;
if (!ctx.ethSigner) {
throw new Error("Can't sign transaction. Provide Signer in options.");
}
const { chain_id } = message.body.transaction_data;
const networkFlag = Object.keys(ChainIds).find((key) => ChainIds[key] === chain_id);
if (!networkFlag) {
throw new Error(`Invalid chain id ${chain_id}`);
}
const verifierDid = message.from ? DID.parse(message.from) : undefined;
const { scope = [] } = message.body;
const zkpResponses = await processZeroKnowledgeProofRequests(
did,
scope,
verifierDid,
this._proofService,
{
ethSigner,
challenge: challenge ?? BytesHelper.bytesToInt(hexToBytes(await ethSigner.getAddress())),
supportedCircuits: this._supportedCircuits
}
);
const methodId = message.body.transaction_data.method_id.replace('0x', '');
switch (methodId) {
case FunctionSignatures.SubmitZKPResponseV2: {
const txHashZkpResponsesMap = await this._zkpVerifier.submitZKPResponseV2(
ethSigner,
message.body.transaction_data,
zkpResponses
);
const response = new Map<string, ZeroKnowledgeInvokeResponse>();
for (const [txHash, zkpResponses] of txHashZkpResponsesMap) {
response.set(txHash, { responses: zkpResponses });
}
// set txHash of the first response
message.body.transaction_data.txHash = txHashZkpResponsesMap.keys().next().value;
return response;
}
case FunctionSignatures.SubmitZKPResponseV1: {
const txHashZkpResponseMap = await this._zkpVerifier.submitZKPResponse(
ethSigner,
message.body.transaction_data,
zkpResponses
);
const response = new Map<string, ZeroKnowledgeInvokeResponse>();
for (const [txHash, zkpResponse] of txHashZkpResponseMap) {
response.set(txHash, { responses: [zkpResponse] });
}
// set txHash of the first response
message.body.transaction_data.txHash = txHashZkpResponseMap.keys().next().value;
return response;
}
case FunctionSignatures.SubmitResponse: {
// We need to
// 1. Generate auth proof from message.body.accept -> authResponse
// 2. Generate proofs for each query in scope -> zkpResponses
// Build auth response from accept
if (!message.to) {
throw new Error(`failed message. empty 'to' field`);
}
// Get first supported accept profile and pass it to processProofAuth
const acceptProfile = this.getFirstSupportedProfile(
PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE,
message.body.accept
);
const identifier = DID.parse(message.to);
const { authProof } = await processProofAuth(identifier, this._proofService, {
supportedCircuits: this._supportedCircuits,
acceptProfile,
senderAddress: await ethSigner.getAddress(),
zkpResponses: zkpResponses
});
// we return txHash because responsesMap could be empty if there are no queries in scope
const txHashZkpResponsesMap = await this._zkpVerifier.submitResponse(
ethSigner,
message.body.transaction_data,
zkpResponses,
authProof
);
message.body.transaction_data.txHash = txHashZkpResponsesMap.keys().next().value;
return txHashZkpResponsesMap;
}
default:
throw new Error(
`Not supported method id. Only '${FunctionSignatures.SubmitZKPResponseV1}, ${FunctionSignatures.SubmitZKPResponseV2} and ${FunctionSignatures.SubmitResponse} are supported.'`
);
}
}
private getFirstSupportedProfile(
responseType: string,
profile?: string[] | undefined
): AcceptProfile {
if (profile?.length) {
for (const acceptProfileString of profile) {
// 1. check protocol version
const acceptProfile = parseAcceptProfile(acceptProfileString);
const responseTypeVersion = Number(responseType.split('/').at(-2));
if (
acceptProfile.protocolVersion !== ProtocolVersion.V1 ||
(acceptProfile.protocolVersion === ProtocolVersion.V1 &&
(responseTypeVersion < 1 || responseTypeVersion >= 2))
) {
continue;
}
// 2. check packer support
if (this._packerMgr.isProfileSupported(acceptProfile.env, acceptProfileString)) {
return acceptProfile;
}
}
}
// if we don't have supported profiles, we use default
return defaultAcceptProfile;
}
/**
* unpacks contract-invoke request
* @beta
* @param {Uint8Array} request - raw byte message
* @returns `Promise<ContractInvokeRequest>`
*/
async parseContractInvokeRequest(request: Uint8Array): Promise<ContractInvokeRequest> {
const { unpackedMessage: message } = await this._packerMgr.unpack(request);
const ciRequest = message as unknown as ContractInvokeRequest;
if (message.type !== PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE) {
throw new Error('Invalid media type');
}
ciRequest.body.scope = ciRequest.body.scope || [];
return ciRequest;
}
/**
* creates contract invoke response
* @private
* @beta
* @param {ContractInvokeRequest} request - ContractInvokeRequest
* @param { Map<string, ZeroKnowledgeInvokeResponse>} responses - map tx hash to array of ZeroKnowledgeInvokeResponse
* @returns `Promise<ContractInvokeResponse>`
*/
private async createContractInvokeResponse(
request: ContractInvokeRequest,
txHashToZkpResponseMap: Map<string, ZeroKnowledgeInvokeResponse>
): Promise<ContractInvokeResponse> {
const contractInvokeResponse: ContractInvokeResponse = {
id: request.id,
thid: request.thid,
type: PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_RESPONSE_MESSAGE_TYPE,
from: request.to,
to: request.from,
body: {
transaction_data: request.body.transaction_data,
scope: []
},
created_time: getUnixTimestamp(new Date())
};
for (const [txHash, zkpResponses] of txHashToZkpResponseMap) {
for (const zkpResponse of zkpResponses.responses) {
contractInvokeResponse.body.scope.push({
txHash,
...zkpResponse
});
}
contractInvokeResponse.body = {
...contractInvokeResponse.body,
crossChainProof: zkpResponses.crossChainProof,
authProof: zkpResponses.authProof
};
}
return contractInvokeResponse;
}
/**
* handle contract invoke request
* supports only 0xb68967e2 method id
* @beta
* @deprecated
* @param {did} did - sender DID
* @param {ContractInvokeRequest} request - contract invoke request
* @param {ContractInvokeHandlerOptions} opts - handler options
* @returns {Map<string, ZeroKnowledgeProofResponse>}` - map of transaction hash - ZeroKnowledgeProofResponse
*/
async handleContractInvokeRequest(
did: DID,
request: Uint8Array,
opts: ContractInvokeHandlerOptions
): Promise<Map<string, ZeroKnowledgeProofResponse>> {
const ciRequest = await this.parseContractInvokeRequest(request);
if (!opts.allowExpiredMessages) {
verifyExpiresTime(ciRequest);
}
if (ciRequest.body.transaction_data.method_id !== FunctionSignatures.SubmitZKPResponseV1) {
throw new Error(`please use handle method to work with other method ids`);
}
if (ciRequest.type !== PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE) {
throw new Error('Invalid message type for contract invoke request');
}
const { ethSigner, challenge } = opts;
if (!ethSigner) {
throw new Error("Can't sign transaction. Provide Signer in options.");
}
const { chain_id } = ciRequest.body.transaction_data;
const networkFlag = Object.keys(ChainIds).find((key) => ChainIds[key] === chain_id);
if (!networkFlag) {
throw new Error(`Invalid chain id ${chain_id}`);
}
const verifierDid = ciRequest.from ? DID.parse(ciRequest.from) : undefined;
const zkpResponses = await processZeroKnowledgeProofRequests(
did,
ciRequest?.body?.scope,
verifierDid,
this._proofService,
{ ethSigner, challenge, supportedCircuits: this._supportedCircuits }
);
return this._zkpVerifier.submitZKPResponse(
ethSigner,
ciRequest.body.transaction_data,
zkpResponses
);
}
}