With the preparations done, the front-end code calling for the generation can be implemented.
Firstly, we define a function for reading the prover data:
/** * Get prover data (separately loaded because the large json should not slow down initial site loading). * * @param path - Path to the prover data json file (relative to the public folder). * @returns JSON object with the prover data. */exportasyncfunctiongetProver(path:string) {constproverText=awaitfetch(path);constparsedFile=JSON.parse(awaitproverText.text());return parsedFile;}
Then we assemble the input data for the ZK proof. It depends on what the proof will be about. In this example, we build an input for a non-trivial proof (KYC, age>=18, fraud investigation). It includes the following custom inputs:
constprovider=newethers.providers.Web3Provider(window.ethereum);// fetch institution pubkey from chain because it is needed as proof inputconstinstitutionContract=newethers.Contract( institutionAddresses,galacticaInstitutionABI.abi,provider.getSigner(),);constinstitutionPubKeys: [string,string][] = [[BigNumber.from(awaitinstitutionContract.institutionPubKey(0)).toString(),BigNumber.from(awaitinstitutionContract.institutionPubKey(1)).toString(),]];constproofInput:any= {// general zkKYC inputs currentTime:awaitgetCurrentBlockTime(), dAppAddress, investigationInstitutionPubKey: institutionPubKeys,// specific inputs to prove that the holder is at least 18 years old currentYear:dateNow.getUTCFullYear().toString(), currentMonth: (dateNow.getUTCMonth() +1).toString(), currentDay:dateNow.getUTCDate().toString(), ageThreshold:'18',// the zkKYC itself is not needed here. It is filled by the snap for user privacy.};
This is everything we need for calling the snap to generate the proof.
import { generateZKProof, ZkCertProof, ZkCertStandard,} from'@galactica-net/snap-api';constres:ZkCertProof=awaitgenerateZKProof({ input: proofInput, prover:awaitgetProver("/provers/exampleMockDApp.json"), requirements: { zkCertStandard:ZkCertStandard.ZkKYC, registryAddress:addresses.zkKYCRegistry, }, userAddress:getUserAddress(), description:"This proof discloses that you hold a valid zkKYC and that your age is at least 18.", publicInputDescriptions: zkKYCAgeProofPublicInputDescriptions,});
On this call, the user reviews what data is disclosed by the proof and either accepts or rejects it. The generation is automatically rejected, if the user has not setup and imported a matching zkCert, in this example a gip1 zkKYC.
If the request fails, it throws an error. It might also happen that the prover is unable to find a proof for the given input. The error then contains a backtrace to the Circom component that fails to satisfy an assertion. This component can give a hint on what condition fails. This could be for example:
Incorrect inputs
The user is less than 18 years old, according to the zkKYC.
Input values having the wrong format. Be careful when converting between Circom field elements and EVM variables.
Circom returns values in decimal form and we need to convert them into hex numbers before sending them in an EVM transaction:
// this function convert the proof output from snarkjs to parameter format for onchain solidity verifierexportfunctionprocessProof(proof:any) {constpiA=proof.pi_a.slice(0,2).map((value:any) =>fromDecToHex(value,true));// for some reason the order of coordinate is reverseconstpiB= [ [proof.pi_b[0][1],proof.pi_b[0][0]].map((value) =>fromDecToHex(value,true), ), [proof.pi_b[1][1],proof.pi_b[1][0]].map((value) =>fromDecToHex(value,true), ), ];constpiC=proof.pi_c.slice(0,2).map((value:any) =>fromDecToHex(value,true));return [piA, piB, piC];}// this function processes the public inputsexportfunctionprocessPublicSignals(publicSignals:any) {returnpublicSignals.map((value:any) =>fromDecToHex(value,true));}
To simplify the user flow, we recommend to directly submit the proof after it has been generated:
// get contract to send proof toconstexampleDAppSC=newethers.Contract(addresses.mockDApp,mockDAppABI.abi, signer);let [a, b, c] =processProof(res.proof);let publicInputs =processPublicSignals(res.publicSignals);// this is the on-chain function that requires a ZKPlet tx =awaitexampleDAppSC.airdropToken(1, a, b, c, publicInputs);constreceipt=awaittx.wait();
On success, most smart contracts mint a verification soul-bound token (SBT) for the user. These usually unlock using the smart contract until the SBT expires. So users do not have to spend time generating a ZKP for every transaction.
The on-chain verification can also fail. The error often reveals which requirement failed. If the verification of the ZKP fails, make sure to check the following:
Is the prover (wasm and zkey) compatible with the verifier in the smart contract? Both are generated from the circom compilation and need to match.
Is the timing correct? ZKPs containing the current time have a validity limit.