catts-sdk
Version:
Facilitates the local development of C-ATTS recipes.
265 lines (264 loc) • 9.43 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.recipeSchema = exports.queriesSchema = exports.querySchema = exports.queryVariablesSchema = void 0;
exports.parseRecipe = parseRecipe;
exports.fetchQuery = fetchQuery;
exports.validateProcessorResult = validateProcessorResult;
exports.validateSchemaItems = validateSchemaItems;
exports.getSchemaUid = getSchemaUid;
const eas_sdk_1 = require("@ethereum-attestation-service/eas-sdk");
const ethers_1 = require("ethers");
const zod_1 = require("zod");
// SDK requests are proxied through the cloudflare caching worker to ensure
// consistent results with the smart contract canister
const CATTS_GQL_PROXY_URL = "https://query.catts.run";
/**
* Zod schema for query variables. Defines the shape of query variables to be used
* in GraphQL queries.
*/
exports.queryVariablesSchema = zod_1.z.lazy(() => zod_1.z.record(zod_1.z.union([exports.queryVariablesSchema, zod_1.z.string(), zod_1.z.number()])));
// Custom validation function for the name
const validateRecipeName = (val) => {
if (val.startsWith("-") || val.endsWith("-")) {
return false;
}
if (/^\d/.test(val)) {
return false;
}
return /^[a-z0-9-]+$/.test(val);
};
// Custom validation function for keywords
const validateKeyword = (keyword) => {
return /^[a-z0-9-]+$/.test(keyword);
};
const toString = zod_1.z.preprocess((input) => {
if (typeof input === "object") {
return JSON.stringify(input);
}
throw new Error("Expected 'variables' to be an object");
}, zod_1.z.string());
/**
* Zod schema for query variables. Defines the components of a GraphQL query,
* including endpoint and variables.
*/
exports.querySchema = zod_1.z
.object({
url: zod_1.z
.string()
.min(1, { message: "Endpoint must be at least 1 character long" })
.max(255, { message: "Endpoint must be at most 255 characters long" }),
filter: zod_1.z.string().optional().nullable(),
headers: zod_1.z.any().optional().nullable(),
body: zod_1.z
.object({
query: zod_1.z
.string()
.min(1, { message: "Query must be at least 1 character long" })
.max(1024, { message: "Query must be at most 1024 characters long" }),
variables: zod_1.z.any(),
})
.optional()
.nullable(),
})
.strict();
/**
* Zod schema for an array of queries.
*/
exports.queriesSchema = zod_1.z.array(exports.querySchema);
/**
* Zod schema for a recipe. Defines the structure of a recipe, including queries
* and output schema.
*/
exports.recipeSchema = zod_1.z
.object({
// Name validation
name: zod_1.z
.string()
.min(3, { message: "Name must be at least 3 characters long" })
.max(50, { message: "Name must be at most 50 characters long" })
.refine((val) => validateRecipeName(val), {
message: "Name must be lowercase, alphanumeric, may contain hyphens, must not start or end with a hyphen, and must not start with a digit",
}),
// Description validation
description: zod_1.z
.string()
.min(3, { message: "Description must be at least 3 characters long" })
.max(160, { message: "Description must be at most 160 characters long" })
.optional(),
// Keywords validation
keywords: zod_1.z
.array(zod_1.z
.string()
.min(3, {
message: "Each keyword must be at least 3 characters long",
})
.max(50, {
message: "Each keyword must be at most 50 characters long",
})
.refine((keyword) => validateKeyword(keyword), {
message: "Each keyword must be lowercase, alphanumeric, and may contain hyphens",
}))
.optional()
.refine((keywords) => keywords && keywords.length > 0, {
message: "Keywords must not be empty",
path: ["keywords"], // Specify the path for the error message
}),
// Queries validation
queries: zod_1.z.array(exports.querySchema),
// Schema validation (length constraints)
schema: zod_1.z
.string()
.min(1, { message: "Schema must be at least 1 character long" })
.max(512, { message: "Schema must be at most 512 characters long" }),
// Resolver validation (length constraint, exactly 42 characters)
resolver: zod_1.z
.string()
.length(42, { message: "Resolver must be exactly 42 characters long" }),
// Revokable validation
revokable: zod_1.z.boolean().refine((val) => val === false, {
message: "'revokable' should be false, revokable attestations are not yet supported",
}),
})
.strict();
// Define the basic schema value types
const schemaValueBase = zod_1.z.union([
zod_1.z.string(),
zod_1.z.boolean(),
zod_1.z.number(),
zod_1.z.bigint(),
]);
// Define the more complex schema value types
const schemaValueComplex = zod_1.z.union([
zod_1.z.record(zod_1.z.unknown()), // Record<string, unknown>
zod_1.z.array(zod_1.z.record(zod_1.z.unknown())), // Record<string, unknown>[]
zod_1.z.array(zod_1.z.unknown()), // unknown[]
]);
// Combine the base and complex schemas into a single SchemaValue schema
const SchemaValue = zod_1.z.union([schemaValueBase, schemaValueComplex]);
// Define the SchemaItem schema
const SchemaItem = zod_1.z
.object({
name: zod_1.z.string(),
type: zod_1.z.string(),
value: SchemaValue,
})
.strict();
function parseRecipe(input) {
return exports.recipeSchema.parse(input);
}
function substitutePlaceholders(args) {
const placeholders = {
"{user_eth_address}": args.placeHolderValues?.userEthAddress ||
"0x0000000000000000000000000000000000000000",
"{user_eth_address_lowercase}": args.placeHolderValues?.userEthAddress?.toLowerCase() ||
"0x0000000000000000000000000000000000000000",
};
let url = args.query.url;
for (const [key, value] of Object.entries(placeholders)) {
url = url.split(key).join(value);
}
let query = {
...args.query,
url,
};
if (args.query.filter) {
let filter = args.query.filter;
for (const [key, value] of Object.entries(placeholders)) {
filter = filter.split(key).join(value);
}
query = {
...query,
filter,
};
}
if (args.query.headers) {
let headers = JSON.stringify(args.query.headers);
for (const [key, value] of Object.entries(placeholders)) {
headers = headers.split(key).join(value);
}
query = {
...query,
headers: JSON.parse(headers),
};
}
if (args.query.body?.variables) {
let variables = JSON.stringify(args.query.body.variables);
for (const [key, value] of Object.entries(placeholders)) {
variables = variables.split(key).join(value);
}
query = {
...query,
body: {
...args.query.body,
variables: JSON.parse(variables),
},
};
}
return query;
}
/**
* Fetches the result of a query from the GraphQl endpoint specified in the query.
*
* @returns The result of the query in JSON format.
*/
async function fetchQuery(args) {
const query = substitutePlaceholders(args);
let proxyUrl = args?.proxyUrl || CATTS_GQL_PROXY_URL;
const cacheKey = args?.cacheKey || Math.random().toString(36).substring(2, 15);
proxyUrl = `${proxyUrl}/${cacheKey}`;
const options = {
method: "POST",
body: JSON.stringify(query),
};
if (args?.verbose) {
console.log("Query:", query);
}
const response = await fetch(proxyUrl, options);
if (args?.verbose) {
console.log("Response status:", response.status);
}
if (!response.ok) {
throw new Error(`Failed to fetch query from ${proxyUrl}`);
}
return response.json();
}
/**
* Validates that the processor result matches the output schema specified in the recipe.
*
* @param processorResult The raw result of the processor script as a string.
*
* @returns The result of the processor script in JSON format. On success, the output should match outputSchema specified in the recipe.
*/
async function validateProcessorResult({ processorResult, }) {
if (typeof processorResult !== "string") {
throw new Error("Processor result must be a string");
}
const json = JSON.parse(processorResult);
if (!Array.isArray(json)) {
throw new Error("Processor result must be an array");
}
if (json.length === 0) {
throw new Error("Processor returned an empty array");
}
let schemaItems;
try {
schemaItems = json.map((item) => SchemaItem.parse(item));
}
catch (error) {
throw new Error("Invalid processor result");
}
return schemaItems;
}
/**
* Validates that an array of schema items matches what is expected by the schema.
*/
async function validateSchemaItems({ schemaItems, schema, }) {
const schemaEncoder = new eas_sdk_1.SchemaEncoder(schema);
return schemaEncoder.encodeData(schemaItems);
}
/**
* An EAS schema UID is a hash of the schema, resolver and revokable flag.
*/
function getSchemaUid({ schema, resolver, revokable, }) {
return (0, ethers_1.solidityPackedKeccak256)(["string", "address", "bool"], [schema, resolver, revokable]);
}