From a75632acc753994694db013a4e8aa4a13e843c6a Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 28 Nov 2023 15:29:48 -0800 Subject: [PATCH] PremintClient updates: PremintApiClient is now a class, createPremintClient now follows pattern of createMintClient. executePremint renamed to makeMintParemeters (#390) * premint client now follows standard mint client functinoality * added changeset * updated readme with better examples. renamed premint mint function * lint fix --- .changeset/pink-turtles-watch.md | 5 + packages/protocol-sdk/README.md | 145 ++++++++++-------- .../src/premint/premint-api-client.ts | 90 ++++++++--- .../src/premint/premint-client.test.ts | 41 ++--- .../src/premint/premint-client.ts | 83 +++++----- 5 files changed, 216 insertions(+), 148 deletions(-) create mode 100644 .changeset/pink-turtles-watch.md diff --git a/.changeset/pink-turtles-watch.md b/.changeset/pink-turtles-watch.md new file mode 100644 index 000000000..65e056711 --- /dev/null +++ b/.changeset/pink-turtles-watch.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/protocol-sdk": patch +--- + +premintClient can have http methods overridable via DI, and now takes publicClient and http overrides in `createPremintClient` function. it no longer takes `publicClient` as an argument in functions, and rather uses them from the constructor. `executePremint` has been renamed ot `makeMintParameters` diff --git a/packages/protocol-sdk/README.md b/packages/protocol-sdk/README.md index bab6150ad..d231a7e8d 100644 --- a/packages/protocol-sdk/README.md +++ b/packages/protocol-sdk/README.md @@ -214,53 +214,71 @@ export async function createContract({ } ``` -### Creating a premint: +### Creating a token for free (gasless creation): ```ts -import { PremintAPI } from "@zoralabs/protocol-sdk"; -import type { Address, WalletClient } from "viem"; +import {createPremintClient} from "@zoralabs/protocol-sdk"; +import type {Address, PublicClient, WalletClient} from "viem"; -async function makePremint(walletClient: WalletClient) { - // Create premint - const premint = await createPremintAPI(walletClient.chain).createPremint({ - // Extra step to check the signature on-chain before attempting to sign +async function createForFree({ + walletClient, + creatorAccount, +}: { + // wallet client that will submit the transaction + walletClient: WalletClient; + // address of the token contract + creatorAccount: Address; +}) { + const premintClient = createPremintClient({ chain: walletClient.chain! }); + + // create and sign a free token creation. + const createdPremint = await premintClient.createPremint({ + walletClient, + account: creatorAccount, + // if true, will validate that the creator is authorized to create premints on the contract. checkSignature: true, - // Collection information that this premint NFT will exist in once minted. + // collection info of collection to create collection: { - contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + contractAdmin: creatorAccount, contractName: "Testing Contract", contractURI: "ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e", }, - // WalletClient doing the signature - walletClient, - // Token information, falls back to defaults set in DefaultMintArguments. + // token info of token to create token: { tokenURI: "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", }, }); - console.log(`created ZORA premint, link: ${premint.url}`); - return premint; -} + const premintUid = createdPremint.uid; + const premintCollectionAddress = createdPremint.verifyingContract; + + return { + // unique id of created premint, which can be used later to + // update or delete the premint + uid: premintUid, + tokenContract: premintCollectionAddress, + } +} ``` -### Updating a premint: +### Updating a token that was created for free (before it was brought onchain): + +Before a token that was created for free is brought onchain, it can be updated by the original creator of that token, by having that creator sign a message indicating the update. This is useful for updating the tokenURI, other metadata, or token sale configuration (price, duration, limits, etc.): ```ts -import { PremintAPI } from "@zoralabs/premint-sdk"; -import type { Address, WalletClient } from "viem"; +import {createPremintClient} from "@zoralabs/protocol-sdk"; +import type {Address, PublicClient, WalletClient} from "viem"; -async function updatePremint(walletClient: WalletClient) { +async function updateCreatedForFreeToken(walletClient: WalletClient, premintUid: number) { // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). - const premintAPI = createPremintAPI(walletClient.chain); + const premintClient = createPremintClient({ chain: walletClient.chain! }); - // Create premint - const premint = await premintAPI.updatePremint({ - // Extra step to check the signature on-chain before attempting to sign + // sign a message to update the created for free token, and store the update + await premintClient.updatePremint({ collection: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - uid: 23, + uid: premintUid, // WalletClient doing the signature walletClient, // Token information, falls back to defaults set in DefaultMintArguments. @@ -269,73 +287,72 @@ async function updatePremint(walletClient: WalletClient) { "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", }, }); - - console.log(`updated ZORA premint, link: ${premint.url}`); - return premint; } ``` -### Deleting a premint: +### Deleting a token that was created for free (before it was brought onchain): + +Before a token that was created for free is brought onchain, it can be deleted by the original creator of that token, by having that creator sign a message indicating the deletion: ```ts -import { PremintAPI } from "@zoralabs/premint-sdk"; -import type { Address, WalletClient } from "viem"; +import {createPremintClient} from "@zoralabs/protocol-sdk"; +import type {Address, PublicClient, WalletClient} from "viem"; -async function deletePremint(walletClient: WalletClient) { - // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). - const premintAPI = createPremintClient({ chain: walletClient.chain }); +async function deleteCreatedForFreeToken(walletClient: WalletClient) { + const premintClient = createPremintClient({ chain: walletClient.chain! }); - // Create premint - const premint = await premintAPI.deletePremint({ + // sign a message to delete the premint, and store the deletion + await premintClient.deletePremint({ // Extra step to check the signature on-chain before attempting to sign collection: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", uid: 23, // WalletClient doing the signature walletClient, }); - - console.log(`updated ZORA premint, link: ${premint.url}`); - return premint; } ``` -### Executing a premint: +### Minting a token that was created for free (and bringing it onchain): ```ts -import { PremintAPI } from "@zoralabs/premint-sdk"; -import type { Address, WalletClient } from "viem"; +import {createPremintClient} from "@zoralabs/protocol-sdk"; +import type {Address, PublicClient, WalletClient} from "viem"; -async function executePremint( +async function mintCreatedForFreeToken( walletClient: WalletClient, - premintAddress: Address, - premintUID: number, + publicClient: PublicClient, + minterAccount: Address, ) { - const premintAPI = createPremintClient({ chain: walletClient.chain }); - - return await premintAPI.executePremintWithWallet({ - data: premintAPI.getPremintData(premintAddress, premintUID), - walletClient, + const premintClient = createPremintClient({ chain: walletClient.chain! }); + + const simulateContractParameters = await premintClient.makeMintParameters({ + account: minterAccount, + data: await premintClient.getPremintData({ + address: "0xf8dA7f53c283d898818af7FB9d98103F559bDac2", + uid: 3, + }), mintArguments: { quantityToMint: 1, + mintComment: "", }, }); -} -``` -### Deleting a premint: + // simulate the transaction and get any validation errors + const { request } = await publicClient.simulateContract(simulateContractParameters); -```ts -import {PremintAPI} from '@zoralabs/premint-sdk'; -import type {Address, WalletClient} from 'viem'; + // submit the transaction to the network + const txHash = await walletClient.writeContract(request); -async function deletePremint(walletClient: WalletClient, collection: Address, uid: number) { - const premintAPI = createPremintClient({chain: walletClient.chain}); + // wait for the transaction to be complete + const receipt = await publicClient.waitForTransactionReceipt({hash: txHash}); - return await premintAPI.deletePremint({ - walletClient, - uid, - collection - }); -} + const { urls } = await premintClient.getDataFromPremintReceipt(receipt); -``` + // block explorer url: + console.log(urls.explorer); + // collect url: + console.log(urls.zoraCollect); + // manage url: + console.log(urls.zoraManage); +} +``` \ No newline at end of file diff --git a/packages/protocol-sdk/src/premint/premint-api-client.ts b/packages/protocol-sdk/src/premint/premint-api-client.ts index c4be78df2..5b7c45b41 100644 --- a/packages/protocol-sdk/src/premint/premint-api-client.ts +++ b/packages/protocol-sdk/src/premint/premint-api-client.ts @@ -1,6 +1,11 @@ -import { post, retries, get } from "../apis/http-api-base"; +import { + IHttpClient, + httpClient as defaultHttpClient, +} from "../apis/http-api-base"; import { components, paths } from "../apis/generated/premint-api-types"; import { ZORA_API_BASE } from "../constants"; +import { NetworkConfig } from "src/apis/chain-constants"; +import { getApiNetworkConfigForChain } from "src/mint/mint-api-client"; type SignaturePostType = paths["/signature"]["post"]; type PremintSignatureRequestBody = @@ -22,36 +27,85 @@ type PremintSignatureGetPathParameters = export type PremintSignatureGetResponse = SignaturePremintGetType["responses"][200]["content"]["application/json"]; +export type PremintCollection = PremintSignatureGetResponse["collection"]; + export type BackendChainNames = components["schemas"]["ChainName"]; -const postSignature = async ( - data: PremintSignatureRequestBody, -): Promise => +const postSignature = async ({ + httpClient: { post, retries } = defaultHttpClient, + ...data +}: PremintSignatureRequestBody & { + httpClient?: Pick; +}): Promise => retries(() => post(`${ZORA_API_BASE}premint/signature`, data), ); -const getNextUID = async ( - path: PremintNextUIDGetPathParameters, -): Promise => +const getNextUID = async ({ + chain_name, + collection_address, + httpClient: { retries, get } = defaultHttpClient, +}: PremintNextUIDGetPathParameters & { + httpClient?: Pick; +}): Promise => retries(() => get( - `${ZORA_API_BASE}premint/signature/${path.chain_name}/${path.collection_address}/next_uid`, + `${ZORA_API_BASE}premint/signature/${chain_name}/${collection_address}/next_uid`, ), ); -const getSignature = async ( - path: PremintSignatureGetPathParameters, -): Promise => +const getSignature = async ({ + collection_address, + uid, + chain_name, + httpClient: { retries, get } = defaultHttpClient, +}: PremintSignatureGetPathParameters & { + httpClient?: Pick; +}): Promise => retries(() => get( - `${ZORA_API_BASE}premint/signature/${path.chain_name}/${path.collection_address}/${path.uid}`, + `${ZORA_API_BASE}premint/signature/${chain_name}/${collection_address}/${uid}`, ), ); -export const PremintAPIClient = { - postSignature, - getSignature, - getNextUID, -}; -export { ZORA_API_BASE }; +type OmitChainName = Omit; + +class PremintAPIClient { + httpClient: IHttpClient; + networkConfig: NetworkConfig; + + constructor(chainId: number, httpClient?: IHttpClient) { + this.httpClient = httpClient || defaultHttpClient; + this.networkConfig = getApiNetworkConfigForChain(chainId); + } + postSignature = async ( + data: OmitChainName, + ): Promise => + postSignature({ + ...data, + chain_name: this.networkConfig.zoraBackendChainName, + httpClient: this.httpClient, + }); + + getNextUID = async ( + path: OmitChainName, + ): Promise => + getNextUID({ + ...path, + chain_name: this.networkConfig.zoraBackendChainName, + httpClient: this.httpClient, + }); + + getSignature = async ({ + collection_address, + uid, + }: OmitChainName): Promise => + getSignature({ + collection_address, + uid, + chain_name: this.networkConfig.zoraBackendChainName, + httpClient: this.httpClient, + }); +} + +export { ZORA_API_BASE, PremintAPIClient }; diff --git a/packages/protocol-sdk/src/premint/premint-client.test.ts b/packages/protocol-sdk/src/premint/premint-client.test.ts index 076ed7af0..7a656df6e 100644 --- a/packages/protocol-sdk/src/premint/premint-client.test.ts +++ b/packages/protocol-sdk/src/premint/premint-client.test.ts @@ -2,14 +2,16 @@ import { foundry } from "viem/chains"; import { describe, expect, vi } from "vitest"; import { createPremintClient } from "./premint-client"; import { anvilTest } from "src/anvil"; -import { BackendChainNamesLookup } from "src/apis/chain-constants"; describe("ZoraCreator1155Premint", () => { anvilTest( "can sign on the forked premint contract", async ({ viemClients: { walletClient, publicClient } }) => { const [deployerAccount] = await walletClient.getAddresses(); - const premintClient = createPremintClient({ chain: foundry }); + const premintClient = createPremintClient({ + chain: foundry, + publicClient, + }); premintClient.apiClient.getNextUID = vi .fn() @@ -20,7 +22,6 @@ describe("ZoraCreator1155Premint", () => { await premintClient.createPremint({ walletClient, - publicClient, account: deployerAccount!, checkSignature: true, collection: { @@ -36,7 +37,6 @@ describe("ZoraCreator1155Premint", () => { }); expect(premintClient.apiClient.postSignature).toHaveBeenCalledWith({ - chain_name: BackendChainNamesLookup.ZORA_GOERLI, collection: { contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", contractName: "Testing Contract", @@ -71,7 +71,10 @@ describe("ZoraCreator1155Premint", () => { anvilTest( "can validate premint on network", async ({ viemClients: { publicClient } }) => { - const premintClient = createPremintClient({ chain: foundry }); + const premintClient = createPremintClient({ + chain: foundry, + publicClient, + }); const premintData = { collection: { @@ -106,7 +109,6 @@ describe("ZoraCreator1155Premint", () => { const signatureValid = await premintClient.isValidSignature({ // @ts-ignore: Fix enum type data: premintData, - publicClient, }); expect(signatureValid.isValid).toBe(true); }, @@ -149,19 +151,22 @@ describe("ZoraCreator1155Premint", () => { }); premintClient.apiClient.postSignature = vi.fn(); - const { request } = await premintClient.executePremint({ - account: deployerAccount!, - data: await premintClient.getPremintData({ - address: "0xf8dA7f53c283d898818af7FB9d98103F559bDac2", - uid: 3, - }), - mintArguments: { - quantityToMint: 1, - mintComment: "", + const simulateContractParameters = await premintClient.makeMintParameters( + { + account: deployerAccount!, + data: await premintClient.getPremintData({ + address: "0xf8dA7f53c283d898818af7FB9d98103F559bDac2", + uid: 3, + }), + mintArguments: { + quantityToMint: 1, + mintComment: "", + }, }, - }); - const { request: simulateRequest } = - await publicClient.simulateContract(request); + ); + const { request: simulateRequest } = await publicClient.simulateContract( + simulateContractParameters, + ); const hash = await walletClient.writeContract(simulateRequest); const receipt = await publicClient.waitForTransactionReceipt({ hash }); const { premintedLog, urls } = diff --git a/packages/protocol-sdk/src/premint/premint-client.ts b/packages/protocol-sdk/src/premint/premint-client.ts index 790e58a9e..33abcbb35 100644 --- a/packages/protocol-sdk/src/premint/premint-client.ts +++ b/packages/protocol-sdk/src/premint/premint-client.ts @@ -1,4 +1,4 @@ -import { decodeEventLog } from "viem"; +import { createPublicClient, decodeEventLog, http } from "viem"; import type { Account, Address, @@ -21,9 +21,10 @@ import type { } from "./premint-api-client"; import { PremintAPIClient } from "./premint-api-client"; import type { DecodeEventLogReturnType } from "viem"; -import { ClientBase } from "../apis/client-base"; import { OPEN_EDITION_MINT_SIZE } from "../constants"; import { REWARD_PER_TOKEN } from "src/apis/chain-constants"; +import { IHttpClient } from "src/apis/http-api-base"; +import { getApiNetworkConfigForChain } from "src/mint/mint-api-client"; type MintArgumentsSettings = { tokenURI: string; @@ -143,16 +144,20 @@ export const encodePremintForAPI = ({ * Preminter API to access ZORA Premint functionality. * Currently only supports V1 premints. */ -class PremintClient extends ClientBase { - apiClient: typeof PremintAPIClient; - - constructor(chain: Chain, apiClient?: typeof PremintAPIClient) { - super(chain); - - if (!apiClient) { - apiClient = PremintAPIClient; - } - this.apiClient = apiClient; +class PremintClient { + readonly apiClient: PremintAPIClient; + readonly publicClient: PublicClient; + readonly chain: Chain; + + constructor( + chain: Chain, + publicClient?: PublicClient, + httpClient?: IHttpClient, + ) { + this.chain = chain; + this.apiClient = new PremintAPIClient(chain.id, httpClient); + this.publicClient = + publicClient || createPublicClient({ chain, transport: http() }); } /** @@ -219,7 +224,6 @@ class PremintClient extends ClientBase { collection: Address; }): Promise { const signatureResponse = await this.apiClient.getSignature({ - chain_name: this.network.zoraBackendChainName, collection_address: collection.toLowerCase(), uid: uid, }); @@ -241,7 +245,6 @@ class PremintClient extends ClientBase { account, checkSignature: false, verifyingContract: collection, - publicClient: this.getPublicClient(), uid: uid, collection: { ...signerData.collection, @@ -270,16 +273,13 @@ class PremintClient extends ClientBase { uid, account, collection, - publicClient, }: { walletClient: WalletClient; - publicClient: PublicClient; uid: number; account?: Account | Address; collection: Address; }) { const signatureResponse = await this.apiClient.getSignature({ - chain_name: this.network.zoraBackendChainName, collection_address: collection.toLowerCase(), uid: uid, }); @@ -298,7 +298,6 @@ class PremintClient extends ClientBase { account, checkSignature: false, verifyingContract: collection, - publicClient: this.getPublicClient(publicClient), uid: uid, collection: signerData.collection, premintConfig: signerData.premint, @@ -313,7 +312,6 @@ class PremintClient extends ClientBase { */ private async signAndSubmitPremint({ walletClient, - publicClient, verifyingContract, premintConfig, uid, @@ -321,7 +319,6 @@ class PremintClient extends ClientBase { checkSignature, collection, }: { - publicClient: PublicClient; uid: number; walletClient: WalletClient; verifyingContract: Address; @@ -347,7 +344,7 @@ class PremintClient extends ClientBase { }); if (checkSignature) { - const [isValidSignature] = await publicClient.readContract({ + const [isValidSignature] = await this.publicClient.readContract({ abi: zoraCreator1155PremintExecutorImplABI, address: this.getExecutorAddress(), functionName: "isValidSignature", @@ -361,7 +358,6 @@ class PremintClient extends ClientBase { const apiData = { collection, premint: encodePremintForAPI(premintConfig), - chain_name: this.network.zoraBackendChainName, signature: signature, }; @@ -394,7 +390,6 @@ class PremintClient extends ClientBase { account, collection, token, - publicClient, walletClient, executionSettings, checkSignature = false, @@ -404,15 +399,12 @@ class PremintClient extends ClientBase { walletClient: WalletClient; collection: PremintSignatureGetResponse["collection"]; token: MintArgumentsSettings; - publicClient?: PublicClient; executionSettings?: { deleted?: boolean; uid?: number; }; }) { - publicClient = this.getPublicClient(publicClient); - - const newContractAddress = await publicClient.readContract({ + const newContractAddress = await this.publicClient.readContract({ address: this.getExecutorAddress(), abi: zoraCreator1155PremintExecutorImplABI, functionName: "getContractAddress", @@ -429,7 +421,6 @@ class PremintClient extends ClientBase { let uid = executionSettings?.uid; if (!uid) { const uidResponse = await this.apiClient.getNextUID({ - chain_name: this.network.zoraBackendChainName, collection_address: newContractAddress.toLowerCase(), }); uid = uidResponse.next_uid; @@ -454,7 +445,6 @@ class PremintClient extends ClientBase { premintConfig, checkSignature, account, - publicClient, walletClient, collection, }); @@ -475,7 +465,6 @@ class PremintClient extends ClientBase { uid: number; }): Promise { return await this.apiClient.getSignature({ - chain_name: this.network.zoraBackendChainName, collection_address: address, uid, }); @@ -489,19 +478,15 @@ class PremintClient extends ClientBase { */ async isValidSignature({ data, - publicClient, }: { data: PremintSignatureGetResponse; - publicClient?: PublicClient; }): Promise<{ isValid: boolean; contractAddress: Address; recoveredSigner: Address; }> { - publicClient = this.getPublicClient(publicClient); - const [isValid, contractAddress, recoveredSigner] = - await publicClient.readContract({ + await this.publicClient.readContract({ abi: zoraCreator1155PremintExecutorImplABI, address: this.getExecutorAddress(), functionName: "isValidSignature", @@ -530,19 +515,21 @@ class PremintClient extends ClientBase { const zoraTokenPath = uid ? `premint-${uid}` : tokenId; + const network = getApiNetworkConfigForChain(this.chain.id); + return { explorer: tokenId ? `https://${this.chain.blockExplorers?.default.url}/token/${address}/instance/${tokenId}` : null, zoraCollect: `https://${ - this.network.isTestnet ? "testnet." : "" + network.isTestnet ? "testnet." : "" }zora.co/collect/${ - this.network.zoraPathChainName + network.zoraPathChainName }:${address}/${zoraTokenPath}`, zoraManage: `https://${ - this.network.isTestnet ? "testnet." : "" + network.isTestnet ? "testnet." : "" }zora.co/collect/${ - this.network.zoraPathChainName + network.zoraPathChainName }:${address}/${zoraTokenPath}`, }; } @@ -559,7 +546,7 @@ class PremintClient extends ClientBase { * @param settings.publicClient Optional public client for preflight checks. * @returns receipt, log, zoraURL */ - async executePremint({ + async makeMintParameters({ data, account, mintArguments, @@ -570,7 +557,7 @@ class PremintClient extends ClientBase { quantityToMint: number; mintComment?: string; }; - }): Promise<{ request: SimulateContractParameters }> { + }): Promise { if (mintArguments && mintArguments?.quantityToMint < 1) { throw new Error("Quantity to mint cannot be below 1"); } @@ -591,7 +578,7 @@ class PremintClient extends ClientBase { const value = numberToMint * REWARD_PER_TOKEN; - const request = { + const request: SimulateContractParameters = { account, abi: zoraCreator1155PremintExecutorImplABI, functionName: "premint", @@ -600,18 +587,18 @@ class PremintClient extends ClientBase { args, }; - return { - request, - }; + return request; } } export function createPremintClient({ chain, - premintAPIClient, + httpClient, + publicClient, }: { chain: Chain; - premintAPIClient?: typeof PremintAPIClient; + publicClient?: PublicClient; + httpClient?: IHttpClient; }) { - return new PremintClient(chain, premintAPIClient); + return new PremintClient(chain, publicClient, httpClient); }