From 4b662518027b18ccb24c1d38bf3c382b601a03e8 Mon Sep 17 00:00:00 2001 From: aphex Date: Thu, 27 Feb 2025 23:59:55 +0000 Subject: [PATCH 1/4] add update allowlist function --- packages/sdk/src/sdk/sdk.ts | 57 +++++++++++++ packages/sdk/src/utils/get-auction-lot.ts | 20 +++++ packages/sdk/src/utils/index.ts | 1 + .../utils/update-allowlist/get-config.test.ts | 84 +++++++++++++++++++ .../src/utils/update-allowlist/get-config.ts | 65 ++++++++++++++ .../sdk/src/utils/update-allowlist/index.ts | 2 + .../sdk/src/utils/update-allowlist/schema.ts | 10 +++ .../sdk/src/utils/update-allowlist/types.ts | 13 +++ 8 files changed, 252 insertions(+) create mode 100644 packages/sdk/src/utils/get-auction-lot.ts create mode 100644 packages/sdk/src/utils/index.ts create mode 100644 packages/sdk/src/utils/update-allowlist/get-config.test.ts create mode 100644 packages/sdk/src/utils/update-allowlist/get-config.ts create mode 100644 packages/sdk/src/utils/update-allowlist/index.ts create mode 100644 packages/sdk/src/utils/update-allowlist/schema.ts create mode 100644 packages/sdk/src/utils/update-allowlist/types.ts diff --git a/packages/sdk/src/sdk/sdk.ts b/packages/sdk/src/sdk/sdk.ts index bdcd88f0..dccdb19a 100644 --- a/packages/sdk/src/sdk/sdk.ts +++ b/packages/sdk/src/sdk/sdk.ts @@ -15,6 +15,7 @@ import * as periphery from "../periphery"; import * as registry from "../registry"; import { MetadataClient, + SdkError, type CuratorClient, type CuratorRouter, type OriginConfig, @@ -48,6 +49,8 @@ import type { RegisterAuctionParams, RegisterAuctionConfig, } from "../registry"; +import { updateAllowlist } from "../utils"; +import type { UpdateAllowlistParams } from "../utils"; const defaultConfig: OriginConfig = { environment: Environment.PRODUCTION, @@ -308,6 +311,60 @@ class OriginSdk { registerAuction(params: RegisterAuctionParams): RegisterAuctionConfig { return this.registry.registerAuction.getConfig(params); } + + /** + * Generates the contract configurations required for updating the allowlist of an auction, + * which involves storing the new allowlist offchain and calculating the merkle root + * + * This function validates the provided parameters, fetches auction metadata, updates the allowlist, + * and stores the new metadata on IPFS. + * + * @param {UpdateAllowlistParams} params - The parameters required for updating the allowlist. + * @returns {Promise} A promise that resolves to an array containing contract configurations: + * - `RegisterConfig`: Configuration for registering auction metadata. + * - `SetMerkleRootConfig`: Configuration for updating the Merkle root of the allowlist. + * + * @throws {SdkError} If the provided parameters are invalid. + * @throws {SdkError} If the auction lot cannot be found on the specified chain. + * @throws {Error} If the metadata is invalid + * @throws {Error} If the {MetadataClient} is unable to store the data + * + * @example + * import { sdk } from "./sdk"; + * + * try { + * const config = await sdk.updateAllowlist({ + * id: "auction-123", + * lotId: 1, + * auctionHouse: "0x123456789", + * isTestnet: true, + * allowlist: ["0xabc..."], + * callback: "0x987654321" + * chainId: 1 + * }, metadataClient); + * + * console.log(config); + * } catch (error) { + * if (error instanceof SdkError) { + * console.error(error.message, error.issues); + * } else { + * console.error("Unexpected error:", error); + * } + * } + */ + async updateAllowlist(params: UpdateAllowlistParams) { + if (!this.metadataClient) { + throw new SdkError( + "Unable to use updateAllowlist due to unexisting MetadataClient", + ); + } + + return updateAllowlist.getConfig( + params, + this.metadataClient, + this.config.subgraph, + ); + } } const createSdk = (config?: OriginConfig) => new OriginSdk(config); diff --git a/packages/sdk/src/utils/get-auction-lot.ts b/packages/sdk/src/utils/get-auction-lot.ts new file mode 100644 index 00000000..5e44ff27 --- /dev/null +++ b/packages/sdk/src/utils/get-auction-lot.ts @@ -0,0 +1,20 @@ +import { + request, + GetBatchAuctionLotDocument, + GetBatchAuctionLotQuery, +} from "@axis-finance/subgraph-client"; + +type GetAuctionLotParams = { + endpoint: string; + lotId: number; +}; + +export const getAuctionLot = (params: GetAuctionLotParams) => { + return request( + params.endpoint, + GetBatchAuctionLotDocument, + { + id: params.lotId, + }, + ); +}; diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts new file mode 100644 index 00000000..f6f04928 --- /dev/null +++ b/packages/sdk/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./update-allowlist"; diff --git a/packages/sdk/src/utils/update-allowlist/get-config.test.ts b/packages/sdk/src/utils/update-allowlist/get-config.test.ts new file mode 100644 index 00000000..521cce7a --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/get-config.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi } from "vitest"; +import { getConfig } from "./get-config"; +import { zeroAddress } from "viem"; +import { MetadataClient } from "../metadata-client"; +import { abis } from "@axis-finance/abis"; +import { deployments } from "@axis-finance/deployments"; +import { base } from "viem/chains"; +import { UpdateAllowlistParams } from "./types"; + +const mockAllowlist = [ + ["0x0000000000000000000000000000000000000004", "5000000000000000000"], + ["0x0000000000000000000000000000000000000020", "0"], + ["0x0000000000000000000000000000000000000021", "50"], + ["0x0000000000000000000000000000000000000022", "60"], +]; + +const mockMerkleRootResult = + "0xae177673a8db8795a6e8287bbfe67c9425307898c97394d8c87dae3ecf0c5e3c"; +const mockCID = "bafkreihv223uekzoo24w3zxkzjj25yenpvr5xfc6jychur5wjhy4rhnme4"; + +const mockAddress = zeroAddress; +const lotId = 1; + +const registryAddress = deployments[base.id].registry; + +const mockParams = { + lotId, + callback: mockAddress, + allowlist: { + types: ["address", "uint256"], + values: mockAllowlist, + }, + auctionHouse: mockAddress, + isTestnet: false, + chainId: base.id, +} satisfies UpdateAllowlistParams; + +// vi.mock("../metadata-client.ts", () => { +// return { +// MetadataClient: vi.fn().mockImplementation(() => ({ +// save: vi.fn(() => "mocked-cid"), +// })), +// }; +// }); + +const mockAuction = { + batchAuctionLot: { + info: { + id: "auction-12341234", + }, + }, +}; +vi.mock("../get-auction-lot.ts", () => { + return { + getAuctionLot: vi.fn(() => mockAuction), + }; +}); + +describe("getConfig()", () => { + it("returns contract configuration", async () => { + const metadataClient = new MetadataClient({ + fleekApplicationClientId: "1337", + }); + + vi.spyOn(metadataClient, "save").mockResolvedValue(mockCID); + + const result = await getConfig(mockParams, metadataClient); + + expect(result.registerAuctionConfig).toStrictEqual({ + chainId: base.id, + abi: abis.axisMetadataRegistry, + address: registryAddress, + functionName: "registerAuction", + args: [mockAddress, BigInt(mockParams.lotId), mockCID], + }); + + expect(result.setMerkleRootConfig).toStrictEqual({ + abi: abis.merkleAllowlist, + address: zeroAddress, + functionName: "setMerkleRoot", + args: [BigInt(mockParams.lotId), mockMerkleRootResult], + }); + }); +}); diff --git a/packages/sdk/src/utils/update-allowlist/get-config.ts b/packages/sdk/src/utils/update-allowlist/get-config.ts new file mode 100644 index 00000000..6ab6d187 --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/get-config.ts @@ -0,0 +1,65 @@ +import * as v from "valibot"; +import { setMerkleRoot } from "../../periphery/callbacks"; +import { registerAuction } from "../../registry"; +import { MetadataClient } from "../metadata-client"; +import { schema } from "./schema"; +import { SdkError } from "../../types"; +import { getAuctionLot } from "../get-auction-lot"; +import { deployments } from "@axis-finance/deployments"; +import type { OriginConfig } from "../../types"; +import { UpdateAllowlistParams, UpdateAllowlistResult } from "./types"; + +export const getConfig = async ( + params: UpdateAllowlistParams, + metadataClient: MetadataClient, + subgraph?: OriginConfig["subgraph"], +): Promise => { + const parsedParams = v.safeParse(schema, params); + + if (!parsedParams.success) { + throw new SdkError( + "Invalid parameters supplied to getConfig()", + parsedParams.issues, + ); + } + + const { lotId, auctionHouse, isTestnet, allowlist, callback, chainId } = + params; + + const subgraphEndpoint = subgraph + ? subgraph[chainId].url + : deployments[chainId].subgraphURL; + + const result = await getAuctionLot({ + endpoint: subgraphEndpoint, + lotId, + }); + + const auction = result.batchAuctionLot; + + if (!auction) { + throw new SdkError(`Auction with ${lotId} not found on ${chainId}`); + } + + const metadata = { ...auction.info, allowlist }; + + const ipfsCID = await metadataClient.save({ + metadata: JSON.stringify(metadata), + id: auction.info?.key ?? undefined, + }); + + const registerAuctionConfig = registerAuction.getConfig({ + ipfsCID, + lotId, + auctionHouse, + isTestnet, + }); + + const setMerkleRootConfig = setMerkleRoot.getConfig({ + lotId, + allowlist, + callback, + }); + + return { registerAuctionConfig, setMerkleRootConfig }; +}; diff --git a/packages/sdk/src/utils/update-allowlist/index.ts b/packages/sdk/src/utils/update-allowlist/index.ts new file mode 100644 index 00000000..f6155971 --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/index.ts @@ -0,0 +1,2 @@ +export * as updateAllowlist from "./get-config"; +export * from "./types"; diff --git a/packages/sdk/src/utils/update-allowlist/schema.ts b/packages/sdk/src/utils/update-allowlist/schema.ts new file mode 100644 index 00000000..0fbf7e78 --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/schema.ts @@ -0,0 +1,10 @@ +import * as v from "valibot"; +import { schema as registerAuctionSchema } from "../../registry/register-auction/schema"; +import { schema as setMerkleRootSchema } from "../../periphery/callbacks/set-merkle-root/schema"; + +const _registerAuctionSchema = v.omit(registerAuctionSchema, ["ipfsCID"]); + +export const schema = v.object({ + ..._registerAuctionSchema.entries, + ...setMerkleRootSchema.entries, +}); diff --git a/packages/sdk/src/utils/update-allowlist/types.ts b/packages/sdk/src/utils/update-allowlist/types.ts new file mode 100644 index 00000000..ef7f8784 --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/types.ts @@ -0,0 +1,13 @@ +import { SetMerkleRootConfig, SetMerkleRootParams } from "../../periphery"; +import { RegisterAuctionConfig, RegisterAuctionParams } from "../../registry"; +import { SaveParams } from "../metadata-client"; + +export type UpdateAllowlistParams = Omit< + RegisterAuctionParams & SetMerkleRootParams & SaveParams, + "ipfsCID" | "metadata" | "id" +> & { chainId: number }; + +export type UpdateAllowlistResult = { + registerAuctionConfig: RegisterAuctionConfig; + setMerkleRootConfig: SetMerkleRootConfig; +}; From 7b63a380b7e093536e9b709fbbed23a2d8f9b8c4 Mon Sep 17 00:00:00 2001 From: aphex Date: Fri, 28 Feb 2025 00:34:07 +0000 Subject: [PATCH 2/4] clean up and add failing test case --- .../utils/update-allowlist/get-config.test.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/utils/update-allowlist/get-config.test.ts b/packages/sdk/src/utils/update-allowlist/get-config.test.ts index 521cce7a..611a5248 100644 --- a/packages/sdk/src/utils/update-allowlist/get-config.test.ts +++ b/packages/sdk/src/utils/update-allowlist/get-config.test.ts @@ -35,14 +35,6 @@ const mockParams = { chainId: base.id, } satisfies UpdateAllowlistParams; -// vi.mock("../metadata-client.ts", () => { -// return { -// MetadataClient: vi.fn().mockImplementation(() => ({ -// save: vi.fn(() => "mocked-cid"), -// })), -// }; -// }); - const mockAuction = { batchAuctionLot: { info: { @@ -81,4 +73,21 @@ describe("getConfig()", () => { args: [BigInt(mockParams.lotId), mockMerkleRootResult], }); }); + + it("throws an error if invalid params are supplied", async () => { + const invalidParams = { ...mockParams, lotId: "invalid" }; + const metadataClient = new MetadataClient({ + fleekApplicationClientId: "1337", + }); + + const result = getConfig( + // @ts-expect-error - deliberately testing invalid params + invalidParams, + metadataClient, + ); + + expect(result).rejects.toThrowErrorMatchingInlineSnapshot( + `[OriginSdkError: Invalid parameters supplied to getConfig()]`, + ); + }); }); From d132b7be22b63ade7725b6449a5d8e4404039aa1 Mon Sep 17 00:00:00 2001 From: aphex Date: Fri, 28 Feb 2025 12:26:28 +0000 Subject: [PATCH 3/4] remove params that can be inferred from the auction query --- packages/sdk/src/sdk/sdk.ts | 5 +++-- .../src/utils/update-allowlist/get-config.test.ts | 3 +-- .../sdk/src/utils/update-allowlist/get-config.ts | 13 ++++++++++--- packages/sdk/src/utils/update-allowlist/schema.ts | 9 +++++++-- packages/sdk/src/utils/update-allowlist/types.ts | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/sdk/src/sdk/sdk.ts b/packages/sdk/src/sdk/sdk.ts index dccdb19a..55b65705 100644 --- a/packages/sdk/src/sdk/sdk.ts +++ b/packages/sdk/src/sdk/sdk.ts @@ -326,8 +326,9 @@ class OriginSdk { * * @throws {SdkError} If the provided parameters are invalid. * @throws {SdkError} If the auction lot cannot be found on the specified chain. - * @throws {Error} If the metadata is invalid - * @throws {Error} If the {MetadataClient} is unable to store the data + * @throws {SdkError} If the auction lot doesn't have a callback address defined. + * @throws {Error} If the metadata is invalid. + * @throws {Error} If the {MetadataClient} is unable to store the data. * * @example * import { sdk } from "./sdk"; diff --git a/packages/sdk/src/utils/update-allowlist/get-config.test.ts b/packages/sdk/src/utils/update-allowlist/get-config.test.ts index 611a5248..b07209e9 100644 --- a/packages/sdk/src/utils/update-allowlist/get-config.test.ts +++ b/packages/sdk/src/utils/update-allowlist/get-config.test.ts @@ -25,18 +25,17 @@ const registryAddress = deployments[base.id].registry; const mockParams = { lotId, - callback: mockAddress, allowlist: { types: ["address", "uint256"], values: mockAllowlist, }, auctionHouse: mockAddress, - isTestnet: false, chainId: base.id, } satisfies UpdateAllowlistParams; const mockAuction = { batchAuctionLot: { + callbacks: mockAddress, info: { id: "auction-12341234", }, diff --git a/packages/sdk/src/utils/update-allowlist/get-config.ts b/packages/sdk/src/utils/update-allowlist/get-config.ts index 6ab6d187..a4fe8b96 100644 --- a/packages/sdk/src/utils/update-allowlist/get-config.ts +++ b/packages/sdk/src/utils/update-allowlist/get-config.ts @@ -8,6 +8,7 @@ import { getAuctionLot } from "../get-auction-lot"; import { deployments } from "@axis-finance/deployments"; import type { OriginConfig } from "../../types"; import { UpdateAllowlistParams, UpdateAllowlistResult } from "./types"; +import { Address, isAddress } from "viem"; export const getConfig = async ( params: UpdateAllowlistParams, @@ -23,13 +24,15 @@ export const getConfig = async ( ); } - const { lotId, auctionHouse, isTestnet, allowlist, callback, chainId } = - params; + const { lotId, auctionHouse, allowlist, chainId } = params; const subgraphEndpoint = subgraph ? subgraph[chainId].url : deployments[chainId].subgraphURL; + const { chain } = deployments[chainId]; + const isTestnet = !!chain.testnet; + const result = await getAuctionLot({ endpoint: subgraphEndpoint, lotId, @@ -41,6 +44,10 @@ export const getConfig = async ( throw new SdkError(`Auction with ${lotId} not found on ${chainId}`); } + if (!isAddress(auction.callbacks)) { + throw new SdkError(`Provided Auction doesn't have a callback address`); + } + const metadata = { ...auction.info, allowlist }; const ipfsCID = await metadataClient.save({ @@ -58,7 +65,7 @@ export const getConfig = async ( const setMerkleRootConfig = setMerkleRoot.getConfig({ lotId, allowlist, - callback, + callback: auction.callbacks as Address, }); return { registerAuctionConfig, setMerkleRootConfig }; diff --git a/packages/sdk/src/utils/update-allowlist/schema.ts b/packages/sdk/src/utils/update-allowlist/schema.ts index 0fbf7e78..74cab44e 100644 --- a/packages/sdk/src/utils/update-allowlist/schema.ts +++ b/packages/sdk/src/utils/update-allowlist/schema.ts @@ -2,9 +2,14 @@ import * as v from "valibot"; import { schema as registerAuctionSchema } from "../../registry/register-auction/schema"; import { schema as setMerkleRootSchema } from "../../periphery/callbacks/set-merkle-root/schema"; -const _registerAuctionSchema = v.omit(registerAuctionSchema, ["ipfsCID"]); +const _registerAuctionSchema = v.omit(registerAuctionSchema, [ + "ipfsCID", + "isTestnet", +]); + +const _setMerkleRootSchema = v.omit(setMerkleRootSchema, ["callback"]); export const schema = v.object({ ..._registerAuctionSchema.entries, - ...setMerkleRootSchema.entries, + ..._setMerkleRootSchema.entries, }); diff --git a/packages/sdk/src/utils/update-allowlist/types.ts b/packages/sdk/src/utils/update-allowlist/types.ts index ef7f8784..677ca742 100644 --- a/packages/sdk/src/utils/update-allowlist/types.ts +++ b/packages/sdk/src/utils/update-allowlist/types.ts @@ -4,7 +4,7 @@ import { SaveParams } from "../metadata-client"; export type UpdateAllowlistParams = Omit< RegisterAuctionParams & SetMerkleRootParams & SaveParams, - "ipfsCID" | "metadata" | "id" + "ipfsCID" | "metadata" | "id" | "isTestnet" | "callback" > & { chainId: number }; export type UpdateAllowlistResult = { From 09458cde650de02c5fb9bdba975cbed196887f6a Mon Sep 17 00:00:00 2001 From: aphex Date: Fri, 28 Feb 2025 23:11:14 +0000 Subject: [PATCH 4/4] add hook for updateAllowlist --- .../src/react/hooks/use-update-allowlist.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 packages/sdk/src/react/hooks/use-update-allowlist.ts diff --git a/packages/sdk/src/react/hooks/use-update-allowlist.ts b/packages/sdk/src/react/hooks/use-update-allowlist.ts new file mode 100644 index 00000000..0f7d4b87 --- /dev/null +++ b/packages/sdk/src/react/hooks/use-update-allowlist.ts @@ -0,0 +1,110 @@ +import { useQuery } from "@tanstack/react-query"; +import { UpdateAllowlistParams } from "../../utils/update-allowlist"; +import { useSdk } from "./use-sdk"; +import { + useSimulateContract, + useWaitForTransactionReceipt, + useWriteContract, + UseWaitForTransactionReceiptReturnType, +} from "wagmi"; + +type UseUpdateAllowlistResult = { + submitRegisterAuction: () => void; + isWaitingForRegisterAuction: boolean; + registerAuctionReceipt: UseWaitForTransactionReceiptReturnType; + registerAuctionError: Error | null; + + submitMerkleRootTx: () => void; + isWaitingForMerkleRoot: boolean; + merkleRootReceipt: UseWaitForTransactionReceiptReturnType; + merkleRootError: Error | null; +}; + +/** + * Updates an allowlist by saving it to a metadata server (defaulting to IPFS), + * updating the IPFS CID on the subgraph, and calculating and submitting the allowlist Merkle root. + * + * @param {UpdateAllowlistParams} params - Parameters for updating the allowlist + * @returns {Object} - The hook's return values and functions. + * @returns {Function} return.submitRegisterAuction - Submits the register auction transaction. + * @returns {boolean} return.isWaitingForRegisterAuction - Indicates if the register auction transaction is pending. + * @returns {Object} return.registerAuctionReceipt - Transaction receipt for the register auction. + * @returns {Error | null} return.registerAuctionError - Any error that occurred during the register auction process. + * + * @returns {Function} return.submitMerkleRootTx - Submits the Merkle root transaction. + * @returns {boolean} return.isWaitingForMerkleRoot - Indicates if the Merkle root transaction is pending. + * @returns {Object} return.merkleRootReceipt - Transaction receipt for setting the Merkle root. + * @returns {Error | null} return.merkleRootError - Any error that occurred during the Merkle root process. + */ +export function useUpdateAllowlist( + params: UpdateAllowlistParams, +): UseUpdateAllowlistResult { + const sdk = useSdk(); + + const config = useQuery({ + queryFn: async () => sdk.updateAllowlist(params), + queryKey: ["update-allowlist", params.lotId, params.chainId], + }); + + const merkleRootSim = useSimulateContract({ + ...config.data?.setMerkleRootConfig, + query: { + enabled: config.isSuccess, + }, + }); + + const registerAuctionSim = useSimulateContract({ + ...config.data?.registerAuctionConfig, + query: { + enabled: config.isSuccess, + }, + }); + + const registerAuctionTx = useWriteContract(); + const merkleRootTx = useWriteContract(); + + const submitRegisterAuction = () => { + if (registerAuctionSim.data) { + registerAuctionTx.writeContract(registerAuctionSim.data.request); + } + }; + + const submitMerkleRootTx = () => { + if (merkleRootSim.data) { + merkleRootTx.writeContract(merkleRootSim.data.request); + } + }; + + const registerAuctionReceipt = useWaitForTransactionReceipt({ + hash: registerAuctionTx.data, + }); + + const merkleRootReceipt = useWaitForTransactionReceipt({ + hash: merkleRootTx.data, + }); + + const isWaitingForRegisterAuction = + registerAuctionTx.isPending || registerAuctionReceipt.isLoading; + const isWaitingForMerkleRoot = + merkleRootTx.isPending || merkleRootReceipt.isLoading; + + const registerAuctionError = + registerAuctionSim.error || + registerAuctionTx.error || + registerAuctionTx.error; + + const merkleRootError = + merkleRootSim.error || merkleRootTx.error || merkleRootReceipt.error; + + return { + submitRegisterAuction, + isWaitingForRegisterAuction, + registerAuctionReceipt, + registerAuctionError, + + submitMerkleRootTx, + merkleRootError, + merkleRootReceipt, + isWaitingForMerkleRoot, + }; +}