diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 911ff8157..84f336565 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -23,3 +23,7 @@ jobs: - name: Build js package run: | npx turbo run build + + - name: Test js package + run: | + npx turbo run test:js diff --git a/packages/1155-contracts/src/version/ContractVersionBase.sol b/packages/1155-contracts/src/version/ContractVersionBase.sol index 52fcb687f..2d263da4d 100644 --- a/packages/1155-contracts/src/version/ContractVersionBase.sol +++ b/packages/1155-contracts/src/version/ContractVersionBase.sol @@ -1,5 +1,5 @@ // This file is automatically generated by code; do not manually update -// Last updated on 2023-11-09T00:33:12.481Z +// Last updated on 2023-11-02T19:45:49.899Z // SPDX-License-Identifier: MIT pragma solidity 0.8.17; diff --git a/packages/premint-sdk/.env.anvil b/packages/premint-sdk/.env.anvil deleted file mode 100644 index ce8d9a6d7..000000000 --- a/packages/premint-sdk/.env.anvil +++ /dev/null @@ -1,2 +0,0 @@ -FORK_RPC_URL="https://rpc.zora.co/" -FORK_BLOCK_NUMBER=5141442 \ No newline at end of file diff --git a/packages/protocol-deployments/package/chainConfigs.ts b/packages/protocol-deployments/package/chainConfigs.ts index f9ecddcef..afa4b70e2 100644 --- a/packages/protocol-deployments/package/chainConfigs.ts +++ b/packages/protocol-deployments/package/chainConfigs.ts @@ -52,4 +52,9 @@ export const chainConfigs = { MINT_FEE_RECIPIENT: "0xE84DBB2B25F761751231a9D0DAfbdD4dC3aa8252", PROTOCOL_REWARDS: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", }, + [999999999]: { + FACTORY_OWNER: "0xdae22ce69Afcb7f4bc37D32E267645722949DE0E", + MINT_FEE_RECIPIENT: "0xdae22ce69Afcb7f4bc37D32E267645722949DE0E", + PROTOCOL_REWARDS: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + }, }; diff --git a/packages/protocol-deployments/script/signDeploymentTransactions.ts b/packages/protocol-deployments/script/signDeploymentTransactions.ts index 2e5cac57a..4f0ea9ca6 100644 --- a/packages/protocol-deployments/script/signDeploymentTransactions.ts +++ b/packages/protocol-deployments/script/signDeploymentTransactions.ts @@ -88,9 +88,10 @@ async function signAndSaveUpgradeGate({ proxyName, }: { turnkeyAccount: LocalAccount; - chainConfigs: { - chainId: number; owner: Address - }[]; + chainConfigs: { + chainId: number; + owner: Address; + }[]; proxyName: "upgradeGate"; }) { const configFolder = path.resolve( diff --git a/packages/premint-sdk/CHANGELOG.md b/packages/protocol-sdk/CHANGELOG.md similarity index 100% rename from packages/premint-sdk/CHANGELOG.md rename to packages/protocol-sdk/CHANGELOG.md diff --git a/packages/protocol-sdk/README.md b/packages/protocol-sdk/README.md index f9d8a69ac..be448ca39 100644 --- a/packages/protocol-sdk/README.md +++ b/packages/protocol-sdk/README.md @@ -1,115 +1,147 @@ # Premint SDK -Premint SDK allows users to manage zora premints +Protocol SDK allows users to manage zora mints and collects. + +## Installing + +[viem](https://viem.sh/) is a peerDependency of protocol-sdk. If not already installed, it needs to be installed alongside the protocol sdk. + +- `npm install viem` +- `npm install ` + +### Creating a mint from an on-chain contract: + +```ts +import { MintAPI } from "@zoralabs/protocol-sdk"; +import type { Address, WalletClient } from "viem"; + +async function mintNFT( + walletClient: WalletClient, + address: Address, + tokenId: bigint, +) { + const mintAPI = new MintAPI(walletClient.chain); + await mintAPI.mintNFT({ + walletClient, + address, + tokenId, + mintArguments: { + quantityToMint: 23, + mintComment: "Helo", + }, + }); +} +``` ### Creating a premint: -```js -import {PremintAPI} from '@zoralabs/premint-sdk'; -import type {Address, WalletClient} from 'viem'; +```ts +import { PremintAPI } from "@zoralabs/protocol-sdk"; +import type { Address, WalletClient } from "viem"; async function makePremint(walletClient: WalletClient) { - // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). - const premintAPI = new PremintAPI(walletClient.chain); - - // Create premint - const premint = await premintAPI.createPremint({ - // Extra step to check the signature on-chain before attempting to sign - checkSignature: true, - // Collection information that this premint NFT will exist in once minted. - collection: { - contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - contractName: "Testing Contract", - contractURI: "ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e", - }, - // WalletClient doing the signature - walletClient, - // Token information, falls back to defaults set in DefaultMintArguments. - token: { - tokenURI: - "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", - }, - }); - - console.log(`created ZORA premint, link: ${premint.url}`) - return premint; + // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). + const premintAPI = new PremintAPI(walletClient.chain); + + // Create premint + const premint = await premintAPI.createPremint({ + // Extra step to check the signature on-chain before attempting to sign + checkSignature: true, + // Collection information that this premint NFT will exist in once minted. + collection: { + contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + contractName: "Testing Contract", + contractURI: + "ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e", + }, + // WalletClient doing the signature + walletClient, + // Token information, falls back to defaults set in DefaultMintArguments. + token: { + tokenURI: + "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", + }, + }); + + console.log(`created ZORA premint, link: ${premint.url}`); + return premint; } - ``` ### Updating a premint: -```js -import {PremintAPI} from '@zoralabs/premint-sdk'; -import type {Address, WalletClient} from 'viem'; +```ts +import { PremintAPI } from "@zoralabs/premint-sdk"; +import type { Address, WalletClient } from "viem"; async function updatePremint(walletClient: WalletClient) { - // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). - const premintAPI = new PremintAPI(walletClient.chain); - - // Create premint - const premint = await premintAPI.updatePremint({ - // Extra step to check the signature on-chain before attempting to sign - collection: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - uid: 23, - // WalletClient doing the signature - walletClient, - // Token information, falls back to defaults set in DefaultMintArguments. - token: { - tokenURI: - "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", - }, - }); - - console.log(`updated ZORA premint, link: ${premint.url}`) - return premint; + // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). + const premintAPI = new PremintAPI(walletClient.chain); + + // Create premint + const premint = await premintAPI.updatePremint({ + // Extra step to check the signature on-chain before attempting to sign + collection: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + uid: 23, + // WalletClient doing the signature + walletClient, + // Token information, falls back to defaults set in DefaultMintArguments. + token: { + tokenURI: + "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", + }, + }); + + console.log(`updated ZORA premint, link: ${premint.url}`); + return premint; } - ``` ### Deleting a premint: -```js -import {PremintAPI} from '@zoralabs/premint-sdk'; -import type {Address, WalletClient} from 'viem'; +```ts +import { PremintAPI } from "@zoralabs/premint-sdk"; +import type { Address, 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 = new PremintAPI(walletClient.chain); - - // Create premint - const premint = await premintAPI.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; + // Create premint API object passing in the current wallet chain (only zora and zora testnet are supported currently). + const premintAPI = new PremintAPI(walletClient.chain); + + // Create premint + const premint = await premintAPI.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: -```js -import {PremintAPI} from '@zoralabs/premint-sdk'; -import type {Address, WalletClient} from 'viem'; - -async function executePremint(walletClient: WalletClient, premintAddress: Address, premintUID: number) { - const premintAPI = new PremintAPI(walletClient.chain); - - return await premintAPI.executePremintWithWallet({ - data: premintAPI.getPremintData(premintAddress, premintUID), - walletClient, - mintArguments: { - quantityToMint: 1, - } - }); +```ts +import { PremintAPI } from "@zoralabs/premint-sdk"; +import type { Address, WalletClient } from "viem"; + +async function executePremint( + walletClient: WalletClient, + premintAddress: Address, + premintUID: number, +) { + const premintAPI = new PremintAPI(walletClient.chain); + + return await premintAPI.executePremintWithWallet({ + data: premintAPI.getPremintData(premintAddress, premintUID), + walletClient, + mintArguments: { + quantityToMint: 1, + }, + }); } - ``` ### Deleting a premint: diff --git a/packages/premint-sdk/package.json b/packages/protocol-sdk/package.json similarity index 76% rename from packages/premint-sdk/package.json rename to packages/protocol-sdk/package.json index b137144fb..402c07f01 100644 --- a/packages/premint-sdk/package.json +++ b/packages/protocol-sdk/package.json @@ -1,6 +1,6 @@ { - "name": "@zoralabs/premint-sdk", - "version": "0.1.1", + "name": "@zoralabs/protocol-sdk", + "version": "0.2.0", "repository": "https://github.com/ourzora/zora-protocol", "license": "MIT", "main": "./dist/index.js", @@ -10,8 +10,7 @@ "build": "tsup", "prepack": "yarn build", "test:js": "vitest src", - "generate-types": "npx openapi-typescript https://api.zora.co/premint/openapi.json -o src/generated/premint-api-types.ts && npx openapi-typescript https://api.zora.co/discover/openapi.json -o src/generated/discover-api-types.ts", - "anvil": "source .env.anvil && anvil --fork-url $FORK_RPC_URL --fork-block-number $FORK_BLOCK_NUMBER --chain-id 31337" + "generate-types": "npx openapi-typescript https://api.zora.co/premint/openapi.json -o src/generated/premint-api-types.ts && npx openapi-typescript https://api.zora.co/discover/openapi.json -o src/generated/discover-api-types.ts" }, "dependencies": { "@zoralabs/protocol-deployments": "*", diff --git a/packages/protocol-sdk/src/anvil.ts b/packages/protocol-sdk/src/anvil.ts new file mode 100644 index 000000000..60e1d8bff --- /dev/null +++ b/packages/protocol-sdk/src/anvil.ts @@ -0,0 +1,84 @@ +import { spawn } from "node:child_process"; +import { join } from "path"; +import { test } from "vitest"; +import { + PublicClient, + TestClient, + WalletClient, + createPublicClient, + createTestClient, + createWalletClient, + http, +} from "viem"; +import { foundry } from "viem/chains"; + +export interface AnvilViemClientsTest { + viemClients: { + walletClient: WalletClient; + publicClient: PublicClient; + testClient: TestClient; + }; +} + +async function waitForAnvilInit(anvil: any) { + return new Promise((resolve) => { + anvil.stdout.once("data", () => { + resolve(true); + }); + }); +} + +export const anvilTest = test.extend({ + viemClients: async ({task}, use) => { + console.log('setting up clients for ', task.name); + const port = Math.floor(Math.random() * 2000) + 4000; + const anvil = spawn( + "anvil", + [ + "--port", + `${port}`, + "--fork-url", + "https://rpc.zora.co/", + "--fork-block-number", + "6133407", + "--chain-id", + "31337", + ], + { + cwd: join(__dirname, ".."), + killSignal: "SIGINT", + }, + ); + const anvilHost = `http://0.0.0.0:${port}`; + await waitForAnvilInit(anvil); + + const chain = { + ...foundry, + }; + + const walletClient = createWalletClient({ + chain, + transport: http(anvilHost), + }); + + const testClient = createTestClient({ + chain, + mode: "anvil", + transport: http(anvilHost), + }); + + const publicClient = createPublicClient({ + chain, + transport: http(anvilHost), + }); + + await use({ + publicClient, + walletClient, + testClient, + }); + + // clean up function, called once after all tests run + anvil.kill("SIGINT"); + }, +}); diff --git a/packages/protocol-sdk/src/apis/chain-constants.ts b/packages/protocol-sdk/src/apis/chain-constants.ts new file mode 100644 index 000000000..f999f1701 --- /dev/null +++ b/packages/protocol-sdk/src/apis/chain-constants.ts @@ -0,0 +1,101 @@ +import { + base, + baseGoerli, + foundry, + goerli, + mainnet, + optimism, + optimismGoerli, + zora, + zoraTestnet, +} from "viem/chains"; +import type { components } from "./generated/premint-api-types"; +import { parseEther } from "viem"; +import { getSubgraph } from "../constants"; + +export type NetworkConfig = { + chainId: number; + zoraPathChainName: string; + zoraBackendChainName: components["schemas"]["ChainName"]; + isTestnet: boolean; + subgraphUrl: string; +}; + +export const REWARD_PER_TOKEN = parseEther("0.000777"); + +export const BackendChainNamesLookup = { + ZORA_MAINNET: "ZORA-MAINNET", + ZORA_GOERLI: "ZORA-GOERLI", + OPTIMISM_MAINNET: "OPTIMISM-MAINNET", + OPTIMISM_GOERLI: "OPTIMISM-GOERLI", + ETHEREUM_MAINNET: "ETHEREUM-MAINNET", + ETHEREUM_GOERLI: "ETHEREUM-GOERLI", + BASE_MAINNET: "BASE-MAINNET", + BASE_GOERLI: "BASE-GOERLI", +} as const; + +export const networkConfigByChain: Record = { + [mainnet.id]: { + chainId: mainnet.id, + isTestnet: false, + zoraPathChainName: "eth", + zoraBackendChainName: BackendChainNamesLookup.ETHEREUM_MAINNET, + subgraphUrl: getSubgraph("zora-create-mainnet", "stable"), + }, + [goerli.id]: { + chainId: goerli.id, + isTestnet: true, + zoraPathChainName: "gor", + zoraBackendChainName: BackendChainNamesLookup.ETHEREUM_GOERLI, + subgraphUrl: getSubgraph("zora-create-goerli", "stable"), + }, + [zora.id]: { + chainId: zora.id, + isTestnet: false, + zoraPathChainName: "zora", + zoraBackendChainName: BackendChainNamesLookup.ZORA_MAINNET, + subgraphUrl: getSubgraph("zora-create-zora-mainnet", "stable"), + }, + [zoraTestnet.id]: { + chainId: zora.id, + isTestnet: true, + zoraPathChainName: "zgor", + zoraBackendChainName: BackendChainNamesLookup.ZORA_GOERLI, + subgraphUrl: getSubgraph("zora-create-zora-testnet", "stable"), + }, + [optimism.id]: { + chainId: optimism.id, + isTestnet: false, + zoraPathChainName: "opt", + zoraBackendChainName: BackendChainNamesLookup.OPTIMISM_MAINNET, + subgraphUrl: getSubgraph("zora-create-optimism", "stable"), + }, + [optimismGoerli.id]: { + chainId: optimismGoerli.id, + isTestnet: true, + zoraPathChainName: "ogor", + zoraBackendChainName: BackendChainNamesLookup.OPTIMISM_GOERLI, + subgraphUrl: getSubgraph("zora-create-optimism-goerli", "stable"), + }, + [base.id]: { + chainId: base.id, + isTestnet: false, + zoraPathChainName: "base", + zoraBackendChainName: BackendChainNamesLookup.BASE_MAINNET, + subgraphUrl: getSubgraph("zora-create-base-mainnet", "stable"), + }, + [baseGoerli.id]: { + chainId: baseGoerli.id, + isTestnet: true, + zoraPathChainName: "bgor", + zoraBackendChainName: BackendChainNamesLookup.BASE_GOERLI, + subgraphUrl: getSubgraph("zora-create-base-goerli", "stable"), + }, + [foundry.id]: { + chainId: foundry.id, + isTestnet: true, + zoraPathChainName: "zgor", + zoraBackendChainName: BackendChainNamesLookup.ZORA_GOERLI, + subgraphUrl: getSubgraph("zora-create-zora-testnet", "stable"), + }, +}; diff --git a/packages/protocol-sdk/src/apis/client-base.ts b/packages/protocol-sdk/src/apis/client-base.ts new file mode 100644 index 000000000..3ee4a1e1e --- /dev/null +++ b/packages/protocol-sdk/src/apis/client-base.ts @@ -0,0 +1,29 @@ +import { Chain, PublicClient, createPublicClient, http } from "viem"; +import { NetworkConfig, networkConfigByChain } from "./chain-constants"; + +export abstract class ClientBase { + network: NetworkConfig; + chain: Chain; + + constructor(chain: Chain) { + this.chain = chain; + const networkConfig = networkConfigByChain[chain.id]; + if (!networkConfig) { + throw new Error(`Not configured for chain ${chain.id}`); + } + this.network = networkConfig; + } + + /** + * 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. + */ + protected getPublicClient(publicClient?: PublicClient): PublicClient { + if (publicClient) { + return publicClient; + } + return createPublicClient({ chain: this.chain, transport: http() }); + } +} diff --git a/packages/protocol-sdk/src/apis/generated/discover-api-types.ts b/packages/protocol-sdk/src/apis/generated/discover-api-types.ts new file mode 100644 index 000000000..23ed51ec8 --- /dev/null +++ b/packages/protocol-sdk/src/apis/generated/discover-api-types.ts @@ -0,0 +1,2138 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + + +export interface paths { + "/": { + /** Root */ + get: operations["root__get"]; + }; + "/feed": { + /** Get Feed */ + get: operations["get_feed_feed_get"]; + }; + "/relevant_mintables/{chain_name}/{collection_address}/{token_id}": { + /** Gen Relevant Mintables */ + get: operations["gen_relevant_mintables_relevant_mintables__chain_name___collection_address___token_id__get"]; + }; + "/relevant_mintables/{chain_name}/{collection_address}": { + /** Gen Relevant Mintables */ + get: operations["gen_relevant_mintables_relevant_mintables__chain_name___collection_address__get"]; + }; + "/feed_item_check/{chain_name}/{collection_address}/{token_id}": { + /** Gen Feed Item Check */ + get: operations["gen_feed_item_check_feed_item_check__chain_name___collection_address___token_id__get"]; + }; + "/feed_item_check/{chain_name}/{collection_address}": { + /** Gen Feed Item Check */ + get: operations["gen_feed_item_check_feed_item_check__chain_name___collection_address__get"]; + }; + "/mintables/{chain_name}/{collection_address}/{token_id}": { + /** Gen Mintable */ + get: operations["gen_mintable_mintables__chain_name___collection_address___token_id__get"]; + }; + "/mintables/{chain_name}/{collection_address}": { + /** Gen Mintable */ + get: operations["gen_mintable_mintables__chain_name___collection_address__get"]; + }; + "/premints/{chain_name}": { + /** Gen All Mintables */ + get: operations["gen_all_mintables_premints__chain_name__get"]; + }; + "/mintables_v2/{chain_name}/{collection_address}": { + /** Gen Mintable V2 */ + get: operations["gen_mintable_v2_mintables_v2__chain_name___collection_address__get"]; + }; + "/contract/backfill": { + /** Gen Backfill Contract */ + post: operations["gen_backfill_contract_contract_backfill_post"]; + }; + "/contract/{chain_name}/{collection_address}": { + /** Get Contract */ + get: operations["get_contract_contract__chain_name___collection_address__get"]; + }; + "/contract_summaries": { + /** Gen Contract Summaries */ + post: operations["gen_contract_summaries_contract_summaries_post"]; + }; + "/mint_counts": { + /** + * Gen Mint Counts + * @deprecated + * @description Use /contract_summaries instead + */ + post: operations["gen_mint_counts_mint_counts_post"]; + }; + "/contracts": { + /** Gen Contracts */ + post: operations["gen_contracts_contracts_post"]; + }; + "/tokens/{chain_name}/{collection_address}": { + /** Gen Contract Tokens */ + get: operations["gen_contract_tokens_tokens__chain_name___collection_address__get"]; + }; + "/user/{wallet}": { + /** Get User */ + get: operations["get_user_user__wallet__get"]; + }; + "/user/{wallet_address}/tokens": { + /** Gen User Tokens */ + get: operations["gen_user_tokens_user__wallet_address__tokens_get"]; + }; + "/user/{wallet_address}/contracts": { + /** Gen User Contracts */ + get: operations["gen_user_contracts_user__wallet_address__contracts_get"]; + }; + "/trending": { + /** Gen Trending */ + get: operations["gen_trending_trending_get"]; + }; + "/trending_feed": { + /** Gen Trending Feed */ + get: operations["gen_trending_feed_trending_feed_get"]; + }; + "/search": { + /** Gen Search */ + get: operations["gen_search_search_get"]; + }; + "/stream/token_mint/{chain_name}/{collection_address}": { + /** Stream Token Mint */ + get: operations["stream_token_mint_stream_token_mint__chain_name___collection_address__get"]; + }; + "/token_mints/{chain_name}/{collection_address}": { + /** Gen Token Mints */ + get: operations["gen_token_mints_token_mints__chain_name___collection_address__get"]; + }; + "/stream/mint_comment/{chain_name}/{collection_address}": { + /** Stream Mint Comment */ + get: operations["stream_mint_comment_stream_mint_comment__chain_name___collection_address__get"]; + }; + "/mint_comments/{chain_name}/{collection_address}": { + /** Gen Mint Comments */ + get: operations["gen_mint_comments_mint_comments__chain_name___collection_address__get"]; + }; + "/notifications": { + /** Gen Notifications */ + get: operations["gen_notifications_notifications_get"]; + }; + "/stream/notifications": { + /** Stream Notification */ + get: operations["stream_notification_stream_notifications_get"]; + }; + "/refresh-blocklist": { + /** Refresh Blocklist */ + get: operations["refresh_blocklist_refresh_blocklist_get"]; + }; + "/schema": { + /** Schema */ + get: operations["schema_schema_get"]; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: { + /** BaseModel */ + BaseModel: Record; + /** + * Chain + * @description An enumeration. + * @enum {string} + */ + Chain: "ETHEREUM" | "ZORA"; + /** + * ChainName + * @description An enumeration. + * @enum {string} + */ + ChainName: "ETHEREUM-MAINNET" | "ETHEREUM-ROPSTEN" | "ETHEREUM-RINKEBY" | "ETHEREUM-GOERLI" | "ETHEREUM-SEPOLIA" | "OPTIMISM-MAINNET" | "OPTIMISM-GOERLI" | "ZORA-GOERLI" | "ZORA-MAINNET" | "BASE-MAINNET" | "BASE-GOERLI" | "PGN-MAINNET"; + /** Collection */ + Collection: { + /** Address */ + address: string; + /** Name */ + name?: string; + /** Symbol */ + symbol?: string; + token_standard?: components["schemas"]["TokenStandard"]; + /** Description */ + description?: string; + /** Image */ + image?: string; + }; + /** + * CollectionCreationConfig + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + CollectionCreationConfig: { + /** Contractadmin */ + contractAdmin: string; + /** Contracturi */ + contractURI: string; + /** Contractname */ + contractName: string; + }; + /** CollectionTokenId */ + CollectionTokenId: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: number | string; + }; + /** Collector */ + Collector: { + /** Address */ + address: string; + /** Ens Name */ + ens_name?: string; + }; + /** CollectorSummary */ + CollectorSummary: { + /** Num Unique Collectors */ + num_unique_collectors: number; + /** Collector Previews */ + collector_previews: components["schemas"]["Collector"][]; + }; + /** ContractBackfillResponse */ + ContractBackfillResponse: { + /** Success */ + success: boolean; + /** Message */ + message: string; + }; + /** ContractQueryPayload */ + ContractQueryPayload: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + }; + /** ContractQueryResponse */ + ContractQueryResponse: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + contract_result?: components["schemas"]["ContractResult"]; + /** + * Err Code + * @default 0 + */ + err_code?: number; + /** + * Err Msg + * @default + */ + err_msg?: string; + }; + /** ContractResult */ + ContractResult: { + /** Chain Name */ + chain_name: string; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + collection: components["schemas"]["Collection"]; + contract_type: components["schemas"]["MintableType"]; + /** Creator Address */ + creator_address?: string; + mintable?: components["schemas"]["Mintable"]; + media?: components["schemas"]["Media"]; + metadata?: components["schemas"]["zora__collect__contract__models__metadata__Metadata"]; + /** Owner */ + owner?: string; + /** Total Minted */ + total_minted?: number; + }; + /** ContractSummary */ + ContractSummary: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + /** Mint Count */ + mint_count: number; + /** Comment Count */ + comment_count: number; + /** Unique Collector Count */ + unique_collector_count: number; + }; + /** ContractSummaryResponse */ + ContractSummaryResponse: { + /** Contract Summary List */ + contract_summary_list: components["schemas"]["ContractSummary"][]; + }; + /** ContractsQueryOutput */ + ContractsQueryOutput: { + /** Contract Results */ + contract_results: components["schemas"]["ContractQueryResponse"][]; + }; + /** ContractsResponse */ + ContractsResponse: { + /** Results */ + results: components["schemas"]["ContractResult"][]; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Has Next Page */ + has_next_page: boolean; + /** + * Api Version + * @default 1 + */ + api_version?: number; + }; + /** + * Cursor + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + Cursor: { + /** First */ + first?: string; + /** Last */ + last?: string; + }; + /** ENSRecord */ + ENSRecord: { + /** Address */ + address: string; + /** Ens Name */ + ens_name: string; + text_records: components["schemas"]["ENSTextRecords"]; + }; + /** ENSTextRecords */ + ENSTextRecords: { + /** Avatar */ + avatar?: string; + /** Url */ + url?: string; + /** Description */ + description?: string; + /** Github */ + github?: string; + /** Twitter */ + twitter?: string; + /** Instagram */ + instagram?: string; + /** Discord */ + discord?: string; + /** Tiktok */ + tiktok?: string; + }; + /** + * EntityType + * @description An enumeration. + * @enum {string} + */ + EntityType: "Collection" | "Token"; + /** FeedItemCheck */ + FeedItemCheck: { + feed_item?: components["schemas"]["FeedResult"]; + /** + * Err Msgs + * @default [] + */ + err_msgs?: string[]; + }; + /** + * FeedItemType + * @description An enumeration. + * @enum {string} + */ + FeedItemType: "MINTABLE"; + /** FeedResponse */ + FeedResponse: { + /** Results */ + results: components["schemas"]["FeedResult"][]; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Has Next Page */ + has_next_page: boolean; + /** + * Api Version + * @default 1 + */ + api_version?: number; + feed_type?: components["schemas"]["FeedType"]; + }; + /** FeedResult */ + FeedResult: { + feed_item_type: components["schemas"]["FeedItemType"]; + /** Uuid */ + uuid?: string; + feed_item: components["schemas"]["Mintable"]; + media?: components["schemas"]["Media"]; + }; + /** + * FeedType + * @description An enumeration. + * @enum {string} + */ + FeedType: "curated" | "most_recent" | "heuristic" | "trending" | "recommendations"; + /** + * FixedPrice + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + FixedPrice: { + /** Token Id */ + token_id?: number; + /** Sale Start */ + sale_start?: string; + /** Sale End */ + sale_end?: string; + /** Max Tokens Per Address */ + max_tokens_per_address?: string; + /** Price Per Token */ + price_per_token?: string; + /** Funds Recipient */ + funds_recipient?: string; + }; + /** FoundationMintContext */ + FoundationMintContext: { + /** + * Mint Context Type + * @default foundation + * @enum {string} + */ + mint_context_type?: "foundation"; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * InferredMintContext + * @description Mint context passed to feed via mintables + * + * Corresponds to the following TypeScript type: + * interface InferredMintContext { + * price_per_token: string + * function_abi: any + * function_name: string + * contract_address: string + * default_eth_value: string + * default_args: (string | number)[] + * quantity_arg_index: number | null + * use_proxy_contract: boolean + * } + */ + InferredMintContext: { + /** Reference Tx */ + reference_tx?: string; + /** Price Per Token */ + price_per_token: string; + /** Function Abi */ + function_abi: Record; + /** Function Name */ + function_name: string; + /** Contract Address */ + contract_address: string; + /** Default Eth Value */ + default_eth_value: string; + /** Default Args */ + default_args: (string | number)[]; + /** Expected Tokens Returned */ + expected_tokens_returned: number; + /** User Address Arg Index */ + user_address_arg_index?: number; + /** Quantity Arg Index */ + quantity_arg_index?: number; + /** + * Use Proxy Contract + * @default false + */ + use_proxy_contract?: boolean; + /** + * Mint Context Type + * @default inferred + * @enum {string} + */ + mint_context_type?: "inferred"; + }; + /** + * InfinitePageWithTotal[MintComment] + * @description This is a generic model for pagination in an infitite scrolling context. + */ + InfinitePageWithTotal_MintComment_: { + /** Data */ + data: components["schemas"]["MintComment"][]; + /** Limit */ + limit: number; + /** Has More */ + has_more: boolean; + cursor: components["schemas"]["Cursor"]; + /** Total */ + total: number; + }; + /** + * InfinitePageWithTotal[TokenMint] + * @description This is a generic model for pagination in an infitite scrolling context. + */ + InfinitePageWithTotal_TokenMint_: { + /** Data */ + data: components["schemas"]["TokenMint"][]; + /** Limit */ + limit: number; + /** Has More */ + has_more: boolean; + cursor: components["schemas"]["Cursor"]; + /** Total */ + total: number; + }; + /** + * InfinitePage[Mintable] + * @description This is a generic model for pagination in an infitite scrolling context. + */ + InfinitePage_Mintable_: { + /** Data */ + data: components["schemas"]["Mintable"][]; + /** Limit */ + limit: number; + /** Has More */ + has_more: boolean; + cursor: components["schemas"]["Cursor"]; + }; + /** ManifoldMintContext */ + ManifoldMintContext: { + /** + * Mint Context Type + * @default manifold + * @enum {string} + */ + mint_context_type?: "manifold"; + /** Creator Contract Address */ + creator_contract_address: string; + /** Claim Index */ + claim_index: number; + /** Mint Index */ + mint_index: number; + /** + * Merkle Proof + * Format: binary + */ + merkle_proof: string; + }; + /** Media */ + Media: { + image_preview?: components["schemas"]["zora__media_encoding__media__MediaURI"]; + /** Image Carousel */ + image_carousel?: components["schemas"]["zora__media_encoding__media__MediaURI"][]; + content_preview?: components["schemas"]["zora__media_encoding__media__MediaURI"]; + /** Content Carousel */ + content_carousel?: components["schemas"]["zora__media_encoding__media__MediaURI"][]; + /** Mime Type */ + mime_type?: string; + }; + /** MediaAttribute */ + MediaAttribute: { + /** Trait Type */ + trait_type?: string; + /** Value */ + value?: string; + /** Display Type */ + display_type?: string; + }; + /** Mint */ + Mint: { + image_preview: components["schemas"]["zora__collect__trending__models__models__MediaURI"]; + content_preview?: components["schemas"]["zora__collect__trending__models__models__MediaURI"]; + /** Collection Name */ + collection_name: string; + /** Token Name */ + token_name?: string; + /** Token Id */ + token_id: string; + minter: components["schemas"]["Wallet"]; + /** + * Mint Time + * Format: date-time + */ + mint_time: string; + /** Chain Name */ + chain_name: string; + }; + /** MintComment */ + MintComment: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + token_standard?: components["schemas"]["TokenStandard"]; + /** From Address */ + from_address: string; + /** Comment */ + comment: string; + /** Quantity */ + quantity: number; + transaction_info: components["schemas"]["TransactionInfo"]; + }; + /** MintCommentNotification */ + MintCommentNotification: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + token_standard?: components["schemas"]["TokenStandard"]; + /** + * Notification Type + * @default MINT_COMMENT + * @enum {string} + */ + notification_type?: "MINT_COMMENT"; + /** + * Timestamp + * Format: date-time + */ + timestamp: string; + /** Read */ + read: boolean; + /** Minter Address */ + minter_address: string; + /** Comment */ + comment: string; + metadata?: components["schemas"]["zora__ethereum__models__metadata__Metadata"]; + media?: components["schemas"]["zora__media_encoding__media__MediaURI"]; + }; + /** MintCountResponse */ + MintCountResponse: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + /** Mint Count */ + mint_count: number; + }; + /** MintCountsOutput */ + MintCountsOutput: { + /** Mint Count Results */ + mint_count_results: components["schemas"]["MintCountResponse"][]; + }; + /** MintNotification */ + MintNotification: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + token_standard?: components["schemas"]["TokenStandard"]; + /** + * Notification Type + * @default MINT + * @enum {string} + */ + notification_type?: "MINT"; + /** + * Timestamp + * Format: date-time + */ + timestamp: string; + /** Read */ + read: boolean; + /** Minter Address */ + minter_address: string; + /** Value */ + value: number; + metadata?: components["schemas"]["zora__ethereum__models__metadata__Metadata"]; + media?: components["schemas"]["zora__media_encoding__media__MediaURI"]; + }; + /** Mintable */ + Mintable: { + chain_name: components["schemas"]["ChainName"]; + mintable_type: components["schemas"]["MintableType"]; + token_standard: components["schemas"]["TokenStandard"]; + /** Contract Address */ + contract_address: string; + /** Creator Address */ + creator_address?: string; + /** Token Creator */ + token_creator?: string; + collection: components["schemas"]["Collection"]; + /** Token Id */ + token_id?: string; + /** Token Name */ + token_name?: string; + /** Mint Context */ + mint_context?: components["schemas"]["ManifoldMintContext"] | components["schemas"]["ZoraCreateMintContext"] | components["schemas"]["FoundationMintContext"] | components["schemas"]["ZoraCreate1155MintContext"] | components["schemas"]["ZoraCreatePremintContext"] | components["schemas"]["InferredMintContext"]; + /** Is Active */ + is_active: boolean; + cost: components["schemas"]["zora__collect__feed__models__price_at_time__PriceAtTime"]; + total_mint_volume: components["schemas"]["zora__collect__feed__models__price_at_time__PriceAtTime"]; + /** Total Supply */ + total_supply?: number; + /** Total Minted */ + total_minted: number; + /** Wallet Max */ + wallet_max?: number; + /** + * Start Datetime + * Format: date-time + */ + start_datetime?: string; + /** + * End Datetime + * Format: date-time + */ + end_datetime?: string; + collector_summary: components["schemas"]["CollectorSummary"]; + metadata?: components["schemas"]["zora__collect__contract__models__metadata__Metadata"]; + status?: components["schemas"]["MintableStatus"]; + /** Uuid */ + uuid?: string; + }; + /** + * MintableSortKey + * @description An enumeration. + * @enum {string} + */ + MintableSortKey: "CREATED" | "MINTED"; + /** + * MintableStatus + * @description An enumeration. + * @enum {string} + */ + MintableStatus: "ACTIVE" | "BLOCKED" | "EXPIRED" | "INVALID_MEDIA" | "MINTED_OUT" | "DELETED"; + /** + * MintableType + * @description An enumeration. + * @enum {string} + */ + MintableType: "FOUNDATION_DROP_COLLECTION" | "FOUNDATION_TIMED_EDITION" | "MANIFOLD_ERC1155" | "MANIFOLD_ERC721" | "ZORA_CREATE" | "ZORA_CREATE_1155" | "ZORA_CREATE_1155_B2R_REDEEM_TOKEN" | "ZORA_CREATE_1155_PREMINT_TOKEN" | "ZORA_EDITION" | "ZORA_DROP" | "INFERRED"; + /** + * Network + * @description An enumeration. + * @enum {string} + */ + Network: "MAINNET" | "GOERLI"; + /** Notification */ + Notification: components["schemas"]["MintNotification"] | components["schemas"]["MintCommentNotification"]; + /** + * NotificationsResponse + * @description This is a generic model for pagination in an infitite scrolling context. + */ + NotificationsResponse: { + /** Data */ + data: components["schemas"]["Notification"][]; + /** Limit */ + limit: number; + /** Has More */ + has_more: boolean; + cursor: components["schemas"]["Cursor"]; + /** Total Unread */ + total_unread: number; + }; + /** + * Platform + * @description An enumeration. + * @enum {string} + */ + Platform: "FOUNDATION" | "ZORA" | "MANIFOLD" | "INFERRED"; + /** + * PremintConfig + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + PremintConfig: { + tokenConfig: components["schemas"]["TokenCreationConfig"]; + /** Uid */ + uid: number; + /** Version */ + version: number; + /** Deleted */ + deleted: boolean; + }; + /** + * Presale + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + Presale: { + /** Token Id */ + token_id?: number; + /** Presale Start */ + presale_start?: string; + /** Presale End */ + presale_end?: string; + /** Merkle Root */ + merkle_root?: string; + /** Funds Recipient */ + funds_recipient?: string; + }; + /** Price */ + Price: { + /** Value */ + value: string; + }; + /** + * Royalties + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + Royalties: { + /** Token Id */ + token_id?: number; + /** User */ + user?: string; + /** Royalty Bps */ + royalty_bps?: string; + /** Royaltyrecipient */ + royaltyRecipient?: string; + /** Royalty Mint Schedule */ + royalty_mint_schedule?: number; + }; + /** + * SaleStrategies + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + SaleStrategies: { + /** Sale Strategies Type */ + sale_strategies_type?: string; + fixed_price?: components["schemas"]["FixedPrice"]; + presale?: components["schemas"]["Presale"]; + /** Redeem Minter */ + redeem_minter?: boolean; + }; + /** SearchCollection */ + SearchCollection: { + /** + * Entity Type + * @default Collection + * @enum {string} + */ + entity_type?: "Collection"; + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + /** Name */ + name?: string; + /** Description */ + description?: string; + /** Creator Address */ + creator_address?: string; + /** Total Minted */ + total_minted?: number; + }; + /** SearchResponse */ + SearchResponse: { + /** Results */ + results: components["schemas"]["SearchResult"][]; + /** Total Results */ + total_results: number; + /** Blocked Results */ + blocked_results: number; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Has Next Page */ + has_next_page: boolean; + /** + * Api Version + * @default 1 + */ + api_version?: number; + /** Search Id */ + search_id: string; + }; + /** SearchResult */ + SearchResult: { + /** Name */ + name?: string; + /** Description */ + description?: string; + entity_type: components["schemas"]["EntityType"]; + /** Entity */ + entity: components["schemas"]["SearchCollection"] | components["schemas"]["SearchToken"]; + media?: components["schemas"]["Media"]; + /** + * Rank + * @default 0 + */ + rank?: number; + }; + /** + * SearchSortKey + * @description An enumeration. + * @enum {string} + */ + SearchSortKey: "CREATED" | "POPULARITY"; + /** SearchToken */ + SearchToken: { + /** + * Entity Type + * @default Token + * @enum {string} + */ + entity_type?: "Token"; + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id: string; + /** Name */ + name?: string; + /** Description */ + description?: string; + /** Owner */ + owner?: string; + }; + /** + * SimpleResponse + * @description A simple wrapper for data + */ + SimpleResponse: { + data: components["schemas"]["BaseModel"]; + }; + /** + * SortDirection + * @description An enumeration. + * @enum {string} + */ + SortDirection: "ASC" | "DESC"; + /** + * TokenCreationConfig + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + TokenCreationConfig: { + /** Tokenuri */ + tokenURI: string; + /** Maxsupply */ + maxSupply: string; + /** Maxtokensperaddress */ + maxTokensPerAddress: string; + /** Pricepertoken */ + pricePerToken: string; + /** Mintstart */ + mintStart: string; + /** Mintduration */ + mintDuration: string; + /** Royaltymintschedule */ + royaltyMintSchedule: number; + /** Royaltybps */ + royaltyBPS: number; + /** Royaltyrecipient */ + royaltyRecipient: string; + /** Fixedpriceminter */ + fixedPriceMinter: string; + }; + /** TokenMint */ + TokenMint: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + token_standard?: components["schemas"]["TokenStandard"]; + /** Originator Address */ + originator_address?: string; + /** To Address */ + to_address: string; + fee: components["schemas"]["Price"]; + /** Value */ + value?: number; + transaction_info: components["schemas"]["TransactionInfo"]; + }; + /** TokenResult */ + TokenResult: { + chain_name: components["schemas"]["ChainName"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id: string; + token_standard?: components["schemas"]["TokenStandard"]; + /** Owner */ + owner?: string; + metadata?: components["schemas"]["zora__collect__contract__models__metadata__Metadata"]; + mintable?: components["schemas"]["Mintable"]; + media?: components["schemas"]["Media"]; + }; + /** + * TokenStandard + * @description An enumeration. + * @enum {string} + */ + TokenStandard: "ERC721" | "ERC1155"; + /** TokensResponse */ + TokensResponse: { + /** Results */ + results: components["schemas"]["TokenResult"][]; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Has Next Page */ + has_next_page: boolean; + /** + * Api Version + * @default 1 + */ + api_version?: number; + }; + /** Transaction */ + Transaction: { + /** Chain Name */ + chain_name: string; + /** Transaction Hash */ + transaction_hash: string; + /** Block Number */ + block_number: number; + /** Block Timestamp */ + block_timestamp: string; + }; + /** TransactionInfo */ + TransactionInfo: { + /** Block Number */ + block_number: number; + /** + * Block Timestamp + * Format: date-time + */ + block_timestamp: string; + /** Transaction Hash */ + transaction_hash: string; + }; + /** TrendingItem */ + TrendingItem: { + /** Uuid */ + uuid: string; + platform: components["schemas"]["Platform"]; + /** Collection Address */ + collection_address: string; + /** Token Id */ + token_id?: string; + /** Name */ + name: string; + /** Token Name */ + token_name?: string; + /** Num Mints Last Hour */ + num_mints_last_hour: number; + /** + * End Datetime + * Format: date-time + */ + end_datetime?: string; + creator: components["schemas"]["Wallet"]; + /** Num Mints */ + num_mints: number; + /** + * Num Unique Minters In Window + * @default 0 + */ + num_unique_minters_in_window?: number; + /** Total Supply */ + total_supply?: number; + /** Total Num Unique Collectors */ + total_num_unique_collectors: number; + cost: components["schemas"]["zora__collect__trending__models__price_at_time__PriceAtTime"]; + /** Latest Mints */ + latest_mints: components["schemas"]["Mint"][]; + /** Rank */ + rank: number; + /** Trending Duration */ + trending_duration: number; + chain_name?: components["schemas"]["ChainName"]; + }; + /** TrendingResponse */ + TrendingResponse: { + /** Trending Items */ + trending_items: components["schemas"]["TrendingItem"][]; + }; + /** + * TrendingWindow + * @description An enumeration. + * @enum {string} + */ + TrendingWindow: "now" | "1h" | "1d" | "7d"; + /** User */ + User: { + /** Address */ + address: string; + /** Total Minted */ + total_minted: number; + /** Total Created */ + total_created: number; + /** Total Owned */ + total_owned: number; + first_transaction?: components["schemas"]["Transaction"]; + ens_record?: components["schemas"]["ENSRecord"]; + }; + /** UserOwnedToken */ + UserOwnedToken: { + /** Balance */ + balance: number; + token: components["schemas"]["TokenResult"]; + }; + /** UserTokensResponse */ + UserTokensResponse: { + /** Results */ + results: components["schemas"]["UserOwnedToken"][]; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Has Next Page */ + has_next_page: boolean; + /** + * Api Version + * @default 1 + */ + api_version?: number; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + /** Wallet */ + Wallet: { + /** Address */ + address: string; + /** Ens */ + ens?: string; + }; + /** ZoraCreate1155MintContext */ + ZoraCreate1155MintContext: { + /** + * Mint Context Type + * @default zora_create_1155 + * @enum {string} + */ + mint_context_type?: "zora_create_1155"; + /** Renderer Contract */ + renderer_contract?: string; + /** Permissions */ + permissions: components["schemas"]["ZoraCreatePermissions"][]; + /** Sale Strategies */ + sale_strategies: components["schemas"]["SaleStrategies"][]; + /** Royalties */ + royalties: components["schemas"]["Royalties"][]; + /** Contract Version */ + contract_version: string; + /** Created At Block */ + created_at_block: number; + /** Uri */ + uri?: string; + /** B2R Redeemable */ + b2r_redeemable?: boolean; + }; + /** ZoraCreateMintContext */ + ZoraCreateMintContext: { + /** + * Mint Context Type + * @default zora_create + * @enum {string} + */ + mint_context_type?: "zora_create"; + /** Purchase */ + purchase: string; + /** Price Per Token */ + price_per_token?: string; + /** Purchase Presale */ + purchase_presale?: number; + /** Presale Merkle Root */ + presale_merkle_root?: string; + /** Max Sale Purchase Per Address */ + max_sale_purchase_per_address?: string; + /** Contract Version */ + contract_version?: string; + }; + /** + * ZoraCreatePermissions + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + ZoraCreatePermissions: { + /** User */ + user?: string; + /** Token Id */ + token_id?: number; + /** Is Admin */ + is_admin?: boolean; + /** Is Minter */ + is_minter?: boolean; + /** Is Sales Manager */ + is_sales_manager?: boolean; + /** Is Metadata Manager */ + is_metadata_manager?: boolean; + /** Is Funds Manager */ + is_funds_manager?: boolean; + }; + /** + * ZoraCreatePremintContext + * @description ObjectBase extends Pydantic's BaseModel class to support extra functionality + * (store_as, override_name), as well as provides other convinience methods. + * ObjectBase allows for validation and type enforcement, and should be used inheritedfor any + * complex type we include on a stored entity. + * + * Example:: + * >>> class Foo(ObjectBase): + * ...: found_at_height: int = field(override_name="address", store_as=str) + * ...: other: str = "default" + */ + ZoraCreatePremintContext: { + /** + * Mint Context Type + * @default zora_create_premint + * @enum {string} + */ + mint_context_type?: "zora_create_premint"; + collection: components["schemas"]["CollectionCreationConfig"]; + premint: components["schemas"]["PremintConfig"]; + chain_name: components["schemas"]["ChainName"]; + /** Signature */ + signature: string; + }; + /** Metadata */ + zora__collect__contract__models__metadata__Metadata: { + /** Name */ + name?: string; + /** Description */ + description?: string; + /** Attributes */ + attributes: components["schemas"]["MediaAttribute"][]; + }; + /** Currency */ + zora__collect__feed__models__price_at_time__Currency: { + /** Name */ + name?: string; + /** Address */ + address: string; + /** + * Decimals + * @default 18 + */ + decimals?: number; + }; + /** CurrencyAmount */ + zora__collect__feed__models__price_at_time__CurrencyAmount: { + currency: components["schemas"]["zora__collect__feed__models__price_at_time__Currency"]; + /** Raw */ + raw: string; + /** Decimal */ + decimal: number; + }; + /** PriceAtTime */ + zora__collect__feed__models__price_at_time__PriceAtTime: { + native_price: components["schemas"]["zora__collect__feed__models__price_at_time__CurrencyAmount"]; + /** Block Number */ + block_number: number; + eth_price?: components["schemas"]["zora__collect__feed__models__price_at_time__CurrencyAmount"]; + usdc_price?: components["schemas"]["zora__collect__feed__models__price_at_time__CurrencyAmount"]; + }; + /** MediaURI */ + zora__collect__trending__models__models__MediaURI: { + /** Raw */ + raw: string; + /** Mime Type */ + mime_type?: string; + /** Encoded Large */ + encoded_large?: string; + /** Encoded Preview */ + encoded_preview?: string; + }; + /** Currency */ + zora__collect__trending__models__price_at_time__Currency: { + /** Name */ + name?: string; + /** Address */ + address: string; + /** + * Decimals + * @default 18 + */ + decimals?: number; + }; + /** CurrencyAmount */ + zora__collect__trending__models__price_at_time__CurrencyAmount: { + currency: components["schemas"]["zora__collect__trending__models__price_at_time__Currency"]; + /** Raw */ + raw: string; + /** Decimal */ + decimal: number; + }; + /** PriceAtTime */ + zora__collect__trending__models__price_at_time__PriceAtTime: { + native_price: components["schemas"]["zora__collect__trending__models__price_at_time__CurrencyAmount"]; + /** Block Number */ + block_number: number; + eth_price?: components["schemas"]["zora__collect__trending__models__price_at_time__CurrencyAmount"]; + usdc_price?: components["schemas"]["zora__collect__trending__models__price_at_time__CurrencyAmount"]; + }; + /** Metadata */ + zora__ethereum__models__metadata__Metadata: { + /** Name */ + name: string; + }; + /** MediaURI */ + zora__media_encoding__media__MediaURI: { + /** Raw */ + raw: string; + /** Mime Type */ + mime_type?: string; + /** Encoded Large */ + encoded_large?: string; + /** Encoded Preview */ + encoded_preview?: string; + /** Encoded Thumbnail */ + encoded_thumbnail?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type $defs = Record; + +export type external = Record; + +export interface operations { + + /** Root */ + root__get: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** Get Feed */ + get_feed_feed_get: { + parameters: { + query?: { + offset?: number; + limit?: number; + mintable_types?: components["schemas"]["MintableType"][]; + feed_type?: components["schemas"]["FeedType"]; + allow_expired?: boolean; + chain?: components["schemas"]["Chain"]; + network?: components["schemas"]["Network"]; + }; + cookie?: { + device_id?: string; + session_id?: string; + wallet_address?: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Relevant Mintables */ + gen_relevant_mintables_relevant_mintables__chain_name___collection_address___token_id__get: { + parameters: { + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + token_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Relevant Mintables */ + gen_relevant_mintables_relevant_mintables__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Feed Item Check */ + gen_feed_item_check_feed_item_check__chain_name___collection_address___token_id__get: { + parameters: { + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + token_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedItemCheck"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Feed Item Check */ + gen_feed_item_check_feed_item_check__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedItemCheck"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Mintable */ + gen_mintable_mintables__chain_name___collection_address___token_id__get: { + parameters: { + query?: { + mintable_type?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + token_id: string; + }; + cookie?: { + device_id?: string; + session_id?: string; + wallet_address?: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Mintable"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Mintable */ + gen_mintable_mintables__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + mintable_type?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + cookie?: { + device_id?: string; + session_id?: string; + wallet_address?: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Mintable"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen All Mintables */ + gen_all_mintables_premints__chain_name__get: { + parameters: { + query?: { + limit?: number; + sort_direction?: components["schemas"]["SortDirection"]; + signer?: string; + cursor?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["InfinitePage_Mintable_"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Mintable V2 */ + gen_mintable_v2_mintables_v2__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedResult"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Backfill Contract */ + gen_backfill_contract_contract_backfill_post: { + parameters: { + query: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ContractBackfillResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Get Contract */ + get_contract_contract__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ContractResult"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Contract Summaries */ + gen_contract_summaries_contract_summaries_post: { + requestBody: { + content: { + "application/json": components["schemas"]["CollectionTokenId"][]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ContractSummaryResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Gen Mint Counts + * @deprecated + * @description Use /contract_summaries instead + */ + gen_mint_counts_mint_counts_post: { + requestBody: { + content: { + "application/json": components["schemas"]["ContractQueryPayload"][]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["MintCountsOutput"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Contracts */ + gen_contracts_contracts_post: { + requestBody: { + content: { + "application/json": components["schemas"]["ContractQueryPayload"][]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ContractsQueryOutput"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Contract Tokens */ + gen_contract_tokens_tokens__chain_name___collection_address__get: { + parameters: { + query?: { + offset?: number; + limit?: number; + sort_key?: components["schemas"]["MintableSortKey"]; + sort_direction?: components["schemas"]["SortDirection"]; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["TokensResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Get User */ + get_user_user__wallet__get: { + parameters: { + query?: { + chain_names?: components["schemas"]["ChainName"][]; + ens_chain_name?: components["schemas"]["ChainName"]; + chain_name?: components["schemas"]["ChainName"]; + }; + path: { + wallet: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen User Tokens */ + gen_user_tokens_user__wallet_address__tokens_get: { + parameters: { + query?: { + chain_names?: components["schemas"]["ChainName"][]; + offset?: number; + limit?: number; + sort_direction?: components["schemas"]["SortDirection"]; + }; + path: { + wallet_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["UserTokensResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen User Contracts */ + gen_user_contracts_user__wallet_address__contracts_get: { + parameters: { + query?: { + chain_names?: components["schemas"]["ChainName"][]; + offset?: number; + limit?: number; + sort_direction?: components["schemas"]["SortDirection"]; + }; + path: { + wallet_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ContractsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Trending */ + gen_trending_trending_get: { + parameters: { + query?: { + trending_window?: components["schemas"]["TrendingWindow"]; + chain_names?: unknown; + multichain?: unknown; + }; + cookie?: { + wallet_address?: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["TrendingResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Trending Feed */ + gen_trending_feed_trending_feed_get: { + parameters: { + query?: { + trending_window?: components["schemas"]["TrendingWindow"]; + chain_names?: unknown; + multichain?: unknown; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["FeedResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Search */ + gen_search_search_get: { + parameters: { + query: { + text: string; + chain_names?: components["schemas"]["ChainName"][]; + offset?: number; + limit?: number; + sort_key?: components["schemas"]["SearchSortKey"]; + }; + cookie?: { + device_id?: string; + session_id?: string; + wallet_address?: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SearchResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Stream Token Mint */ + stream_token_mint_stream_token_mint__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + timeout?: number; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SimpleResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Token Mints */ + gen_token_mints_token_mints__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + limit?: number; + sort_direction?: components["schemas"]["SortDirection"]; + cursor?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["InfinitePageWithTotal_TokenMint_"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Stream Mint Comment */ + stream_mint_comment_stream_mint_comment__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + timeout?: number; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SimpleResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Mint Comments */ + gen_mint_comments_mint_comments__chain_name___collection_address__get: { + parameters: { + query?: { + token_id?: string; + limit?: number; + sort_direction?: components["schemas"]["SortDirection"]; + cursor?: string; + }; + path: { + chain_name: components["schemas"]["ChainName"]; + collection_address: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["InfinitePageWithTotal_MintComment_"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Gen Notifications */ + gen_notifications_notifications_get: { + parameters: { + query?: { + chain_names?: components["schemas"]["ChainName"][]; + limit?: number; + sort_direction?: components["schemas"]["SortDirection"]; + cursor?: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["NotificationsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Stream Notification */ + stream_notification_stream_notifications_get: { + parameters: { + query?: { + chain_names?: components["schemas"]["ChainName"][]; + timeout?: number; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SimpleResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Refresh Blocklist */ + refresh_blocklist_refresh_blocklist_get: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** Schema */ + schema_schema_get: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; +} diff --git a/packages/premint-sdk/src/generated/premint-api-types.ts b/packages/protocol-sdk/src/apis/generated/premint-api-types.ts similarity index 100% rename from packages/premint-sdk/src/generated/premint-api-types.ts rename to packages/protocol-sdk/src/apis/generated/premint-api-types.ts diff --git a/packages/premint-sdk/src/http-api-base.ts b/packages/protocol-sdk/src/apis/http-api-base.ts similarity index 89% rename from packages/premint-sdk/src/http-api-base.ts rename to packages/protocol-sdk/src/apis/http-api-base.ts index 6625a9083..bb2df588d 100644 --- a/packages/premint-sdk/src/http-api-base.ts +++ b/packages/protocol-sdk/src/apis/http-api-base.ts @@ -33,7 +33,7 @@ export const get = async (url: string) => { throw new BadResponseError( `Invalid response, status ${response.status}`, response.status, - json + json, ); } return (await response.json()) as T; @@ -65,7 +65,7 @@ export const post = async (url: string, data: any) => { throw new BadResponseError( `Bad response: ${response.status}`, response.status, - json + json, ); } return (await response.json()) as T; @@ -75,15 +75,17 @@ export const retries = async ( tryFn: () => T, maxTries: number = 3, atTry: number = 1, - linearBackoffMS: number = 200 + linearBackoffMS: number = 200, ): Promise => { try { return await tryFn(); } catch (err: any) { if (err instanceof BadResponseError) { - if (atTry <= maxTries) { - await wait(atTry * linearBackoffMS); - return await retries(tryFn, maxTries, atTry++); + if (err.status >= 500) { + if (atTry <= maxTries) { + await wait(atTry * linearBackoffMS); + return await retries(tryFn, maxTries, atTry + 1); + } } } throw err; diff --git a/packages/protocol-sdk/src/constants.ts b/packages/protocol-sdk/src/constants.ts new file mode 100644 index 000000000..a63c86806 --- /dev/null +++ b/packages/protocol-sdk/src/constants.ts @@ -0,0 +1,10 @@ +export const ZORA_API_BASE = "https://api.zora.co/"; +export const OPEN_EDITION_MINT_SIZE = BigInt("18446744073709551615"); + +// Subgraph base settings +const SUBGRAPH_CONFIG_BASE = + "https://api.goldsky.com/api/public/project_clhk16b61ay9t49vm6ntn4mkz/subgraphs"; + +export function getSubgraph(name: string, version: string): string { + return `${SUBGRAPH_CONFIG_BASE}/${name}/${version}/gn`; +} diff --git a/packages/protocol-sdk/src/create/1155-create-helper.test.ts b/packages/protocol-sdk/src/create/1155-create-helper.test.ts new file mode 100644 index 000000000..bf303cba5 --- /dev/null +++ b/packages/protocol-sdk/src/create/1155-create-helper.test.ts @@ -0,0 +1,90 @@ +import { parseEther } from "viem"; +import { describe, expect } from "vitest"; +import { + createNew1155Token, + getTokenIdFromCreateReceipt, +} from "./1155-create-helper"; +import { anvilTest } from "src/anvil"; + +const demoTokenMetadataURI = "ipfs://DUMMY/token.json"; +const demoContractMetadataURI = "ipfs://DUMMY/contract.json"; + +describe("create-helper", () => { + anvilTest( + "creates a new contract given arguments", + async ({ viemClients: { testClient, publicClient, walletClient } }) => { + const addresses = await walletClient.getAddresses(); + const creatorAddress = addresses[0]!; + await testClient.setBalance({ + address: creatorAddress, + value: parseEther("1"), + }); + const new1155TokenRequest = await createNew1155Token({ + publicClient, + contract: { + name: "testContract", + uri: demoContractMetadataURI, + }, + tokenMetadataURI: demoTokenMetadataURI, + account: creatorAddress, + mintToCreatorCount: 1, + }); + const hash = await new1155TokenRequest.send(walletClient); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + expect(receipt).not.toBeNull(); + expect(receipt.to).to.equal("0x777777c338d93e2c7adf08d102d45ca7cc4ed021"); + expect(getTokenIdFromCreateReceipt(receipt)).to.be.equal(1n); + }, + 20 * 1000, + ); + anvilTest( + "creates a new contract, than creates a new token on this existing contract", + async ({ viemClients: { publicClient, walletClient } }) => { + const addresses = await walletClient.getAddresses(); + const creatorAccount = addresses[0]!; + + const new1155TokenRequest = await createNew1155Token({ + publicClient, + contract: { + name: "testContract2", + uri: demoContractMetadataURI, + }, + tokenMetadataURI: demoTokenMetadataURI, + account: creatorAccount, + mintToCreatorCount: 1, + }); + expect(new1155TokenRequest.contractAddress).to.be.equal( + "0xb1A8928dF830C21eD682949Aa8A83C1C215194d3", + ); + expect(new1155TokenRequest.contractExists).to.be.false; + const hash = await new1155TokenRequest.send(walletClient); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + const firstTokenId = getTokenIdFromCreateReceipt(receipt); + expect(firstTokenId).to.be.equal(1n); + expect(receipt).not.toBeNull(); + + const newTokenOnExistingContract = await createNew1155Token({ + publicClient, + contract: { + name: "testContract2", + uri: demoContractMetadataURI, + }, + tokenMetadataURI: demoTokenMetadataURI, + account: creatorAccount, + mintToCreatorCount: 1, + }); + expect(newTokenOnExistingContract.contractAddress).to.be.equal( + "0xb1A8928dF830C21eD682949Aa8A83C1C215194d3", + ); + expect(newTokenOnExistingContract.contractExists).to.be.true; + const newHash = await newTokenOnExistingContract.send(walletClient); + const newReceipt = await publicClient.waitForTransactionReceipt({ + hash: newHash, + }); + const tokenId = getTokenIdFromCreateReceipt(newReceipt); + expect(tokenId).to.be.equal(2n); + expect(newReceipt).not.toBeNull(); + }, + 20 * 1000, + ); +}); diff --git a/packages/protocol-sdk/src/create/1155-create-helper.ts b/packages/protocol-sdk/src/create/1155-create-helper.ts new file mode 100644 index 000000000..66b8f3340 --- /dev/null +++ b/packages/protocol-sdk/src/create/1155-create-helper.ts @@ -0,0 +1,342 @@ +import { + zoraCreator1155FactoryImplABI, + zoraCreator1155FactoryImplAddress, + zoraCreator1155ImplABI, + zoraCreatorFixedPriceSaleStrategyABI, +} from "@zoralabs/protocol-deployments"; +import type { + Address, + Hex, + PublicClient, + TransactionReceipt, + WalletClient, +} from "viem"; +import { decodeEventLog, encodeFunctionData, zeroAddress } from "viem"; +import { OPEN_EDITION_MINT_SIZE } from "../constants"; + +// Sales end forever amount (uint64 max) +const SALE_END_FOREVER = 18446744073709551615n; + +// Default royalty bps +const ROYALTY_BPS_DEFAULT = 1000; + +type SalesConfigParamsType = { + // defaults to 0 + pricePerToken?: bigint; + // defaults to 0, in seconds + saleStart?: bigint; + // defaults to forever, in seconds + saleEnd?: bigint; + // max tokens that can be minted per address + maxTokensPerAddress?: bigint; + fundsRecipient?: Address; +}; + +export const DEFAULT_SALE_SETTINGS = { + fundsRecipient: zeroAddress, + // Free Mint + pricePerToken: 0n, + // Sale start time – defaults to beginning of unix time + saleStart: 0n, + // This is the end of uint64, plenty of time + saleEnd: SALE_END_FOREVER, + // 0 Here means no limit + maxTokensPerAddress: 0n, +}; + +// Hardcode the permission bit for the minter +const PERMISSION_BIT_MINTER = 2n ** 2n; + +type ContractType = + | { + name: string; + uri: string; + defaultAdmin?: Address; + } + | Address; + +type RoyaltySettingsType = { + royaltyBPS: number; + royaltyRecipient: Address; +}; + +export function create1155TokenSetupArgs({ + nextTokenId, + // How many NFTs upon initialization to mint to the creator + mintToCreatorCount, + tokenMetadataURI, + // Fixed price minter address – required minter + fixedPriceMinterAddress, + // Address to use as the create referral, optional. + createReferral, + // Optional max supply of the token. Default unlimited + maxSupply, + // wallet sending the transaction + account, + salesConfig, + royaltySettings, +}: { + maxSupply?: bigint | number; + createReferral?: Address; + nextTokenId: bigint; + mintToCreatorCount: bigint | number; + // wallet sending the transaction + account: Address; + tokenMetadataURI: string; + fixedPriceMinterAddress: Address; + salesConfig: SalesConfigParamsType; + royaltySettings?: RoyaltySettingsType; +}) { + if (!maxSupply) { + maxSupply = OPEN_EDITION_MINT_SIZE; + } + maxSupply = BigInt(maxSupply); + mintToCreatorCount = BigInt(mintToCreatorCount); + + const salesConfigWithDefaults = { + // Set static sales default. + ...DEFAULT_SALE_SETTINGS, + // Override with user settings. + ...salesConfig, + }; + + const setupActions = [ + encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "addPermission", + args: [0n, fixedPriceMinterAddress, PERMISSION_BIT_MINTER], + }), + encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "assumeLastTokenIdMatches", + args: [nextTokenId - 1n], + }), + createReferral + ? encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "setupNewTokenWithCreateReferral", + args: [tokenMetadataURI, maxSupply, createReferral], + }) + : encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "setupNewToken", + args: [tokenMetadataURI, maxSupply], + }), + encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "callSale", + args: [ + nextTokenId, + fixedPriceMinterAddress, + encodeFunctionData({ + abi: zoraCreatorFixedPriceSaleStrategyABI, + functionName: "setSale", + args: [nextTokenId, salesConfigWithDefaults], + }), + ], + }), + ]; + + if (mintToCreatorCount) { + setupActions.push( + encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "adminMint", + args: [account, nextTokenId, mintToCreatorCount, "0x"], + }), + ); + } + + if (royaltySettings) { + setupActions.push( + encodeFunctionData({ + abi: zoraCreator1155ImplABI, + functionName: "updateRoyaltiesForToken", + args: [ + nextTokenId, + { + royaltyMintSchedule: 0, + royaltyBPS: royaltySettings?.royaltyBPS || ROYALTY_BPS_DEFAULT, + royaltyRecipient: royaltySettings?.royaltyRecipient || account, + }, + ], + }), + ); + } + + return setupActions; +} + +export const getTokenIdFromCreateReceipt = ( + receipt: TransactionReceipt, +): bigint | undefined => { + for (const data of receipt.logs) { + try { + const decodedLog = decodeEventLog({ + abi: zoraCreator1155ImplABI, + eventName: "SetupNewToken", + ...data, + }); + if (decodedLog && decodedLog.eventName === "SetupNewToken") { + return decodedLog.args.tokenId; + } + } catch (err: any) {} + } +}; + +async function getContractExists( + publicClient: PublicClient, + contract: ContractType, + // Account that is the creator of the contract + account: Address, +) { + let contractAddress; + let contractExists = false; + if (typeof contract !== "string") { + contractAddress = await publicClient.readContract({ + abi: zoraCreator1155FactoryImplABI, + // Since this address is deterministic we can hardcode a chain id safely here. + address: zoraCreator1155FactoryImplAddress[999], + functionName: "deterministicContractAddress", + args: [ + account, + contract.uri, + contract.name, + contract.defaultAdmin || account, + ], + }); + + try { + await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: contractAddress, + functionName: "contractVersion", + }); + contractExists = true; + } catch (e: any) { + // This logic branch is hit if the contract doesn't exist + // falling back to contractExists to false. + } + return { contractAddress, contractExists }; + } + + return { + contractExists: true, + contractAddress: contract, + }; +} + +// Create new 1155 token +export async function createNew1155Token({ + publicClient, + contract, + tokenMetadataURI, + mintToCreatorCount = 1, + salesConfig = {}, + maxSupply, + account, + royaltySettings, + getAdditionalSetupActions, +}: { + publicClient: PublicClient; + account: Address; + maxSupply?: bigint | number; + royaltySettings?: RoyaltySettingsType; + royaltyBPS?: number; + contract: ContractType; + tokenMetadataURI: string; + mintToCreatorCount?: bigint | number; + salesConfig?: SalesConfigParamsType; + getAdditionalSetupActions?: (args: { + tokenId: bigint; + contractAddress: Address; + }) => Hex[]; +}) { + // Check if contract exists either from metadata or the static address passed in. + // If a static address is passed in, this fails if that contract does not exist. + const { contractExists, contractAddress } = await getContractExists( + publicClient, + contract, + account, + ); + + // Assume the next token id is the first token available for a new contract. + let nextTokenId = 1n; + + if (contractExists) { + nextTokenId = await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + functionName: "nextTokenId", + address: contractAddress, + }); + } + + // Get the fixed price minter to use within the new token to set the sales configuration. + const fixedPriceMinterAddress = await publicClient.readContract({ + abi: zoraCreator1155FactoryImplABI, + address: zoraCreator1155FactoryImplAddress[999], + functionName: "fixedPriceMinter", + }); + + let tokenSetupActions = create1155TokenSetupArgs({ + tokenMetadataURI, + nextTokenId, + salesConfig, + maxSupply, + fixedPriceMinterAddress, + account, + mintToCreatorCount, + royaltySettings, + }); + if (getAdditionalSetupActions) { + tokenSetupActions = [ + ...getAdditionalSetupActions({ tokenId: nextTokenId, contractAddress }), + ...tokenSetupActions, + ]; + } + + if (!contractAddress && typeof contract === "string") { + throw new Error("Invariant: contract cannot be missing and an address"); + } + if (!contractExists && typeof contract !== "string") { + const { request } = await publicClient.simulateContract({ + abi: zoraCreator1155FactoryImplABI, + functionName: "createContractDeterministic", + account, + address: zoraCreator1155FactoryImplAddress[999], + args: [ + contract.uri, + contract.name, + { + // deprecated + royaltyMintSchedule: 0, + royaltyBPS: royaltySettings?.royaltyBPS || ROYALTY_BPS_DEFAULT, + royaltyRecipient: royaltySettings?.royaltyRecipient || account, + }, + contract.defaultAdmin || account, + tokenSetupActions, + ], + }); + return { + send: (walletClient: WalletClient) => walletClient.writeContract(request), + tokenSetupActions, + contractAddress, + contractExists, + }; + } else if (contractExists) { + const { request } = await publicClient.simulateContract({ + abi: zoraCreator1155ImplABI, + functionName: "multicall", + account, + address: contractAddress, + args: [tokenSetupActions], + }); + return { + send: (walletClient: WalletClient) => walletClient.writeContract(request), + tokenSetupActions, + contractAddress, + contractExists, + }; + } + throw new Error("Unsupported contract argument type"); +} diff --git a/packages/protocol-sdk/src/index.ts b/packages/protocol-sdk/src/index.ts index ac4355a77..ba462be8f 100644 --- a/packages/protocol-sdk/src/index.ts +++ b/packages/protocol-sdk/src/index.ts @@ -1,5 +1,9 @@ -export * from "./premint-client"; +export * from "./premint/premint-client"; -export * from "./preminter"; +export * from "./premint/preminter"; -export * from "./premint-api-client"; +export * from "./premint/premint-api-client"; + +export * from "./mint/mint-api-client"; + +export * from "./create/1155-create-helper"; diff --git a/packages/protocol-sdk/src/mint/mint-api-client.ts b/packages/protocol-sdk/src/mint/mint-api-client.ts new file mode 100644 index 000000000..3918ab3e2 --- /dev/null +++ b/packages/protocol-sdk/src/mint/mint-api-client.ts @@ -0,0 +1,52 @@ +import { retries, get, post } from "../apis/http-api-base"; +import { paths } from "../apis/generated/discover-api-types"; +import { ZORA_API_BASE } from "../constants"; + +export type MintableGetToken = + paths["/mintables/{chain_name}/{collection_address}"]; +type MintableGetTokenPathParameters = + MintableGetToken["get"]["parameters"]["path"]; +type MintableGetTokenGetQueryParameters = + MintableGetToken["get"]["parameters"]["query"]; +export type MintableGetTokenResponse = + MintableGetToken["get"]["responses"][200]["content"]["application/json"]; + +function encodeQueryParameters(params: Record) { + return new URLSearchParams(params).toString(); +} + +const getMintable = async ( + path: MintableGetTokenPathParameters, + query: MintableGetTokenGetQueryParameters, +): Promise => + retries(() => { + return get( + `${ZORA_API_BASE}discover/mintables/${path.chain_name}/${ + path.collection_address + }${query?.token_id ? `?${encodeQueryParameters(query)}` : ""}`, + ); + }); + +export const getSalesConfigFixedPrice = async ({ + contractAddress, + tokenId, + subgraphUrl, +}: { + contractAddress: string; + tokenId: string; + subgraphUrl: string; +}): Promise => + retries(async () => { + const response = await post(subgraphUrl, { + query: + "query($id: ID!) {\n zoraCreateToken(id: $id) {\n id\n salesStrategies{\n fixedPrice {\n address\n }\n }\n }\n}", + variables: { id: `${contractAddress.toLowerCase()}-${tokenId}` }, + }); + return response.zoraCreateToken?.salesStrategies?.find(() => true) + ?.fixedPriceMinterAddress; + }); + +export const MintAPIClient = { + getMintable, + getSalesConfigFixedPrice, +}; diff --git a/packages/protocol-sdk/src/mint/mint-client.test.ts b/packages/protocol-sdk/src/mint/mint-client.test.ts new file mode 100644 index 000000000..58c3b2514 --- /dev/null +++ b/packages/protocol-sdk/src/mint/mint-client.test.ts @@ -0,0 +1,117 @@ +import { parseAbi, parseEther } from "viem"; +import { zora } from "viem/chains"; +import { describe, expect } from "vitest"; +import { MintClient } from "./mint-client"; +import { zoraCreator1155ImplABI } from "@zoralabs/protocol-deployments"; +import { anvilTest } from "src/anvil"; + +const erc721ABI = parseAbi([ + "function balanceOf(address owner) public view returns (uint256)", +] as const); + +describe("mint-helper", () => { + anvilTest( + "mints a new 1155 token", + async ({ viemClients }) => { + const { testClient, walletClient, publicClient } = viemClients; + const creatorAccount = (await walletClient.getAddresses())[0]!; + await testClient.setBalance({ + address: creatorAccount, + value: parseEther("2000"), + }); + const targetContract = "0xa2fea3537915dc6c7c7a97a82d1236041e6feb2e"; + const targetTokenId = 1n; + const minter = new MintClient(zora); + + const { simulateContractParameters: params } = + await minter.makePrepareMintTokenParams({ + publicClient, + minterAccount: creatorAccount, + mintable: await minter.getMintable({ + tokenId: targetTokenId, + tokenContract: targetContract, + }), + mintArguments: { + mintToAddress: creatorAccount, + quantityToMint: 1, + }, + }); + + const oldBalance = await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: targetContract, + functionName: "balanceOf", + args: [creatorAccount, targetTokenId], + }); + + const simulationResult = await publicClient.simulateContract(params); + + const hash = await walletClient.writeContract(simulationResult.request); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + const newBalance = await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: targetContract, + functionName: "balanceOf", + args: [creatorAccount, targetTokenId], + }); + expect(receipt).to.not.be.null; + expect(oldBalance).to.be.equal(0n); + expect(newBalance).to.be.equal(1n); + }, + 12 * 1000, + ); + + anvilTest( + "mints a new 721 token", + async ({ viemClients }) => { + const { testClient, walletClient, publicClient } = viemClients; + const creatorAccount = (await walletClient.getAddresses())[0]!; + await testClient.setBalance({ + address: creatorAccount, + value: parseEther("2000"), + }); + + const targetContract = "0x7aae7e67515A2CbB8585C707Ca6db37BDd3EA839"; + const targetTokenId = undefined; + const minter = new MintClient(zora); + + const { simulateContractParameters: prepared } = + await minter.makePrepareMintTokenParams({ + mintable: await minter.getMintable({ + tokenContract: targetContract, + tokenId: targetTokenId, + }), + publicClient, + minterAccount: creatorAccount, + mintArguments: { + mintToAddress: creatorAccount, + quantityToMint: 1, + }, + }); + const oldBalance = await publicClient.readContract({ + abi: erc721ABI, + address: targetContract, + functionName: "balanceOf", + args: [creatorAccount], + }); + + const simulated = await publicClient.simulateContract(prepared); + + const hash = await walletClient.writeContract(simulated.request); + + const receipt = await publicClient.getTransactionReceipt({ hash }); + expect(receipt).not.to.be.null; + + const newBalance = await publicClient.readContract({ + abi: erc721ABI, + address: targetContract, + functionName: "balanceOf", + args: [creatorAccount], + }); + + expect(oldBalance).to.be.equal(0n); + expect(newBalance).to.be.equal(1n); + }, + 12 * 1000, + ); +}); diff --git a/packages/protocol-sdk/src/mint/mint-client.ts b/packages/protocol-sdk/src/mint/mint-client.ts new file mode 100644 index 000000000..c7d2f7a30 --- /dev/null +++ b/packages/protocol-sdk/src/mint/mint-client.ts @@ -0,0 +1,218 @@ +import { + Address, + Chain, + PublicClient, + encodeAbiParameters, + parseAbi, + parseAbiParameters, + zeroAddress, +} from "viem"; +import { ClientBase } from "../apis/client-base"; +import { MintAPIClient, MintableGetTokenResponse } from "./mint-api-client"; +import { SimulateContractParameters } from "viem"; +import { + zoraCreator1155ImplABI, + zoraCreatorFixedPriceSaleStrategyAddress, +} from "@zoralabs/protocol-deployments"; + +class MintError extends Error {} +class MintInactiveError extends Error {} + +export const Errors = { + MintError, + MintInactiveError, +}; + +type MintArguments = { + quantityToMint: number; + mintComment?: string; + mintReferral?: Address; + mintToAddress: Address; +}; + +type MintParameters = { + mintArguments: MintArguments; + publicClient: PublicClient; + mintable: MintableGetTokenResponse; + sender: Address; +}; + +const zora721Abi = parseAbi([ + "function mintWithRewards(address recipient, uint256 quantity, string calldata comment, address mintReferral) external payable", + "function zoraFeeForAmount(uint256 amount) public view returns (address, uint256)", +] as const); + +export class MintClient extends ClientBase { + apiClient: typeof MintAPIClient; + + constructor(chain: Chain, apiClient?: typeof MintAPIClient) { + super(chain); + + if (!apiClient) { + apiClient = MintAPIClient; + } + this.apiClient = apiClient; + } + + async getMintable({ + tokenContract, + tokenId, + }: { + tokenContract: Address; + tokenId?: bigint | number | string; + }) { + return this.apiClient.getMintable( + { + chain_name: this.network.zoraBackendChainName, + collection_address: tokenContract, + }, + { token_id: tokenId?.toString() }, + ); + } + + async makePrepareMintTokenParams({ + publicClient, + minterAccount, + mintable, + mintArguments, + }: { + publicClient: PublicClient; + mintable: MintableGetTokenResponse; + minterAccount: Address; + mintArguments: MintArguments; + }): Promise<{simulateContractParameters: SimulateContractParameters}> { + if (!mintable) { + throw new MintError("No mintable found"); + } + + if (!mintable.is_active) { + throw new MintInactiveError("Minting token is inactive"); + } + + if (!mintable.mint_context) { + throw new MintError("No minting context data from zora API"); + } + + if ( + !["zora_create", "zora_create_1155"].includes( + mintable.mint_context?.mint_context_type!, + ) + ) { + throw new MintError( + `Mintable type ${mintable.mint_context.mint_context_type} is currently unsupported.`, + ); + } + + const thisPublicClient = this.getPublicClient(publicClient); + + if (mintable.mint_context.mint_context_type === "zora_create_1155") { + return { + simulateContractParameters: await this.prepareMintZora1155({ + publicClient: thisPublicClient, + mintArguments, + sender: minterAccount, + mintable, + }), + }; + } + if (mintable.mint_context.mint_context_type === "zora_create") { + return { + simulateContractParameters: await this.prepareMintZora721({ + publicClient: thisPublicClient, + mintArguments, + sender: minterAccount, + mintable, + }), + }; + } + + throw new Error("Mintable type not found or recognized."); + } + + private async prepareMintZora1155({ + mintable, + sender, + publicClient, + mintArguments, + }: MintParameters) { + const mintQuantity = BigInt(mintArguments.quantityToMint); + + const address = mintable.collection.address as Address; + + const mintFee = await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + functionName: "mintFee", + address, + }); + + const tokenFixedPriceMinter = await this.apiClient.getSalesConfigFixedPrice( + { + contractAddress: mintable.contract_address, + tokenId: mintable.token_id!, + subgraphUrl: this.network.subgraphUrl, + }, + ); + + const result: SimulateContractParameters< + typeof zoraCreator1155ImplABI, + "mintWithRewards" + > = { + abi: zoraCreator1155ImplABI, + functionName: "mintWithRewards", + account: sender, + value: (mintFee + BigInt(mintable.cost.native_price.raw)) * mintQuantity, + address, + /* args: minter, tokenId, quantity, minterArguments, mintReferral */ + args: [ + (tokenFixedPriceMinter || + zoraCreatorFixedPriceSaleStrategyAddress[999]) as Address, + BigInt(mintable.token_id!), + mintQuantity, + encodeAbiParameters(parseAbiParameters("address, string"), [ + mintArguments.mintToAddress, + mintArguments.mintComment || "", + ]), + mintArguments.mintReferral || zeroAddress, + ], + }; + + return result; + } + + private async prepareMintZora721({ + mintable, + publicClient, + sender, + mintArguments, + }: MintParameters) { + const [_, mintFee] = await publicClient.readContract({ + abi: zora721Abi, + address: mintable.contract_address as Address, + functionName: "zoraFeeForAmount", + args: [BigInt(mintArguments.quantityToMint)], + }); + + const result: SimulateContractParameters< + typeof zora721Abi, + "mintWithRewards" + > = { + abi: zora721Abi, + address: mintable.contract_address as Address, + account: sender, + functionName: "mintWithRewards", + value: + mintFee + + BigInt(mintable.cost.native_price.raw) * + BigInt(mintArguments.quantityToMint), + /* args: mint recipient, quantity to mint, mint comment, mintReferral */ + args: [ + mintArguments.mintToAddress, + BigInt(mintArguments.quantityToMint), + mintArguments.mintComment || "", + mintArguments.mintReferral || zeroAddress, + ], + }; + + return result; + } +} diff --git a/packages/premint-sdk/src/premint-api-client.ts b/packages/protocol-sdk/src/premint/premint-api-client.ts similarity index 66% rename from packages/premint-sdk/src/premint-api-client.ts rename to packages/protocol-sdk/src/premint/premint-api-client.ts index 546b4c5ba..c4be78df2 100644 --- a/packages/premint-sdk/src/premint-api-client.ts +++ b/packages/protocol-sdk/src/premint/premint-api-client.ts @@ -1,7 +1,6 @@ -import { post, retries, get } from "./http-api-base"; -import { components, paths } from "./generated/premint-api-types"; - -export const ZORA_API_BASE = "https://api.zora.co/premint/"; +import { post, retries, get } from "../apis/http-api-base"; +import { components, paths } from "../apis/generated/premint-api-types"; +import { ZORA_API_BASE } from "../constants"; type SignaturePostType = paths["/signature"]["post"]; type PremintSignatureRequestBody = @@ -25,29 +24,29 @@ export type PremintSignatureGetResponse = export type BackendChainNames = components["schemas"]["ChainName"]; - const postSignature = async ( - data: PremintSignatureRequestBody -): Promise => { - return retries(() => post("signature", data)); -}; + data: PremintSignatureRequestBody, +): Promise => + retries(() => + post(`${ZORA_API_BASE}premint/signature`, data), + ); const getNextUID = async ( - path: PremintNextUIDGetPathParameters + path: PremintNextUIDGetPathParameters, ): Promise => retries(() => get( - `${ZORA_API_BASE}signature/${path.chain_name}/${path.collection_address}/next_uid` - ) + `${ZORA_API_BASE}premint/signature/${path.chain_name}/${path.collection_address}/next_uid`, + ), ); const getSignature = async ( - path: PremintSignatureGetPathParameters + path: PremintSignatureGetPathParameters, ): Promise => retries(() => get( - `signature/${path.chain_name}/${path.collection_address}/${path.uid}` - ) + `${ZORA_API_BASE}premint/signature/${path.chain_name}/${path.collection_address}/${path.uid}`, + ), ); export const PremintAPIClient = { @@ -55,3 +54,4 @@ export const PremintAPIClient = { getSignature, getNextUID, }; +export { ZORA_API_BASE }; diff --git a/packages/premint-sdk/src/premint-client.test.ts b/packages/protocol-sdk/src/premint/premint-client.test.ts similarity index 63% rename from packages/premint-sdk/src/premint-client.test.ts rename to packages/protocol-sdk/src/premint/premint-client.test.ts index 3075e4973..7b119b368 100644 --- a/packages/premint-sdk/src/premint-client.test.ts +++ b/packages/protocol-sdk/src/premint/premint-client.test.ts @@ -1,61 +1,15 @@ -import { - createTestClient, - http, - createWalletClient, - createPublicClient, - Address, -} from "viem"; import { foundry } from "viem/chains"; -import { describe, it, beforeEach, expect, vi, afterEach } from "vitest"; -import { parseEther } from "viem"; -import { BackendChainNamesLookup, PremintClient } from "./premint-client"; - -const chain = foundry; - -const walletClient = createWalletClient({ - chain, - transport: http(), -}); - -const testClient = createTestClient({ - chain, - mode: "anvil", - transport: http(), -}); - -const publicClient = createPublicClient({ - chain, - transport: http(), -}); - -// JSON-RPC Account -const [deployerAccount, secondWallet] = (await walletClient.getAddresses()) as [ - Address, - Address -]; +import { describe, expect, vi } from "vitest"; +import { PremintClient } from "./premint-client"; +import { anvilTest } from "src/anvil"; +import { BackendChainNamesLookup } from "src/apis/chain-constants"; describe("ZoraCreator1155Premint", () => { - beforeEach(async () => { - // deploy signature minter contract - await testClient.setBalance({ - address: deployerAccount, - value: parseEther("1"), - }); - - await testClient.setBalance({ - address: secondWallet, - value: parseEther("1"), - }); - }, 20 * 1000); - - afterEach(() => { - testClient.reset(); - }, 4 * 1000); - - it( + anvilTest( "can sign on the forked premint contract", - async () => { - const premintClient = new PremintClient(chain); + async ({ viemClients: { walletClient, publicClient } }) => { + const [deployerAccount] = await walletClient.getAddresses(); + const premintClient = new PremintClient(foundry); premintClient.apiClient.getNextUID = vi .fn() @@ -67,10 +21,10 @@ describe("ZoraCreator1155Premint", () => { await premintClient.createPremint({ walletClient, publicClient, - account: deployerAccount, + account: deployerAccount!, checkSignature: true, collection: { - contractAdmin: deployerAccount, + contractAdmin: deployerAccount!, contractName: "Testing Contract", contractURI: "ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e", @@ -111,57 +65,58 @@ describe("ZoraCreator1155Premint", () => { "0x588d19641de9ba1dade4d2bb5387c8dc96f4a990fef69787534b60caead759e6334975a6be10a796da948cd7d1d4f5580b3f84d49d9fa4e0b41c97759507975a1c", }); }, - 20 * 1000 + 20 * 1000, ); - it("can validate premint on network", async () => { - const premintClient = new PremintClient(chain); + anvilTest( + "can validate premint on network", + async ({ viemClients: { publicClient } }) => { + const premintClient = new PremintClient(foundry); - const premintData = { - collection: { - contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - contractName: "Testing Contract", - contractURI: - "ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e", - }, - premint: { - uid: 3, - version: 1, - deleted: false, - tokenConfig: { - maxSupply: "18446744073709551615", - maxTokensPerAddress: "0", - pricePerToken: "0", - mintDuration: "604800", - mintStart: "0", - royaltyMintSchedule: 0, - royaltyBPS: 1000, - fixedPriceMinter: "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", - royaltyRecipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - tokenURI: - "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", + const premintData = { + collection: { + contractAdmin: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + contractName: "Testing Contract", + contractURI: + "ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e", }, - }, - chain_name: "ZORA-TESTNET", - signature: - "0x588d19641de9ba1dade4d2bb5387c8dc96f4a990fef69787534b60caead759e6334975a6be10a796da948cd7d1d4f5580b3f84d49d9fa4e0b41c97759507975a1c", - } as const; - const publicClient = createPublicClient({ - chain: foundry, - transport: http(), - }); - const signatureValid = await premintClient.isValidSignature({ - // @ts-ignore: Fix enum type - data: premintData, - publicClient, - }); - expect(signatureValid.isValid).toBe(true); - }); + premint: { + uid: 3, + version: 1, + deleted: false, + tokenConfig: { + maxSupply: "18446744073709551615", + maxTokensPerAddress: "0", + pricePerToken: "0", + mintDuration: "604800", + mintStart: "0", + royaltyMintSchedule: 0, + royaltyBPS: 1000, + fixedPriceMinter: "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", + royaltyRecipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + tokenURI: + "ipfs://bafkreice23maski3x52tsfqgxstx3kbiifnt5jotg3a5ynvve53c4soi2u", + }, + }, + chain_name: "ZORA-TESTNET", + signature: + "0x588d19641de9ba1dade4d2bb5387c8dc96f4a990fef69787534b60caead759e6334975a6be10a796da948cd7d1d4f5580b3f84d49d9fa4e0b41c97759507975a1c", + } as const; + + const signatureValid = await premintClient.isValidSignature({ + // @ts-ignore: Fix enum type + data: premintData, + publicClient, + }); + expect(signatureValid.isValid).toBe(true); + }, + ); - it( + anvilTest( "can execute premint on network", - async () => { - const premintClient = new PremintClient(chain); + async ({ viemClients: { walletClient, publicClient } }) => { + const [deployerAccount] = await walletClient.getAddresses(); + const premintClient = new PremintClient(foundry); premintClient.apiClient.getSignature = vi.fn().mockResolvedValue({ chain_name: "ZORA-TESTNET", @@ -236,6 +191,6 @@ describe("ZoraCreator1155Premint", () => { uid: 3, }); }, - 20 * 1000 + 20 * 1000, ); }); diff --git a/packages/premint-sdk/src/premint-client.ts b/packages/protocol-sdk/src/premint/premint-client.ts similarity index 89% rename from packages/premint-sdk/src/premint-client.ts rename to packages/protocol-sdk/src/premint/premint-client.ts index ca97746ec..c708359a4 100644 --- a/packages/premint-sdk/src/premint-client.ts +++ b/packages/protocol-sdk/src/premint/premint-client.ts @@ -1,4 +1,4 @@ -import { createPublicClient, decodeEventLog, http, parseEther } from "viem"; +import { decodeEventLog } from "viem"; import type { Account, Address, @@ -13,50 +13,16 @@ import { zoraCreator1155PremintExecutorImplAddress, zoraCreatorFixedPriceSaleStrategyAddress, } from "@zoralabs/protocol-deployments"; -import { foundry, zora, zoraTestnet } from "viem/chains"; import { PremintConfig, preminterTypedDataDefinition } from "./preminter"; import type { - BackendChainNames as BackendChainNamesType, PremintSignatureGetResponse, PremintSignatureResponse, } from "./premint-api-client"; import { PremintAPIClient } from "./premint-api-client"; import type { DecodeEventLogReturnType } from "viem"; - -export const BackendChainNamesLookup = { - ZORA_MAINNET: "ZORA-MAINNET", - ZORA_GOERLI: "ZORA-GOERLI", -} as const; - -export type NetworkConfig = { - chainId: number; - zoraPathChainName: string; - zoraBackendChainName: BackendChainNamesType; - isTestnet: boolean; -}; - -export const REWARD_PER_TOKEN = parseEther("0.000777"); - -export const networkConfigByChain: Record = { - [zora.id]: { - chainId: zora.id, - isTestnet: false, - zoraPathChainName: "zora", - zoraBackendChainName: BackendChainNamesLookup.ZORA_MAINNET, - }, - [zoraTestnet.id]: { - chainId: zora.id, - isTestnet: true, - zoraPathChainName: "zgor", - zoraBackendChainName: BackendChainNamesLookup.ZORA_GOERLI, - }, - [foundry.id]: { - chainId: foundry.id, - isTestnet: true, - zoraPathChainName: "zgor", - zoraBackendChainName: BackendChainNamesLookup.ZORA_GOERLI, - }, -}; +import { ClientBase } from "../apis/client-base"; +import { OPEN_EDITION_MINT_SIZE } from "../constants"; +import { REWARD_PER_TOKEN } from "src/apis/chain-constants"; type MintArgumentsSettings = { tokenURI: string; @@ -95,7 +61,6 @@ type ExecutedPremintResponse = { urls: URLSReturnType; }; -const OPEN_EDITION_MINT_SIZE = BigInt("18446744073709551615"); export const DefaultMintArguments = { maxSupply: OPEN_EDITION_MINT_SIZE, maxTokensPerAddress: 0n, @@ -113,7 +78,7 @@ export const DefaultMintArguments = { * @returns Premint event arguments */ export function getPremintedLogFromReceipt( - receipt: TransactionReceipt + receipt: TransactionReceipt, ): PremintedLogType | undefined { for (const data of receipt.logs) { try { @@ -136,7 +101,7 @@ export function getPremintedLogFromReceipt( * @returns Viem type-compatible premint object */ export const convertPremint = ( - premint: PremintSignatureGetResponse["premint"] + premint: PremintSignatureGetResponse["premint"], ) => ({ ...premint, tokenConfig: { @@ -152,7 +117,7 @@ export const convertPremint = ( }); export const convertCollection = ( - collection: PremintSignatureGetResponse["collection"] + collection: PremintSignatureGetResponse["collection"], ) => ({ ...collection, contractAdmin: collection.contractAdmin as Address, @@ -183,22 +148,16 @@ export const encodePremintForAPI = ({ * Preminter API to access ZORA Premint functionality. * Currently only supports V1 premints. */ -export class PremintClient { - network: NetworkConfig; - chain: Chain; +export class PremintClient extends ClientBase { apiClient: typeof PremintAPIClient; constructor(chain: Chain, apiClient?: typeof PremintAPIClient) { - this.chain = chain; + super(chain); + if (!apiClient) { apiClient = PremintAPIClient; } this.apiClient = apiClient; - const networkConfig = networkConfigByChain[chain.id]; - if (!networkConfig) { - throw new Error(`Not configured for chain ${chain.id}`); - } - this.network = networkConfig; } /** @@ -222,19 +181,6 @@ export class PremintClient { return zoraCreatorFixedPriceSaleStrategyAddress[999]; } - /** - * 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. - */ - protected getPublicClient(publicClient?: PublicClient): PublicClient { - if (publicClient) { - return publicClient; - } - return createPublicClient({ chain: this.chain, transport: http() }); - } - /** * Update existing premint given collection address and UID of existing premint. * diff --git a/packages/protocol-sdk/src/premint/preminter.test.ts b/packages/protocol-sdk/src/premint/preminter.test.ts new file mode 100644 index 000000000..574bcae6a --- /dev/null +++ b/packages/protocol-sdk/src/premint/preminter.test.ts @@ -0,0 +1,502 @@ +import { keccak256, Hex, concat, recoverAddress, hashDomain, Address } from "viem"; +import { foundry } from "viem/chains"; +import { describe, expect } from "vitest"; +import { parseEther } from "viem"; +import { + zoraCreator1155PremintExecutorImplABI as preminterAbi, + zoraCreator1155PremintExecutorImplAddress as zoraCreator1155PremintExecutorAddress, + zoraCreator1155ImplABI, + zoraCreator1155FactoryImplAddress, + zoraCreator1155FactoryImplConfig, +} from "@zoralabs/protocol-deployments"; + +import { + ContractCreationConfig, + PremintConfig, + TokenCreationConfig, + preminterTypedDataDefinition, +} from "./preminter"; +import { AnvilViemClientsTest, anvilTest } from "src/anvil"; + +// create token and contract creation config: +const defaultContractConfig = ({ + contractAdmin, +}: { + contractAdmin: Address; +}): ContractCreationConfig => ({ + contractAdmin, + contractURI: "ipfs://asdfasdfasdf", + contractName: "My fun NFT", +}); + +const defaultTokenConfig = ( + fixedPriceMinterAddress: Address, + creatorAccount: Address, +): TokenCreationConfig => ({ + tokenURI: "ipfs://tokenIpfsId0", + maxSupply: 100n, + maxTokensPerAddress: 10n, + pricePerToken: 0n, + mintStart: 0n, + mintDuration: 100n, + royaltyMintSchedule: 30, + royaltyBPS: 200, + royaltyRecipient: creatorAccount, + fixedPriceMinter: fixedPriceMinterAddress, +}); + +const defaultPremintConfig = ( + fixedPriceMinter: Address, + creatorAccount: Address, +): PremintConfig => ({ + tokenConfig: defaultTokenConfig(fixedPriceMinter, creatorAccount), + deleted: false, + uid: 105, + version: 0, +}); + +const ZORA_MINT_FEE = parseEther("0.000777"); + +const PREMINTER_ADDRESS = zoraCreator1155PremintExecutorAddress[999]; + +async function setupContracts({ + viemClients: { walletClient, testClient, publicClient }, +}: AnvilViemClientsTest) { + // JSON-RPC Account + const [deployerAccount, creatorAccount, collectorAccount] = + (await walletClient.getAddresses()) as [Address, Address, Address, Address]; + + // deploy signature minter contract + await testClient.setBalance({ + address: deployerAccount, + value: parseEther("10"), + }); + + const fixedPriceMinterAddress = await publicClient.readContract({ + abi: zoraCreator1155FactoryImplConfig.abi, + address: zoraCreator1155FactoryImplAddress[999], + functionName: "fixedPriceMinter", + }); + + return { + accounts: { + deployerAccount, + creatorAccount, + collectorAccount, + }, + fixedPriceMinterAddress, + }; +} + +describe("ZoraCreator1155Preminter", () => { + // skip for now - we need to make this work on zora testnet chain too + anvilTest( + "can sign on the forked premint contract", + async ({ viemClients }) => { + const { + fixedPriceMinterAddress, + accounts: { creatorAccount }, + } = await setupContracts({ viemClients }); + const premintConfig = defaultPremintConfig( + fixedPriceMinterAddress, + creatorAccount, + ); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); + + const preminterAddress = zoraCreator1155PremintExecutorAddress[999]; + + const contractAddress = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getContractAddress", + args: [contractConfig], + }); + + const signedMessage = await viemClients.walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: contractAddress, + chainId: 999, + premintConfig, + }), + account: creatorAccount, + }); + + console.log({ + creatorAccount, + signedMessage, + contractConfig, + premintConfig, + contractAddress, + }); + }, + 20 * 1000, + ); + anvilTest( + "can sign and recover a signature", + async ({ viemClients }) => { + const { + fixedPriceMinterAddress, + accounts: { creatorAccount }, + } = await setupContracts({ viemClients }); + + const premintConfig = defaultPremintConfig( + fixedPriceMinterAddress, + creatorAccount, + ); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); + + const contractAddress = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "getContractAddress", + args: [contractConfig], + }); + + // sign message containing contract and token creation config and uid + const signedMessage = await viemClients.walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: contractAddress, + // we need to sign here for the anvil chain, cause thats where it is run on + chainId: foundry.id, + premintConfig, + }), + account: creatorAccount, + }); + + // recover and verify address is correct + const recoveredAddress = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "recoverSigner", + args: [premintConfig, contractAddress, signedMessage], + }); + + expect(recoveredAddress).to.equal(creatorAccount); + }, + + 20 * 1000, + ); + anvilTest( + "can sign and mint multiple tokens", + async ({ viemClients }) => { + const { + fixedPriceMinterAddress, + accounts: { creatorAccount, collectorAccount }, + } = await setupContracts({ viemClients }); + // setup contract and token creation parameters + const premintConfig = defaultPremintConfig( + fixedPriceMinterAddress, + creatorAccount, + ); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); + + // lets make it a random number to not break the existing tests that expect fresh data + premintConfig.uid = Math.round(Math.random() * 1000000); + + let contractAddress = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "getContractAddress", + args: [contractConfig], + }); + + // have creator sign the message to create the contract + // and the token + const signedMessage = await viemClients.walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: contractAddress, + // we need to sign here for the anvil chain, cause thats where it is run on + chainId: foundry.id, + premintConfig, + }), + account: creatorAccount, + }); + + const quantityToMint = 2n; + + const valueToSend = + (ZORA_MINT_FEE + premintConfig.tokenConfig.pricePerToken) * + quantityToMint; + + const comment = "I love this!"; + + await viemClients.testClient.setBalance({ + address: collectorAccount, + value: parseEther("10"), + }); + + // get the premint status - it should not be minted + let [contractCreated, tokenId] = + await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "premintStatus", + args: [contractAddress, premintConfig.uid], + }); + + expect(contractCreated).toBe(false); + expect(tokenId).toBe(0n); + + // now have the collector execute the first signed message; + // it should create the contract, the token, + // and min the quantity to mint tokens to the collector + // the signature along with contract + token creation + // parameters are required to call this function + const mintHash = await viemClients.walletClient.writeContract({ + abi: preminterAbi, + functionName: "premint", + account: collectorAccount, + chain: foundry, + address: PREMINTER_ADDRESS, + args: [ + contractConfig, + premintConfig, + signedMessage, + quantityToMint, + comment, + ], + value: valueToSend, + }); + + // ensure it succeeded + const receipt = await viemClients.publicClient.waitForTransactionReceipt({ + hash: mintHash, + }); + + expect(receipt.status).toBe("success"); + + // fetch the premint token id + [contractCreated, tokenId] = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "premintStatus", + args: [contractAddress, premintConfig.uid], + }); + + expect(tokenId).not.toBe(0n); + + // now use what was created, to get the balance from the created contract + const tokenBalance = await viemClients.publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: contractAddress, + functionName: "balanceOf", + args: [collectorAccount, tokenId], + }); + + // get token balance - should be amount that was created + expect(tokenBalance).toBe(quantityToMint); + + const premintConfig2 = { + ...premintConfig, + uid: premintConfig.uid + 1, + tokenConfig: { + ...premintConfig.tokenConfig, + tokenURI: "ipfs://tokenIpfsId2", + pricePerToken: parseEther("0.05"), + }, + }; + + // sign the message to create the second token + const signedMessage2 = await viemClients.walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: contractAddress, + chainId: foundry.id, + premintConfig: premintConfig2, + }), + account: creatorAccount, + }); + + const quantityToMint2 = 4n; + + const valueToSend2 = + (ZORA_MINT_FEE + premintConfig2.tokenConfig.pricePerToken) * + quantityToMint2; + + // now have the collector execute the second signed message. + // it should create a new token against the existing contract + const mintHash2 = await viemClients.walletClient.writeContract({ + abi: preminterAbi, + functionName: "premint", + account: collectorAccount, + chain: foundry, + address: PREMINTER_ADDRESS, + args: [ + contractConfig, + premintConfig2, + signedMessage2, + quantityToMint2, + comment, + ], + value: valueToSend2, + }); + + expect( + ( + await viemClients.publicClient.waitForTransactionReceipt({ + hash: mintHash2, + }) + ).status, + ).toBe("success"); + + // now premint status for the second mint, it should be minted + [, tokenId] = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "premintStatus", + args: [contractAddress, premintConfig2.uid], + }); + + expect(tokenId).not.toBe(0n); + + // get balance of second token + const tokenBalance2 = await viemClients.publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: contractAddress, + functionName: "balanceOf", + args: [collectorAccount, tokenId], + }); + + expect(tokenBalance2).toBe(quantityToMint2); + }, + // 10 second timeout + 40 * 1000, + ); + + anvilTest( + "can decode the CreatorAttribution event", + async ({ viemClients }) => { + const { + fixedPriceMinterAddress, + accounts: { creatorAccount, collectorAccount }, + } = await setupContracts({ viemClients }); + const premintConfig = defaultPremintConfig( + fixedPriceMinterAddress, + creatorAccount, + ); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); + + // lets make it a random number to not break the existing tests that expect fresh data + premintConfig.uid = Math.round(Math.random() * 1000000); + + let contractAddress = await viemClients.publicClient.readContract({ + abi: preminterAbi, + address: PREMINTER_ADDRESS, + functionName: "getContractAddress", + args: [contractConfig], + }); + + // have creator sign the message to create the contract + // and the token + const signedMessage = await viemClients.walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: contractAddress, + // we need to sign here for the anvil chain, cause thats where it is run on + chainId: foundry.id, + premintConfig, + }), + account: creatorAccount, + }); + + const quantityToMint = 2n; + + const valueToSend = + (ZORA_MINT_FEE + premintConfig.tokenConfig.pricePerToken) * + quantityToMint; + + const comment = "I love this!"; + + await viemClients.testClient.setBalance({ + address: collectorAccount, + value: parseEther("10"), + }); + + // now have the collector execute the first signed message; + // it should create the contract, the token, + // and min the quantity to mint tokens to the collector + // the signature along with contract + token creation + // parameters are required to call this function + const mintHash = await viemClients.walletClient.writeContract({ + abi: preminterAbi, + functionName: "premint", + account: collectorAccount, + chain: foundry, + address: PREMINTER_ADDRESS, + args: [ + contractConfig, + premintConfig, + signedMessage, + quantityToMint, + comment, + ], + value: valueToSend, + }); + + // ensure it succeeded + const receipt = await viemClients.publicClient.waitForTransactionReceipt({ + hash: mintHash, + }); + + expect(receipt.status).toBe("success"); + + // get the CreatorAttribution event from the erc1155 contract: + const topics = await viemClients.publicClient.getContractEvents({ + abi: zoraCreator1155ImplABI, + address: contractAddress, + eventName: "CreatorAttribution", + }); + + expect(topics.length).toBe(1); + + const creatorAttributionEvent = topics[0]!; + + const { creator, domainName, signature, structHash, version } = + creatorAttributionEvent.args; + + const chainId = foundry.id; + + // hash the eip712 domain based on the parameters emitted from the event: + const hashedDomain = hashDomain({ + domain: { + chainId, + name: domainName, + verifyingContract: contractAddress, + version, + }, + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + }, + }); + + // re-build the eip-712 typed data hash, consisting of the hashed domain and the structHash emitted from the event: + const parts: Hex[] = ["0x1901", hashedDomain, structHash!]; + + const hashedTypedData = keccak256(concat(parts)); + + const recoveredSigner = await recoverAddress({ + hash: hashedTypedData, + signature: signature!, + }); + + expect(recoveredSigner).toBe(creator); + }, + ); +}); diff --git a/packages/premint-sdk/src/preminter.ts b/packages/protocol-sdk/src/premint/preminter.ts similarity index 98% rename from packages/premint-sdk/src/preminter.ts rename to packages/protocol-sdk/src/premint/preminter.ts index dd9a93b0a..35e83161d 100644 --- a/packages/premint-sdk/src/preminter.ts +++ b/packages/protocol-sdk/src/premint/preminter.ts @@ -68,7 +68,5 @@ export const preminterTypedDataDefinition = ({ primaryType: "CreatorAttribution", }; - // console.log({ result, deleted }); - return result; }; diff --git a/packages/protocol-sdk/src/preminter.ts b/packages/protocol-sdk/src/preminter.ts new file mode 100644 index 000000000..35e83161d --- /dev/null +++ b/packages/protocol-sdk/src/preminter.ts @@ -0,0 +1,72 @@ +import { Address } from "abitype"; +import { ExtractAbiFunction, AbiParametersToPrimitiveTypes } from "abitype"; +import { zoraCreator1155PremintExecutorImplABI as preminterAbi } from "@zoralabs/protocol-deployments"; +import { TypedDataDefinition } from "viem"; + +type PremintInputs = ExtractAbiFunction< + typeof preminterAbi, + "premint" +>["inputs"]; + +type PreminterHashDataTypes = AbiParametersToPrimitiveTypes; + +export type ContractCreationConfig = PreminterHashDataTypes[0]; +export type PremintConfig = PreminterHashDataTypes[1]; +export type TokenCreationConfig = PremintConfig["tokenConfig"]; + +// Convenience method to create the structured typed data +// needed to sign for a premint contract and token +export const preminterTypedDataDefinition = ({ + verifyingContract, + premintConfig, + chainId, +}: { + verifyingContract: Address; + premintConfig: PremintConfig; + chainId: number; +}) => { + const { tokenConfig, uid, version, deleted } = premintConfig; + const types = { + CreatorAttribution: [ + { name: "tokenConfig", type: "TokenCreationConfig" }, + // unique id scoped to the contract and token to create. + // ensure that a signature can be replaced, as long as the replacement + // has the same uid, and a newer version. + { name: "uid", type: "uint32" }, + { name: "version", type: "uint32" }, + // if this update should result in the signature being deleted. + { name: "deleted", type: "bool" }, + ], + TokenCreationConfig: [ + { name: "tokenURI", type: "string" }, + { name: "maxSupply", type: "uint256" }, + { name: "maxTokensPerAddress", type: "uint64" }, + { name: "pricePerToken", type: "uint96" }, + { name: "mintStart", type: "uint64" }, + { name: "mintDuration", type: "uint64" }, + { name: "royaltyMintSchedule", type: "uint32" }, + { name: "royaltyBPS", type: "uint32" }, + { name: "royaltyRecipient", type: "address" }, + { name: "fixedPriceMinter", type: "address" }, + ], + }; + + const result: TypedDataDefinition = { + domain: { + chainId, + name: "Preminter", + version: "1", + verifyingContract: verifyingContract, + }, + types, + message: { + tokenConfig, + uid, + version, + deleted, + }, + primaryType: "CreatorAttribution", + }; + + return result; +}; diff --git a/packages/premint-sdk/tsconfig.json b/packages/protocol-sdk/tsconfig.json similarity index 92% rename from packages/premint-sdk/tsconfig.json rename to packages/protocol-sdk/tsconfig.json index 3da7aa253..1c35d0c9d 100644 --- a/packages/premint-sdk/tsconfig.json +++ b/packages/protocol-sdk/tsconfig.json @@ -21,5 +21,5 @@ "outDir": "dist" }, "exclude": ["node_modules/**", "dist/**"], - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "vitest.config.ts"] } diff --git a/packages/premint-sdk/tsup.config.js b/packages/protocol-sdk/tsup.config.js similarity index 100% rename from packages/premint-sdk/tsup.config.js rename to packages/protocol-sdk/tsup.config.js diff --git a/turbo.json b/turbo.json index e88ee7159..fb0a342f2 100644 --- a/turbo.json +++ b/turbo.json @@ -15,6 +15,9 @@ "test": { "dependsOn": ["^test"] }, + "test:js": { + "dependsOn": ["^test:js"] + }, "test:fork": { "dependsOn": ["^test:fork"] }, @@ -38,4 +41,4 @@ "persistent": true } } -} \ No newline at end of file +}