diff --git a/packages/1155-contracts/package/premint-api.test.ts b/packages/1155-contracts/package/premint-api.test.ts index f30e4dfe2..a86e090fc 100644 --- a/packages/1155-contracts/package/premint-api.test.ts +++ b/packages/1155-contracts/package/premint-api.test.ts @@ -5,28 +5,12 @@ import { createPublicClient, Address, } from "viem"; -import { foundry, zoraTestnet } from "viem/chains"; +import { foundry } from "viem/chains"; import { describe, it, beforeEach, expect, vi } from "vitest"; import { parseEther } from "viem"; import { - zoraCreator1155PremintExecutorImplABI as preminterAbi, - zoraCreator1155PremintExecutorImplAddress as zoraCreator1155PremintExecutorAddress, - zoraCreator1155ImplABI, zoraCreator1155FactoryImplAddress, - zoraCreator1155FactoryImplConfig, } from "./wagmiGenerated"; -import ZoraCreator1155Attribution from "../out/ZoraCreator1155Attribution.sol/ZoraCreator1155Attribution.json"; -import zoraCreator1155PremintExecutor from "../out/ZoraCreator1155PremintExecutorImpl.sol/ZoraCreator1155PremintExecutorImpl.json"; -import zoraCreator1155Impl from "../out/ZoraCreator1155Impl.sol/ZoraCreator1155Impl.json"; -import zoraCreator1155FactoryImpl from "../out/ZoraCreator1155FactoryImpl.sol/ZoraCreator1155FactoryImpl.json"; -import zoraCreatorFixedPriceSaleStrategy from "../out/ZoraCreatorFixedPriceSaleStrategy.sol/ZoraCreatorFixedPriceSaleStrategy.json"; -import protocolRewards from "../out/ProtocolRewards.sol/ProtocolRewards.json"; -import { - ContractCreationConfig, - PremintConfig, - TokenCreationConfig, - preminterTypedDataDefinition, -} from "./preminter"; import { BackendChainNames, PreminterAPI } from "./premint-api"; const chain = foundry; @@ -48,8 +32,10 @@ const publicClient = createPublicClient({ }); // JSON-RPC Account -const [deployerAccount, creatorAccount, collectorAccount] = - (await walletClient.getAddresses()) as [Address, Address, Address]; +const [deployerAccount, secondWallet] = (await walletClient.getAddresses()) as [ + Address, + Address +]; type TestContext = { preminterAddress: `0x${string}`; @@ -60,36 +46,23 @@ type TestContext = { }; describe("ZoraCreator1155Preminter", () => { - beforeEach(async (ctx) => { + beforeEach(async () => { // deploy signature minter contract await testClient.setBalance({ address: deployerAccount, - value: parseEther("10"), + value: parseEther("1"), }); - ctx.forkedChainId = zoraTestnet.id; - ctx.anvilChainId = foundry.id; - - let preminterAddress: Address; - - const factoryProxyAddress = - zoraCreator1155FactoryImplAddress[ctx.forkedChainId]; - // ctx.fixedPriceMinterAddress = await publicClient.readContract({ - // abi: zoraCreator1155FactoryImplConfig.abi, - // address: zoraCreator1155FactoryImplAddress[ctx.forkedChainId], - // functionName: "fixedPriceMinter", - // }); - preminterAddress = zoraCreator1155PremintExecutorAddress[ctx.forkedChainId]; - - ctx.zoraMintFee = parseEther("0.000777"); - - ctx.preminterAddress = preminterAddress; + await testClient.setBalance({ + address: secondWallet, + value: parseEther("1"), + }); }, 20 * 1000); // skip for now - we need to make this work on zora testnet chain too it( "can sign on the forked premint contract", - async ({ fixedPriceMinterAddress, forkedChainId, anvilChainId }) => { + async () => { const preminterApi = new PreminterAPI(chain); preminterApi.get = vi.fn().mockResolvedValue({ next_uid: 3 }); @@ -181,6 +154,7 @@ describe("ZoraCreator1155Preminter", () => { transport: http(), }); const signatureValid = await preminter.isValidSignature({ + // @ts-ignore: Fix enum type data: premintData, publicClient, }); @@ -237,7 +211,31 @@ describe("ZoraCreator1155Preminter", () => { }, }); - console.log({ premint }); + expect(premint.log).toEqual({ + contractAddress: "0x0bdD2Fcb03912403c0B4699EDBB6bDAd65dACf62", + contractConfig: { + contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + contractName: "Testing Contract", + contractURI: "https://zora.co/testing/contract.json", + }, + createdNewContract: false, + minter: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + quantityMinted: 1n, + tokenConfig: { + fixedPriceMinter: "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", + maxSupply: 18446744073709551615n, + maxTokensPerAddress: 0n, + mintDuration: 604800n, + mintStart: 0n, + pricePerToken: 0n, + royaltyBPS: 1000, + royaltyMintSchedule: 0, + royaltyRecipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + tokenURI: "https://zora.co/testing/token.json", + }, + tokenId: 1n, + uid: 3, + }); }, 20 * 1000 ); diff --git a/packages/1155-contracts/package/premint-api.ts b/packages/1155-contracts/package/premint-api.ts index 3a3e30a7a..793b18565 100644 --- a/packages/1155-contracts/package/premint-api.ts +++ b/packages/1155-contracts/package/premint-api.ts @@ -1,10 +1,11 @@ -import { createPublicClient, http, isAddressEqual } from "viem"; +import { createPublicClient, decodeEventLog, http } from "viem"; import type { Account, Address, Chain, Hex, PublicClient, + TransactionReceipt, WalletClient, } from "viem"; import { @@ -74,6 +75,22 @@ const DefaultMintArguments = { royaltyBPS: 1000, // 10%, }; +function getLogFromReceipt(receipt: TransactionReceipt) { + for (const data of receipt.logs) { + try { + const decodedLog = decodeEventLog({ + abi: zoraCreator1155PremintExecutorImplABI, + eventName: "Preminted", + ...data, + }); + return decodedLog.args; + } catch (err: any) {} + } +} + +/** + * Premint server response type. + */ export type PremintResponse = { collection: { contractAdmin: Address; @@ -101,6 +118,11 @@ export type PremintResponse = { signature: Hex; }; +/** + * Convert server to on-chain types for a premint + * @param premint Premint object from the server to convert to one that's compatible with viem + * @returns Viem type-compatible premint object + */ export const convertPremint = (premint: PremintResponse["premint"]) => ({ ...premint, tokenConfig: { @@ -113,6 +135,11 @@ export const convertPremint = (premint: PremintResponse["premint"]) => ({ }, }); +/** + * Convert on-chain types for a premint to a server safe type + * @param premint Premint object from viem to convert to a JSON compatible type. + * @returns JSON compatible premint + */ export const encodePremintForAPI = ({ tokenConfig, ...premint @@ -128,8 +155,15 @@ export const encodePremintForAPI = ({ }, }); +/** + * Zora API Server base URL + */ const ZORA_PREMINT_API_BASE = "https://api.zora.co/premint/"; +/** + * Preminter API to access ZORA Premint functionality. + * Currently only supports V1 premints. + */ export class PreminterAPI { network: NetworkConfig; chain: Chain; @@ -144,19 +178,52 @@ export class PreminterAPI { this.network = networkConfig; } + /** + * The premint executor address is deployed to the same address across all chains. + * Can be overridden as needed by making a parent class. + * + * @returns Executor address for premints + */ getExecutorAddress() { return zoraCreator1155PremintExecutorImplAddress[999]; } + /** + * The fixed price minter address is the same across all chains for our current + * deployer strategy. + * Can be overridden as needed by making a parent class. + * + * @returns Fixed price sale strategy + */ getFixedPriceMinterAddress() { return zoraCreatorFixedPriceSaleStrategyAddress[999]; } + /** + * A simple fetch() wrapper for HTTP gets. + * Can be overridden as needed. + * + * @param path Path to run HTTP JSON get against + * @returns JSON object response + * @throws Error when HTTP response fails + */ async get(path: string) { const response = await fetch(path, { method: "GET" }); + if (response.status !== 200) { + throw new Error(`Invalid response, status ${response.status}`); + } return await response.json(); } + /** + * A simple fetch() wrapper for HTTP post. + * Can be overridden as needed. + * + * @param path Path to run HTTP JSON POST against + * @param data Data to POST to the server, converted to JSON + * @returns JSON object response + * @throws Error when HTTP response fails + */ async post(path: string, data: any) { const response = await fetch(path, { method: "POST", @@ -172,6 +239,34 @@ export class PreminterAPI { return await response.json(); } + /** + * Getter for public client that instantiates a publicClient as needed + * + * @param publicClient Optional viem public client + * @returns Existing public client or makes a new one for the given chain as needed. + */ + getPublicClient(publicClient?: PublicClient) { + if (publicClient) { + return publicClient; + } + return createPublicClient({ chain: this.chain, transport: http() }); + } + + /** + * Create premint + * + * @param settings Settings for the new premint + * @param settings.account Account to sign the premint with. Taken from walletClient if none passed in. + * @param settings.collection Collection information for the mint + * @param settings.mint Mint argument settings, optional settings are overridden with sensible defaults. + * @param settings.publicClient Public client (optional) – instantiated if not passed in with defaults. + * @param settings.walletClient Required wallet client for signing the premint message. + * @param settings.executionSettings Execution settings for premint options + * @param settings.executionSettings.deleted If this UID should be deleted. If omitted, set to false. + * @param settings.executionSettings.uid the UID to use – optional and retrieved as a fresh UID from ZORA by default. + * @param settings.checkSignature if the signature should have a pre-flight check. Not required but helpful for debugging. + * @returns premint url, uid, newContractAddress, and premint object + */ async createPremint({ account, collection, @@ -179,7 +274,7 @@ export class PreminterAPI { publicClient, walletClient, executionSettings, - checkSignature = true, + checkSignature = false, }: { account: Address; checkSignature?: boolean; @@ -192,12 +287,8 @@ export class PreminterAPI { uid?: number; }; }) { - if (!publicClient) { - publicClient = createPublicClient({ - chain: this.chain, - transport: http(), - }); - } + publicClient = this.getPublicClient(publicClient); + const newContractAddress = await publicClient.readContract({ address: this.getExecutorAddress(), abi: zoraCreator1155PremintExecutorImplABI, @@ -277,6 +368,13 @@ export class PreminterAPI { }; } + /** + * Fetches given premint data from the ZORA API. + * + * @param address Address for the premint contract + * @param uid UID for the desired premint + * @returns PremintResponse of premint data from the API + */ async getPremintData(address: string, uid: number): Promise { const response = await this.get( `${ZORA_PREMINT_API_BASE}signature/${this.network.zoraBackendChainName}/${address}/${uid}` @@ -284,17 +382,25 @@ export class PreminterAPI { return response as PremintResponse; } + /** + * Check user signature for v1 + * + * @param data Signature data from the API + * @returns isValid = signature is valid or not, contractAddress = assumed contract address, recoveredSigner = signer from contract + */ async isValidSignature({ data, publicClient, }: { data: PremintResponse; - publicClient: PublicClient; + publicClient?: PublicClient; }): Promise<{ isValid: boolean; contractAddress: Address; recoveredSigner: Address; }> { + publicClient = this.getPublicClient(publicClient); + const [isValid, contractAddress, recoveredSigner] = await publicClient.readContract({ abi: zoraCreator1155PremintExecutorImplABI, @@ -306,6 +412,17 @@ export class PreminterAPI { return { isValid, contractAddress, recoveredSigner }; } + /** + * + * @param settings.data Data from the API for the mint + * @param settings.account Optional account (if omitted taken from wallet client) for the account executing the premint. + * @param settings.walletClient WalletClient to send execution from. + * @param settings.mintArguments User minting arguments. + * @param settings.mintArguments.quantityToMint Quantity to mint, optional, defaults to 1. + * @param settings.mintArguments.mintComment Optional mint comment, optional, omits when not included. + * @param settings.publicClient Optional public client for preflight checks. + * @returns receipt, log, zoraURL + */ async executePremintWithWallet({ data, account, @@ -316,47 +433,58 @@ export class PreminterAPI { data: PremintResponse; walletClient: WalletClient; account?: Account | Address; - mintArguments: { - quantityToMint: bigint; - mintComment: string; + mintArguments?: { + quantityToMint: number; + mintComment?: string; }; publicClient?: PublicClient; }) { + publicClient = this.getPublicClient(publicClient); + + if (mintArguments && mintArguments?.quantityToMint < 1) { + throw new Error("Quantity to mint cannot be below 1"); + } + const targetAddress = this.getExecutorAddress(); + const numberToMint = BigInt(mintArguments?.quantityToMint || 1); const args = [ data.collection, convertPremint(data.premint), data.signature, - mintArguments.quantityToMint, - mintArguments.mintComment, + numberToMint, + mintArguments?.mintComment || "", ] as const; if (!account) { - account = walletClient.account!; + account = walletClient.account; } - const value = mintArguments.quantityToMint * this.rewardPerToken; - - if (publicClient) { - const { request } = await publicClient.simulateContract({ - account, - abi: zoraCreator1155PremintExecutorImplABI, - functionName: "premint", - value, - address: targetAddress, - args, - }); - return await walletClient.writeContract(request); + if (!account) { + throw new Error("Wallet not passed in"); } - return await walletClient.writeContract({ - abi: zoraCreator1155PremintExecutorImplABI, + const value = numberToMint * this.rewardPerToken; + + const { request } = await publicClient.simulateContract({ account, - value, - chain: this.chain, + abi: zoraCreator1155PremintExecutorImplABI, functionName: "premint", + value, address: targetAddress, args, }); + const hash = await walletClient.writeContract(request); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + const log = getLogFromReceipt(receipt); + + return { + receipt, + log, + zoraUrl: log + ? `https://${this.network.isTestnet ? "testnet." : ""}zora.co/collect/${ + this.network.zoraPathChainName + }:${log.contractAddress}/${log.tokenId}` + : null, + }; } }