Skip to content

Commit

Permalink
makePrepareMintTOkenParams returns single object
Browse files Browse the repository at this point in the history
publicClient is optional in test
  • Loading branch information
oveddan committed Nov 28, 2023
1 parent 71dfef9 commit 36d98a7
Show file tree
Hide file tree
Showing 5 changed files with 537 additions and 237 deletions.
11 changes: 11 additions & 0 deletions .changeset/chilled-seahorses-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@zoralabs/protocol-sdk": patch
---

`MintClient.makePrepareMintTokenParams` has the following changes:
* returns a `SimulateContractParams`, instead of an object containing it indexed by key
* PublicClient as an optional argument


Internally, MintClient is refactored to extract some functionality into static helpers that could eventually be publicly exposed

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
102 changes: 68 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,8 @@
import { retries, get, post } from "../apis/http-api-base";
import * as httpClientBase 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 +17,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: typeof httpClientBase;
networkConfig: NetworkConfig;

constructor(chainId: number, httpClient?: typeof httpClientBase) {
this.httpClient = httpClient || httpClientBase;
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() },
);
}
}
50 changes: 23 additions & 27 deletions packages/protocol-sdk/src/mint/mint-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,17 @@ describe("mint-helper", () => {
const targetTokenId = 1n;
const minter = createMintClient({ chain: zora });

const { simulateContractParameters: params } =
await minter.makePrepareMintTokenParams({
publicClient,
minterAccount: creatorAccount,
mintable: await minter.getMintable({
tokenId: targetTokenId,
tokenContract: targetContract,
}),
mintArguments: {
mintToAddress: creatorAccount,
quantityToMint: 1,
},
});
const params = await minter.makePrepareMintTokenParams({
minterAccount: creatorAccount,
mintable: await minter.getMintable({
tokenId: targetTokenId,
tokenContract: targetContract,
}),
mintArguments: {
mintToAddress: creatorAccount,
quantityToMint: 1,
},
});

const oldBalance = await publicClient.readContract({
abi: zoraCreator1155ImplABI,
Expand Down Expand Up @@ -75,27 +73,25 @@ describe("mint-helper", () => {
const targetTokenId = undefined;
const minter = createMintClient({ chain: zora });

const { simulateContractParameters: prepared } =
await minter.makePrepareMintTokenParams({
mintable: await minter.getMintable({
tokenContract: targetContract,
tokenId: targetTokenId,
}),
publicClient,
minterAccount: creatorAccount,
mintArguments: {
mintToAddress: creatorAccount,
quantityToMint: 1,
},
});
const params = await minter.makePrepareMintTokenParams({
mintable: await minter.getMintable({
tokenContract: targetContract,
tokenId: targetTokenId,
}),
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 simulated = await publicClient.simulateContract(params);

const hash = await walletClient.writeContract(simulated.request);

Expand Down
Loading

0 comments on commit 36d98a7

Please sign in to comment.