Skip to content

Commit

Permalink
protocol sdk - make MintAPIClient a class, with overridable http meth…
Browse files Browse the repository at this point in the history
…ods. add full fledged mint examples. refactor MintClient (#368)

* makePrepareMintTOkenParams returns single object
publicClient is optional in test

* updated changeset

* better tsdoc comments.  dont expose all methods

* make token id optional

* added interface for IHttpClient

* fix prettier
  • Loading branch information
oveddan authored and kulkarohan committed Dec 8, 2023
1 parent 6a87133 commit d1c345b
Show file tree
Hide file tree
Showing 6 changed files with 606 additions and 229 deletions.
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

0 comments on commit d1c345b

Please sign in to comment.