Skip to content

Implement attestation validator #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,66 @@ export const DEFAULT_ENVIRONMENT: Environment = "production";

// The APIs we expose

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

const ENDPOINTS: { [key: string]: string } = {
test: "https://staging-api.hypercerts.org",
production: "https://api.hypercerts.org",
};

const SUPPORTED_EAS_SCHEMAS: { [key: string]: { [key: string]: string | boolean } } = {
BASIC_EVALUATION: {
uid: "0x2f4f575d5df78ac52e8b124c4c900ec4c540f1d44f5b8825fac0af5308c91449",
schema:
"uint256 chain_id,address contract_address,uint256 token_id,uint8 evaluate_basic,uint8 evaluate_work,uint8 evaluate_contributors,uint8 evaluate_properties,string comments,string[] tags",
resolver: ZERO_ADDRESS,
revocable: true,
},
CREATOR_FEED: {
uid: "0x48e3e1be1e08084b408a7035ac889f2a840b440bbf10758d14fb722831a200c3",
schema:
"uint256 chain_id,address contract_address,uint256 token_id,string title,string description,string[] sources",
resolver: ZERO_ADDRESS,
revocable: false,
},
};

// These are the deployments we manage
const DEPLOYMENTS: { [key in SupportedChainIds]: Deployment } = {
10: {
chainId: 10,
addresses: deployments[10],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
} as const,
42220: {
chainId: 42220,
addresses: deployments[42220],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
},
8453: {
chainId: 8453,
addresses: deployments[8453],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
} as const,
11155111: {
chainId: 11155111,
addresses: deployments[11155111],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: true,
} as const,
84532: {
chainId: 84532,
addresses: deployments[84532],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: true,
} as const,
42161: {
chainId: 42161,
addresses: deployments[42161],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
} as const,
421614: {
Expand Down
3 changes: 2 additions & 1 deletion src/types/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ export type Contracts =
| "StrategyHypercertFractionOffer";

/**
* Represents a deployment of a contract on a specific network.
* Represents the hypercerts deployments on a specific network.
*/
export type Deployment = {
chainId: SupportedChainIds;
/** The address of the deployed contract. */
addresses: Partial<Record<Contracts, `0x${string}`>>;
isTestnet: boolean;
easSchemas?: { [key: string]: { [key: string]: string | boolean } };
};

/**
Expand Down
61 changes: 61 additions & 0 deletions src/utils/tokenIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// https://github.com/hypercerts-org/hypercerts/blob/7671d06762c929bc2890a31e5dc392f8a30065c6/contracts/test/foundry/protocol/Bitshifting.t.sol

/**
* The maximum value that can be represented as an uint256.
* @type {BigInt}
*/
const MAX = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");

/**
* A mask that represents the base id of the token. It is created by shifting the maximum uint256 value left by 128 bits.
* @type {BigInt}
*/
const TYPE_MASK = MAX << BigInt(128);

/**
* A mask that represents the index of a non-fungible token. It is created by shifting the maximum uint256 value right by 128 bits.
* @type {BigInt}
*/
const NF_INDEX_MASK = MAX >> BigInt(128);

/**
* Checks if a token ID represents a base type token.
*
* A token ID is considered to represent a base type token if:
* - The bitwise AND of the token ID and the TYPE_MASK equals the token ID.
* - The bitwise AND of the token ID and the NF_INDEX_MASK equals 0.
*
* @param {BigInt} id - The token ID to check.
* @returns {boolean} - Returns true if the token ID represents a base type token, false otherwise.
*/
const isBaseType = (id: bigint) => {
return (id & TYPE_MASK) === id && (id & NF_INDEX_MASK) === BigInt(0);
};

/**
* Checks if a token ID represents a claim token.
*
* A token ID is considered to represent a claim token if it is not null and it represents a base type token.
*
* @param {BigInt} tokenId - The token ID to check. It can be undefined.
* @returns {boolean} - Returns true if the token ID represents a claim token, false otherwise.
*/
export const isHypercertToken = (tokenId?: bigint) => {
if (!tokenId) {
return false;
}
return isBaseType(tokenId);
};

/**
* Gets the claim token ID from a given token ID.
*
* The claim token ID is obtained by applying the TYPE_MASK to the given token ID using the bitwise AND operator.
* The result is logged to the console for debugging purposes.
*
* @param {BigInt} tokenId - The token ID to get the claim token ID from.
* @returns {BigInt} - Returns the claim token ID.
*/
export const getHypercertTokenId = (tokenId: bigint) => {
return tokenId & TYPE_MASK;
};
51 changes: 46 additions & 5 deletions src/validator/base/SchemaValidator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import Ajv, { Schema, ErrorObject } from "ajv";
import { IValidator, ValidationError, ValidationResult } from "../interfaces";
import Ajv, { Schema as AjvSchema, ErrorObject } from "ajv";
import { z } from "zod";

export abstract class SchemaValidator<T> implements IValidator<T> {
// Base interface for all validators
export interface ISchemaValidator<T> extends IValidator<T> {
validate(data: unknown): ValidationResult<T>;
}

// AJV-based validator
export abstract class AjvSchemaValidator<T> implements ISchemaValidator<T> {
protected ajv: Ajv;
protected schema: Schema;
protected schema: AjvSchema;

constructor(schema: Schema, additionalSchemas: Schema[] = []) {
constructor(schema: AjvSchema, additionalSchemas: AjvSchema[] = []) {
this.ajv = new Ajv({ allErrors: true });
// Add any additional schemas first
additionalSchemas.forEach((schema) => this.ajv.addSchema(schema));
this.schema = schema;
}
Expand Down Expand Up @@ -38,3 +44,38 @@ export abstract class SchemaValidator<T> implements IValidator<T> {
}));
}
}

// Zod-based validator
export abstract class ZodSchemaValidator<T> implements ISchemaValidator<T> {
protected schema: z.ZodType<T>;

constructor(schema: z.ZodType<T>) {
this.schema = schema;
}

validate(data: unknown): ValidationResult<T> {
const result = this.schema.safeParse(data);

if (!result.success) {
return {
isValid: false,
errors: this.formatErrors(result.error),
};
}

return {
isValid: true,
data: result.data,
errors: [],
};
}

protected formatErrors(error: z.ZodError): ValidationError[] {
return error.issues.map((issue) => ({
code: issue.code || "SCHEMA_VALIDATION_ERROR",
message: issue.message,
field: issue.path.join("."),
details: issue,
}));
}
}
115 changes: 115 additions & 0 deletions src/validator/validators/AttestationValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { z } from "zod";
import { DEPLOYMENTS } from "../../constants";
import { ZodSchemaValidator } from "../base/SchemaValidator";
import { isHypercertToken } from "src/utils/tokenIds";

const AttestationSchema = z
.object({
chain_id: z.coerce.bigint(),
contract_address: z.string(),
token_id: z.coerce.bigint(),
})
.passthrough()
.refine(
(data) => {
return Number(data.chain_id) in DEPLOYMENTS;
},
(data) => ({
code: "INVALID_CHAIN_ID",
message: `Chain ID ${data.chain_id.toString()} is not supported`,
path: ["chain_id"],
}),
)
.refine(
(data) => {
const deployment = DEPLOYMENTS[Number(data.chain_id) as keyof typeof DEPLOYMENTS];
if (!deployment?.addresses) {
return false;
}
const knownAddresses = Object.values(deployment.addresses).map((addr) => addr.toLowerCase());
return knownAddresses.includes(data.contract_address.toLowerCase());
},
(data) => ({
code: "INVALID_CONTRACT_ADDRESS",
message: `Contract address ${data.contract_address} is not deployed on chain ${data.chain_id.toString()}`,
path: ["contract_address"],
}),
)
.refine(
(data) => {
return isHypercertToken(data.token_id);
},
(data) => ({
code: "INVALID_TOKEN_ID",
message: `Token ID ${data.token_id.toString()} is not a valid hypercert token`,
path: ["token_id"],
}),
);

type AttestationData = z.infer<typeof AttestationSchema>;

// Example raw attestation

// {
// "uid": "0x4f923f7485e013d3c64b55268304c0773bb84d150b4289059c77af0e28aea3f6",
// "data": "0x000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000822f17a9a5eecfd66dbaff7946a8071c265d1d0700000000000000000000000000009c0900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b5a757a616c752032303233000000000000000000000000000000000000000000",
// "time": 1727969021,
// "refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
// "schema": "0x2f4f575d5df78ac52e8b124c4c900ec4c540f1d44f5b8825fac0af5308c91449",
// "attester": "0x676703E18b2d03Aa36d6A3124B4F58716dBf61dB",
// "recipient": "0x0000000000000000000000000000000000000000",
// "revocable": false,
// "expirationTime": 0,
// "revocationTime": 0
// }

// Example decoded attestation data

// {
// "tags": [
// "Zuzalu 2023"
// ],
// "chain_id": 10,
// "comments": "",
// "token_id": 1.3592579146656887e+43,
// "evaluate_work": 1,
// "evaluate_basic": 1,
// "contract_address": "0x822F17A9A5EeCFd66dBAFf7946a8071C265D1d07",
// "evaluate_properties": 1,
// "evaluate_contributors": 1
// }

// Example raw attestation data

// {
// "uid": "0xc6b717cfbf9df516c0cbdc670fdd7d098ae0a7d30b2fb2c1ff7bd15a822bf1f4",
// "data": "0x0000000000000000000000000000000000000000000000000000000000aa36a7000000000000000000000000a16dfb32eb140a6f3f2ac68f41dad8c7e83c4941000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000001e54657374696e67206164646974696f6e616c206174746573746174696f6e0000000000000000000000000000000000000000000000000000000000000000000877757575757575740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000003a7b2274797065223a2275726c222c22737263223a2268747470733a2f2f7078686572652e636f6d2f656e2f70686f746f2f31333833373237227d00000000000000000000000000000000000000000000000000000000000000000000000000b27b2274797065223a22696d6167652f6a706567222c226e616d65223a22676f61745f62726f776e5f616e696d616c5f6e61747572655f62696c6c795f676f61745f6d616d6d616c5f63726561747572655f686f726e732d3635363235322d313533313531373336392e6a7067222c22737263223a226261666b72656964676d613237367a326d756178717a79797467676979647437627a617073736479786b7333376737736f37377372347977776775227d0000000000000000000000000000",
// "time": 1737648084,
// "refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
// "schema": "0x48e3e1be1e08084b408a7035ac889f2a840b440bbf10758d14fb722831a200c3",
// "attester": "0xdf2C3dacE6F31e650FD03B8Ff72beE82Cb1C199A",
// "recipient": "0x0000000000000000000000000000000000000000",
// "revocable": false,
// "expirationTime": 0,
// "revocationTime": 0
// }

// Example decoded attestation data

// {
// "title": "Testing additional attestation",
// "sources": [
// "{\"type\":\"url\",\"src\":\"https://pxhere.com/en/photo/1383727\"}",
// "{\"type\":\"image/jpeg\",\"name\":\"goat_brown_animal_nature_billy_goat_mammal_creature_horns-656252-1531517369.jpg\",\"src\":\"bafkreidgma276z2muaxqzyytggiydt7bzapssdyxks37g7so77sr4ywwgu\"}"
// ],
// "chain_id": 11155111,
// "token_id": 2.0416942015256308e+41,
// "description": "wuuuuuut",
// "contract_address": "0xa16DFb32Eb140a6f3F2AC68f41dAd8c7e83C4941"
// }

export class AttestationValidator extends ZodSchemaValidator<AttestationData> {
constructor() {
super(AttestationSchema);
}
}
6 changes: 3 additions & 3 deletions src/validator/validators/MetadataValidator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HypercertClaimdata, HypercertMetadata } from "src/types/metadata";
import { SchemaValidator } from "../base/SchemaValidator";
import { AjvSchemaValidator } from "../base/SchemaValidator";
import claimDataSchema from "../../resources/schema/claimdata.json";
import metaDataSchema from "../../resources/schema/metadata.json";
import { PropertyValidator } from "./PropertyValidator";

export class MetadataValidator extends SchemaValidator<HypercertMetadata> {
export class MetadataValidator extends AjvSchemaValidator<HypercertMetadata> {
private propertyValidator: PropertyValidator;

constructor() {
Expand Down Expand Up @@ -36,7 +36,7 @@ export class MetadataValidator extends SchemaValidator<HypercertMetadata> {
}
}

export class ClaimDataValidator extends SchemaValidator<HypercertClaimdata> {
export class ClaimDataValidator extends AjvSchemaValidator<HypercertClaimdata> {
constructor() {
super(claimDataSchema);
}
Expand Down
4 changes: 2 additions & 2 deletions src/validator/validators/PropertyValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ValidationError } from "../interfaces";
import { SchemaValidator } from "../base/SchemaValidator";
import { AjvSchemaValidator } from "../base/SchemaValidator";
import { HypercertMetadata } from "src/types";
import metaDataSchema from "../../resources/schema/metadata.json";

Expand Down Expand Up @@ -65,7 +65,7 @@ class GeoJSONValidationStrategy implements PropertyValidationStrategy {
}
}

export class PropertyValidator extends SchemaValidator<PropertyValue> {
export class PropertyValidator extends AjvSchemaValidator<PropertyValue> {
private readonly validationStrategies: Record<string, PropertyValidationStrategy> = {
geoJSON: new GeoJSONValidationStrategy(),
};
Expand Down
26 changes: 26 additions & 0 deletions test/utils/tokenIds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, it, describe } from "vitest";

import { isHypercertToken, getHypercertTokenId } from "../../src/utils/tokenIds";

const claimTokenId = 340282366920938463463374607431768211456n;
const fractionTokenId = 340282366920938463463374607431768211457n;

describe("isClaimTokenId", () => {
it("should return true for a claim token id", () => {
expect(isHypercertToken(claimTokenId)).toBe(true);
});

it("should return false for a non-claim token id", () => {
expect(isHypercertToken(fractionTokenId)).toBe(false);
});
});

describe("getClaimTokenId", () => {
it("should return the claim token id", () => {
expect(getHypercertTokenId(claimTokenId)).toBe(claimTokenId);
});

it("should return the claim token id for a fraction token id", () => {
expect(getHypercertTokenId(fractionTokenId)).toBe(claimTokenId);
});
});
Loading