From 59aa5702502559c28bd7adc8662422ff09f917fb Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Fri, 2 Aug 2024 22:39:24 +0200 Subject: [PATCH] fix(api): updated api client and minting flow --- pnpm-lock.yaml | 14 +- sdk/lib/hypercerts-api | 2 +- sdk/package.json | 3 +- sdk/src/__generated__/api.ts | 370 ++++++++++++---------- sdk/src/client.ts | 194 +++++------- sdk/src/index.ts | 11 + sdk/src/storage.ts | 21 +- sdk/src/types/storage.ts | 22 +- sdk/src/utils/allowlist.ts | 6 +- sdk/src/utils/config.ts | 37 +-- sdk/test/client.test.ts | 30 +- sdk/test/client/allowlist.minting.test.ts | 63 ++-- sdk/test/utils/config.test.ts | 10 +- 13 files changed, 404 insertions(+), 379 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d28c647..68b1cb58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,8 +236,8 @@ importers: specifier: 2.0.0-alpha.1 version: 2.0.0-alpha.1(bufferutil@4.0.8)(ts-node@10.9.1(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) '@openzeppelin/merkle-tree': - specifier: ^1.0.5 - version: 1.0.5 + specifier: ^1.0.7 + version: 1.0.7 '@swc/core': specifier: ^1.6.3 version: 1.6.3 @@ -2227,8 +2227,8 @@ packages: '@nomicfoundation/hardhat-verify': optional: true - '@openzeppelin/merkle-tree@1.0.5': - resolution: {integrity: sha512-JkwG2ysdHeIphrScNxYagPy6jZeNONgDRyqU6lbFgE8HKCZFSkcP8r6AjZs+3HZk4uRNV0kNBBzuWhKQ3YV7Kw==} + '@openzeppelin/merkle-tree@1.0.7': + resolution: {integrity: sha512-i93t0YYv6ZxTCYU3CdO5Q+DXK0JH10A4dCBOMlzYbX+ujTXm+k1lXiEyVqmf94t3sqmv8sm/XT5zTa0+efnPgQ==} '@openzeppelin/upgrades-core@1.33.1': resolution: {integrity: sha512-YRxIRhTY1b+j7+NUUu8Uuem5ugxKexEMVd8dBRWNgWeoN1gS1OCrhgUg0ytL+54vzQ+SGWZDfNnzjVuI1Cj1Zw==} @@ -12863,10 +12863,12 @@ snapshots: - supports-color - utf-8-validate - '@openzeppelin/merkle-tree@1.0.5': + '@openzeppelin/merkle-tree@1.0.7': dependencies: '@ethersproject/abi': 5.7.0 - ethereum-cryptography: 1.2.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 '@openzeppelin/upgrades-core@1.33.1': dependencies: diff --git a/sdk/lib/hypercerts-api b/sdk/lib/hypercerts-api index 3fa43414..b082eb2e 160000 --- a/sdk/lib/hypercerts-api +++ b/sdk/lib/hypercerts-api @@ -1 +1 @@ -Subproject commit 3fa434148d425d8f7a993c5173672d1c55b48f5c +Subproject commit b082eb2e44aae0444a77af622f6be65d92098cff diff --git a/sdk/package.json b/sdk/package.json index 89aa05d4..f15e3966 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -23,7 +23,7 @@ "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "@hypercerts-org/contracts": "2.0.0-alpha.1", - "@openzeppelin/merkle-tree": "^1.0.5", + "@openzeppelin/merkle-tree": "^1.0.7", "@swc/core": "^1.6.3", "ajv": "^8.11.2", "axios": "^1.7.2", @@ -59,7 +59,6 @@ }, "scripts": { "build": "pnpm types:json && pnpm codegen:api && rollup -c", - "codegen:graph": "graphql-codegen", "codegen:api": "npx orval --input ./lib/hypercerts-api/src/__generated__/swagger.json --output ./src/__generated__/api.ts", "clean": "rm -rf ./dist", "prebuild": "pnpm clean", diff --git a/sdk/src/__generated__/api.ts b/sdk/src/__generated__/api.ts index b49c4b05..99acc66f 100644 --- a/sdk/src/__generated__/api.ts +++ b/sdk/src/__generated__/api.ts @@ -7,81 +7,26 @@ */ import axios from "axios"; import type { AxiosRequestConfig, AxiosResponse } from "axios"; -export type ValidateAllowList200AnyOfTwo = { - errors?: unknown; - message: string; - success?: unknown; - valid: boolean; -}; - -export type ValidateAllowList200AnyOf = { - errors: RecordStringStringOrStringArray; - message: string; - success: boolean; - valid?: unknown; -}; - -export type ValidateAllowList200 = ValidateAllowList200AnyOf | ValidateAllowList200AnyOfTwo; - -export type StoreAllowList201AnyOfTwoData = { - cid: string; -}; - -export type StoreAllowList201AnyOfTwo = { - data: StoreAllowList201AnyOfTwoData; - errors?: unknown; - message?: unknown; - success: boolean; +export type ValidateOrder200DataItem = { + id: string; + invalidated: boolean; + validator_codes: OrderValidatorCode[]; }; -export type StoreAllowList201AnyOf = { - data?: unknown; - errors: RecordStringStringOrStringArray; +export type ValidateOrder200 = { + data: ValidateOrder200DataItem[]; message: string; success: boolean; }; -export type StoreAllowList201 = StoreAllowList201AnyOf | StoreAllowList201AnyOfTwo; - -export type UpdateOrderNonce200Data = { - address: string; - chain_id: number; - created_at: string; - nonce_counter: number; -}; - export type UpdateOrderNonce200 = { - data: UpdateOrderNonce200Data; + data: unknown; message: string; success: boolean; }; -export type StoreOrder201AnyOfTwoData = { - additionalParameters: string; - amounts: number[]; - chainId: number; - collection: string; - collectionType: number; - createdAt: string; - currency: string; - endTime: number; - globalNonce: string; - hash: string; - id: string; - itemIds: string[]; - orderNonce: string; - price: string; - quoteType: number; - signature: string; - signer: string; - startTime: number; - status: string; - strategyId: number; - subsetNonce: number; -}; - export type StoreOrder201AnyOfTwo = { - data: StoreOrder201AnyOfTwoData; + data: unknown; error?: unknown; message: string; success: boolean; @@ -96,88 +41,78 @@ export type StoreOrder201AnyOf = { export type StoreOrder201 = StoreOrder201AnyOf | StoreOrder201AnyOfTwo; -export type ValidateMetadata200AnyOfSix = { - errors?: unknown; - message: string; - valid: boolean; -}; - -export type ValidateMetadata200 = ValidateMetadata200AnyOf | ValidateMetadata200AnyOfSix; - -export type ValidateMetadata200AnyOfErrorsAnyOfThree = { - message: string; - name?: unknown; - receivedAllowlistCID: string; -}; - -export type ValidateMetadata200AnyOfErrorsAnyOfTwo = { - message: string; - name?: unknown; - receivedAllowlistCID?: unknown; -}; - -export type ValidateMetadata200AnyOfErrorsAnyOf = { - message: string; - name: string; - receivedAllowlistCID?: unknown; -}; - -export type ValidateMetadata200AnyOfErrors = - | ValidateMetadata200AnyOfErrorsAnyOf - | ValidateMetadata200AnyOfErrorsAnyOfTwo - | ValidateMetadata200AnyOfErrorsAnyOfThree; - -export type ValidateMetadata200AnyOf = { - errors: ValidateMetadata200AnyOfErrors; - message: string; - valid: boolean; -}; - -export type StoreMetadata201 = StoreMetadata201AnyOf | StoreMetadata201AnyOfTwo; - -export type StoreMetadata201AnyOfTwoErrorsAnyOfThree = { - message: string; - name?: unknown; - receivedAllowlistCID: string; -}; - -export type StoreMetadata201AnyOfTwoErrorsAnyOfTwo = { - message: string; - name?: unknown; - receivedAllowlistCID?: unknown; -}; - -export type StoreMetadata201AnyOfTwoErrorsAnyOf = { - message: string; - name: string; - receivedAllowlistCID?: unknown; -}; - -export type StoreMetadata201AnyOfTwoErrors = - | StoreMetadata201AnyOfTwoErrorsAnyOf - | StoreMetadata201AnyOfTwoErrorsAnyOfTwo - | StoreMetadata201AnyOfTwoErrorsAnyOfThree; - -export type StoreMetadata201AnyOfTwo = { - errors: StoreMetadata201AnyOfTwoErrors; - message: string; - valid: boolean; -}; - -export type StoreMetadata201AnyOf = { - cid: string; -}; +/** + * Interface for validating an allow list dump. + */ +export interface ValidateAllowListRequest { + allowList: string; + totalUnits?: string; +} /** - * Request body for creating a new allowlist. + * Interface for storing an allow list dump on IPFS */ -export interface CreateAllowListRequest { - /** The dump of the OpenZeppelin MerkleTree containing [address, uint256] entries. See https://github.com/OpenZeppelin/merkle-tree for more information. */ +export interface StoreAllowListRequest { allowList: string; - /** The total amount of units distributed via the allowlist. The total should amount to 1 eth in wei (1e18) units. */ - totalUnits: string; + totalUnits?: string; +} + +export interface ValidateOrderRequest { + chainId: number; + tokenIds: string[]; } +/** + * Error errors returned by the order validator contract + */ +export type OrderValidatorCode = (typeof OrderValidatorCode)[keyof typeof OrderValidatorCode]; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const OrderValidatorCode = { + NUMBER_0: 0, + NUMBER_101: 101, + NUMBER_111: 111, + NUMBER_112: 112, + NUMBER_113: 113, + NUMBER_201: 201, + NUMBER_211: 211, + NUMBER_212: 212, + NUMBER_213: 213, + NUMBER_301: 301, + NUMBER_311: 311, + NUMBER_312: 312, + NUMBER_321: 321, + NUMBER_322: 322, + NUMBER_401: 401, + NUMBER_402: 402, + NUMBER_411: 411, + NUMBER_412: 412, + NUMBER_413: 413, + NUMBER_414: 414, + NUMBER_415: 415, + NUMBER_421: 421, + NUMBER_422: 422, + NUMBER_501: 501, + NUMBER_502: 502, + NUMBER_503: 503, + NUMBER_601: 601, + NUMBER_611: 611, + NUMBER_612: 612, + NUMBER_621: 621, + NUMBER_622: 622, + NUMBER_623: 623, + NUMBER_631: 631, + NUMBER_632: 632, + NUMBER_633: 633, + NUMBER_634: 634, + NUMBER_701: 701, + NUMBER_702: 702, + NUMBER_801: 801, + NUMBER_802: 802, + NUMBER_901: 901, + NUMBER_902: 902, +} as const; + export interface UpdateOrderNonceRequest { address: string; chainId: number; @@ -203,24 +138,32 @@ export interface CreateOrderRequest { subsetNonce: number; } -export type ApiResponseErrors = RecordStringStringOrStringArray | Error[]; +export type ApiResponseValidationResultErrors = RecordStringStringOrStringArray | Error[]; -export interface ApiResponse { +/** + * Interface for a validation response. + */ +export interface ValidationResult { data?: unknown; - errors?: ApiResponseErrors; - message: string; - success: boolean; + errors?: RecordStringStringOrStringArray; + valid: boolean; } /** - * Response object for a validation request. + * Interface for a generic API response. */ -export interface ValidationResponse { - errors?: RecordStringStringOrStringArray; - message: string; - valid: boolean; +export interface ApiResponseValidationResult { + data?: ValidationResult; + errors?: ApiResponseValidationResultErrors; + message?: string; + success: boolean; } +/** + * Interface for a validation response. + */ +export type ValidationResponse = ApiResponseValidationResult; + export type HypercertMetadataPropertiesItem = { trait_type?: string; value?: string; @@ -249,6 +192,29 @@ export interface HypercertMetadata { version?: string; } +/** + * Interface for validating metadata. + */ +export interface ValidateMetadataRequest { + metadata: HypercertMetadata; +} + +/** + * Interface for storing metadata and allow list dump on IPFS. + */ +export interface StoreMetadataWithAllowlistRequest { + allowList: string; + metadata: HypercertMetadata; + totalUnits?: string; +} + +/** + * Interface for storing metadata on IPFS. + */ +export interface StoreMetadataRequest { + metadata: HypercertMetadata; +} + /** * Work time period. The value is UNIX time in seconds from epoch. */ @@ -331,16 +297,39 @@ export interface HypercertClaimdata { [key: string]: unknown; } +export type ApiResponseErrors = RecordStringStringOrStringArray | Error[]; + /** - * Response object for a store request. + * Interface for a generic API response. */ -export interface StoreResponse { +export interface ApiResponse { data?: unknown; - errors?: StoreResponseErrors; - message: string; + errors?: ApiResponseErrors; + message?: string; success: boolean; } +export type ApiResponseCidStringErrors = RecordStringStringOrStringArray | Error[]; + +export type ApiResponseCidStringData = { + cid: string; +}; + +/** + * Interface for a generic API response. + */ +export interface ApiResponseCidString { + data?: ApiResponseCidStringData; + errors?: ApiResponseCidStringErrors; + message?: string; + success: boolean; +} + +/** + * Interface for a storage response. + */ +export type StorageResponse = ApiResponseCidString; + export interface Error { message: string; name: string; @@ -354,26 +343,50 @@ export interface RecordStringStringOrStringArray { [key: string]: string | string[]; } -export type StoreResponseErrors = RecordStringStringOrStringArray | Error[]; +/** + * Submits a new hypercert metadata object for validation and storage on IPFS. +When an allowlist URI is provided the service will validate the allowlist data before storing the metadata. +Note that this might lead to a race condition when uploading metadata and the allowlist separately in rapid succession. +In that case we recommend using POST /metadata/with-allowlist instead. + */ +export const storeMetadata = >( + storeMetadataRequest: StoreMetadataRequest, + options?: AxiosRequestConfig, +): Promise => { + return axios.post(`/v1/metadata`, storeMetadataRequest, options); +}; /** - * Submits a new hypercert metadata object for validation and storage on IPFS. While we maintain a database of allowlists, the allowlist itself is stored on IPFS. + * Submits a new hypercert metadata object paired with allowlist data for validation and storage on IPFS. +The service will parse and validate the allow list data and the metadata. +After successful validation, the allow list data will be uploaded to IPFS and the URI of the allowlist will be attached to the hypercert metadata. +If an allow list URI is already present, the service will return an error. */ -export const storeMetadata = >( - hypercertMetadata: HypercertMetadata, +export const storeMetadataWithAllowlist = >( + storeMetadataWithAllowlistRequest: StoreMetadataWithAllowlistRequest, options?: AxiosRequestConfig, ): Promise => { - return axios.post(`/v1/metadata`, hypercertMetadata, options); + return axios.post(`/v1/metadata/with-allowlist`, storeMetadataWithAllowlistRequest, options); }; /** - * Submits a new hypercert metadata object for validation. + * Validates a hypercert metadata object. When an allowlist URI is provided the service will validate the allowlist data as well. */ -export const validateMetadata = >( - hypercertMetadata: HypercertMetadata, +export const validateMetadata = >( + validateMetadataRequest: ValidateMetadataRequest, options?: AxiosRequestConfig, ): Promise => { - return axios.post(`/v1/metadata/validate`, hypercertMetadata, options); + return axios.post(`/v1/metadata/validate`, validateMetadataRequest, options); +}; + +/** + * Validates a hypercert metadata object paired with allowlist data. + */ +export const validateMetadataWithAllowlist = >( + storeMetadataWithAllowlistRequest: StoreMetadataWithAllowlistRequest, + options?: AxiosRequestConfig, +): Promise => { + return axios.post(`/v1/metadata/with-allowlist/validate`, storeMetadataWithAllowlistRequest, options); }; /** @@ -396,17 +409,27 @@ export const updateOrderNonce = >( return axios.post(`/v1/marketplace/order-nonce`, updateOrderNonceRequest, options); }; +/** + * Validates an order and marks it as invalid if validation fails. + */ +export const validateOrder = >( + validateOrderRequest: ValidateOrderRequest, + options?: AxiosRequestConfig, +): Promise => { + return axios.post(`/v1/marketplace/orders/validate`, validateOrderRequest, options); +}; + /** * Submits a new allowlist for validation and storage on IPFS. While we maintain a database of allowlists, the allowlist itself is stored on IPFS. Try to keep a backup of the allowlist for recovery purposes. Provide the dump of the OpenZeppelin MerkleTree and the total units. */ -export const storeAllowList = >( - createAllowListRequest: CreateAllowListRequest, +export const storeAllowList = >( + storeAllowListRequest: StoreAllowListRequest, options?: AxiosRequestConfig, ): Promise => { - return axios.post(`/v1/allowlists`, createAllowListRequest, options); + return axios.post(`/v1/allowlists`, storeAllowListRequest, options); }; /** @@ -414,16 +437,19 @@ export const storeAllowList = >( Provide the dump of the OpenZeppelin MerkleTree and the total units. */ -export const validateAllowList = >( - createAllowListRequest: CreateAllowListRequest, +export const validateAllowList = >( + validateAllowListRequest: ValidateAllowListRequest, options?: AxiosRequestConfig, ): Promise => { - return axios.post(`/v1/allowlists/validate`, createAllowListRequest, options); + return axios.post(`/v1/allowlists/validate`, validateAllowListRequest, options); }; -export type StoreMetadataResult = AxiosResponse; -export type ValidateMetadataResult = AxiosResponse; +export type StoreMetadataResult = AxiosResponse; +export type StoreMetadataWithAllowlistResult = AxiosResponse; +export type ValidateMetadataResult = AxiosResponse; +export type ValidateMetadataWithAllowlistResult = AxiosResponse; export type StoreOrderResult = AxiosResponse; export type UpdateOrderNonceResult = AxiosResponse; -export type StoreAllowListResult = AxiosResponse; -export type ValidateAllowListResult = AxiosResponse; +export type ValidateOrderResult = AxiosResponse; +export type StoreAllowListResult = AxiosResponse; +export type ValidateAllowListResult = AxiosResponse; diff --git a/sdk/src/client.ts b/sdk/src/client.ts index 5bc958b6..8950f6d8 100644 --- a/sdk/src/client.ts +++ b/sdk/src/client.ts @@ -26,11 +26,10 @@ import { verifyMerkleProof, verifyMerkleProofs } from "./validator"; import { handleSimulatedContractError } from "./utils/errors"; import { parseAllowListEntriesToMerkleTree } from "./utils/allowlist"; import { getClaimStoredDataFromTxHash } from "./utils"; -import { ParserReturnType } from "./utils/txParser"; import { isClaimOnChain } from "./utils/chains"; -import { StoreAllowList201AnyOfTwoData, StoreMetadata201AnyOf } from "./__generated__/api"; import { HypercertStorage } from "./types/storage"; import { fetchFromHttpsOrIpfs } from "./utils/fetchers"; +import { StandardMerkleTree } from "@openzeppelin/merkle-tree/dist/standard"; /** * The `HypercertClient` is a core class in the hypercerts SDK, providing a high-level interface to interact with the hypercerts system. @@ -62,7 +61,7 @@ export class HypercertClient implements HypercertClientInterface { * @throws {ClientError} Will throw a `ClientError` if the public client cannot be connected. */ constructor(config: Partial) { - this._config = getConfig({ config }); + this._config = getConfig(config); this._walletClient = this._config?.walletClient; this._publicClient = this._config?.publicClient; this._storage = getStorage({ environment: this._config.environment }); @@ -106,6 +105,9 @@ export class HypercertClient implements HypercertClientInterface { return getDeploymentsForEnvironment(this._config.environment); }; + /** + * @deprecated Use `mintHypercert` instead. + */ mintClaim = async ( metaData: HypercertMetadata, totalUnits: bigint, @@ -116,43 +118,32 @@ export class HypercertClient implements HypercertClientInterface { return await this.mintHypercert({ metaData, totalUnits, transferRestriction, allowList, overrides }); }; - mintHypercert = async ({ - metaData, - totalUnits, - transferRestriction, - allowList, - overrides, - }: MintParams): Promise<`0x${string}` | undefined> => { + mintHypercert = async ({ metaData, totalUnits, transferRestriction, allowList, overrides }: MintParams) => { const { account } = this.getConnected(); - let allowListCid; let root; + let tree: StandardMerkleTree<(string | bigint)[]> | undefined; if (allowList) { let allowListEntries: AllowlistEntry[] = []; if (typeof allowList === "string") { - // fetch the csv contents + // fetch the csv contents from the provided uri const csvContents = await fetchFromHttpsOrIpfs(allowList); - if (!csvContents) { - throw new ClientError("No contents found in the csv", { allowList }); + if (!csvContents || typeof csvContents !== "string") { + throw new ClientError("Invalid or no contents found in the csv", { allowList }); } - if (typeof csvContents !== "string") { - throw new ClientError("Invalid contents found in the csv", { allowList }); - } // parse the csv contents into an array of AllowlistEntry - // get first row as headers - const headers = (csvContents as string).split("\n")[0].split(","); - // map headers onto other rows - const rows = (csvContents as string) - .split("\n") - .slice(1) - .map((row) => { - const values = row.split(","); - return Object.fromEntries(headers.map((header, i) => [header, values[i]])); - }); - allowListEntries = rows.map((entry) => { + const [headerLine, ...lines] = csvContents.split("\n"); + const headers = headerLine.split(","); + + allowListEntries = lines.map((line) => { + const values = line.split(","); + const entry = headers.reduce((acc, header, i) => { + acc[header] = values[i]; + return acc; + }, {} as Record); const { address, units } = entry; return { address, units: BigInt(units) }; }); @@ -160,52 +151,40 @@ export class HypercertClient implements HypercertClientInterface { allowListEntries = allowList; } - const tree = parseAllowListEntriesToMerkleTree(allowListEntries); - - // store allowlist on IPFS - const allowlistStoreRes = await this.storage.storeAllowlist( - { allowList: JSON.stringify(tree.dump()), totalUnits: totalUnits.toString() }, - { timeout: overrides?.timeout }, - ); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!allowlistStoreRes.data || !allowlistStoreRes.data.data) - throw new ClientError("No CID found", { allowlistStoreRes }); - - const { cid } = allowlistStoreRes.data.data as StoreAllowList201AnyOfTwoData; - allowListCid = cid; + tree = parseAllowListEntriesToMerkleTree(allowListEntries); root = tree.root; } - const metadataToStore = allowListCid ? { ...metaData, allowList: allowListCid } : metaData; + if (allowList && !tree) { + throw new ClientError("No tree found", { allowList }); + } // validate and store metadata - const metadataRes = await this.storage.storeMetadata(metadataToStore, { timeout: overrides?.timeout }); + const config = { timeout: overrides?.timeout }; + const metadataRes = + metaData && allowList && tree + ? await this.storage.storeMetadataWithAllowlist( + { + metadata: metaData, + allowList: JSON.stringify(tree.dump()), + totalUnits: totalUnits.toString(), + }, + config, + ) + : await this.storage.storeMetadata({ metadata: metaData }, config); if (!metadataRes || !metadataRes.data) { throw new ClientError("No CID found", { metadataRes }); } - const data = metadataRes.data as StoreMetadata201AnyOf; - - let request; - - if (allowList && allowListCid) { - request = await this.simulateRequest( - account, - "createAllowlist", - [account?.address, totalUnits, root, data.cid, transferRestriction], - overrides, - ); - } else { - request = await this.simulateRequest( - account, - "mintClaim", - [account?.address, totalUnits, data.cid, transferRestriction], - overrides, - ); - } + const cid = metadataRes.data.data?.cid; + const method = allowList && tree ? "createAllowlist" : "mintClaim"; + const params = + allowList && tree + ? [account?.address, totalUnits, root, cid, transferRestriction] + : [account?.address, totalUnits, cid, transferRestriction]; + const request = await this.simulateRequest(account, method, params, overrides); return this.submitRequest(request); }; @@ -223,7 +202,7 @@ export class HypercertClient implements HypercertClientInterface { return await readContract.read.readTransferRestriction([fractionId]).then((res) => res as TransferRestrictions); }; - transferFraction = async ({ fractionId, to, overrides }: TransferParams): Promise<`0x${string}` | undefined> => { + transferFraction = async ({ fractionId, to, overrides }: TransferParams) => { const { account } = this.getConnected(); const request = await this.simulateRequest( @@ -236,11 +215,7 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; - batchTransferFractions = async ({ - fractionIds, - to, - overrides, - }: BatchTransferParams): Promise<`0x${string}` | undefined> => { + batchTransferFractions = async ({ fractionIds, to, overrides }: BatchTransferParams) => { const { account } = this.getConnected(); const request = await this.simulateRequest( @@ -253,46 +228,17 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; + /** + * @deprecated Use `mintHypercert` instead. + */ createAllowlist = async ( allowList: AllowlistEntry[], metaData: HypercertMetadata, totalUnits: bigint, transferRestriction: TransferRestrictions, overrides?: SupportedOverrides, - ): Promise<`0x${string}` | undefined> => { - const { account } = this.getConnected(); - - // create allowlist - const tree = parseAllowListEntriesToMerkleTree(allowList); - - // store allowlist on IPFS - const allowlistStoreRes = await this.storage.storeAllowlist( - { allowList: JSON.stringify(tree.dump()), totalUnits: totalUnits.toString() }, - { timeout: overrides?.timeout }, - ); - - console.debug("allowlistStoreRes", allowlistStoreRes); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!allowlistStoreRes.data) throw new ClientError("No CID found", { allowlistStoreRes }); - - const data = allowlistStoreRes.data as unknown as StoreAllowList201AnyOfTwoData; - - console.debug("Storing metadata", { ...metaData, allowList: data.cid }); - - // store metadata on IPFS - const metadataCID = await this.storage.storeMetadata( - { ...metaData, allowList: data.cid }, - { timeout: overrides?.timeout }, - ); - const request = await this.simulateRequest( - account, - "createAllowlist", - [account?.address, totalUnits, tree.root, metadataCID, transferRestriction], - overrides, - ); - - return this.submitRequest(request); + ) => { + return await this.mintHypercert({ metaData, totalUnits, transferRestriction, allowList, overrides }); }; splitFraction = async ({ @@ -325,15 +271,14 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; - splitFractionUnits = async ( - fractionId: bigint, - fractions: bigint[], - overrides?: SupportedOverrides, - ): Promise<`0x${string}` | undefined> => { + /** + * @deprecated Use `splitFraction` instead. + */ + splitFractionUnits = async (fractionId: bigint, fractions: bigint[], overrides?: SupportedOverrides) => { return this.splitFraction({ fractionId, fractions, overrides }); }; - mergeFractions = async ({ fractionIds, overrides }: MergeFractionsParams): Promise<`0x${string}` | undefined> => { + mergeFractions = async ({ fractionIds, overrides }: MergeFractionsParams) => { const { account } = this.getConnected(); const readContract = this._getContract(); @@ -356,14 +301,14 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; - mergeFractionUnits = async ( - fractionIds: bigint[], - overrides?: SupportedOverrides, - ): Promise<`0x${string}` | undefined> => { + /** + * @deprecated Use `mergeFractions` instead. + */ + mergeFractionUnits = async (fractionIds: bigint[], overrides?: SupportedOverrides) => { return this.mergeFractions({ fractionIds, overrides }); }; - burnFraction = async ({ fractionId, overrides }: BurnFractionParams): Promise<`0x${string}` | undefined> => { + burnFraction = async ({ fractionId, overrides }: BurnFractionParams) => { const { account } = this.getConnected(); const readContract = this._getContract(); @@ -379,7 +324,10 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; - burnClaimFraction = async (claimId: bigint, overrides?: SupportedOverrides): Promise<`0x${string}` | undefined> => { + /** + * @deprecated Use `burnFraction` instead. + */ + burnClaimFraction = async (claimId: bigint, overrides?: SupportedOverrides) => { return this.burnFraction({ fractionId: claimId, overrides }); }; @@ -389,7 +337,7 @@ export class HypercertClient implements HypercertClientInterface { proof, root, overrides, - }: ClaimFractionFromAllowlistParams): Promise<`0x${string}` | undefined> => { + }: ClaimFractionFromAllowlistParams) => { const { account } = this.getConnected(); //verify the proof using the OZ merkle tree library @@ -413,13 +361,16 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; + /** + * @deprecated Use `claimFractionFromAllowlist` instead. + */ mintClaimFractionFromAllowlist = async ( claimId: bigint, units: bigint, proof: (Hex | ByteArray)[], root?: Hex | ByteArray, overrides?: SupportedOverrides, - ): Promise<`0x${string}` | undefined> => { + ) => { return this.claimFractionFromAllowlist({ hypercertTokenId: claimId, units, proof, root, overrides }); }; @@ -429,7 +380,7 @@ export class HypercertClient implements HypercertClientInterface { proofs, roots, overrides, - }: BatchClaimFractionsFromAllowlistsParams): Promise<`0x${string}` | undefined> => { + }: BatchClaimFractionsFromAllowlistsParams) => { const { account } = this.getConnected(); //verify the proof using the OZ merkle tree library @@ -454,17 +405,20 @@ export class HypercertClient implements HypercertClientInterface { return this.submitRequest(request); }; + /** + * @deprecated Use `batchClaimFractionsFromAllowlists` instead. + */ batchMintClaimFractionsFromAllowlists = async ( claimIds: bigint[], units: bigint[], proofs: (Hex | ByteArray)[][], roots?: (Hex | ByteArray)[], overrides?: SupportedOverrides, - ): Promise<`0x${string}` | undefined> => { + ) => { return this.batchClaimFractionsFromAllowlists({ hypercertTokenIds: claimIds, units, proofs, roots, overrides }); }; - getClaimStoredDataFromTxHash = async (hash: `0x${string}`): Promise => { + getClaimStoredDataFromTxHash = async (hash: `0x${string}`) => { const { publicClient } = this.getConnected(); const { data, errors, success } = await getClaimStoredDataFromTxHash(publicClient, hash); diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 7ae205c3..27e00ac8 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -1,3 +1,14 @@ +// eslint-disable-next-line @typescript-eslint/no-redeclare, @typescript-eslint/no-unused-vars +declare global { + interface BigInt { + toJSON: () => string; + } +} + +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + import { HypercertMinterAbi, HypercertExchangeAbi, diff --git a/sdk/src/storage.ts b/sdk/src/storage.ts index 78b74b4a..0a0f7c35 100644 --- a/sdk/src/storage.ts +++ b/sdk/src/storage.ts @@ -1,5 +1,12 @@ -import { Environment, HypercertMetadata } from "./types"; -import { CreateAllowListRequest, storeAllowList, storeMetadata } from "./__generated__/api"; +import { Environment } from "./types"; +import { + storeAllowList, + StoreAllowListRequest, + storeMetadata, + StoreMetadataRequest, + storeMetadataWithAllowlist, + StoreMetadataWithAllowlistRequest, +} from "./__generated__/api"; import axios, { AxiosRequestConfig } from "axios"; import { ENDPOINTS } from "./constants"; import { HypercertStorage } from "./types/storage"; @@ -24,9 +31,11 @@ export const getStorage = ({ }; return { - storeMetadata: async (metadata: HypercertMetadata, config: AxiosRequestConfig = {}) => - storeMetadata(metadata, { ..._config, ...config }), - storeAllowlist: async (createAllowListRequest: CreateAllowListRequest, config: AxiosRequestConfig = {}) => - storeAllowList(createAllowListRequest, { ..._config, ...config }), + storeMetadata: async (request: StoreMetadataRequest, config: AxiosRequestConfig = {}) => + storeMetadata(request, { ..._config, ...config }), + storeAllowlist: async (request: StoreAllowListRequest, config: AxiosRequestConfig = {}) => + storeAllowList(request, { ..._config, ...config }), + storeMetadataWithAllowlist: async (request: StoreMetadataWithAllowlistRequest, config: AxiosRequestConfig = {}) => + storeMetadataWithAllowlist(request, { ..._config, ...config }), }; }; diff --git a/sdk/src/types/storage.ts b/sdk/src/types/storage.ts index edf32b53..edb35db3 100644 --- a/sdk/src/types/storage.ts +++ b/sdk/src/types/storage.ts @@ -1,16 +1,22 @@ import { AxiosRequestConfig, AxiosResponse } from "axios"; -import { StoreMetadata201, CreateAllowListRequest, StoreAllowList201 } from "../__generated__/api"; -import { HypercertMetadata } from "./metadata"; +import { + StorageResponse, + StoreAllowListRequest, + StoreMetadataRequest, + StoreMetadataWithAllowlistRequest, +} from "../__generated__/api"; export interface HypercertStorage { storeMetadata: ( - metadata: HypercertMetadata, + request: StoreMetadataRequest, config?: AxiosRequestConfig, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise>; + ) => Promise>; storeAllowlist: ( - createAllowListRequest: CreateAllowListRequest, + request: StoreAllowListRequest, config?: AxiosRequestConfig, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise>; + ) => Promise>; + storeMetadataWithAllowlist: ( + request: StoreMetadataWithAllowlistRequest, + config?: AxiosRequestConfig, + ) => Promise>; } diff --git a/sdk/src/utils/allowlist.ts b/sdk/src/utils/allowlist.ts index 8d0c425c..5705fce4 100644 --- a/sdk/src/utils/allowlist.ts +++ b/sdk/src/utils/allowlist.ts @@ -4,10 +4,8 @@ import { logger } from "./logger"; import { AllowlistEntry } from "../types"; const parseAllowListEntriesToMerkleTree = (allowList: AllowlistEntry[]) => { - const tuples = allowList.map((p) => [p.address, p.units.toString()]); - const tree = StandardMerkleTree.of(tuples, ["address", "uint256"]); - - return tree; + const tuples = allowList.map((p) => [p.address, p.units]); + return StandardMerkleTree.of(tuples, ["address", "uint256"]); }; const getMerkleTreeFromIPFS = async (cidOrIpfsUri: string) => { diff --git a/sdk/src/utils/config.ts b/sdk/src/utils/config.ts index 4775bcd1..9c190527 100644 --- a/sdk/src/utils/config.ts +++ b/sdk/src/utils/config.ts @@ -23,13 +23,12 @@ import { createPublicClient, http } from "viem"; * @throws {InvalidOrMissingError} Will throw an `InvalidOrMissingError` if the `unsafeForceOverrideConfig` flag is set but the required overrides are not provided. * @throws {UnsupportedChainError} Will throw an `UnsupportedChainError` if the default configuration for the provided chain ID is missing. */ -export const getConfig = ({ - config = { environment: DEFAULT_ENVIRONMENT }, -}: { - config?: Partial>; -}): HypercertClientConfig => { +export const getConfig = ( + config: Partial>, +) => { + if (!config) throw new Error("Missing config"); + const _config = { - // Let the user override from environment variables ...getEnvironment(config), ...getWalletClient(config), ...getPublicClient(config), @@ -38,16 +37,6 @@ export const getConfig = ({ readOnly: true, }; - const missingKeys = []; - - for (const [key, value] of Object.entries(_config)) { - if (!value) { - missingKeys.push(key); - } - } - - if (missingKeys.length > 0) logger.debug(`Missing properties in config: ${missingKeys.join(", ")}`); - const chainId = _config.walletClient?.chain?.id; const writeAbleChainIds = Object.entries(_config.deployments).map(([_, deployment]) => deployment.chainId); @@ -109,11 +98,23 @@ export const getDeploymentsForChainId = (chainId: SupportedChainIds) => { }; const getEnvironment = (config: Partial) => { - return { environment: config.environment || DEFAULT_ENVIRONMENT }; + const environment = config.environment; + + if (!environment) throw new Error("Missing environment"); + if (!ENDPOINTS[environment]) + throw new Error(`Invalid environment ${environment}. [${Object.keys(ENDPOINTS).join(", ")}]`); + + return { environment }; }; const getGraphUrl = (config: Partial) => { - return { graphUrl: `${ENDPOINTS[config.environment || DEFAULT_ENVIRONMENT]}/v1/graphql` }; + const environment = config.environment; + + if (!environment) throw new Error("Missing environment"); + if (!ENDPOINTS[environment]) + throw new Error(`Invalid environment ${environment}. [${Object.keys(ENDPOINTS).join(", ")}]`); + + return { graphUrl: `${ENDPOINTS[environment]}/v1/graphql` }; }; const getWalletClient = (config: Partial) => { diff --git a/sdk/test/client.test.ts b/sdk/test/client.test.ts index 89e01f08..0a0e2c5c 100644 --- a/sdk/test/client.test.ts +++ b/sdk/test/client.test.ts @@ -41,9 +41,9 @@ describe("HypercertClient setup tests", () => { try { const metaData = { name: "test" } as HypercertMetadata; const totalUnits = 1n; - const transferRestrictions = TransferRestrictions.AllowAll; + const transferRestriction = TransferRestrictions.AllowAll; - await client.mintClaim(metaData, totalUnits, transferRestrictions); + await client.mintHypercert({ metaData, totalUnits, transferRestriction }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); @@ -55,12 +55,12 @@ describe("HypercertClient setup tests", () => { // createAllowlist try { - const allowlist: AllowlistEntry[] = [{ address: "0x0000000", units: 100n }]; + const allowList: AllowlistEntry[] = [{ address: "0x0000000", units: 100n }]; const metaData = { name: "test" } as HypercertMetadata; const totalUnits = 1n; - const transferRestrictions = TransferRestrictions.AllowAll; + const transferRestriction = TransferRestrictions.AllowAll; - await client.createAllowlist(allowlist, metaData, totalUnits, transferRestrictions); + await client.mintHypercert({ allowList, metaData, totalUnits, transferRestriction }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); @@ -72,10 +72,10 @@ describe("HypercertClient setup tests", () => { // splitClaimUnits try { - const claimId = 1n; + const fractionId = 1n; const fractions = [100n, 200n]; - await client.splitFractionUnits(claimId, fractions); + await client.splitFraction({ fractionId, fractions }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); @@ -87,9 +87,9 @@ describe("HypercertClient setup tests", () => { // mergeClaimUnits try { - const claimIds = [1n, 2n]; + const fractionIds = [1n, 2n]; - await client.mergeFractionUnits(claimIds); + await client.mergeFractions({ fractionIds }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); @@ -101,9 +101,9 @@ describe("HypercertClient setup tests", () => { // burnClaimFraction try { - const claimId = 1n; + const fractionId = 1n; - await client.burnClaimFraction(claimId); + await client.burnFraction({ fractionId }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); @@ -115,12 +115,12 @@ describe("HypercertClient setup tests", () => { // mintClaimFractionFromAllowlist try { - const claimId = 1n; + const hypercertTokenId = 1n; const units = 100n; const proof = ["0x1", "0x2", "0x3"] as `0x${string}`[]; const root = "0x4" as `0x${string}`; - await client.mintClaimFractionFromAllowlist(claimId, units, proof, root); + await client.claimFractionFromAllowlist({ hypercertTokenId, units, proof, root }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); @@ -132,12 +132,12 @@ describe("HypercertClient setup tests", () => { // batchMintClaimFractionsFromAllowlist try { - const claimIds = [1n, 2n]; + const hypercertTokenIds = [1n, 2n]; const units = [100n, 200n]; const proofs = [["0x1", "0x2", "0x3"] as `0x${string}`[], ["0x4", "0x5", "0x6"] as `0x${string}`[]]; const roots = ["0x7", "0x8"] as `0x${string}`[]; - await client.batchMintClaimFractionsFromAllowlists(claimIds, units, proofs, roots); + await client.batchClaimFractionsFromAllowlists({ hypercertTokenIds, units, proofs, roots }); expect.fail("Should throw ClientError"); } catch (e) { expect(e).to.be.instanceOf(ClientError); diff --git a/sdk/test/client/allowlist.minting.test.ts b/sdk/test/client/allowlist.minting.test.ts index 234e7c34..f4d3a16c 100644 --- a/sdk/test/client/allowlist.minting.test.ts +++ b/sdk/test/client/allowlist.minting.test.ts @@ -16,6 +16,7 @@ const mocks = vi.hoisted(() => { return { storeAllowList: vi.fn(), storeMetadata: vi.fn(), + storeMetadataWithAllowlist: vi.fn(), }; }); @@ -23,6 +24,7 @@ vi.mock("../../src/__generated__/api", () => { return { storeAllowList: mocks.storeAllowList, storeMetadata: mocks.storeMetadata, + storeMetadataWithAllowlist: mocks.storeMetadataWithAllowlist, }; }); @@ -85,10 +87,15 @@ describe("Allows for minting claims from an allowlist", () => { const { allowlist, totalUnits } = getAllowlist(); const metaData = getFormattedMetadata(); - mocks.storeAllowList.mockResolvedValue({ data: { cid: someData.cid } }); + mocks.storeMetadataWithAllowlist.mockResolvedValue({ data: { cid: someData.cid } }); writeSpy = writeSpy.resolves(mintClaimResult); - const hash = await client.createAllowlist(allowlist, metaData, totalUnits, TransferRestrictions.FromCreatorOnly); + const hash = await client.mintHypercert({ + allowList: allowlist, + metaData, + totalUnits, + transferRestriction: TransferRestrictions.FromCreatorOnly, + }); expect(isHex(hash)).to.be.true; expect(readSpy.callCount).to.eq(0); @@ -100,7 +107,7 @@ describe("Allows for minting claims from an allowlist", () => { const { allowlist, totalUnits } = getAllowlist(); const metaData = getFormattedMetadata(); - mocks.storeAllowList.mockRejectedValue( + mocks.storeMetadataWithAllowlist.mockRejectedValue( new MalformedDataError("Allowlist validation failed", { units: "Total units in allowlist must match total units [expected: 11, got: 10]", }), @@ -108,7 +115,12 @@ describe("Allows for minting claims from an allowlist", () => { let hash; try { - hash = await client.createAllowlist(allowlist, metaData, totalUnits + 1n, TransferRestrictions.FromCreatorOnly); + hash = await client.mintHypercert({ + allowList: allowlist, + metaData, + totalUnits: totalUnits + 1n, + transferRestriction: TransferRestrictions.FromCreatorOnly, + }); } catch (e) { expect(e).to.be.instanceOf(MalformedDataError); @@ -133,14 +145,19 @@ describe("Allows for minting claims from an allowlist", () => { allowlist[0].units = 0n; - mocks.storeAllowList.mockRejectedValue( + mocks.storeMetadataWithAllowlist.mockRejectedValue( new MalformedDataError("Allowlist validation failed", { units: "Total units in allowlist must match total units [expected: 10, got: 9]", }), ); try { - hash = await client.createAllowlist(allowlist, metaData, totalUnits, TransferRestrictions.FromCreatorOnly); + hash = await client.mintHypercert({ + allowList: allowlist, + metaData, + totalUnits, + transferRestriction: TransferRestrictions.FromCreatorOnly, + }); } catch (e) { expect(e).to.be.instanceOf(MalformedDataError); @@ -164,11 +181,11 @@ describe("Allows for minting claims from an allowlist", () => { writeSpy = writeSpy.resolves(mintClaimFromAllowlistResult); - const hash = await client.mintClaimFractionFromAllowlist( - 1n, - allowlist[0].units, - merkleTree.getProof([allowlist[0].address, allowlist[0].units.toString()]) as `0x${string}`[], - ); + const hash = await client.claimFractionFromAllowlist({ + hypercertTokenId: 1n, + units: allowlist[0].units, + proof: merkleTree.getProof([allowlist[0].address, allowlist[0].units.toString()]) as `0x${string}`[], + }); expect(isHex(hash)).to.be.true; expect(readSpy.callCount).to.eq(0); @@ -181,12 +198,12 @@ describe("Allows for minting claims from an allowlist", () => { writeSpy = writeSpy.resolves(mintClaimFromAllowlistResult); - const hash = await client.mintClaimFractionFromAllowlist( - 1n, - allowlist[0].units, - merkleTree.getProof([allowlist[0].address, allowlist[0].units.toString()]) as `0x${string}`[], - merkleTree.root as `0x${string}`, - ); + const hash = await client.claimFractionFromAllowlist({ + hypercertTokenId: 1n, + units: allowlist[0].units, + proof: merkleTree.getProof([allowlist[0].address, allowlist[0].units.toString()]) as `0x${string}`[], + root: merkleTree.root as `0x${string}`, + }); expect(isHex(hash)).to.be.true; expect(readSpy.callCount).to.eq(0); @@ -201,12 +218,12 @@ describe("Allows for minting claims from an allowlist", () => { let hash; try { - hash = await client.mintClaimFractionFromAllowlist( - 1n, - allowlist[0].units, - merkleTree.getProof([allowlist[0].address, allowlist[0].units.toString()]) as `0x${string}`[], - mockRoot, - ); + hash = await client.claimFractionFromAllowlist({ + hypercertTokenId: 1n, + units: allowlist[0].units, + proof: merkleTree.getProof([allowlist[0].address, allowlist[0].units.toString()]) as `0x${string}`[], + root: mockRoot, + }); } catch (e) { expect(e instanceof MintingError).to.be.true; diff --git a/sdk/test/utils/config.test.ts b/sdk/test/utils/config.test.ts index cfbf62a9..a96e44f8 100644 --- a/sdk/test/utils/config.test.ts +++ b/sdk/test/utils/config.test.ts @@ -18,7 +18,7 @@ describe("Config: graphUrl", () => { }); it("should return the default indexer environment when no overrides are specified", () => { - const result = getConfig({}); + const result = getConfig({ environment: DEFAULT_ENVIRONMENT }); expect(result.environment).to.equal(DEFAULT_ENVIRONMENT); }); @@ -26,7 +26,7 @@ describe("Config: graphUrl", () => { const overrides: Partial = { environment: "production", }; - const result = getConfig({ config: overrides }); + const result = getConfig(overrides); expect(result.readOnly).to.be.true; }); }); @@ -40,9 +40,10 @@ describe("Config: getPublicClient", () => { it("should return the operator specified by overrides", () => { const config: Partial = { + environment: DEFAULT_ENVIRONMENT, publicClient, }; - const result = getConfig({ config }); + const result = getConfig(config); expect(result.publicClient).to.equal(config.publicClient); }); }); @@ -56,9 +57,10 @@ describe("Config: getWalletClient", () => { it("should return the operator specified by overrides", () => { const config: Partial = { + environment: DEFAULT_ENVIRONMENT, walletClient, }; - const result = getConfig({ config }); + const result = getConfig(config); expect(result.walletClient).to.equal(config.walletClient); }); });