import "dotenv/config";
import {
  prepare,
  credential,
  evm,
  credType,
  errors,
  user,
  issuer,
  utils,
} from "@galxe-identity-protocol/sdk";
import { ethers } from "ethers";
import { UpaClient, CircuitIdProofAndInputs, Proof } from "@nebrazkp/upa/sdk";
import * as fs from "fs";
// conviniently unwrap the result of a function call by throwing an error if the result is an error.
const unwrap = errors.unwrap;
// Use Ankr's free open rpc in this example.
const RPC = "https://rpc.ankr.com/eth_sepolia";
const provider = new ethers.JsonRpcProvider(RPC);
const context = "Proof Aggregation Tutorial Example Context";
// registered with this tx:
// https://sepolia.etherscan.io/tx/0x89c0cf5af6f6c0b6750bd6b3a4b93c02605d0ba1541c26387ed7da3ae7df3ffa
const circuidId = BigInt(
  "535783125321978663259414080602879573584328345995263811920911450103380255481"
);
// This is a dummy issuer's EVM address that has been registered on sepolia.
const dummyIssuerEvmAddr = "0xdeee54e0f3cbb7d5c4b2cb91d39c9c9b48a1b532";
// demonstration of the issuingProcess.
async function issuingProcess(userEvmAddr: string, userIdc: bigint) {
  const typeSpec = credType.primitiveTypes.unit;
  const tp = unwrap(credType.createTypeFromSpec(typeSpec));
  const contextID = credential.computeContextID(context);
  // Now, let's create the credential.
  const newCred = unwrap(
    credential.Credential.create(
      {
        type: tp,
        contextID: contextID,
        userID: BigInt(userEvmAddr),
      },
      {}
    )
  );
  newCred.attachments["creativity"] = "uncountable";
  // signing the credential.
  const issuerID = BigInt(dummyIssuerEvmAddr);
  const issuerChainID = BigInt(11155111); // sepolia
  // A mock private key for the signer, which is used to sign the credential.
  // This key has been registered and activated on sepolia by the dummy issuer.
  // pub key: 0x0435be315dd7c00c9ba151f7c811bde6598c2e1b1f30552a3fb07a34b6c91416a99883f708f3389842c8f43c1c272bb8210a68d25f09201887354d85e8e58beff0
  const dummyKey = utils.decodeFromHex(
    "0x8d06429619a08325ea79a575e1df14787be5223614403a9142360616811f7aea"
  );
  const issuerPk = dummyKey;
  // create a new issuer object using the private key, issuerID, and issuerChainID.
  const myIssuer = new issuer.BabyzkIssuer(issuerPk, issuerID, issuerChainID);
  // sign the credential to user's identity commitment, with a unique signature id and expiration date.
  myIssuer.sign(newCred, {
    sigID: BigInt(100),
    expiredAt: BigInt(
      Math.ceil(new Date().getTime() / 1000) + 7 * 24 * 60 * 60
    ), // assuming the credential will be expired after 7 days
    identityCommitment: userIdc,
  });
  // all done, return the credential to the owner.
  return newCred;
}
// demonstration of the proofGenProcess.
async function proofGenProcess(myCred: credential.Credential, u: user.User) {
  const externalNullifier = utils.computeExternalNullifier(
    "Galxe Identity Protocol tutorial's verification"
  );
  console.log("downloading proof generation gadgets...");
  const proofGenGagets = await user.User.fetchProofGenGadgetsByTypeID(
    myCred.header.type,
    provider
  );
  console.log("proof generation gadgets are downloaded successfully.");
  // Let's generate the proof.
  // Assume that we want to verify that the credential is still valid after 3 days.
  const expiredAtLowerBound = BigInt(
    Math.ceil(new Date().getTime() / 1000) + 3 * 24 * 60 * 60
  );
  // Do not reveal the credential's actual id, which is the evm address in this example
  const equalCheckId = BigInt(0);
  // Instead, claim to be Mr.Deadbeef. It's verifier's responsibility to verify that the pseudonym is who
  // he claims to be, after verifying the proof.
  const pseudonym = BigInt("0xdeadbeef");
  // Here we ignore the conditions and just use the options in the query, since unit credential needs no conditions.
  const proof = await u.genBabyzkProofWithQuery(
    u.getIdentityCommitment("evm")!,
    myCred,
    proofGenGagets,
    `
    {
      "options": {
        "expiredAtLowerBound": "${expiredAtLowerBound}",
        "externalNullifier": "${externalNullifier}",
        "equalCheckId": "${equalCheckId}",
        "pseudonym": "${pseudonym}"
      }
    }
    `
  );
  return proof;
}
async function verifyByCallingAggregatedStatefulVerifier(
  publicSignals: string[]
): Promise<boolean> {
  // As a verifier, we must decide the expected
  // contextID, issuerID, and typeID of the credential.
  const expectedContextID = credential.computeContextID(context);
  const expectedIssuerID = BigInt(dummyIssuerEvmAddr);
  const expectedTypeID = credType.primitiveTypes.unit.type_id;
  // Let's take a look at the on-chain verification first.
  // It is just 1 simple function call.
  const aggregatedStatefulVerifier =
    evm.v1.createAggregatedBabyzkStatefulVerifier({
      signerOrProvider: provider,
    });
  const statefulVerifierResult =
    await aggregatedStatefulVerifier.verifyProofFull(
      expectedTypeID,
      expectedContextID,
      expectedIssuerID,
      circuidId,
      publicSignals
    );
  if (statefulVerifierResult !== evm.VerifyResult.OK) {
    console.error(
      "Proof verification failed, reason: ",
      evm.verifyResultToString(statefulVerifierResult)
    );
  } else {
    console.log("On-chain stateful proof verification is successful.");
  }
  return true;
}
async function main() {
  // prepare must be called by the application before any other function.
  await prepare();
  // setup UPA
  const upaInstanceDescriptor = JSON.parse(
    fs.readFileSync("upa.instance", "ascii")
  );
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  const signer = new ethers.Wallet(process.env.NEBRA_SIGNER_PK!, provider);
  const upaClient = new UpaClient(signer, upaInstanceDescriptor);
  // The very first step is to create a user with a random identity.
  // This should be done on user's device and the identity should be stored securely.
  const u = new user.User();
  const evmIdSlice = u.createNewIdentitySlice("evm");
  // User's identity commitment is computed based on the secrets of the identity slice.
  // You can also retrive the identity commitment from the identity slice.
  const userIdc = user.User.computeIdentityCommitment(evmIdSlice);
  // let's use a famous Ethereum address in this example.
  const userEvmAddr = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
  // Issuer's process: issuing a credential to the user.
  const myCred = await issuingProcess(userEvmAddr, userIdc);
  console.log("Credential is issued successfully.");
  console.log(myCred.marshal(2));
  // User's process: generating a zk proof to prove some statements about the credential.
  const proof = await proofGenProcess(myCred, u);
  console.log("Proof is generated successfully.", proof);
  const circuitIdProofAndInputs: CircuitIdProofAndInputs[] = [
    {
      circuitId: circuidId,
      proof: Proof.from_snarkjs(proof.proof),
      inputs: proof.publicSignals.map((x) => BigInt(x)),
    },
  ];
  const submissionHandle = await upaClient.submitProofs(
    circuitIdProofAndInputs
  );
  const submitProofTxReceipt = await upaClient.waitForSubmissionVerified(
    submissionHandle
  );
  console.log("Proof is submitted successfully.", submitProofTxReceipt);
  // Now that our proof submission has been verified, we can just verify the
  // proof using public signals only.
  await verifyByCallingAggregatedStatefulVerifier(proof.publicSignals);
  process.exit(0);
}
main();