Skip to content

Commit

Permalink
feat(sdk): ERC-20 minting (#386)
Browse files Browse the repository at this point in the history
* feat(sdk): ERC-20 minting

* feat: init of erc20 sdk doc

* updated readme

* erc20 minting sdk - no need to pass currenty or price per token to sdk

* updated readme to reflect new logic

* updated changeset

---------

Co-authored-by: Isabella Smallcombe <[email protected]>
Co-authored-by: Dan Oved <[email protected]>
  • Loading branch information
3 people authored May 24, 2024
1 parent ee24f92 commit 344f452
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 74 deletions.
5 changes: 5 additions & 0 deletions .changeset/thirty-goats-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoralabs/protocol-sdk": patch
---

Adds support for ERC-20 minting on 1155s using ERC20 minters within the function `makePrepareMintTokenParams`.
44 changes: 42 additions & 2 deletions packages/protocol-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Protocol SDK allows developers to create tokens using the Zora Protocol and mint
- `npm install viem`
- `npm install `


## Examples

- [Creating a mint from an on-chain contract](#creating-a-mint-from-an-on-chain-contract)
Expand Down Expand Up @@ -83,6 +82,44 @@ async function mintNFT({
}
```

### Minting an NFT with an ERC20 minter

If an 1155 token is set to be an ERC20 mint, use the `makePrepareMintTokenParams` the same way as for
eth based mints. The underlying function should handle preparing arguments for the ERC20 mint.

```ts
import type { PublicClient } from "viem";

async function mint1155TokenWithERC20Currency(
publicClient: PublicClient,
minterAddress: Address,
) {
const mintClient = createMintClient({ chain: zora });
const exampleCurrency = "0xb6b701878a1f80197dF2c209D0BDd292EA73164D";
// nft collection address
const targetContract = "0x689bc305456c38656856d12469aed282fbd89fe0";
const tokenId = 17;
// the NFT token price in WEI value
const erc20PricePerToken = 2000000000000000000;

const tokenParams = await mintClient.makePrepareMintTokenParams({
minterAccount: minterAddress,
tokenId: tokenId,
tokenAddress: targetContract,
mintArguments: {
mintToAddress: minterAddress,
quantityToMint: 1,
},
});

const simulationResult = await publicClient.simulateContract(params);
const hash = await walletClient.writeContract(simulationResult.request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });

return receipt;
}
```

#### Using wagmi

```tsx
Expand Down Expand Up @@ -260,7 +297,10 @@ async function createForFree({
checkSignature: true,
// collection info of collection to create
collection: {
contractAdmin: typeof creatorAccount === "string" ? creatorAccount : creatorAccount.address,
contractAdmin:
typeof creatorAccount === "string"
? creatorAccount
: creatorAccount.address,
contractName: "Testing Contract",
contractURI:
"ipfs://bafkreiainxen4b4wz4ubylvbhons6rembxdet4a262nf2lziclqvv7au3e",
Expand Down
36 changes: 36 additions & 0 deletions packages/protocol-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,39 @@ export 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 const NFT_SALE_QUERY = `
fragment SaleStrategy on SalesStrategyConfig {
type
fixedPrice {
address
pricePerToken
saleEnd
saleStart
maxTokensPerAddress
}
erc20Minter {
address
pricePerToken
currency
saleEnd
saleStart
maxTokensPerAddress
}
}
query ($id: ID!) {
zoraCreateToken(id: $id) {
id
contract {
mintFeePerQuantity
salesStrategies(where: {type_in: ["FIXED_PRICE", "ERC_20_MINTER"]}) {
...SaleStrategy
}
}
salesStrategies(where: {type_in: ["FIXED_PRICE", "ERC_20_MINTER"]}) {
...SaleStrategy
}
}
}
`;
152 changes: 107 additions & 45 deletions packages/protocol-sdk/src/mint/mint-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Address } from "viem";
import {
httpClient as defaultHttpClient,
IHttpClient,
} from "../apis/http-api-base";
import { NetworkConfig, networkConfigByChain } from "src/apis/chain-constants";
import { GenericTokenIdTypes } from "src/types";
import { Address } from "viem";
import { NFT_SALE_QUERY } from "src/constants";

export type SaleType = "fixedPrice" | "erc20";

type FixedPriceSaleStrategyResult = {
address: Address;
Expand All @@ -14,12 +17,48 @@ type FixedPriceSaleStrategyResult = {
maxTokensPerAddress: string;
};

type SaleStrategyResult = {
fixedPrice: FixedPriceSaleStrategyResult;
type ERC20SaleStrategyResult = FixedPriceSaleStrategyResult & {
currency: Address;
};

type SalesStrategyResult =
| {
type: "FIXED_PRICE";
fixedPrice: FixedPriceSaleStrategyResult;
}
| {
type: "ERC_20_MINTER";
erc20Minter: ERC20SaleStrategyResult;
};

type TokenQueryResult = {
tokenId?: string;
salesStrategies?: SalesStrategyResult[];
contract: {
mintFeePerQuantity: "string";
salesStrategies: SalesStrategyResult[];
};
};

type SaleStrategy<T extends SaleType> = {
saleType: T;
address: Address;
pricePerToken: bigint;
saleEnd: string;
saleStart: string;
maxTokensPerAddress: bigint;
};

type FixedPriceSaleStrategy = SaleStrategy<"fixedPrice">;

type ERC20SaleStrategy = SaleStrategy<"erc20"> & {
currency: Address;
};

type SaleStrategies = FixedPriceSaleStrategy | ERC20SaleStrategy;

export type SalesConfigAndTokenInfo = {
fixedPrice: FixedPriceSaleStrategyResult;
salesConfig: SaleStrategies;
mintFeePerQuantity: bigint;
};

Expand All @@ -42,40 +81,16 @@ export class MintAPIClient {
async getSalesConfigAndTokenInfo({
tokenAddress,
tokenId,
saleType,
}: {
tokenAddress: Address;
tokenId?: GenericTokenIdTypes;
saleType?: SaleType;
}): Promise<SalesConfigAndTokenInfo> {
const { retries, post } = this.httpClient;
return retries(async () => {
const response = await post<any>(this.networkConfig.subgraphUrl, {
query: `
fragment SaleStrategy on SalesStrategyConfig {
type
fixedPrice {
address
pricePerToken
saleEnd
saleStart
maxTokensPerAddress
}
}
query ($id: ID!) {
zoraCreateToken(id: $id) {
id
contract {
mintFeePerQuantity
salesStrategies(where: { type: "FIXED_PRICE" }) {
...SaleStrategy
}
}
salesStrategies(where: { type: "FIXED_PRICE" }) {
...SaleStrategy
}
}
}
`,
query: NFT_SALE_QUERY,
variables: {
id:
tokenId !== undefined
Expand All @@ -85,31 +100,78 @@ export class MintAPIClient {
},
});

const token = response.data?.zoraCreateToken;
const token = response.data?.zoraCreateToken as TokenQueryResult;

if (!token) {
throw new Error("Cannot find a token to mint");
}

const saleStrategies: SaleStrategyResult[] =
tokenId !== undefined
const allStrategies =
(typeof tokenId !== "undefined"
? token.salesStrategies
: token.contract.salesStrategies;
: token.contract.salesStrategies) || [];

const fixedPrice = saleStrategies
?.sort((a: SaleStrategyResult, b: SaleStrategyResult) =>
BigInt(a.fixedPrice.saleEnd) > BigInt(b.fixedPrice.saleEnd) ? 1 : -1,
const saleStrategies = allStrategies.sort((a, b) =>
BigInt(
a.type === "ERC_20_MINTER"
? a.erc20Minter.saleEnd
: a.fixedPrice.saleEnd,
) >
BigInt(
b.type === "FIXED_PRICE"
? b.fixedPrice.saleEnd
: b.erc20Minter.saleEnd,
)
?.find(() => true)?.fixedPrice;
? 1
: -1,
);

if (!fixedPrice) {
throw new Error("Cannot find fixed price sale strategy");
let targetStrategy: SalesStrategyResult | undefined;

if (!saleType) {
targetStrategy = saleStrategies[0];
if (!targetStrategy) {
throw new Error("Cannot find sale strategy");
}
} else {
const mappedSaleType =
saleType === "erc20" ? "ERC_20_MINTER" : "FIXED_PRICE";
targetStrategy = saleStrategies.find(
(strategy: SalesStrategyResult) => strategy.type === mappedSaleType,
);
if (!targetStrategy) {
throw new Error(`Cannot find sale strategy for ${mappedSaleType}`);
}
}

if (targetStrategy.type === "FIXED_PRICE") {
return {
salesConfig: {
saleType: "fixedPrice",
...targetStrategy.fixedPrice,
maxTokensPerAddress: BigInt(
targetStrategy.fixedPrice.maxTokensPerAddress,
),
pricePerToken: BigInt(targetStrategy.fixedPrice.pricePerToken),
},
mintFeePerQuantity: BigInt(token.contract.mintFeePerQuantity),
};
}
if (targetStrategy.type === "ERC_20_MINTER") {
return {
salesConfig: {
saleType: "erc20",
...targetStrategy.erc20Minter,
maxTokensPerAddress: BigInt(
targetStrategy.erc20Minter.maxTokensPerAddress,
),
pricePerToken: BigInt(targetStrategy.erc20Minter.pricePerToken),
},
mintFeePerQuantity: BigInt(token.contract.mintFeePerQuantity),
};
}

return {
fixedPrice,
mintFeePerQuantity: BigInt(token.contract.mintFeePerQuantity),
};
throw new Error("Invalid saleType");
});
}
}
Loading

0 comments on commit 344f452

Please sign in to comment.