@patchworkdev/pdk
Version:
Patchwork Development Kit
216 lines (200 loc) • 6.78 kB
text/typescript
import { FieldType } from '@patchworkdev/common';
export type ContractJSONSchema = {
$schema: string;
scopeName: string;
name: string;
symbol: string;
schemaURI?: string;
imageURI?: string;
fields: ContractJSONSchemaField[];
};
export type ContractJSONSchemaField = {
id: number;
key: string;
permissionId?: number;
description?: string;
type: keyof FieldTypeMap;
arrayLength?: number;
visibility?: 'public' | 'private';
slot: number;
offset: number;
};
export type FieldTypeMap = {
bool: boolean;
int8: number;
int16: number;
int32: number;
uint8: number;
uint16: number;
uint32: number;
int64: bigint;
int128: bigint;
int256: bigint;
uint64: bigint;
uint128: bigint;
uint256: bigint;
char8: string;
char16: string;
char32: string;
char64: string;
bytes8: string;
bytes16: string;
bytes32: string;
literef: string;
address: `0x${string}`;
string: string;
};
export type InferSchemaType<
S extends ContractJSONSchema,
ExcludedKeys extends string = never, // Keys to exclude (default: none)
ExcludedTypes extends keyof FieldTypeMap = never, // Types to exclude (default: none)
> = {
[F in S['fields'][number]as F['key'] extends ExcludedKeys ? never : F['type'] extends ExcludedTypes ? never : F['key']]: F['arrayLength'] extends
| 1
| undefined
? FieldTypeMap[F['type']] | undefined
: FieldTypeMap[F['type']][] | undefined;
};
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
/*
* Given a parsed contract JSON schema and packed metadata (uint256 array), walks through each field and
* attemps to unpack its corresponding metadata from the packed array
*/
export async function unpackMetadata<S extends ContractJSONSchema, ExcludedKeys extends string = never, ExcludedTypes extends keyof FieldTypeMap = never>(
schema: S,
packedMetadata: readonly bigint[],
excludedKeys: ExcludedKeys[] = [],
excludedTypes: ExcludedTypes[] = [],
): Promise<Prettify<InferSchemaType<S, ExcludedKeys, ExcludedTypes>>> {
if (!schema?.fields?.length) throw new Error('Schema is empty');
const processedFields: Partial<InferSchemaType<S, ExcludedKeys, ExcludedTypes>> = {};
for (const field of schema.fields) {
// Skip fields based on key or type
if (excludedKeys.includes(field.key as ExcludedKeys)) continue;
if (excludedTypes.includes(field.type as ExcludedTypes)) continue;
const values = splitPackedValues(packedMetadata, Number(field.slot), Number(field.arrayLength || 1), getBitLength(field.type), Number(field.offset));
(processedFields as any)[field.key] =
field.arrayLength === 1 || field.arrayLength === undefined
? convertValue(values[0], field.type) // Single value
: values.map((v) => convertValue(v, field.type)); // Array of values
}
// Cast processedFields to InferSchemaType<S> safely
return processedFields as Prettify<InferSchemaType<S, ExcludedKeys, ExcludedTypes>>;
}
/*
* Given a bit window and full array of bitpacked uint256 bigints, walks through the array and attempts to split
* relevant bits into a new array of unpacked bigints of various lengths for the specific field
*/
export function splitPackedValues(uint256array: readonly bigint[], startingslot: number, fieldlength: number, bitlength: number, offset: number): bigint[] {
const values: bigint[] = [];
for (let i = 0; i < fieldlength; i++) {
const value = (uint256array[startingslot]! >> BigInt(offset)) & ((1n << BigInt(bitlength)) - 1n);
values.push(value);
offset += bitlength;
if (offset >= 256) {
startingslot++;
offset = 0;
}
}
return values;
}
/*
* Given a field type from a JSON contract schema, returns bit length of the field
*/
export function getBitLength(fieldType: FieldType): number {
switch (fieldType) {
case 'bool':
return 1;
case 'int8':
case 'uint8':
return 8;
case 'int16':
case 'uint16':
return 16;
case 'int32':
case 'uint32':
return 32;
case 'int64':
case 'uint64':
return 64;
case 'int128':
case 'uint128':
return 128;
case 'int256':
case 'uint256':
return 256;
case 'char8':
return 8 * 8;
case 'char16':
return 16 * 8;
case 'char32':
return 32 * 8;
case 'char64':
return 64 * 8;
case 'bytes8':
return 8 * 8;
case 'bytes16':
return 16 * 8;
case 'bytes32':
return 32 * 8;
case 'literef':
return 64;
case 'address':
return 160;
case 'string':
throw new Error('string type is not supported for packed metadata');
default:
throw new Error(`Unsupported field type: ${fieldType}`);
}
}
/*
* Given a raw bigint and a field type from a JSON contract schema, returns the converted value
*/
export function convertValue(value: bigint, fieldType: FieldType): boolean | number | string | bigint {
switch (fieldType) {
case 'bool':
return value !== 0n;
case 'int8':
case 'int16':
case 'int32':
case 'uint8':
case 'uint16':
case 'uint32':
case 'literef':
return Number(value);
case 'int64':
case 'int128':
case 'int256':
case 'uint64':
case 'uint128':
case 'uint256':
return BigInt(value);
case 'char8':
case 'char16':
case 'char32':
case 'char64':
// Convert bigint to a buffer and then to a string
const hexString = value.toString(16);
const utf8String = Buffer.from(hexString, 'hex')
.toString('utf8')
.replace(/[\0]+$/g, '');
const jankyCharacters = /[\uFFFD\u0000-\u001F]/;
if (jankyCharacters.test(utf8String)) {
return hexString;
} else {
return utf8String;
}
case 'bytes8':
case 'bytes16':
case 'bytes32':
return value.toString(16) as `0x${string}`;
case 'address':
return value.toString(16).padStart(40, '0') as `0x${string}`;
case 'string':
throw new Error('STRING type is not supported for packed metadata');
default:
throw new Error(`Unsupported field type for conversion: ${fieldType}`);
}
}