Files
self/contracts/test/utils/formatter.ts
Evi Nova 8c5b90e89f Contracts cleanup (#1311)
* refactor: use singular ETHERSCAN_API_KEY in .env

Etherscan has unified all keys of associated explorers like Celoscan into a singular key rather than different keys for different networks.

* refactor: use one .env instead of separate .env.test + .env files

* refactor: deploy contracts with runs of 1000 instead of 200

Decreases gas cost of function calls on deployed contracts

* clean: remove duplicate/redundant deploy modules + scripts

* clean: cleanup empty script file

* refactor: cleanup default network of scripts

Read network from .env instead of using defaults of alfajores (outdated) or staging

* clean: remove references to Alfajores, replace with Sepolia

* chore: add default .env variables

* chore: update build-all script to include aardhaar circuit

* chore: update broken Powers of Tau download link (use iden3)

* chore: remove duplicate script

* fix: use stable version 18 for disclose circuits

* test: update test import paths to allow for .ts version of generateProof

* test: fix broken tests

* test: uncomment critical code for registration, change error names to updated names, fix broken import paths, update disclose tests for new scope generation/handling

* fix: broken import path

* test: fix Airdrop tests to use V2 logic

* docs: update docs for necessary prerequisite programs

* chore: yarn prettier formatting

* fix: CI errors occuring when deploying contracts as can't read .env

Using a dummy key for CI builds

* chore: yarn prettier

* refactor: change runs to 100000
2025-10-27 11:50:19 +01:00

312 lines
10 KiB
TypeScript

export class Formatter {
static MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH = 40;
static formatName(input: string): [string, string] {
let lastName = "";
let firstName = "";
let i = 0;
while (i < input.length && input[i] !== "<") {
lastName += input[i];
i++;
}
i += 2;
while (i < input.length) {
if (input[i] === "<") {
if (i + 1 < input.length && input[i + 1] === "<") {
break;
}
firstName += " ";
} else {
firstName += input[i];
}
i++;
}
return [firstName, lastName];
}
static formatDate(date: string): string {
if (date.length !== 6) {
throw new Error("InvalidDateLength");
}
const dateBytes = Array.from(date);
for (let i = 0; i < 6; i++) {
if (dateBytes[i] < "0" || dateBytes[i] > "9") {
throw new Error("InvalidAsciiCode");
}
}
if (dateBytes[2] > "1" || (dateBytes[2] === "1" && dateBytes[3] > "2")) {
throw new Error("InvalidMonthRange");
}
if (dateBytes[4] > "3" || (dateBytes[4] === "3" && dateBytes[5] > "1")) {
throw new Error("InvalidDayRange");
}
const year = date.substring(0, 2);
const month = date.substring(2, 4);
const day = date.substring(4, 6);
return `${day}-${month}-${year}`;
}
static numAsciiToUint(numAscii: number): number {
if (numAscii < 48 || numAscii > 57) {
throw new Error("InvalidAsciiCode");
}
return numAscii - 48;
}
static fieldElementsToBytes(publicSignals: [bigint, bigint, bigint]): Uint8Array {
const bytesCount = [31, 31, 31];
const totalLength = 93;
const bytesArray = new Uint8Array(totalLength);
let index = 0;
for (let i = 0; i < 3; i++) {
let element = publicSignals[i];
for (let j = 0; j < bytesCount[i]; j++) {
const byte = Number(element & 0xffn);
bytesArray[index++] = byte;
element = element >> 8n;
}
}
return bytesArray;
}
static bytesToHexString(bytes: Uint8Array): string {
return (
"0x" +
Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
);
}
static extractForbiddenCountriesFromPacked(
revealedData_packed: string | string[],
id_type: "passport" | "id",
): string[] {
// If revealedData_packed is not an array, convert it to an array
const packedArray = Array.isArray(revealedData_packed) ? revealedData_packed : [revealedData_packed];
const bytesCount = id_type === "passport" ? [31, 31, 31] : [31, 31, 31, 27]; // nb of bytes in each of the first three field elements
const bytesArray = packedArray.flatMap((element: string, index: number) => {
const bytes = bytesCount[index] || 31; // Use 31 as default if index is out of range
const elementBigInt = BigInt(element);
const byteMask = BigInt(255); // 0xFF
const bytesOfElement = [...Array(bytes)].map((_, byteIndex) => {
return (elementBigInt >> (BigInt(byteIndex) * BigInt(8))) & byteMask;
});
return bytesOfElement;
});
return bytesArray.map((byte: bigint) => String.fromCharCode(Number(byte)));
}
static proofDateToUnixTimestamp(dateNum: number[]): number {
if (dateNum.length !== 6) {
throw new Error("Invalid dateNum length");
}
let date = "";
for (let i = 0; i < 6; i++) {
date += String.fromCharCode(48 + (dateNum[i] % 10));
}
return Formatter.dateToUnixTimestamp(date);
}
static dateToUnixTimestamp(date: string): number {
if (date.length !== 6) {
throw new Error("InvalidDateLength");
}
const yearPart = Formatter.substring(date, 0, 2);
const monthPart = Formatter.substring(date, 2, 4);
const dayPart = Formatter.substring(date, 4, 6);
const year = Formatter.parseDatePart(yearPart) + 2000;
const month = Formatter.parseDatePart(monthPart);
const day = Formatter.parseDatePart(dayPart);
return Formatter.toTimestamp(year, month, day);
}
static substring(str: string, startIndex: number, endIndex: number): string {
return str.substring(startIndex, endIndex);
}
static parseDatePart(value: string): number {
if (value.length === 0) {
return 0;
}
let result = 0;
for (let i = 0; i < value.length; i++) {
const digit = value.charCodeAt(i) - 48;
result = result * 10 + digit;
}
return result;
}
static toTimestamp(year: number, month: number, day: number): number {
let timestamp = 0;
const secondsInDay = 86400;
for (let i = 1970; i < year; i++) {
timestamp += Formatter.isLeapYear(i) ? 366 * secondsInDay : 365 * secondsInDay;
}
const monthDayCounts = [31, Formatter.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for (let i = 1; i < month; i++) {
timestamp += monthDayCounts[i - 1] * secondsInDay;
}
timestamp += (day - 1) * secondsInDay;
return timestamp;
}
static isLeapYear(year: number): boolean {
if (year % 4 !== 0) {
return false;
} else if (year % 100 !== 0) {
return true;
} else if (year % 400 !== 0) {
return false;
} else {
return true;
}
}
}
export class CircuitAttributeHandler {
static ISSUING_STATE_START = 2;
static ISSUING_STATE_END = 4;
static NAME_START = 5;
static NAME_END = 43;
static PASSPORT_NUMBER_START = 44;
static PASSPORT_NUMBER_END = 52;
static NATIONALITY_START = 54;
static NATIONALITY_END = 56;
static DATE_OF_BIRTH_START = 57;
static DATE_OF_BIRTH_END = 62;
static GENDER_START = 64;
static GENDER_END = 64;
static EXPIRY_DATE_START = 65;
static EXPIRY_DATE_END = 70;
static OLDER_THAN_START = 88;
static OLDER_THAN_END = 89;
static OFAC_START = 90;
static OFAC_END = 92;
static getIssuingState(input: string | Uint8Array): string {
const charcodes = this.normalizeInput(input);
return this.extractStringAttribute(charcodes, this.ISSUING_STATE_START, this.ISSUING_STATE_END);
}
static getName(input: string | Uint8Array): [string, string] {
const charcodes = this.normalizeInput(input);
const rawName = this.extractStringAttribute(charcodes, this.NAME_START, this.NAME_END);
return Formatter.formatName(rawName);
}
static getPassportNumber(input: string | Uint8Array): string {
const charcodes = this.normalizeInput(input);
return this.extractStringAttribute(charcodes, this.PASSPORT_NUMBER_START, this.PASSPORT_NUMBER_END);
}
static getNationality(input: string | Uint8Array): string {
const charcodes = this.normalizeInput(input);
return this.extractStringAttribute(charcodes, this.NATIONALITY_START, this.NATIONALITY_END);
}
static getDateOfBirth(input: string | Uint8Array): string {
const charcodes = this.normalizeInput(input);
const rawDate = this.extractStringAttribute(charcodes, this.DATE_OF_BIRTH_START, this.DATE_OF_BIRTH_END);
return Formatter.formatDate(rawDate);
}
static getGender(input: string | Uint8Array): string {
const charcodes = this.normalizeInput(input);
return this.extractStringAttribute(charcodes, this.GENDER_START, this.GENDER_END);
}
static getExpiryDate(input: string | Uint8Array): string {
const charcodes = this.normalizeInput(input);
const rawDate = this.extractStringAttribute(charcodes, this.EXPIRY_DATE_START, this.EXPIRY_DATE_END);
return Formatter.formatDate(rawDate);
}
static getOlderThan(input: string | Uint8Array): number {
const charcodes = this.normalizeInput(input);
const digit1 = Formatter.numAsciiToUint(charcodes[this.OLDER_THAN_START]);
const digit2 = Formatter.numAsciiToUint(charcodes[this.OLDER_THAN_START + 1]);
return digit1 * 10 + digit2;
}
static getPassportNoOfac(input: string | Uint8Array): number {
const charcodes = this.normalizeInput(input);
return charcodes[this.OFAC_START];
}
static getNameAndDobOfac(input: string | Uint8Array): number {
const charcodes = this.normalizeInput(input);
return charcodes[this.OFAC_START + 1];
}
static getNameAndYobOfac(input: string | Uint8Array): number {
const charcodes = this.normalizeInput(input);
return charcodes[this.OFAC_START + 2];
}
static compareOlderThan(input: string | Uint8Array, olderThan: number): boolean {
const charcodes = this.normalizeInput(input);
return this.getOlderThan(charcodes) >= olderThan;
}
/**
* Performs selective OFAC checks based on provided flags.
* @param input The input string or byte array containing passport attribute data.
* @param checkPassportNo Whether to check the passport number OFAC status.
* @param checkNameAndDob Whether to check the name and date of birth OFAC status.
* @param checkNameAndYob Whether to check the name and year of birth OFAC status.
* @returns True if all enabled checks pass (equal 1), false if any enabled check fails.
* @remarks Checks are only performed for flags that are set to true. If a flag is false,
* that particular check is considered to have passed regardless of its actual value.
*/
static compareOfac(
input: string | Uint8Array,
checkPassportNo: boolean,
checkNameAndDob: boolean,
checkNameAndYob: boolean,
): boolean {
const charcodes = this.normalizeInput(input);
return (
(!checkPassportNo || this.getPassportNoOfac(charcodes) === 1) &&
(!checkNameAndDob || this.getNameAndDobOfac(charcodes) === 1) &&
(!checkNameAndYob || this.getNameAndYobOfac(charcodes) === 1)
);
}
private static normalizeInput(input: string | Uint8Array): Uint8Array {
if (typeof input === "string") {
if (input.startsWith("0x")) {
const hex = input.slice(2);
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
return new TextEncoder().encode(input);
}
return input;
}
static extractStringAttribute(input: string | Uint8Array, start: number, end: number): string {
const charcodes = this.normalizeInput(input);
if (charcodes.length <= end) {
throw new Error("InsufficientCharcodeLen");
}
const attributeBytes = charcodes.slice(start, end + 1);
return new TextDecoder("utf-8").decode(attributeBytes);
}
}