Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

protocol sdk - make MintAPIClient a class, with overridable http methods. add full fledged mint examples. refactor MintClient #368

Merged
merged 6 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/chilled-seahorses-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@zoralabs/protocol-sdk": patch
---

`MintAPIClient` is now a class, that takes a chain id and httpClient in the constructor, enabling the httpClient methods `fetch`, `post`, and `retries` to be overridden.

new methods on `MintAPIClient`:

`getMintableForToken` - takes a token id and token contract address and returns the mintable for it. Easier to use for fetching specific tokens than `getMintable`.

`MintClient` now takes the optional `PublicClient` in the constructor instead of in each function, and stores it or creates a default one if none is provided in the constructor. It also takes an optional `httpClient` param in the constructor, allowing the `fetch`, `post`, and `retries` methods to be overridden when using the api. It now internally creates the MintAPIClient.

`MintClient.makePrepareMintTokenParams` has the following changes:
* returns a `SimulateContractParams`, instead of an object containing it indexed by key
* no longer takes a `PublicClient` as an argument (it should be specified in the constructor instead)

new function `MintClient.getMintCosts` takes a mintable and quantity to mint and returns the mintFee, paidMintPrice, and totalCost.
177 changes: 162 additions & 15 deletions packages/protocol-sdk/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Premint SDK
# Zora Protocol SDK

Protocol SDK allows users to manage zora mints and collects.
Protocol SDK allows users to create tokens using the Zora Protocol, and mint them.

## Installing

Expand All @@ -11,28 +11,175 @@ Protocol SDK allows users to manage zora mints and collects.

### Creating a mint from an on-chain contract:

#### Using viem

```ts
import { createMintClient } from "@zoralabs/protocol-sdk";
import type { Address, WalletClient } from "viem";
import {createMintClient} from "@zoralabs/protocol-sdk";
import type {Address, PublicClient, WalletClient} from "viem";

async function mintNFT(
walletClient: WalletClient,
address: Address,
tokenId: bigint,
) {
const mintAPI = createMintClient({ chain: walletClient.chain });
await mintAPI.mintNFT({
walletClient,
address,
async function mintNFT({
walletClient,
publicClient,
tokenContract,
tokenId,
mintToAddress,
quantityToMint,
mintReferral,
}: {
// wallet client that will submit the transaction
walletClient: WalletClient;
// public client that will simulate the transaction
publicClient: PublicClient;
// address of the token contract
tokenContract: Address;
// id of the token to mint
tokenId: bigint;
// address that will receive the minted token
mintToAddress: Address;
// quantity of tokens to mint
quantityToMint: number;
// optional address that will receive a mint referral reward
mintReferral?: Address;
}) {
const mintClient = createMintClient({chain: walletClient.chain!});

// get mintable information about the token.
const mintable = await mintClient.getMintable({
tokenContract,
tokenId,
});

// prepare the mint transaction, which can be simulated via an rpc with the public client.
const prepared = await mintClient.makePrepareMintTokenParams({
// token to mint
mintable,
mintArguments: {
quantityToMint: 23,
mintComment: "Helo",
// address that will receive the token
mintToAddress,
// quantity of tokens to mint
quantityToMint,
// comment to include with the mint
mintComment: "My comment",
// optional address that will receive a mint referral reward
mintReferral,
},
// account that is to invoke the mint transaction
minterAccount: walletClient.account!.address,
});

// simulate the transaction and get any validation errors
const { request } = await publicClient.simulateContract(prepared);

// submit the transaction to the network
const txHash = await walletClient.writeContract(request);

// wait for the transaction to be complete
await publicClient.waitForTransactionReceipt({hash: txHash});
}
```

#### Using wagmi

```tsx
import {createMintClient, Mintable} from "@zoralabs/protocol-sdk";
import {useEffect, useMemo, useState} from "react";
import {BaseError, SimulateContractParameters, stringify} from "viem";
import {Address, useAccount, useContractWrite, useNetwork, usePrepareContractWrite, usePublicClient, useWaitForTransaction} from "wagmi";

// custom hook that gets the mintClient for the current chain
const useMintClient = () => {
const publicClient = usePublicClient();

const {chain} = useNetwork();

const mintClient = useMemo(() => chain && createMintClient({chain, publicClient}), [chain, publicClient]);

return mintClient;
};

export const Mint = ({tokenId, tokenContract}: {tokenId: string; tokenContract: Address}) => {
// call custom hook to get the mintClient
const mintClient = useMintClient();

// value will be set by the form
const [quantityToMint, setQuantityToMint] = useState<number>(1);

// fetched mintable info from the sdk
const [mintable, setMintable] = useState<Mintable>();

useEffect(() => {
// fetch the mintable token info
const fetchMintable = async () => {
if (mintClient) {
const mintable = await mintClient.getMintable({tokenId, tokenContract});
setMintable(mintable);
}
};

fetchMintable();
}, [mintClient, tokenId, tokenContract]);

// params for the prepare contract write hook
const [params, setParams] = useState<SimulateContractParameters>();

const {address} = useAccount();

useEffect(() => {
if (!mintable || !mintClient || !address) return;

const makeParams = async () => {
// make the params for the prepare contract write hook
const params = await mintClient.makePrepareMintTokenParams({
mintable,
minterAccount: address,
mintArguments: {
mintToAddress: address,
quantityToMint,
},
});
setParams(params);
};

makeParams();
}, [mintable, mintClient, address, quantityToMint]);

const {config} = usePrepareContractWrite(params);

const {write, data, error, isLoading, isError} = useContractWrite(config);
const {data: receipt, isLoading: isPending, isSuccess} = useWaitForTransaction({hash: data?.hash});

return (
<>
<h3>Mint a token</h3>
<form
onSubmit={(e) => {
e.preventDefault();
write?.();
}}
>
{/* input for quantity to mint: */}
<input placeholder="quantity to mint" onChange={(e) => setQuantityToMint(Number(e.target.value))} />
<button disabled={!write} type="submit">
Mint
</button>
</form>

{isLoading && <div>Check wallet...</div>}
{isPending && <div>Transaction pending...</div>}
{isSuccess && (
<>
<div>Transaction Hash: {data?.hash}</div>
<div>
Transaction Receipt: <pre>{stringify(receipt, null, 2)}</pre>
</div>
</>
)}
{isError && <div>{(error as BaseError)?.shortMessage}</div>}
</>
);
};
```

### Creating an 1155 contract:

If an object with {name, uri} is passed in to this helper, it uses the creatorAccount and those values to either 1) create or 2) mint to that existing contract.
Expand Down
12 changes: 12 additions & 0 deletions packages/protocol-sdk/src/apis/http-api-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,15 @@ export const retries = async <T>(
throw err;
}
};

export interface IHttpClient {
get: typeof get;
post: typeof post;
retries: typeof retries;
}

export const httpClient: IHttpClient = {
get,
post,
retries,
};
105 changes: 71 additions & 34 deletions packages/protocol-sdk/src/mint/mint-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { retries, get, post } from "../apis/http-api-base";
import {
httpClient as defaultHttpClient,
IHttpClient,
} from "../apis/http-api-base";
import { paths } from "../apis/generated/discover-api-types";
import { ZORA_API_BASE } from "../constants";
import { NetworkConfig, networkConfigByChain } from "src/apis/chain-constants";
import { Address } from "viem";

export type MintableGetToken =
paths["/mintables/{chain_name}/{collection_address}"];
Expand All @@ -15,38 +20,70 @@ function encodeQueryParameters(params: Record<string, string>) {
return new URLSearchParams(params).toString();
}

const getMintable = async (
path: MintableGetTokenPathParameters,
query: MintableGetTokenGetQueryParameters,
): Promise<MintableGetTokenResponse> =>
retries(() => {
return get<MintableGetTokenResponse>(
`${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<undefined | string> =>
retries(async () => {
const response = await post<any>(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}` },
export const getApiNetworkConfigForChain = (chainId: number): NetworkConfig => {
if (!networkConfigByChain[chainId]) {
throw new Error(`chain id ${chainId} network not configured `);
}
return networkConfigByChain[chainId]!;
};

export class MintAPIClient {
httpClient: IHttpClient;
networkConfig: NetworkConfig;

constructor(chainId: number, httpClient?: IHttpClient) {
this.httpClient = httpClient || defaultHttpClient;
this.networkConfig = getApiNetworkConfigForChain(chainId);
}

async getMintable(
path: MintableGetTokenPathParameters,
query: MintableGetTokenGetQueryParameters,
): Promise<MintableGetTokenResponse> {
const httpClient = this.httpClient;
return httpClient.retries(() => {
return httpClient.get<MintableGetTokenResponse>(
`${ZORA_API_BASE}discover/mintables/${path.chain_name}/${
path.collection_address
}${query?.token_id ? `?${encodeQueryParameters(query)}` : ""}`,
);
});
return response.zoraCreateToken?.salesStrategies?.find(() => true)
?.fixedPriceMinterAddress;
});
}

export const MintAPIClient = {
getMintable,
getSalesConfigFixedPrice,
};
async getSalesConfigFixedPrice({
contractAddress,
tokenId,
}: {
contractAddress: string;
tokenId: bigint;
}): Promise<undefined | string> {
const { retries, post } = this.httpClient;
return retries(async () => {
const response = await post<any>(this.networkConfig.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.toString()}`,
},
});
return response.zoraCreateToken?.salesStrategies?.find(() => true)
?.fixedPriceMinterAddress;
});
}

async getMintableForToken({
tokenContract,
tokenId,
}: {
tokenContract: Address;
tokenId?: bigint | number | string;
}) {
return await this.getMintable(
{
chain_name: this.networkConfig.zoraBackendChainName,
collection_address: tokenContract,
},
{ token_id: tokenId?.toString() },
);
}
}
Loading
Loading