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, + }; +} diff --git a/packages/sdk/src/sdk/sdk.ts b/packages/sdk/src/sdk/sdk.ts index bdcd88f0..55b65705 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,61 @@ 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 {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"; + * + * 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..b07209e9 --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/get-config.test.ts @@ -0,0 +1,92 @@ +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, + allowlist: { + types: ["address", "uint256"], + values: mockAllowlist, + }, + auctionHouse: mockAddress, + chainId: base.id, +} satisfies UpdateAllowlistParams; + +const mockAuction = { + batchAuctionLot: { + callbacks: mockAddress, + 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], + }); + }); + + 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()]`, + ); + }); +}); 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..a4fe8b96 --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/get-config.ts @@ -0,0 +1,72 @@ +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"; +import { Address, isAddress } from "viem"; + +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, 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, + }); + + const auction = result.batchAuctionLot; + + if (!auction) { + 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({ + metadata: JSON.stringify(metadata), + id: auction.info?.key ?? undefined, + }); + + const registerAuctionConfig = registerAuction.getConfig({ + ipfsCID, + lotId, + auctionHouse, + isTestnet, + }); + + const setMerkleRootConfig = setMerkleRoot.getConfig({ + lotId, + allowlist, + callback: auction.callbacks as Address, + }); + + 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..74cab44e --- /dev/null +++ b/packages/sdk/src/utils/update-allowlist/schema.ts @@ -0,0 +1,15 @@ +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", + "isTestnet", +]); + +const _setMerkleRootSchema = v.omit(setMerkleRootSchema, ["callback"]); + +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..677ca742 --- /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" | "isTestnet" | "callback" +> & { chainId: number }; + +export type UpdateAllowlistResult = { + registerAuctionConfig: RegisterAuctionConfig; + setMerkleRootConfig: SetMerkleRootConfig; +};