Compare commits

..
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
06c24c153a Version Packages (#361)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-11-01 16:13:47 -07:00
aa6d9ed75d [SDK] Support new Drop claim conditions mechanics (#333)
Co-authored-by: adam-maj <[email protected]>
Co-authored-by: Adam Majmudar <[email protected]>
2022-11-01 23:07:10 +00:00
Joaquim VergesandGitHub 42a606588d [SDK] Fix fetching NFTs with numbers as ids (#362) 2022-11-01 22:01:01 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
2820761003 Version Packages (#360)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-11-01 13:49:34 -07:00
Adam MajmudarandGitHub 5200d5d7e7 Add support for TieredDrop in extensions (#329) 2022-11-01 13:48:48 -07:00
Joaquim VergesandGitHub e731c1a7c3 [SDK] Allow fetching claimerProofs from any claim condition (#359) 2022-10-31 23:26:27 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
2e8d3fd6e7 Version Packages (#358)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-28 18:14:35 -07:00
Nacho IacovinoandGitHub cc4613b5fd [React] Add Pack and Multiwrap as NFTContract types (#356) 2022-10-29 01:28:40 +02:00
73 changed files with 6277 additions and 1168 deletions
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/auth
## 2.0.27
## 2.0.26
## 2.0.25
## 2.0.24
## 2.0.23
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "2.0.24",
"version": "2.0.27",
"exports": {
"./next": {
"module": "./next/dist/thirdweb-dev-auth-next.esm.js",
+21
View File
@@ -1,5 +1,26 @@
# thirdweb
## 0.8.13
### Patch Changes
- Updated dependencies [[`5200d5d`](https://github.com/thirdweb-dev/js/commit/5200d5d7e730da58bd4d3e7c222e3c661265f913), [`42a6065`](https://github.com/thirdweb-dev/js/commit/42a606588dfed03c67c57357dd0ee1ad19fad6ea)]:
- @thirdweb-dev/sdk@3.5.0
## 0.8.12
### Patch Changes
- Updated dependencies [[`e731c1a`](https://github.com/thirdweb-dev/js/commit/e731c1a7c341e6f2b9cc42c88d5c1b036dcf4a7c)]:
- @thirdweb-dev/sdk@3.4.5
## 0.8.11
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@3.4.4
## 0.8.10
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.8.10",
"version": "0.8.13",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/contracts-js
## 1.2.0
### Minor Changes
- [#333](https://github.com/thirdweb-dev/js/pull/333) [`aa6d9ed`](https://github.com/thirdweb-dev/js/commit/aa6d9ed75d126d7a0cca9fb8fc389ff94e9d1e14) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update to contracts package v3.2.0
## 1.1.9
### Patch Changes
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistry__factory.cjs.js",
"module": "dist/thirdweb-dev-contracts-js-factories-TWMultichainRegistry__factory.esm.js"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@thirdweb-dev/contracts-js",
"version": "1.1.9",
"version": "1.2.0",
"main": "dist/thirdweb-dev-contracts-js.cjs.js",
"module": "dist/thirdweb-dev-contracts-js.esm.js",
"license": "Apache-2.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/contracts-js",
"dependencies": {
"@thirdweb-dev/contracts": "3.1.8-0"
"@thirdweb-dev/contracts": "3.2.0-4"
},
"devDependencies": {
"@babel/preset-env": "^7.18.10",
+10
View File
@@ -1,5 +1,15 @@
# @thirdweb-dev/react
## 3.5.0
## 3.4.5
## 3.4.4
### Patch Changes
- [#356](https://github.com/thirdweb-dev/js/pull/356) [`cc4613b`](https://github.com/thirdweb-dev/js/commit/cc4613b5fd69840f4f9cfd0ac1c4e6743e62fe52) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Add Pack and Multiwrap as NFTContract types
## 3.4.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.4.3",
"version": "3.5.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -368,8 +368,6 @@ export function useResetClaimConditions(
return conditions.map((c) => ({
...c,
price: c.currencyMetadata.displayValue,
maxQuantity: c.maxQuantity.toString(),
quantityLimitPerTransaction: c.quantityLimitPerTransaction.toString(),
}));
};
+3 -5
View File
@@ -232,11 +232,9 @@ export function useClaimToken(contract: RequiredParam<TokenContract>) {
invariant(data.to, 'No "to" address provided');
if (erc20) {
invariant(erc20?.claimTo, "contract does not support claimTo");
return await erc20.claimTo(
data.to,
data.amount,
data.checkERC20Allowance,
);
return await erc20.claimTo(data.to, data.amount, {
checkERC20Allowance: data.checkERC20Allowance,
});
}
invariant(false, "Smart contract is not a valid erc20 contract");
},
+4
View File
@@ -14,6 +14,8 @@ import type {
ClaimOptions,
NFTMetadataOrUri,
Token,
Multiwrap,
Pack,
} from "@thirdweb-dev/sdk";
import type { Edition } from "@thirdweb-dev/sdk/dist/declarations/src/evm/contracts/prebuilt-implementations/edition";
import type { EditionDrop } from "@thirdweb-dev/sdk/dist/declarations/src/evm/contracts/prebuilt-implementations/edition-drop";
@@ -68,6 +70,8 @@ export type TokenBurnParams = {
export type NFTContract =
| NFTCollection
| Edition
| Pack
| Multiwrap
| Exclude<DropContract, "TokenDrop">;
/**
+34
View File
@@ -1,5 +1,39 @@
# @thirdweb-dev/sdk
## 3.5.0
### Minor Changes
- [#329](https://github.com/thirdweb-dev/js/pull/329) [`5200d5d`](https://github.com/thirdweb-dev/js/commit/5200d5d7e730da58bd4d3e7c222e3c661265f913) Thanks [@adam-maj](https://github.com/adam-maj)! - ### Breaking changes:
1. claim conditions had some changes to support the new drop contracts:
- `maxClaimablePerTransaction` is now named `maxClaimablePerWallet`
- `maxQuantity` is now named `maxClaimable`
2. signature minting now requires a `to` address to be set for security purposees
### Main Changes:
- Support for new optimized Drop contracts
- Support for new claim conditions with overrides
- Don't allow zero address recipient on signature minting
### Patch Changes
- [#362](https://github.com/thirdweb-dev/js/pull/362) [`42a6065`](https://github.com/thirdweb-dev/js/commit/42a606588dfed03c67c57357dd0ee1ad19fad6ea) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - [SDK] Fix fetching NFTs with numbers as ids
- Updated dependencies [[`aa6d9ed`](https://github.com/thirdweb-dev/js/commit/aa6d9ed75d126d7a0cca9fb8fc389ff94e9d1e14)]:
- @thirdweb-dev/contracts-js@1.2.0
## 3.4.5
### Patch Changes
- [#359](https://github.com/thirdweb-dev/js/pull/359) [`e731c1a`](https://github.com/thirdweb-dev/js/commit/e731c1a7c341e6f2b9cc42c88d5c1b036dcf4a7c) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - [SDK] Allow fetching claimerProofs from any claim condition
## 3.4.4
## 3.4.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.4.3",
"version": "3.5.0",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
+2
View File
@@ -103,6 +103,8 @@ export const QuantitySchema = z
.union([AmountSchema, z.literal("unlimited")])
.default("unlimited");
export type Quantity = z.output<typeof QuantitySchema>;
export const RawDateSchema = z.date().transform((i) => {
return BigNumber.from(Math.floor(i.getTime() / 1000));
});
+262 -66
View File
@@ -1,6 +1,8 @@
import { Quantity } from "../../core/schema/shared";
import { NATIVE_TOKEN_ADDRESS } from "../constants";
import { ContractWrapper } from "../core/classes/contract-wrapper";
import {
AbstractClaimConditionContractStruct,
ClaimConditionInputArray,
ClaimConditionInputSchema,
ClaimConditionOutputSchema,
@@ -8,7 +10,6 @@ import {
import {
SnapshotEntry,
SnapshotEntryWithProof,
SnapshotInputSchema,
SnapshotSchema,
} from "../schema/contracts/common/snapshots";
import {
@@ -25,9 +26,13 @@ import {
isNativeToken,
normalizePriceValue,
} from "./currency";
import { ShardedMerkleTree } from "./sharded-merkle-tree";
import {
ShardedMerkleTree,
SnapshotFormatVersion,
} from "./sharded-merkle-tree";
import { createSnapshot } from "./snapshots";
import { IDropClaimCondition } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC20";
import { IDropClaimCondition_V2 } from "@thirdweb-dev/contracts-js/dist/declarations/src/IDropERC20_V2";
import { IClaimCondition } from "@thirdweb-dev/contracts-js/src/IDrop";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import {
BigNumber,
@@ -52,27 +57,56 @@ export async function prepareClaim(
contractWrapper: ContractWrapper<any>,
storage: ThirdwebStorage,
checkERC20Allowance: boolean,
snapshotFormatVersion: SnapshotFormatVersion,
): Promise<ClaimVerification> {
const addressToClaim = await contractWrapper.getSignerAddress();
let maxClaimable = BigNumber.from(0);
let maxClaimable = convertQuantityToBigNumber(
activeClaimCondition.maxClaimablePerWallet,
tokenDecimals,
);
let proofs = [utils.hexZeroPad([0], 32)];
let priceInProof = activeClaimCondition.price; // the price to send to the contract in claim proofs
let currencyAddressInProof = activeClaimCondition.currencyAddress;
try {
if (
!activeClaimCondition.merkleRootHash
.toString()
.startsWith(constants.AddressZero)
) {
const claim = await fetchSnapshotEntryForAddress(
const snapshotEntry = await fetchSnapshotEntryForAddress(
addressToClaim,
activeClaimCondition.merkleRootHash.toString(),
await merkleMetadataFetcher(),
contractWrapper.getProvider(),
storage,
snapshotFormatVersion,
);
if (!claim) {
throw new Error("No claim found for this address");
if (snapshotEntry) {
proofs = snapshotEntry.proof;
// override only if not default values (unlimited for quantity, zero addr for currency)
maxClaimable =
snapshotEntry.maxClaimable === "unlimited"
? ethers.constants.MaxUint256
: ethers.utils.parseUnits(
snapshotEntry.maxClaimable,
tokenDecimals,
);
priceInProof =
snapshotEntry.price === "unlimited"
? ethers.constants.MaxUint256
: await normalizePriceValue(
contractWrapper.getProvider(),
snapshotEntry.price,
snapshotEntry.currencyAddress,
);
currencyAddressInProof = snapshotEntry.currencyAddress;
} else {
// if no snapshot entry, and it's a v1 format (exclusive allowlist) then address can't claim
if (snapshotFormatVersion === SnapshotFormatVersion.V1) {
throw new Error("No claim found for this address");
}
// but if its snapshot v2 (override list behavior) then address can still claim with default settings
}
proofs = claim.proof;
maxClaimable = ethers.utils.parseUnits(claim.maxClaimable, tokenDecimals);
}
} catch (e) {
// have to handle the valid error case that we *do* want to throw on
@@ -87,18 +121,28 @@ export async function prepareClaim(
}
const overrides = (await contractWrapper.getCallOverrides()) || {};
const price = activeClaimCondition.price;
const currencyAddress = activeClaimCondition.currencyAddress;
if (price.gt(0)) {
// the actual price to check allowance against
// if proof price is unlimited, then we use the price from the claim condition
// this mimics the contract behavior
const pricePerToken =
priceInProof.toString() !== ethers.constants.MaxUint256.toString()
? priceInProof
: activeClaimCondition.price;
// same for currency address
const currencyAddress =
currencyAddressInProof !== ethers.constants.AddressZero
? currencyAddressInProof
: activeClaimCondition.currencyAddress;
if (pricePerToken.gt(0)) {
if (isNativeToken(currencyAddress)) {
overrides["value"] = BigNumber.from(price)
overrides["value"] = BigNumber.from(pricePerToken)
.mul(quantity)
.div(ethers.utils.parseUnits("1", tokenDecimals));
} else if (checkERC20Allowance) {
await approveErc20Allowance(
contractWrapper,
currencyAddress,
price,
pricePerToken,
quantity,
tokenDecimals,
);
@@ -107,9 +151,9 @@ export async function prepareClaim(
return {
overrides,
proofs,
maxQuantityPerTransaction: maxClaimable,
price,
currencyAddress,
maxClaimable,
price: priceInProof,
currencyAddress: currencyAddressInProof,
};
}
@@ -139,6 +183,8 @@ export async function fetchSnapshot(
return snapshotData.claims.map((claim) => ({
address: claim.address,
maxClaimable: claim.maxClaimable,
price: claim.price,
currencyAddress: claim.currencyAddress,
}));
}
}
@@ -150,7 +196,9 @@ export async function fetchSnapshotEntryForAddress(
address: string,
merkleRoot: string,
merkleMetadata: Record<string, string> | undefined,
provider: ethers.providers.Provider,
storage: ThirdwebStorage,
snapshotFormatVersion: SnapshotFormatVersion,
): Promise<SnapshotEntryWithProof | null> {
if (!merkleMetadata) {
return null;
@@ -163,7 +211,11 @@ export async function fetchSnapshotEntryForAddress(
raw,
storage,
);
return await merkleTree.getProof(address);
return await merkleTree.getProof(
address,
provider,
snapshotFormatVersion,
);
}
// legacy non-sharded, just fetch it all and filter out
const snapshotData = SnapshotSchema.parse(raw);
@@ -238,6 +290,8 @@ export async function updateExistingClaimConditions(
* @param tokenDecimals
* @param merkleMetadata
* @param storage
* @param provider
* @param snapshotFormatVersion
* @returns - The proof for the current signer for the specified condition.
*/
export async function getClaimerProofs(
@@ -246,38 +300,59 @@ export async function getClaimerProofs(
tokenDecimals: number,
merkleMetadata: Record<string, string>,
storage: ThirdwebStorage,
): Promise<{ maxClaimable: BigNumber; proof: string[] }> {
provider: ethers.providers.Provider,
snapshotFormatVersion: SnapshotFormatVersion,
): Promise<
| {
proof: string[];
maxClaimable: BigNumber;
price: BigNumber;
currencyAddress: string;
}
| undefined
> {
const claim = await fetchSnapshotEntryForAddress(
addressToClaim,
merkleRoot,
merkleMetadata,
provider,
storage,
snapshotFormatVersion,
);
if (!claim) {
return {
proof: [],
maxClaimable: BigNumber.from(0),
};
return undefined;
}
const price =
claim.price === "unlimited"
? ethers.constants.MaxUint256
: await normalizePriceValue(provider, claim.price, claim.currencyAddress);
const maxClaimable =
claim.maxClaimable === "unlimited"
? ethers.constants.MaxUint256
: ethers.utils.parseUnits(claim.maxClaimable, tokenDecimals);
return {
proof: claim.proof,
maxClaimable: ethers.utils.parseUnits(claim.maxClaimable, tokenDecimals),
maxClaimable,
price,
currencyAddress: claim.currencyAddress,
};
}
/**
* Create and uploads snapshots + converts claim conditions to contract format
* @internal
* Decorates claim conditions with merkle roots from snapshots if present
* @param claimConditionInputs
* @param tokenDecimals
* @param provider
* @param storage
* @internal
* @param snapshotFormatVersion
*/
export async function processClaimConditionInputs(
async function processSnapshotData(
claimConditionInputs: ClaimConditionInput[],
tokenDecimals: number,
provider: providers.Provider,
provider: ethers.providers.Provider,
storage: ThirdwebStorage,
snapshotFormatVersion: SnapshotFormatVersion,
) {
const snapshotInfos: SnapshotInfo[] = [];
const inputsWithSnapshots = await Promise.all(
@@ -285,9 +360,11 @@ export async function processClaimConditionInputs(
// check snapshots and upload if provided
if (conditionInput.snapshot && conditionInput.snapshot.length > 0) {
const snapshotInfo = await createSnapshot(
SnapshotInputSchema.parse(conditionInput.snapshot),
conditionInput.snapshot,
tokenDecimals,
provider,
storage,
snapshotFormatVersion,
);
snapshotInfos.push(snapshotInfo);
conditionInput.merkleRootHash = snapshotInfo.merkleRoot;
@@ -299,26 +376,54 @@ export async function processClaimConditionInputs(
return conditionInput;
}),
);
return { inputsWithSnapshots, snapshotInfos };
}
function compare(a: BigNumberish, b: BigNumberish) {
const left = BigNumber.from(a);
const right = BigNumber.from(b);
if (left.eq(right)) {
return 0;
} else if (left.gt(right)) {
return 1;
} else {
return -1;
}
}
/**
* Create and uploads snapshots + converts claim conditions to contract format
* @param claimConditionInputs
* @param tokenDecimals
* @param provider
* @param storage
* @param snapshotFormatVersion
* @internal
*/
export async function processClaimConditionInputs(
claimConditionInputs: ClaimConditionInput[],
tokenDecimals: number,
provider: providers.Provider,
storage: ThirdwebStorage,
snapshotFormatVersion: SnapshotFormatVersion,
) {
const { inputsWithSnapshots, snapshotInfos } = await processSnapshotData(
claimConditionInputs,
tokenDecimals,
provider,
storage,
snapshotFormatVersion,
);
const parsedInputs = ClaimConditionInputArray.parse(inputsWithSnapshots);
// Convert processed inputs to the format the contract expects, and sort by timestamp
const sortedConditions: IDropClaimCondition.ClaimConditionStruct[] = (
const sortedConditions: AbstractClaimConditionContractStruct[] = (
await Promise.all(
parsedInputs.map((c) =>
convertToContractModel(c, tokenDecimals, provider),
convertToContractModel(c, tokenDecimals, provider, storage),
),
)
).sort((a, b) => {
const left = BigNumber.from(a.startTimestamp);
const right = BigNumber.from(b.startTimestamp);
if (left.eq(right)) {
return 0;
} else if (left.gt(right)) {
return 1;
} else {
return -1;
}
return compare(a.startTimestamp, b.startTimestamp);
});
return { snapshotInfos, sortedConditions };
}
@@ -328,41 +433,106 @@ export async function processClaimConditionInputs(
* @param c
* @param tokenDecimals
* @param provider
* @param storage
* @internal
*/
async function convertToContractModel(
c: FilledConditionInput,
tokenDecimals: number,
provider: providers.Provider,
): Promise<IDropClaimCondition.ClaimConditionStruct> {
storage: ThirdwebStorage,
): Promise<AbstractClaimConditionContractStruct> {
const currency =
c.currencyAddress === constants.AddressZero
? NATIVE_TOKEN_ADDRESS
: c.currencyAddress;
let maxClaimableSupply;
let quantityLimitPerTransaction;
if (c.maxQuantity === "unlimited") {
maxClaimableSupply = ethers.constants.MaxUint256.toString();
} else {
maxClaimableSupply = ethers.utils.parseUnits(c.maxQuantity, tokenDecimals);
}
if (c.quantityLimitPerTransaction === "unlimited") {
quantityLimitPerTransaction = ethers.constants.MaxUint256.toString();
} else {
quantityLimitPerTransaction = ethers.utils.parseUnits(
c.quantityLimitPerTransaction,
tokenDecimals,
);
const maxClaimableSupply = convertQuantityToBigNumber(
c.maxClaimableSupply,
tokenDecimals,
);
const maxClaimablePerWallet = convertQuantityToBigNumber(
c.maxClaimablePerWallet,
tokenDecimals,
);
let metadataOrUri;
if (c.metadata) {
if (typeof c.metadata === "string") {
metadataOrUri = c.metadata;
} else {
metadataOrUri = await storage.upload(c.metadata);
}
}
return {
startTimestamp: c.startTime,
maxClaimableSupply,
supplyClaimed: 0,
quantityLimitPerTransaction,
waitTimeInSecondsBetweenClaims: c.waitInSeconds,
maxClaimablePerWallet,
pricePerToken: await normalizePriceValue(provider, c.price, currency),
currency,
merkleRoot: c.merkleRootHash,
merkleRoot: c.merkleRootHash.toString(),
waitTimeInSecondsBetweenClaims: c.waitInSeconds || 0,
metadata: metadataOrUri,
};
}
export function abstractContractModelToLegacy(
model: AbstractClaimConditionContractStruct,
): IDropClaimCondition_V2.ClaimConditionStruct {
return {
startTimestamp: model.startTimestamp,
maxClaimableSupply: model.maxClaimableSupply,
supplyClaimed: model.supplyClaimed,
merkleRoot: model.merkleRoot,
pricePerToken: model.pricePerToken,
currency: model.currency,
quantityLimitPerTransaction: model.maxClaimablePerWallet,
waitTimeInSecondsBetweenClaims: model.waitTimeInSecondsBetweenClaims || 0,
};
}
export function abstractContractModelToNew(
model: AbstractClaimConditionContractStruct,
): IClaimCondition.ClaimConditionStruct {
return {
startTimestamp: model.startTimestamp,
maxClaimableSupply: model.maxClaimableSupply,
supplyClaimed: model.supplyClaimed,
merkleRoot: model.merkleRoot,
pricePerToken: model.pricePerToken,
currency: model.currency,
quantityLimitPerWallet: model.maxClaimablePerWallet,
metadata: model.metadata || "",
};
}
export function legacyContractModelToAbstract(
model: IDropClaimCondition_V2.ClaimConditionStruct,
): AbstractClaimConditionContractStruct {
return {
startTimestamp: model.startTimestamp,
maxClaimableSupply: model.maxClaimableSupply,
supplyClaimed: model.supplyClaimed,
merkleRoot: model.merkleRoot.toString(),
pricePerToken: model.pricePerToken,
currency: model.currency,
maxClaimablePerWallet: model.quantityLimitPerTransaction,
waitTimeInSecondsBetweenClaims: model.waitTimeInSecondsBetweenClaims,
};
}
export function newContractModelToAbstract(
model: IClaimCondition.ClaimConditionStruct,
): AbstractClaimConditionContractStruct {
return {
startTimestamp: model.startTimestamp,
maxClaimableSupply: model.maxClaimableSupply,
supplyClaimed: model.supplyClaimed,
merkleRoot: model.merkleRoot.toString(),
pricePerToken: model.pricePerToken,
currency: model.currency,
maxClaimablePerWallet: model.quantityLimitPerWallet,
waitTimeInSecondsBetweenClaims: 0,
metadata: model.metadata,
};
}
@@ -373,10 +543,11 @@ async function convertToContractModel(
* @param provider
* @param merkleMetadata
* @param storage
* @param shouldDownloadSnapshot
* @internal
*/
export async function transformResultToClaimCondition(
pm: IDropClaimCondition.ClaimConditionStructOutput,
pm: AbstractClaimConditionContractStruct,
tokenDecimals: number,
provider: providers.Provider,
merkleMetadata: Record<string, string> | undefined,
@@ -389,8 +560,8 @@ export async function transformResultToClaimCondition(
pm.maxClaimableSupply,
tokenDecimals,
);
const quantityLimitPerTransaction = convertToReadableQuantity(
pm.quantityLimitPerTransaction,
const maxClaimablePerWallet = convertToReadableQuantity(
pm.maxClaimablePerWallet,
tokenDecimals,
);
const availableSupply = convertToReadableQuantity(
@@ -403,11 +574,11 @@ export async function transformResultToClaimCondition(
);
return ClaimConditionOutputSchema.parse({
startTime: pm.startTimestamp,
maxQuantity: maxClaimableSupply,
maxClaimableSupply,
maxClaimablePerWallet,
currentMintSupply,
availableSupply,
quantityLimitPerTransaction,
waitInSeconds: pm.waitTimeInSecondsBetweenClaims.toString(),
waitInSeconds: pm.waitTimeInSecondsBetweenClaims?.toString(),
price: BigNumber.from(pm.pricePerToken),
currency: pm.currency,
currencyAddress: pm.currency,
@@ -416,10 +587,19 @@ export async function transformResultToClaimCondition(
snapshot: shouldDownloadSnapshot
? await fetchSnapshot(pm.merkleRoot, merkleMetadata, storage)
: undefined,
metadata: pm.metadata,
});
}
function convertToReadableQuantity(bn: BigNumber, tokenDecimals: number) {
/**
* @internal
* @param bn
* @param tokenDecimals
*/
export function convertToReadableQuantity(
bn: BigNumberish,
tokenDecimals: number,
) {
if (bn.toString() === ethers.constants.MaxUint256.toString()) {
return "unlimited";
} else {
@@ -427,6 +607,22 @@ function convertToReadableQuantity(bn: BigNumber, tokenDecimals: number) {
}
}
/**
* @internal
* @param quantity
* @param tokenDecimals
*/
export function convertQuantityToBigNumber(
quantity: Quantity,
tokenDecimals: number,
) {
if (quantity === "unlimited") {
return ethers.constants.MaxUint256;
} else {
return ethers.utils.parseUnits(quantity, tokenDecimals);
}
}
export async function calculateClaimCost(
contractWrapper: ContractWrapper<any>,
pricePerToken: Price,
+27
View File
@@ -0,0 +1,27 @@
import IThirdwebContractABI from "@thirdweb-dev/contracts-js/dist/abis/IThirdwebContract.json";
import { ethers } from "ethers";
export async function getPrebuiltInfo(
address: string,
provider: ethers.providers.Provider,
): Promise<{ type: string; version: number } | undefined> {
try {
const contract = new ethers.Contract(
address,
IThirdwebContractABI,
provider,
);
const [type, version] = await Promise.all([
ethers.utils
.toUtf8String(await contract.contractType()) // eslint-disable-next-line no-control-regex
.replace(/\x00/g, ""),
await contract.contractVersion(),
]);
return {
type,
version,
};
} catch (e) {
return undefined;
}
}
+1 -1
View File
@@ -67,9 +67,9 @@ export async function fetchTokenMetadata(
}
return CommonNFTOutput.parse({
...jsonMetadata,
id: BigNumber.from(tokenId).toString(),
uri: tokenUri,
...jsonMetadata,
});
}
@@ -4,12 +4,14 @@ import {
ShardedSnapshot,
SnapshotEntry,
SnapshotEntryWithProof,
SnapshotEntryWithProofSchema,
SnapshotInputSchema,
} from "../schema";
} from "../schema/contracts/common/snapshots";
import { SnapshotInput } from "../types";
import { hashLeafNode } from "./snapshots";
import { convertQuantityToBigNumber } from "./claim-conditions";
import { fetchCurrencyMetadata } from "./currency";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { utils } from "ethers";
import { ethers, utils } from "ethers";
import { MerkleTree } from "merkletreejs";
// shard using the first 2 hex character of the address
@@ -17,6 +19,11 @@ import { MerkleTree } from "merkletreejs";
// shard files will be 00.json, 01.json, 02.json, ..., ff.json
const SHARD_NYBBLES = 2;
export enum SnapshotFormatVersion {
V1 = 1, // address and maxClaimable
V2 = 2, // address, maxClaimable, price, currencyAddress
}
export class ShardedMerkleTree {
private shardNybbles: number;
private shards: Record<string, ShardData>;
@@ -73,17 +80,58 @@ export class ShardedMerkleTree {
);
}
static hashEntry(entry: SnapshotEntry, tokenDecimals: number): string {
return hashLeafNode(
entry.address,
utils.parseUnits(entry.maxClaimable, tokenDecimals),
);
static hashEntry(
entry: SnapshotEntry,
tokenDecimals: number,
currencyDecimals: number,
snapshotFormatVersion: SnapshotFormatVersion,
): string {
switch (snapshotFormatVersion) {
case SnapshotFormatVersion.V1:
return utils.solidityKeccak256(
["address", "uint256"],
[
entry.address,
convertQuantityToBigNumber(entry.maxClaimable, tokenDecimals),
],
);
case SnapshotFormatVersion.V2:
return utils.solidityKeccak256(
["address", "uint256", "uint256", "address"],
[
entry.address,
convertQuantityToBigNumber(entry.maxClaimable, tokenDecimals),
convertQuantityToBigNumber(entry.price, currencyDecimals),
entry.currencyAddress,
],
);
}
}
static async fetchAndCacheDecimals(
cache: Record<string, number>,
provider: ethers.providers.Provider,
currencyAddress: string,
): Promise<number> {
// cache decimals for each currency to avoid refetching for every address
let currencyDecimals = cache[currencyAddress];
if (!currencyDecimals) {
const currencyMetadata = await fetchCurrencyMetadata(
provider,
currencyAddress,
);
currencyDecimals = currencyMetadata.decimals;
cache[currencyAddress] = currencyDecimals;
}
return currencyDecimals;
}
static async buildAndUpload(
snapshotInput: SnapshotInput,
tokenDecimals: number,
provider: ethers.providers.Provider,
storage: ThirdwebStorage,
snapshotFormatVersion: SnapshotFormatVersion,
shardNybbles = SHARD_NYBBLES,
): Promise<ShardedSnapshot> {
const inputs = SnapshotInputSchema.parse(snapshotInput);
@@ -98,13 +146,28 @@ export class ShardedMerkleTree {
}
shards[shard].push(snapshotEntry);
}
const currencyDecimalMap: Record<string, number> = {};
// create shard => subtree root map
const roots = Object.fromEntries(
Object.entries(shards).map(([shard, entries]) => [
const subTrees = await Promise.all(
Object.entries(shards).map(async ([shard, entries]) => [
shard,
new MerkleTree(
entries.map((entry) =>
ShardedMerkleTree.hashEntry(entry, tokenDecimals),
await Promise.all(
entries.map(async (entry) => {
// cache decimals for each currency to avoid refetching for every address
const currencyDecimals =
await ShardedMerkleTree.fetchAndCacheDecimals(
currencyDecimalMap,
provider,
entry.currencyAddress,
);
return ShardedMerkleTree.hashEntry(
entry,
tokenDecimals,
currencyDecimals,
snapshotFormatVersion,
);
}),
),
utils.keccak256,
{
@@ -113,6 +176,7 @@ export class ShardedMerkleTree {
).getHexRoot(),
]),
);
const roots = Object.fromEntries(subTrees);
// create master tree from shard => subtree root map
const tree = new MerkleTree(Object.values(roots), utils.keccak256, {
sort: true,
@@ -155,22 +219,38 @@ export class ShardedMerkleTree {
public async getProof(
address: string,
provider: ethers.providers.Provider,
snapshotFormatVersion: SnapshotFormatVersion,
): Promise<SnapshotEntryWithProof | null> {
const shardId = address.slice(2, 2 + this.shardNybbles).toLowerCase();
let shard = this.shards[shardId];
const currencyDecimalMap: Record<string, number> = {};
if (shard === undefined) {
try {
shard = this.shards[shardId] =
await this.storage.downloadJSON<ShardData>(
`${this.baseUri}/${shardId}.json`,
);
this.trees[shardId] = new MerkleTree(
shard.entries.map((entry) =>
ShardedMerkleTree.hashEntry(entry, this.tokenDecimals),
),
utils.keccak256,
{ sort: true },
const hashedEntries = await Promise.all(
shard.entries.map(async (entry) => {
// cache decimals for each currency to avoid refetching for every address
const currencyDecimals =
await ShardedMerkleTree.fetchAndCacheDecimals(
currencyDecimalMap,
provider,
entry.currencyAddress,
);
return ShardedMerkleTree.hashEntry(
entry,
this.tokenDecimals,
currencyDecimals,
snapshotFormatVersion,
);
}),
);
this.trees[shardId] = new MerkleTree(hashedEntries, utils.keccak256, {
sort: true,
});
} catch (e) {
console.warn("No merkle entry found for address", address);
return null;
@@ -182,35 +262,34 @@ export class ShardedMerkleTree {
if (!entry) {
return null;
}
const leaf = ShardedMerkleTree.hashEntry(entry, this.tokenDecimals);
const currencyDecimals = await ShardedMerkleTree.fetchAndCacheDecimals(
currencyDecimalMap,
provider,
entry.currencyAddress,
);
const leaf = ShardedMerkleTree.hashEntry(
entry,
this.tokenDecimals,
currencyDecimals,
snapshotFormatVersion,
);
const proof = this.trees[shardId]
.getProof(leaf)
.map((i) => "0x" + i.data.toString("hex"));
return {
address,
return SnapshotEntryWithProofSchema.parse({
...entry,
proof: proof.concat(shard.proofs),
maxClaimable: entry.maxClaimable,
};
});
}
public async getAllEntries(): Promise<SnapshotEntry[]> {
try {
const entries = await this.storage.downloadJSON<SnapshotEntry[]>(
return await this.storage.downloadJSON<SnapshotEntry[]>(
this.originalEntriesUri,
);
return entries;
} catch (e) {
console.warn("Could not fetch original snapshot entries", e);
return [];
}
}
private computeAllShardIds(shardNybbles: number) {
const max = parseInt(`0x${"f".repeat(shardNybbles)}`, 16);
const allShards = [];
for (let i = 0; i <= max; i++) {
allShards.push(i.toString(16).padStart(shardNybbles, "0"));
}
return allShards;
}
}
+12 -19
View File
@@ -1,25 +1,32 @@
import { SnapshotInputSchema } from "../schema/contracts/common/snapshots";
import { SnapshotInputSchema } from "../schema";
import {
SnapshotInfo,
SnapshotInput,
} from "../types/claim-conditions/claim-conditions";
import { DuplicateLeafsError } from "./error";
import { ShardedMerkleTree } from "./sharded-merkle-tree";
import {
ShardedMerkleTree,
SnapshotFormatVersion,
} from "./sharded-merkle-tree";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, utils } from "ethers";
import { ethers } from "ethers";
/**
* Create a snapshot (merkle tree) from a list of addresses and uploads it to IPFS
* @param snapshotInput - the list of addresses to hash
* @param tokenDecimals - the token decimals
* @param provider
* @param storage - the storage to upload to
* @param snapshotFormatVersion
* @returns the generated snapshot and URI
* @internal
*/
export async function createSnapshot(
snapshotInput: SnapshotInput,
tokenDecimals: number,
provider: ethers.providers.Provider,
storage: ThirdwebStorage,
snapshotFormatVersion: SnapshotFormatVersion,
): Promise<SnapshotInfo> {
const input = SnapshotInputSchema.parse(snapshotInput);
const addresses = input.map((i) => i.address);
@@ -30,26 +37,12 @@ export async function createSnapshot(
const tree = await ShardedMerkleTree.buildAndUpload(
input,
tokenDecimals,
provider,
storage,
snapshotFormatVersion,
);
return {
merkleRoot: tree.shardedMerkleInfo.merkleRoot,
snapshotUri: tree.uri,
};
}
/**
* Hash an address and the corresponding claimable amount
* @internal
* @param address - the address
* @param maxClaimableAmount - the amount
*/
export function hashLeafNode(
address: string,
maxClaimableAmount: BigNumberish,
): string {
return utils.solidityKeccak256(
["address", "uint256"],
[address, BigNumber.from(maxClaimableAmount)],
);
}
@@ -17,7 +17,9 @@ import {
FEATURE_NFT_REVEALABLE,
FEATURE_NFT_SIGNATURE_MINTABLE,
FEATURE_NFT_SUPPLY,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V1,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2,
FEATURE_NFT_TIERED_DROP,
} from "./erc721-features";
import {
FEATURE_EDITION,
@@ -59,10 +61,12 @@ export type Feature =
| typeof FEATURE_NFT_BATCH_MINTABLE
| typeof FEATURE_NFT_LAZY_MINTABLE
| typeof FEATURE_NFT_CLAIMABLE
| typeof FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS
| typeof FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V1
| typeof FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2
| typeof FEATURE_NFT_REVEALABLE
| typeof FEATURE_NFT_SIGNATURE_MINTABLE
| typeof FEATURE_NFT_BURNABLE
| typeof FEATURE_NFT_TIERED_DROP
| typeof FEATURE_EDITION
| typeof FEATURE_EDITION_ENUMERABLE
| typeof FEATURE_EDITION_MINTABLE
@@ -1,7 +1,7 @@
import DelayedRevealAbi from "@thirdweb-dev/contracts-js/dist/abis/DelayedReveal.json";
import DropSinglePhase1155 from "@thirdweb-dev/contracts-js/dist/abis/DropSinglePhase1155.json";
import IBurnableERC1155Abi from "@thirdweb-dev/contracts-js/dist/abis/IBurnableERC1155.json";
import IClaimableERC1155 from "@thirdweb-dev/contracts-js/dist/abis/IClaimableERC1155.json";
import IDropSinglePhase1155 from "@thirdweb-dev/contracts-js/dist/abis/IDropSinglePhase1155.json";
import Erc1155Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC1155.json";
import Erc1155EnumerableAbi from "@thirdweb-dev/contracts-js/dist/abis/IERC1155Enumerable.json";
import ILazyMintAbi from "@thirdweb-dev/contracts-js/dist/abis/ILazyMint.json";
@@ -27,7 +27,7 @@ export const FEATURE_EDITION_CLAIMABLE_WITH_CONDITIONS = {
sdk: "sdk.erc1155claimable",
contracts: "DropSinglePhase1155",
},
abis: [Erc1155Abi, ILazyMintAbi, DropSinglePhase1155],
abis: [Erc1155Abi, ILazyMintAbi, IDropSinglePhase1155],
features: {},
} as const;
@@ -2,12 +2,14 @@ import IBurnableERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/IBurnableER
import IClaimableERC721 from "@thirdweb-dev/contracts-js/dist/abis/IClaimableERC721.json";
import DelayedRevealAbi from "@thirdweb-dev/contracts-js/dist/abis/IDelayedReveal.json";
import IDropSinglePhase from "@thirdweb-dev/contracts-js/dist/abis/IDropSinglePhase.json";
import IDropSinglePhaseV1 from "@thirdweb-dev/contracts-js/dist/abis/IDropSinglePhase_V1.json";
import Erc721Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC721.json";
import Erc721EnumerableAbi from "@thirdweb-dev/contracts-js/dist/abis/IERC721Enumerable.json";
import Erc721SupplyAbi from "@thirdweb-dev/contracts-js/dist/abis/IERC721Supply.json";
import ILazyMintAbi from "@thirdweb-dev/contracts-js/dist/abis/ILazyMint.json";
import IMintableERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/IMintableERC721.json";
import MulticallAbi from "@thirdweb-dev/contracts-js/dist/abis/IMulticall.json";
import TieredDropAbi from "@thirdweb-dev/contracts-js/dist/abis/LazyMintWithTier.json";
import SignatureMintERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/SignatureMintERC721.json";
export const FEATURE_NFT_BURNABLE = {
@@ -25,15 +27,26 @@ export const FEATURE_NFT_REVEALABLE = {
name: "ERC721Revealable",
namespace: "nft.drop.revealer",
docLinks: {
sdk: "sdk.drop.delayedreveal",
sdk: "sdk.delayedreveal",
contracts: "DelayedReveal",
},
abis: [Erc721Abi, ILazyMintAbi, DelayedRevealAbi],
features: {},
} as const;
export const FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS = {
name: "ERC721ClaimableWithConditions",
export const FEATURE_NFT_TIERED_DROP = {
name: "ERC721TieredDrop",
namespace: "nft.tieredDrop",
docLinks: {
sdk: "sdk.erc721tiereddrop",
contracts: "TieredDrop",
},
abis: [Erc721Abi, TieredDropAbi],
features: {},
} as const;
export const FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2 = {
name: "ERC721ClaimableWithConditionsV2",
namespace: "nft.drop.claim",
docLinks: {
sdk: "sdk.erc721claimable",
@@ -43,6 +56,17 @@ export const FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS = {
features: {},
} as const;
export const FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V1 = {
name: "ERC721ClaimableWithConditionsV1",
namespace: "nft.drop.claim",
docLinks: {
sdk: "sdk.erc721claimable",
contracts: "DropSinglePhase_V1",
},
abis: [Erc721Abi, ILazyMintAbi, IDropSinglePhaseV1],
features: {},
} as const;
export const FEATURE_NFT_CLAIMABLE = {
name: "ERC721Claimable",
namespace: "nft.drop.claim",
@@ -65,8 +89,10 @@ export const FEATURE_NFT_LAZY_MINTABLE = {
features: {
[FEATURE_NFT_REVEALABLE.name]: FEATURE_NFT_REVEALABLE,
[FEATURE_NFT_CLAIMABLE.name]: FEATURE_NFT_CLAIMABLE,
[FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS.name]:
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS,
[FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2.name]:
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2,
[FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V1.name]:
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V1,
},
} as const;
@@ -143,5 +169,6 @@ export const FEATURE_NFT = {
[FEATURE_NFT_MINTABLE.name]: FEATURE_NFT_MINTABLE,
[FEATURE_NFT_LAZY_MINTABLE.name]: FEATURE_NFT_LAZY_MINTABLE,
[FEATURE_NFT_SIGNATURE_MINTABLE.name]: FEATURE_NFT_SIGNATURE_MINTABLE,
[FEATURE_NFT_TIERED_DROP.name]: FEATURE_NFT_TIERED_DROP,
},
} as const;
+155 -47
View File
@@ -1,6 +1,11 @@
import { getPrebuiltInfo } from "../common/legacy";
import { ALL_ROLES } from "../common/role";
import { getSignerAndProvider } from "../core/classes/rpc-connection-handler";
import type { ContractType, NetworkOrSignerOrProvider } from "../core/types";
import type {
ContractType,
NetworkOrSignerOrProvider,
PrebuiltContractType,
} from "../core/types";
import {
DropErc1155ContractSchema,
DropErc721ContractSchema,
@@ -17,6 +22,7 @@ import { CustomContractSchema } from "../schema/contracts/custom";
import { DropErc20ContractSchema } from "../schema/contracts/drop-erc20";
import { MultiwrapContractSchema } from "../schema/contracts/multiwrap";
import type { ThirdwebStorage } from "@thirdweb-dev/storage";
import { ethers } from "ethers";
type InitalizeParams = [
network: NetworkOrSignerOrProvider,
@@ -34,8 +40,12 @@ export const EditionDropInitializer = {
...[network, address, storage, options]: InitalizeParams
) => {
const [, provider] = getSignerAndProvider(network, options);
const contractInfo = await getPrebuiltInfo(address, provider);
if (!contractInfo || contractInfo.type !== "DropERC1155") {
throw new Error("Contract is not a DropERC1155");
}
const [abi, contract, _network] = await Promise.all([
EditionDropInitializer.getAbi(),
await EditionDropInitializer.getAbi(address, provider),
import("./prebuilt-implementations/edition-drop"),
provider.getNetwork(),
]);
@@ -49,9 +59,21 @@ export const EditionDropInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/DropERC1155.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"edition-drop",
);
return contractInfo.version > 2
? (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC1155.json"))
.default
: (
await import(
"@thirdweb-dev/contracts-js/dist/abis/DropERC1155_V2.json"
)
).default;
},
};
export const EditionInitializer = {
@@ -64,7 +86,7 @@ export const EditionInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
EditionInitializer.getAbi(),
EditionInitializer.getAbi(address, provider),
import("./prebuilt-implementations/edition"),
provider.getNetwork(),
]);
@@ -78,9 +100,12 @@ export const EditionInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC1155.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "edition");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC1155.json")
).default;
},
};
export const MarketplaceInitializer = {
@@ -93,7 +118,7 @@ export const MarketplaceInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
MarketplaceInitializer.getAbi(),
MarketplaceInitializer.getAbi(address, provider),
import("./prebuilt-implementations/marketplace"),
provider.getNetwork(),
]);
@@ -107,9 +132,12 @@ export const MarketplaceInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/Marketplace.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "marketplace");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/Marketplace.json")
).default;
},
};
export const MultiwrapInitializer = {
@@ -122,7 +150,7 @@ export const MultiwrapInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
MultiwrapInitializer.getAbi(),
MultiwrapInitializer.getAbi(address, provider),
import("./prebuilt-implementations/multiwrap"),
provider.getNetwork(),
]);
@@ -136,9 +164,11 @@ export const MultiwrapInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/Multiwrap.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "multiwrap");
return (await import("@thirdweb-dev/contracts-js/dist/abis/Multiwrap.json"))
.default;
},
};
export const NFTCollectionInitializer = {
@@ -152,7 +182,7 @@ export const NFTCollectionInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
NFTCollectionInitializer.getAbi(),
NFTCollectionInitializer.getAbi(address, provider),
import("./prebuilt-implementations/nft-collection"),
provider.getNetwork(),
]);
@@ -166,9 +196,12 @@ export const NFTCollectionInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC721.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "nft-collection");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC721.json")
).default;
},
};
export const NFTDropInitializer = {
@@ -180,8 +213,12 @@ export const NFTDropInitializer = {
...[network, address, storage, options]: InitalizeParams
) => {
const [, provider] = getSignerAndProvider(network, options);
const contractInfo = await getPrebuiltInfo(address, provider);
if (!contractInfo || contractInfo.type !== "DropERC721") {
throw new Error("Contract is not a DropERC721");
}
const [abi, contract, _network] = await Promise.all([
NFTDropInitializer.getAbi(),
NFTDropInitializer.getAbi(address, provider),
import("./prebuilt-implementations/nft-drop"),
provider.getNetwork(),
]);
@@ -195,9 +232,21 @@ export const NFTDropInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/DropERC721.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"nft-drop",
);
return contractInfo.version > 3
? (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC721.json"))
.default
: (
await import(
"@thirdweb-dev/contracts-js/dist/abis/DropERC721_V3.json"
)
).default;
},
};
export const PackInitializer = {
@@ -211,7 +260,7 @@ export const PackInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
PackInitializer.getAbi(),
PackInitializer.getAbi(address, provider),
import("./prebuilt-implementations/pack"),
provider.getNetwork(),
]);
@@ -225,8 +274,11 @@ export const PackInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/Pack.json")).default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "pack");
return (await import("@thirdweb-dev/contracts-js/dist/abis/Pack.json"))
.default;
},
};
export const SignatureDropInitializer = {
@@ -240,7 +292,7 @@ export const SignatureDropInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
SignatureDropInitializer.getAbi(),
SignatureDropInitializer.getAbi(address, provider),
import("./prebuilt-implementations/signature-drop"),
provider.getNetwork(),
]);
@@ -254,9 +306,24 @@ export const SignatureDropInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/SignatureDrop.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"signature-drop",
);
return contractInfo.version > 4
? (
await import(
"@thirdweb-dev/contracts-js/dist/abis/SignatureDrop.json"
)
).default
: (
await import(
"@thirdweb-dev/contracts-js/dist/abis/SignatureDrop_V4.json"
)
).default;
},
};
export const SplitInitializer = {
@@ -270,7 +337,7 @@ export const SplitInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
SplitInitializer.getAbi(),
SplitInitializer.getAbi(address, provider),
import("./prebuilt-implementations/split"),
provider.getNetwork(),
]);
@@ -284,8 +351,11 @@ export const SplitInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/Split.json")).default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "split");
return (await import("@thirdweb-dev/contracts-js/dist/abis/Split.json"))
.default;
},
};
export const TokenDropInitializer = {
@@ -298,8 +368,12 @@ export const TokenDropInitializer = {
...[network, address, storage, options]: InitalizeParams
) => {
const [, provider] = getSignerAndProvider(network, options);
const contractInfo = await getPrebuiltInfo(address, provider);
if (!contractInfo || contractInfo.type !== "DropERC20") {
throw new Error("Contract is not a DropERC20");
}
const [abi, contract, _network] = await Promise.all([
TokenDropInitializer.getAbi(),
TokenDropInitializer.getAbi(address, provider),
import("./prebuilt-implementations/token-drop"),
provider.getNetwork(),
]);
@@ -313,9 +387,18 @@ export const TokenDropInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/DropERC20.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
const contractInfo = await assertContractType(
address,
provider,
"token-drop",
);
return contractInfo.version > 2
? (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC20.json"))
.default
: (await import("@thirdweb-dev/contracts-js/dist/abis/DropERC20_V2.json"))
.default;
},
};
export const TokenInitializer = {
@@ -328,7 +411,7 @@ export const TokenInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
TokenInitializer.getAbi(),
TokenInitializer.getAbi(address, provider),
import("./prebuilt-implementations/token"),
provider.getNetwork(),
]);
@@ -342,9 +425,12 @@ export const TokenInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC20.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "token");
return (
await import("@thirdweb-dev/contracts-js/dist/abis/TokenERC20.json")
).default;
},
};
export const VoteInitializer = {
@@ -358,7 +444,7 @@ export const VoteInitializer = {
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
VoteInitializer.getAbi(),
VoteInitializer.getAbi(address, provider),
import("./prebuilt-implementations/vote"),
provider.getNetwork(),
]);
@@ -372,11 +458,25 @@ export const VoteInitializer = {
_network.chainId,
);
},
getAbi: async () =>
(await import("@thirdweb-dev/contracts-js/dist/abis/VoteERC20.json"))
.default,
getAbi: async (address: string, provider: ethers.providers.Provider) => {
await assertContractType(address, provider, "vote");
return (await import("@thirdweb-dev/contracts-js/dist/abis/VoteERC20.json"))
.default;
},
};
async function assertContractType(
address: string,
provider: ethers.providers.Provider,
type: PrebuiltContractType,
) {
const contractInfo = await getPrebuiltInfo(address, provider);
if (!contractInfo || contractInfo.type !== getContractName(type)) {
throw new Error(`Contract is not a ${type}`);
}
return contractInfo;
}
/**
* a map from contractType -> contract metadata
* @internal
@@ -417,3 +517,11 @@ export function getContractTypeForRemoteName(name: string): ContractType {
?.contractType || "custom"
);
}
export function getContractName(
type: PrebuiltContractType,
): string | undefined {
return Object.values(CONTRACTS_MAP).find(
(contract) => contract.contractType === type,
)?.name;
}
@@ -22,10 +22,10 @@ import {
TransactionResult,
TransactionResultWithId,
} from "../../core/types";
import { PaperCheckout } from "../../integrations/paper-xyz";
import { PaperCheckout } from "../../integrations/checkout";
import { DropErc1155ContractSchema } from "../../schema/contracts/drop-erc1155";
import { SDKOptions } from "../../schema/sdk-options";
import type { DropERC1155 } from "@thirdweb-dev/contracts-js";
import { PrebuiltEditionDrop } from "../../types/eips";
import type ABI from "@thirdweb-dev/contracts-js/dist/abis/DropERC1155.json";
import { ThirdwebStorage, UploadProgressEvent } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, CallOverrides, constants } from "ethers";
@@ -44,21 +44,21 @@ import { BigNumber, BigNumberish, CallOverrides, constants } from "ethers";
*
* @public
*/
export class EditionDrop extends StandardErc1155<DropERC1155> {
export class EditionDrop extends StandardErc1155<PrebuiltEditionDrop> {
private static contractRoles = ["admin", "minter", "transfer"] as const;
public abi: typeof ABI;
public sales: ContractPrimarySale<DropERC1155>;
public platformFees: ContractPlatformFee<DropERC1155>;
public encoder: ContractEncoder<DropERC1155>;
public estimator: GasCostEstimator<DropERC1155>;
public events: ContractEvents<DropERC1155>;
public sales: ContractPrimarySale<PrebuiltEditionDrop>;
public platformFees: ContractPlatformFee<PrebuiltEditionDrop>;
public encoder: ContractEncoder<PrebuiltEditionDrop>;
public estimator: GasCostEstimator<PrebuiltEditionDrop>;
public events: ContractEvents<PrebuiltEditionDrop>;
public metadata: ContractMetadata<
DropERC1155,
PrebuiltEditionDrop,
typeof DropErc1155ContractSchema
>;
public roles: ContractRoles<
DropERC1155,
PrebuiltEditionDrop,
typeof EditionDrop.contractRoles[number]
>;
/**
@@ -79,7 +79,7 @@ export class EditionDrop extends StandardErc1155<DropERC1155> {
* ```
*/
public royalties: ContractRoyalty<
DropERC1155,
PrebuiltEditionDrop,
typeof DropErc1155ContractSchema
>;
/**
@@ -106,18 +106,18 @@ export class EditionDrop extends StandardErc1155<DropERC1155> {
* await contract.claimConditions.set(tokenId, claimConditions);
* ```
*/
public claimConditions: DropErc1155ClaimConditions<DropERC1155>;
public claimConditions: DropErc1155ClaimConditions<PrebuiltEditionDrop>;
/**
* Checkout
* @remarks Create a FIAT currency checkout for your NFT drop.
*/
public checkout: PaperCheckout<DropERC1155>;
public checkout: PaperCheckout<PrebuiltEditionDrop>;
public history: DropErc1155History;
public interceptor: ContractInterceptor<DropERC1155>;
public erc1155: Erc1155<DropERC1155>;
public owner: ContractOwner<DropERC1155>;
public interceptor: ContractInterceptor<PrebuiltEditionDrop>;
public erc1155: Erc1155<PrebuiltEditionDrop>;
public owner: ContractOwner<PrebuiltEditionDrop>;
constructor(
network: NetworkOrSignerOrProvider,
@@ -126,7 +126,7 @@ export class EditionDrop extends StandardErc1155<DropERC1155> {
options: SDKOptions = {},
abi: typeof ABI,
chainId: number,
contractWrapper = new ContractWrapper<DropERC1155>(
contractWrapper = new ContractWrapper<PrebuiltEditionDrop>(
network,
address,
abi,
@@ -4,7 +4,6 @@ import {
} from "../../../core/schema/QueryParams";
import { NFT, NFTMetadata, NFTMetadataOrUri } from "../../../core/schema/nft";
import { getRoleHash } from "../../common";
import { uploadOrExtractURIs } from "../../common/nft";
import { FEATURE_NFT_REVEALABLE } from "../../constants/erc721-features";
import { TransactionTask } from "../../core/classes/TransactionTask";
import { ContractEncoder } from "../../core/classes/contract-encoder";
@@ -27,21 +26,14 @@ import {
TransactionResult,
TransactionResultWithId,
} from "../../core/types";
import { PaperCheckout } from "../../integrations/paper-xyz";
import { PaperCheckout } from "../../integrations/checkout";
import { DropErc721ContractSchema } from "../../schema/contracts/drop-erc721";
import { SDKOptions } from "../../schema/sdk-options";
import { PrebuiltNFTDrop } from "../../types/eips";
import { UploadProgressEvent } from "../../types/events";
import type { DropERC721 } from "@thirdweb-dev/contracts-js";
import type ABI from "@thirdweb-dev/contracts-js/dist/abis/DropERC721.json";
import { TokensLazyMintedEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC721";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import {
BigNumber,
BigNumberish,
CallOverrides,
constants,
ethers,
} from "ethers";
import { BigNumber, BigNumberish, CallOverrides, constants } from "ethers";
/**
* Setup a collection of one-of-one NFTs that are minted as users claim them.
@@ -57,24 +49,27 @@ import {
*
* @public
*/
export class NFTDrop extends StandardErc721<DropERC721> {
export class NFTDrop extends StandardErc721<PrebuiltNFTDrop> {
static contractRoles = ["admin", "minter", "transfer"] as const;
public abi: typeof ABI;
public encoder: ContractEncoder<DropERC721>;
public estimator: GasCostEstimator<DropERC721>;
public encoder: ContractEncoder<PrebuiltNFTDrop>;
public estimator: GasCostEstimator<PrebuiltNFTDrop>;
public metadata: ContractMetadata<
DropERC721,
PrebuiltNFTDrop,
typeof DropErc721ContractSchema
>;
public sales: ContractPrimarySale<DropERC721>;
public platformFees: ContractPlatformFee<DropERC721>;
public events: ContractEvents<DropERC721>;
public roles: ContractRoles<DropERC721, typeof NFTDrop.contractRoles[number]>;
public sales: ContractPrimarySale<PrebuiltNFTDrop>;
public platformFees: ContractPlatformFee<PrebuiltNFTDrop>;
public events: ContractEvents<PrebuiltNFTDrop>;
public roles: ContractRoles<
PrebuiltNFTDrop,
typeof NFTDrop.contractRoles[number]
>;
/**
* @internal
*/
public interceptor: ContractInterceptor<DropERC721>;
public interceptor: ContractInterceptor<PrebuiltNFTDrop>;
/**
* Configure royalties
* @remarks Set your own royalties for the entire contract or per token
@@ -93,7 +88,7 @@ export class NFTDrop extends StandardErc721<DropERC721> {
* ```
*/
public royalties: ContractRoyalty<
DropERC721,
PrebuiltNFTDrop,
typeof DropErc721ContractSchema
>;
/**
@@ -118,7 +113,7 @@ export class NFTDrop extends StandardErc721<DropERC721> {
* await contract.claimConditions.set(claimConditions);
* ```
*/
public claimConditions: DropClaimConditions<DropERC721>;
public claimConditions: DropClaimConditions<PrebuiltNFTDrop>;
/**
* Delayed reveal
* @remarks Create a batch of encrypted NFTs that can be revealed at a later time.
@@ -150,16 +145,16 @@ export class NFTDrop extends StandardErc721<DropERC721> {
* await contract.revealer.reveal(batchId, "my secret password");
* ```
*/
public revealer: DelayedReveal<DropERC721>;
public revealer: DelayedReveal<PrebuiltNFTDrop>;
/**
* Checkout
* @remarks Create a FIAT currency checkout for your NFT drop.
*/
public checkout: PaperCheckout<DropERC721>;
public checkout: PaperCheckout<PrebuiltNFTDrop>;
public erc721: Erc721<DropERC721>;
public owner: ContractOwner<DropERC721>;
public erc721: Erc721<PrebuiltNFTDrop>;
public owner: ContractOwner<PrebuiltNFTDrop>;
constructor(
network: NetworkOrSignerOrProvider,
@@ -168,7 +163,7 @@ export class NFTDrop extends StandardErc721<DropERC721> {
options: SDKOptions = {},
abi: typeof ABI,
chainId: number,
contractWrapper = new ContractWrapper<DropERC721>(
contractWrapper = new ContractWrapper<PrebuiltNFTDrop>(
network,
address,
abi,
@@ -195,7 +190,7 @@ export class NFTDrop extends StandardErc721<DropERC721> {
this.events = new ContractEvents(this.contractWrapper);
this.platformFees = new ContractPlatformFee(this.contractWrapper);
this.erc721 = new Erc721(this.contractWrapper, this.storage, chainId);
this.revealer = new DelayedReveal<DropERC721>(
this.revealer = new DelayedReveal(
this.contractWrapper,
this.storage,
FEATURE_NFT_REVEALABLE.name,
@@ -383,44 +378,7 @@ export class NFTDrop extends StandardErc721<DropERC721> {
onProgress: (event: UploadProgressEvent) => void;
},
): Promise<TransactionResultWithId<NFTMetadata>[]> {
const startFileNumber =
await this.contractWrapper.readContract.nextTokenIdToMint();
const batch = await uploadOrExtractURIs(
metadatas,
this.storage,
startFileNumber.toNumber(),
options,
);
// ensure baseUri is the same for the entire batch
const baseUri = batch[0].substring(0, batch[0].lastIndexOf("/"));
for (let i = 0; i < batch.length; i++) {
const uri = batch[i].substring(0, batch[i].lastIndexOf("/"));
if (baseUri !== uri) {
throw new Error(
`Can only create batches with the same base URI for every entry in the batch. Expected '${baseUri}' but got '${uri}'`,
);
}
}
const receipt = await this.contractWrapper.sendTransaction("lazyMint", [
batch.length,
baseUri.endsWith("/") ? baseUri : `${baseUri}/`,
ethers.utils.toUtf8Bytes(""),
]);
const event = this.contractWrapper.parseLogs<TokensLazyMintedEvent>(
"TokensLazyMinted",
receipt?.logs,
);
const startingIndex = event[0].args.startTokenId;
const endingIndex = event[0].args.endTokenId;
const results: TransactionResultWithId<NFTMetadata>[] = [];
for (let id = startingIndex; id.lte(endingIndex); id = id.add(1)) {
results.push({
id,
receipt,
data: () => this.erc721.getTokenMetadata(id),
});
}
return results;
return this.erc721.lazyMint(metadatas, options);
}
/**
@@ -27,7 +27,7 @@ import {
TransactionResult,
TransactionResultWithId,
} from "../../core/types";
import { PaperCheckout } from "../../integrations/paper-xyz";
import { PaperCheckout } from "../../integrations/checkout";
import { DropErc721ContractSchema } from "../../schema/contracts/drop-erc721";
import { SDKOptions } from "../../schema/sdk-options";
import { ClaimOptions, UploadProgressEvent } from "../../types";
@@ -14,7 +14,7 @@ import { NetworkOrSignerOrProvider, TransactionResult } from "../../core/types";
import { DropErc20ContractSchema } from "../../schema/contracts/drop-erc20";
import { SDKOptions } from "../../schema/sdk-options";
import { Amount, CurrencyValue } from "../../types";
import type { DropERC20 } from "@thirdweb-dev/contracts-js";
import { PrebuiltTokenDrop } from "../../types/eips";
import type ABI from "@thirdweb-dev/contracts-js/dist/abis/DropERC20.json";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { CallOverrides, constants } from "ethers";
@@ -32,20 +32,23 @@ import { CallOverrides, constants } from "ethers";
* ```
*
*/
export class TokenDrop extends StandardErc20<DropERC20> {
export class TokenDrop extends StandardErc20<PrebuiltTokenDrop> {
static contractRoles = ["admin", "transfer"] as const;
public abi: typeof ABI;
public metadata: ContractMetadata<DropERC20, typeof DropErc20ContractSchema>;
public metadata: ContractMetadata<
PrebuiltTokenDrop,
typeof DropErc20ContractSchema
>;
public roles: ContractRoles<
DropERC20,
PrebuiltTokenDrop,
typeof TokenDrop.contractRoles[number]
>;
public encoder: ContractEncoder<DropERC20>;
public estimator: GasCostEstimator<DropERC20>;
public sales: ContractPrimarySale<DropERC20>;
public platformFees: ContractPlatformFee<DropERC20>;
public events: ContractEvents<DropERC20>;
public encoder: ContractEncoder<PrebuiltTokenDrop>;
public estimator: GasCostEstimator<PrebuiltTokenDrop>;
public sales: ContractPrimarySale<PrebuiltTokenDrop>;
public platformFees: ContractPlatformFee<PrebuiltTokenDrop>;
public events: ContractEvents<PrebuiltTokenDrop>;
/**
* Configure claim conditions
* @remarks Define who can claim Tokens, when and how many.
@@ -68,11 +71,11 @@ export class TokenDrop extends StandardErc20<DropERC20> {
* await contract.claimConditions.set(claimConditions);
* ```
*/
public claimConditions: DropClaimConditions<DropERC20>;
public claimConditions: DropClaimConditions<PrebuiltTokenDrop>;
/**
* @internal
*/
public interceptor: ContractInterceptor<DropERC20>;
public interceptor: ContractInterceptor<PrebuiltTokenDrop>;
constructor(
network: NetworkOrSignerOrProvider,
@@ -81,7 +84,7 @@ export class TokenDrop extends StandardErc20<DropERC20> {
options: SDKOptions = {},
abi: typeof ABI,
chainId: number,
contractWrapper = new ContractWrapper<DropERC20>(
contractWrapper = new ContractWrapper<PrebuiltTokenDrop>(
network,
address,
abi,
@@ -105,7 +108,7 @@ export class TokenDrop extends StandardErc20<DropERC20> {
this.sales = new ContractPrimarySale(this.contractWrapper);
this.platformFees = new ContractPlatformFee(this.contractWrapper);
this.interceptor = new ContractInterceptor(this.contractWrapper);
this.claimConditions = new DropClaimConditions<DropERC20>(
this.claimConditions = new DropClaimConditions<PrebuiltTokenDrop>(
this.contractWrapper,
this.metadata,
this.storage,
@@ -210,7 +213,9 @@ export class TokenDrop extends StandardErc20<DropERC20> {
amount: Amount,
checkERC20Allowance = true,
): Promise<TransactionResult> {
return this.erc20.claimTo(destinationAddress, amount, checkERC20Allowance);
return this.erc20.claimTo(destinationAddress, amount, {
checkERC20Allowance,
});
}
/**
@@ -319,7 +319,7 @@ export class Vote implements UpdateableNetwork {
this.contractWrapper.readContract.votingDelay(),
this.contractWrapper.readContract.votingPeriod(),
this.contractWrapper.readContract.token(),
this.contractWrapper.readContract.quorumNumerator(),
this.contractWrapper.readContract["quorumNumerator()"](),
this.contractWrapper.readContract.proposalThreshold(),
]);
const votingTokenMetadata = await fetchCurrencyMetadata(
@@ -387,6 +387,7 @@ export class ContractDeployer extends RPCConnectionHandler {
* @internal
* @param contractType - the type of contract to deploy
* @param contractMetadata - the metadata to deploy the contract with
* @param version
* @returns a promise of the address of the newly deployed contract
*/
public async deployBuiltInContract<
@@ -396,13 +397,25 @@ export class ContractDeployer extends RPCConnectionHandler {
contractMetadata: z.input<
DeploySchemaForPrebuiltContractType<TContractType>
>,
version?: number,
): Promise<string> {
const parsed =
PREBUILT_CONTRACTS_MAP[contractType].schema.deploy.parse(
contractMetadata,
);
const factory = await this.getFactory();
return await factory.deploy(contractType, parsed);
return await factory.deploy(contractType, parsed, version);
}
/**
* @internal
* @param contractType
*/
public async getLatestBuiltInContractVersion<
TContractType extends PrebuiltContractType,
>(contractType: TContractType) {
const factory = await this.getFactory();
return await factory.getLatestVersion(contractType);
}
/**
@@ -18,7 +18,7 @@ import { UploadProgressEvent } from "../../types/events";
import { TransactionResult, TransactionResultWithId } from "../index";
import { ContractWrapper } from "./contract-wrapper";
import type {
DropERC721,
DropERC721_V3,
IThirdwebContract,
SignatureDrop,
} from "@thirdweb-dev/contracts-js";
@@ -33,7 +33,7 @@ import { BigNumber, BigNumberish, ethers } from "ethers";
*/
export class DelayedReveal<
T extends
| DropERC721
| DropERC721_V3
| BaseDelayedRevealERC721
| SignatureDrop
| BaseDelayedRevealERC1155,
@@ -246,7 +246,9 @@ export class DelayedReveal<
return this.contractWrapper.readContract.getBatchIdAtIndex(i);
}
if (hasFunction<DropERC721>("baseURIIndices", this.contractWrapper)) {
if (
hasFunction<DropERC721_V3>("baseURIIndices", this.contractWrapper)
) {
return this.contractWrapper.readContract.baseURIIndices(i);
}
@@ -1,7 +1,12 @@
import { includesErrorMessage } from "../../common";
import {
abstractContractModelToLegacy,
abstractContractModelToNew,
convertQuantityToBigNumber,
fetchSnapshotEntryForAddress,
getClaimerProofs,
legacyContractModelToAbstract,
newContractModelToAbstract,
prepareClaim,
processClaimConditionInputs,
transformResultToClaimCondition,
@@ -12,32 +17,47 @@ import {
detectContractFeature,
hasFunction,
} from "../../common/feature-detection";
import { SnapshotFormatVersion } from "../../common/sharded-merkle-tree";
import { isNode } from "../../common/utils";
import { NATIVE_TOKEN_ADDRESS } from "../../constants/index";
import { ClaimEligibility } from "../../enums";
import { AmountSchema, SnapshotEntryWithProof } from "../../schema";
import {
AbstractClaimConditionContractStruct,
AmountSchema,
SnapshotEntryWithProof,
} from "../../schema";
import {
Amount,
ClaimCondition,
ClaimConditionFetchOptions,
ClaimConditionInput,
ClaimOptions,
ClaimVerification,
} from "../../types";
import { BaseClaimConditionERC721, BaseDropERC20 } from "../../types/eips";
import {
BaseClaimConditionERC721,
BaseDropERC20,
PrebuiltNFTDrop,
PrebuiltTokenDrop,
} from "../../types/eips";
import { TransactionResult } from "../types";
import { TransactionTask } from "./TransactionTask";
import { ContractMetadata } from "./contract-metadata";
import { ContractWrapper } from "./contract-wrapper";
import type {
ContractMetadata as ContractMetadataContract,
DropERC20,
DropERC721,
Drop,
DropERC20_V2,
DropERC721_V3,
DropSinglePhase,
DropSinglePhase_V1,
IDropSinglePhase_V1,
IERC20,
IERC20Metadata,
SignatureDrop,
} from "@thirdweb-dev/contracts-js";
import ERC20Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC20.json";
import { IDropClaimCondition } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC20";
import type { IDropClaimCondition_V2 } from "@thirdweb-dev/contracts-js/dist/declarations/src/IDropERC20_V2";
import type { IDropSinglePhase } from "@thirdweb-dev/contracts-js/src/DropSinglePhase";
import type { IClaimCondition } from "@thirdweb-dev/contracts-js/src/IDrop";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, constants, ethers, utils } from "ethers";
import deepEqual from "fast-deep-equal";
@@ -48,11 +68,10 @@ import deepEqual from "fast-deep-equal";
*/
export class DropClaimConditions<
TContract extends
| DropERC721
| DropERC20
| PrebuiltNFTDrop
| PrebuiltTokenDrop
| BaseClaimConditionERC721
| BaseDropERC20
| SignatureDrop,
| BaseDropERC20,
> {
private contractWrapper;
private metadata;
@@ -92,13 +111,37 @@ export class DropClaimConditions<
);
}
private async get(): Promise<IDropClaimCondition.ClaimConditionStructOutput> {
if (this.isSinglePhaseDropContract(this.contractWrapper)) {
return (await this.contractWrapper.readContract.claimCondition()) as IDropClaimCondition.ClaimConditionStructOutput;
} else if (this.isMultiPhaseDropContract(this.contractWrapper)) {
private async get(
conditionId?: BigNumberish,
): Promise<AbstractClaimConditionContractStruct> {
if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
const contractModel =
(await this.contractWrapper.readContract.claimCondition()) as IDropClaimCondition_V2.ClaimConditionStructOutput;
return legacyContractModelToAbstract(contractModel);
} else if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
const id =
await this.contractWrapper.readContract.getActiveClaimConditionId();
return await this.contractWrapper.readContract.getClaimConditionById(id);
conditionId !== undefined
? conditionId
: await this.contractWrapper.readContract.getActiveClaimConditionId();
const contractModel =
(await this.contractWrapper.readContract.getClaimConditionById(
id,
)) as IDropClaimCondition_V2.ClaimConditionStructOutput;
return legacyContractModelToAbstract(contractModel);
} else if (this.isNewSinglePhaseDrop(this.contractWrapper)) {
const contractModel =
(await this.contractWrapper.readContract.claimCondition()) as IClaimCondition.ClaimConditionStructOutput;
return newContractModelToAbstract(contractModel);
} else if (this.isNewMultiphaseDrop(this.contractWrapper)) {
const id =
conditionId !== undefined
? conditionId
: await this.contractWrapper.readContract.getActiveClaimConditionId();
const contractModel =
(await this.contractWrapper.readContract.getClaimConditionById(
id,
)) as IClaimCondition.ClaimConditionStruct;
return newContractModelToAbstract(contractModel);
} else {
throw new Error("Contract does not support claim conditions");
}
@@ -112,7 +155,10 @@ export class DropClaimConditions<
public async getAll(
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition[]> {
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
if (
this.isLegacyMultiPhaseDrop(this.contractWrapper) ||
this.isNewMultiphaseDrop(this.contractWrapper)
) {
const claimCondition =
(await this.contractWrapper.readContract.claimCondition()) as {
currentStartId: BigNumber;
@@ -120,11 +166,9 @@ export class DropClaimConditions<
};
const startId = claimCondition.currentStartId.toNumber();
const count = claimCondition.count.toNumber();
const conditions: IDropClaimCondition.ClaimConditionStructOutput[] = [];
const conditions: AbstractClaimConditionContractStruct[] = [];
for (let i = startId; i < startId + count; i++) {
conditions.push(
await this.contractWrapper.readContract.getClaimConditionById(i),
);
conditions.push(await this.get(i));
}
const metadata = await this.metadata.get();
const decimals = await this.getTokenDecimals();
@@ -234,82 +278,181 @@ export class DropClaimConditions<
const merkleRootArray = ethers.utils.stripZeros(
claimCondition.merkleRootHash,
);
if (merkleRootArray.length > 0) {
const hasAllowList = merkleRootArray.length > 0;
let allowListEntry;
if (hasAllowList) {
const merkleLower = claimCondition.merkleRootHash.toString();
const metadata = await this.metadata.get();
const proofs = await getClaimerProofs(
allowListEntry = await getClaimerProofs(
addressToCheck,
merkleLower,
await this.getTokenDecimals(),
metadata.merkle,
this.storage,
this.contractWrapper.getProvider(),
this.getSnapshotFormatVersion(),
);
try {
let validMerkleProof;
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId();
// legacy verifyClaimerMerkleProofs function
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
activeConditionIndex,
addressToCheck,
quantity,
proofs.proof,
proofs.maxClaimable,
);
} else if (this.isSinglePhaseDropContract(this.contractWrapper)) {
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
addressToCheck,
quantity,
{
proof: proofs.proof,
maxQuantityInAllowlist: proofs.maxClaimable,
},
);
}
if (!validMerkleProof) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} catch (e) {
if (
!allowListEntry &&
(this.isLegacySinglePhaseDrop(this.contractWrapper) ||
this.isLegacyMultiPhaseDrop(this.contractWrapper))
) {
// exclusive allowlist behavior
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
if (allowListEntry) {
try {
let validMerkleProof;
if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId();
// legacy verifyClaimerMerkleProofs function
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
activeConditionIndex,
addressToCheck,
quantity,
allowListEntry.proof,
allowListEntry.maxClaimable,
);
if (!validMerkleProof) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} else if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
addressToCheck,
quantity,
{
proof: allowListEntry.proof,
maxQuantityInAllowlist: allowListEntry.maxClaimable,
},
);
if (!validMerkleProof) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} else if (this.isNewSinglePhaseDrop(this.contractWrapper)) {
await this.contractWrapper.readContract.verifyClaim(
addressToCheck,
quantity,
claimCondition.currencyAddress,
claimCondition.price,
{
proof: allowListEntry.proof,
quantityLimitPerWallet: allowListEntry.maxClaimable,
currency: allowListEntry.currencyAddress,
pricePerToken: allowListEntry.price,
} as IDropSinglePhase.AllowlistProofStruct,
);
// TODO (cc) in new override format, anyone can claim (no allow list restriction)
// TODO (cc) instead check if maxClaimablePerWallet is 0 and this address has no overrides
// TODO (cc) meaning this address is not allowed to claim
if (
(convertQuantityToBigNumber(
claimCondition.maxClaimablePerWallet,
decimals,
).eq(0) &&
allowListEntry.maxClaimable === ethers.constants.MaxUint256) ||
allowListEntry.maxClaimable === BigNumber.from(0)
) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} else if (this.isNewMultiphaseDrop(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId();
await this.contractWrapper.readContract.verifyClaim(
activeConditionIndex,
addressToCheck,
quantity,
claimCondition.currencyAddress,
claimCondition.price,
{
proof: allowListEntry.proof,
quantityLimitPerWallet: allowListEntry.maxClaimable,
currency: allowListEntry.currencyAddress,
pricePerToken: allowListEntry.price,
} as IDropSinglePhase.AllowlistProofStruct,
);
if (
(convertQuantityToBigNumber(
claimCondition.maxClaimablePerWallet,
decimals,
).eq(0) &&
allowListEntry.maxClaimable === ethers.constants.MaxUint256) ||
allowListEntry.maxClaimable === BigNumber.from(0)
) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
}
} catch (e: any) {
console.warn(
"Merkle proof verification failed:",
"reason" in e ? e.reason : e,
);
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
}
}
// check for claim timestamp between claims
let [lastClaimedTimestamp, timestampForNextClaim] = [
BigNumber.from(0),
BigNumber.from(0),
];
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId();
[lastClaimedTimestamp, timestampForNextClaim] =
await this.contractWrapper.readContract.getClaimTimestamp(
activeConditionIndex,
addressToCheck,
);
} else if (this.isSinglePhaseDropContract(this.contractWrapper)) {
// check for claim timestamp between claims
[lastClaimedTimestamp, timestampForNextClaim] =
await this.contractWrapper.readContract.getClaimTimestamp(
addressToCheck,
);
// TODO (cc) check for max claimable per wallet and how much each wallet has claimed
if (
this.isNewSinglePhaseDrop(this.contractWrapper) ||
this.isNewMultiphaseDrop(this.contractWrapper)
) {
if (!hasAllowList || (hasAllowList && !allowListEntry)) {
if (
convertQuantityToBigNumber(
claimCondition.maxClaimablePerWallet,
decimals,
).eq(0)
) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
}
}
const now = BigNumber.from(Date.now()).div(1000);
// check for claim timestamp between claims (ONLY FOR LEGACY)
if (
this.isLegacySinglePhaseDrop(this.contractWrapper) ||
this.isLegacyMultiPhaseDrop(this.contractWrapper)
) {
let [lastClaimedTimestamp, timestampForNextClaim] = [
BigNumber.from(0),
BigNumber.from(0),
];
if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId();
[lastClaimedTimestamp, timestampForNextClaim] =
await this.contractWrapper.readContract.getClaimTimestamp(
activeConditionIndex,
addressToCheck,
);
} else if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
// check for claim timestamp between claims
[lastClaimedTimestamp, timestampForNextClaim] =
await this.contractWrapper.readContract.getClaimTimestamp(
addressToCheck,
);
}
if (lastClaimedTimestamp.gt(0) && now.lt(timestampForNextClaim)) {
// contract will return MaxUint256 if user has already claimed and cannot claim again
if (timestampForNextClaim.eq(constants.MaxUint256)) {
reasons.push(ClaimEligibility.AlreadyClaimed);
} else {
reasons.push(ClaimEligibility.WaitBeforeNextClaimTransaction);
const now = BigNumber.from(Date.now()).div(1000);
if (lastClaimedTimestamp.gt(0) && now.lt(timestampForNextClaim)) {
// contract will return MaxUint256 if user has already claimed and cannot claim again
if (timestampForNextClaim.eq(constants.MaxUint256)) {
reasons.push(ClaimEligibility.AlreadyClaimed);
} else {
reasons.push(ClaimEligibility.WaitBeforeNextClaimTransaction);
}
}
}
@@ -342,23 +485,25 @@ export class DropClaimConditions<
/**
* Returns allow list information and merkle proofs for the given address.
* @param claimerAddress
* @param claimerAddress - the claimer address
* @param claimConditionId - optional the claim condition id to get the proofs for
*/
public async getClaimerProofs(
claimerAddress: string,
claimConditionId?: BigNumberish,
): Promise<SnapshotEntryWithProof | null> {
const claimCondition = await this.getActive();
const merkeRoot = claimCondition.merkleRootHash;
const merkleRootArray = ethers.utils.stripZeros(
claimCondition.merkleRootHash,
);
const claimCondition = await this.get(claimConditionId);
const merkleRoot = claimCondition.merkleRoot;
const merkleRootArray = ethers.utils.stripZeros(merkleRoot);
if (merkleRootArray.length > 0) {
const metadata = await this.metadata.get();
return await fetchSnapshotEntryForAddress(
claimerAddress,
merkeRoot.toString(),
merkleRoot.toString(),
metadata.merkle,
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
} else {
return null;
@@ -409,16 +554,19 @@ export class DropClaimConditions<
resetClaimEligibilityForAll = false,
): Promise<TransactionResult> {
let claimConditionsProcessed = claimConditionInputs;
if (this.isSinglePhaseDropContract(this.contractWrapper)) {
if (
this.isLegacySinglePhaseDrop(this.contractWrapper) ||
this.isNewSinglePhaseDrop(this.contractWrapper)
) {
resetClaimEligibilityForAll = true;
if (claimConditionInputs.length === 0) {
claimConditionsProcessed = [
{
startTime: new Date(0),
currencyAddress: NATIVE_TOKEN_ADDRESS,
currencyAddress: ethers.constants.AddressZero,
price: 0,
maxQuantity: 0,
quantityLimitPerTransaction: 0,
maxClaimableSupply: 0,
maxClaimablePerWallet: 0,
waitInSeconds: 0,
merkleRootHash: utils.hexZeroPad([0], 32),
snapshot: [],
@@ -437,6 +585,7 @@ export class DropClaimConditions<
await this.getTokenDecimals(),
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
const merkleInfo: { [key: string]: string } = {};
@@ -457,6 +606,8 @@ export class DropClaimConditions<
mergedMetadata,
);
// TODO (cc) we could write the merkle tree info on the claim condition metadata instead
// TODO (cc) but we still need to maintain the behavior here for older contracts
if (
hasFunction<ContractMetadataContract>(
"setContractURI",
@@ -477,17 +628,31 @@ export class DropClaimConditions<
}
const cw = this.contractWrapper;
if (this.isSinglePhaseDropContract(cw)) {
if (this.isLegacySinglePhaseDrop(cw)) {
encoded.push(
cw.readContract.interface.encodeFunctionData("setClaimConditions", [
sortedConditions[0],
abstractContractModelToLegacy(sortedConditions[0]),
resetClaimEligibilityForAll,
]),
);
} else if (this.isMultiPhaseDropContract(cw)) {
} else if (this.isLegacyMultiPhaseDrop(cw)) {
encoded.push(
cw.readContract.interface.encodeFunctionData("setClaimConditions", [
sortedConditions,
sortedConditions.map(abstractContractModelToLegacy),
resetClaimEligibilityForAll,
]),
);
} else if (this.isNewSinglePhaseDrop(cw)) {
encoded.push(
cw.readContract.interface.encodeFunctionData("setClaimConditions", [
abstractContractModelToNew(sortedConditions[0]),
resetClaimEligibilityForAll,
]),
);
} else if (this.isNewMultiphaseDrop(cw)) {
encoded.push(
cw.readContract.interface.encodeFunctionData("setClaimConditions", [
sortedConditions.map(abstractContractModelToNew),
resetClaimEligibilityForAll,
]),
);
@@ -550,21 +715,132 @@ export class DropClaimConditions<
this.contractWrapper,
this.storage,
checkERC20Allowance,
this.getSnapshotFormatVersion(),
);
}
private isSinglePhaseDropContract(
public async getClaimArguments(
destinationAddress: string,
quantity: BigNumberish,
claimVerification: ClaimVerification,
): Promise<any[]> {
const activeClaimCondition = await this.getActive();
if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
return [
destinationAddress,
quantity,
activeClaimCondition.currencyAddress,
activeClaimCondition.price,
claimVerification.proofs,
claimVerification.maxClaimable,
];
} else if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
return [
destinationAddress,
quantity,
activeClaimCondition.currencyAddress,
activeClaimCondition.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxClaimable,
} as IDropSinglePhase_V1.AllowlistProofStruct,
ethers.utils.toUtf8Bytes(""),
];
}
return [
destinationAddress,
quantity,
activeClaimCondition.currencyAddress,
activeClaimCondition.price,
{
proof: claimVerification.proofs,
quantityLimitPerWallet: claimVerification.maxClaimable,
pricePerToken: claimVerification.price,
currency: claimVerification.currencyAddress,
} as IDropSinglePhase.AllowlistProofStruct,
ethers.utils.toUtf8Bytes(""),
];
}
/**
* Construct a claim transaction without executing it.
* This is useful for estimating the gas cost of a claim transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param destinationAddress
* @param quantity
* @param options
*/
public async getClaimTransaction(
destinationAddress: string,
quantity: BigNumberish,
options?: ClaimOptions,
): Promise<TransactionTask> {
if (options?.pricePerToken) {
throw new Error(
"Price per token is be set via claim conditions by calling `contract.erc721.claimConditions.set()`",
);
}
const claimVerification = await this.prepareClaim(
quantity,
options?.checkERC20Allowance === undefined
? true
: options.checkERC20Allowance,
await this.getTokenDecimals(),
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: await this.getClaimArguments(
destinationAddress,
quantity,
claimVerification,
),
overrides: claimVerification.overrides,
});
}
isNewSinglePhaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropSinglePhase> {
return !hasFunction<DropSinglePhase>(
"getClaimConditionById",
contractWrapper,
return (
!hasFunction<DropSinglePhase>("getClaimConditionById", contractWrapper) &&
hasFunction<DropSinglePhase>("getSupplyClaimedByWallet", contractWrapper)
);
}
private isMultiPhaseDropContract(
isNewMultiphaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropERC721 | DropERC20> {
return hasFunction<DropERC721>("getClaimConditionById", contractWrapper);
): contractWrapper is ContractWrapper<Drop> {
return (
hasFunction<Drop>("getClaimConditionById", contractWrapper) &&
hasFunction<Drop>("getSupplyClaimedByWallet", contractWrapper)
);
}
isLegacySinglePhaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropSinglePhase_V1> {
return (
!hasFunction<DropSinglePhase_V1>(
"getClaimConditionById",
contractWrapper,
) && hasFunction<DropSinglePhase_V1>("getClaimTimestamp", contractWrapper)
);
}
isLegacyMultiPhaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropERC721_V3 | DropERC20_V2> {
return (
hasFunction<DropERC721_V3>("getClaimConditionById", contractWrapper) &&
hasFunction<DropERC721_V3>("setWalletClaimCount", contractWrapper)
);
}
private getSnapshotFormatVersion(): SnapshotFormatVersion {
return this.isLegacyMultiPhaseDrop(this.contractWrapper) ||
this.isLegacySinglePhaseDrop(this.contractWrapper)
? SnapshotFormatVersion.V1
: SnapshotFormatVersion.V2;
}
}
@@ -1,7 +1,11 @@
import { includesErrorMessage } from "../../common";
import {
abstractContractModelToLegacy,
abstractContractModelToNew,
fetchSnapshotEntryForAddress,
getClaimerProofs,
legacyContractModelToAbstract,
newContractModelToAbstract,
prepareClaim,
processClaimConditionInputs,
transformResultToClaimCondition,
@@ -9,29 +13,41 @@ import {
} from "../../common/claim-conditions";
import { isNativeToken } from "../../common/currency";
import { hasFunction } from "../../common/feature-detection";
import { SnapshotFormatVersion } from "../../common/sharded-merkle-tree";
import { isNode } from "../../common/utils";
import { NATIVE_TOKEN_ADDRESS } from "../../constants/index";
import { ClaimEligibility } from "../../enums";
import { SnapshotEntryWithProof } from "../../schema";
import {
AbstractClaimConditionContractStruct,
SnapshotEntryWithProof,
} from "../../schema";
import {
ClaimCondition,
ClaimConditionFetchOptions,
ClaimConditionInput,
ClaimConditionsForToken,
ClaimOptions,
ClaimVerification,
} from "../../types";
import { BaseClaimConditionERC1155 } from "../../types/eips";
import { TransactionResult } from "../index";
import {
BaseClaimConditionERC1155,
PrebuiltEditionDrop,
} from "../../types/eips";
import { TransactionResult, TransactionTask } from "../index";
import { ContractMetadata } from "./contract-metadata";
import { ContractWrapper } from "./contract-wrapper";
import type {
ContractMetadata as ContractMetadataContract,
DropERC1155,
Drop1155,
DropERC1155_V2,
DropSinglePhase1155,
DropSinglePhase1155_V1,
IDropSinglePhase,
IDropSinglePhase_V1,
IERC20,
} from "@thirdweb-dev/contracts-js";
import IERC20ABI from "@thirdweb-dev/contracts-js/dist/abis/IERC20.json";
import { IDropClaimCondition } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC1155";
import type { IDropClaimCondition_V2 } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC20_V2";
import type { IClaimCondition } from "@thirdweb-dev/contracts-js/src/IDrop";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, constants, ethers, utils } from "ethers";
import deepEqual from "fast-deep-equal";
@@ -41,7 +57,7 @@ import deepEqual from "fast-deep-equal";
* @public
*/
export class DropErc1155ClaimConditions<
TContract extends DropERC1155 | BaseClaimConditionERC1155,
TContract extends PrebuiltEditionDrop | BaseClaimConditionERC1155,
> {
private contractWrapper;
private metadata;
@@ -84,20 +100,45 @@ export class DropErc1155ClaimConditions<
private async get(
tokenId: BigNumberish,
): Promise<IDropClaimCondition.ClaimConditionStructOutput> {
if (this.isSinglePhaseDropContract(this.contractWrapper)) {
return (await this.contractWrapper.readContract.claimCondition(
tokenId,
)) as IDropClaimCondition.ClaimConditionStructOutput;
} else if (this.isMultiPhaseDropContract(this.contractWrapper)) {
conditionId?: BigNumberish,
): Promise<AbstractClaimConditionContractStruct> {
if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
const contractModel = await (
this.contractWrapper.readContract as DropSinglePhase1155_V1
).claimCondition(tokenId);
return legacyContractModelToAbstract(contractModel);
} else if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
const id =
await this.contractWrapper.readContract.getActiveClaimConditionId(
conditionId !== undefined
? conditionId
: await this.contractWrapper.readContract.getActiveClaimConditionId(
tokenId,
);
const contractModel =
(await this.contractWrapper.readContract.getClaimConditionById(
tokenId,
);
return await this.contractWrapper.readContract.getClaimConditionById(
tokenId,
id,
);
id,
)) as IDropClaimCondition_V2.ClaimConditionStructOutput;
return legacyContractModelToAbstract(contractModel);
} else if (this.isNewSinglePhaseDrop(this.contractWrapper)) {
const contractModel =
(await this.contractWrapper.readContract.claimCondition(
tokenId,
)) as IClaimCondition.ClaimConditionStructOutput;
return newContractModelToAbstract(contractModel);
} else if (this.isNewMultiphaseDrop(this.contractWrapper)) {
const id =
conditionId !== undefined
? conditionId
: await this.contractWrapper.readContract.getActiveClaimConditionId(
tokenId,
);
const contractModel =
(await this.contractWrapper.readContract.getClaimConditionById(
tokenId,
id,
)) as IClaimCondition.ClaimConditionStruct;
return newContractModelToAbstract(contractModel);
} else {
throw new Error("Contract does not support claim conditions");
}
@@ -112,7 +153,10 @@ export class DropErc1155ClaimConditions<
tokenId: BigNumberish,
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition[]> {
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
if (
this.isLegacyMultiPhaseDrop(this.contractWrapper) ||
this.isNewMultiphaseDrop(this.contractWrapper)
) {
const claimCondition =
(await this.contractWrapper.readContract.claimCondition(tokenId)) as {
currentStartId: BigNumber;
@@ -120,14 +164,9 @@ export class DropErc1155ClaimConditions<
};
const startId = claimCondition.currentStartId.toNumber();
const count = claimCondition.count.toNumber();
const conditions: IDropClaimCondition.ClaimConditionStructOutput[] = [];
const conditions: AbstractClaimConditionContractStruct[] = [];
for (let i = startId; i < startId + count; i++) {
conditions.push(
await this.contractWrapper.readContract.getClaimConditionById(
tokenId,
i,
),
);
conditions.push(await this.get(tokenId, i));
}
const metadata = await this.metadata.get();
return Promise.all(
@@ -143,7 +182,7 @@ export class DropErc1155ClaimConditions<
),
);
} else {
return [await this.getActive(tokenId)];
return [await this.getActive(tokenId, options)];
}
}
@@ -232,52 +271,142 @@ export class DropErc1155ClaimConditions<
const merkleRootArray = ethers.utils.stripZeros(
claimCondition.merkleRootHash,
);
if (merkleRootArray.length > 0) {
const hasAllowList = merkleRootArray.length > 0;
let allowListEntry;
if (hasAllowList) {
const merkleLower = claimCondition.merkleRootHash.toString();
const metadata = await this.metadata.get();
const proofs = await getClaimerProofs(
allowListEntry = await getClaimerProofs(
addressToCheck,
merkleLower,
0,
metadata.merkle,
this.storage,
this.contractWrapper.getProvider(),
this.getSnapshotFormatVersion(),
);
try {
let validMerkleProof;
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId(
if (
!allowListEntry &&
(this.isLegacySinglePhaseDrop(this.contractWrapper) ||
this.isLegacyMultiPhaseDrop(this.contractWrapper))
) {
// exclusive allowlist behavior
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
if (allowListEntry) {
try {
let validMerkleProof;
if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId(
tokenId,
);
// legacy verifyClaimerMerkleProofs function
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
activeConditionIndex,
addressToCheck,
tokenId,
quantity,
allowListEntry.proof,
allowListEntry.maxClaimable,
);
if (!validMerkleProof) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} else if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
tokenId,
addressToCheck,
quantity,
{
proof: allowListEntry.proof,
maxQuantityInAllowlist: allowListEntry.maxClaimable,
},
);
if (!validMerkleProof) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} else if (this.isNewSinglePhaseDrop(this.contractWrapper)) {
await this.contractWrapper.readContract.verifyClaim(
tokenId,
addressToCheck,
quantity,
claimCondition.currencyAddress,
claimCondition.price,
{
proof: allowListEntry.proof,
quantityLimitPerWallet: allowListEntry.maxClaimable,
currency: allowListEntry.currencyAddress,
pricePerToken: allowListEntry.price,
} as IDropSinglePhase.AllowlistProofStruct,
);
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
// TODO (cc) in new override format, anyone can claim (no allow list restriction)
// TODO (cc) instead check if maxClaimablePerWallet is 0 and this address has no overrides
// TODO (cc) meaning this address is not allowed to claim
if (
(claimCondition.maxClaimablePerWallet === "0" &&
allowListEntry.maxClaimable === ethers.constants.MaxUint256) ||
allowListEntry.maxClaimable === BigNumber.from(0)
) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} else if (this.isNewMultiphaseDrop(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId(
tokenId,
);
await this.contractWrapper.readContract.verifyClaim(
activeConditionIndex,
addressToCheck,
tokenId,
quantity,
proofs.proof,
proofs.maxClaimable,
);
} else if (this.isSinglePhaseDropContract(this.contractWrapper)) {
[validMerkleProof] =
await this.contractWrapper.readContract.verifyClaimMerkleProof(
tokenId,
addressToCheck,
quantity,
claimCondition.currencyAddress,
claimCondition.price,
{
proof: proofs.proof,
maxQuantityInAllowlist: proofs.maxClaimable,
},
proof: allowListEntry.proof,
quantityLimitPerWallet: allowListEntry.maxClaimable,
currency: allowListEntry.currencyAddress,
pricePerToken: allowListEntry.price,
} as IDropSinglePhase.AllowlistProofStruct,
);
}
if (!validMerkleProof) {
if (
(claimCondition.maxClaimablePerWallet === "0" &&
allowListEntry.maxClaimable === ethers.constants.MaxUint256) ||
allowListEntry.maxClaimable === BigNumber.from(0)
) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
}
} catch (e: any) {
console.warn(
"Merkle proof verification failed:",
"reason" in e ? e.reason : e,
);
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
}
}
// TODO (cc) check for max claimable per wallet and how much each wallet has claimed
if (
this.isNewSinglePhaseDrop(this.contractWrapper) ||
this.isNewMultiphaseDrop(this.contractWrapper)
) {
if (!hasAllowList || (hasAllowList && !allowListEntry)) {
if (claimCondition.maxClaimablePerWallet === "0") {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
} catch (e) {
reasons.push(ClaimEligibility.AddressNotAllowed);
return reasons;
}
}
@@ -286,7 +415,7 @@ export class DropErc1155ClaimConditions<
BigNumber.from(0),
BigNumber.from(0),
];
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
activeConditionIndex =
await this.contractWrapper.readContract.getActiveClaimConditionId(
tokenId,
@@ -297,7 +426,7 @@ export class DropErc1155ClaimConditions<
activeConditionIndex,
addressToCheck,
);
} else if (this.isSinglePhaseDropContract(this.contractWrapper)) {
} else if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
[lastClaimedTimestamp, timestampForNextClaim] =
await this.contractWrapper.readContract.getClaimTimestamp(
tokenId,
@@ -346,25 +475,27 @@ export class DropErc1155ClaimConditions<
/**
* Returns allow list information and merkle proofs for the given address.
* @param tokenId
* @param claimerAddress
* @param tokenId - the token ID to check
* @param claimerAddress - the claimer address
* @param claimConditionId - optional the claim condition id to get the proofs for
*/
public async getClaimerProofs(
tokenId: BigNumberish,
claimerAddress: string,
claimConditionId?: BigNumberish,
): Promise<SnapshotEntryWithProof | null> {
const claimCondition = await this.getActive(tokenId);
const merkeRoot = claimCondition.merkleRootHash;
const merkleRootArray = ethers.utils.stripZeros(
claimCondition.merkleRootHash,
);
const claimCondition = await this.get(tokenId, claimConditionId);
const merkleRoot = claimCondition.merkleRoot;
const merkleRootArray = ethers.utils.stripZeros(merkleRoot);
if (merkleRootArray.length > 0) {
const metadata = await this.metadata.get();
return await fetchSnapshotEntryForAddress(
claimerAddress,
merkeRoot.toString(),
merkleRoot.toString(),
metadata.merkle,
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
} else {
return null;
@@ -469,16 +600,16 @@ export class DropErc1155ClaimConditions<
claimConditionsForToken.map(async ({ tokenId, claimConditions }) => {
// sanitize for single phase deletions
let claimConditionsProcessed = claimConditions;
if (this.isSinglePhaseDropContract(this.contractWrapper)) {
if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
resetClaimEligibilityForAll = true;
if (claimConditions.length === 0) {
claimConditionsProcessed = [
{
startTime: new Date(0),
currencyAddress: NATIVE_TOKEN_ADDRESS,
currencyAddress: ethers.constants.AddressZero,
price: 0,
maxQuantity: 0,
quantityLimitPerTransaction: 0,
maxClaimableSupply: 0,
maxClaimablePerWallet: 0,
waitInSeconds: 0,
merkleRootHash: utils.hexZeroPad([0], 32),
snapshot: [],
@@ -497,6 +628,7 @@ export class DropErc1155ClaimConditions<
0,
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
snapshotInfos.forEach((s) => {
@@ -548,18 +680,48 @@ export class DropErc1155ClaimConditions<
}
processedClaimConditions.forEach(({ tokenId, sortedConditions }) => {
if (this.isSinglePhaseDropContract(this.contractWrapper)) {
if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
encoded.push(
this.contractWrapper.readContract.interface.encodeFunctionData(
"setClaimConditions",
[tokenId, sortedConditions[0], resetClaimEligibilityForAll],
[
tokenId,
abstractContractModelToLegacy(sortedConditions[0]),
resetClaimEligibilityForAll,
],
),
);
} else if (this.isMultiPhaseDropContract(this.contractWrapper)) {
} else if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
encoded.push(
this.contractWrapper.readContract.interface.encodeFunctionData(
"setClaimConditions",
[tokenId, sortedConditions, resetClaimEligibilityForAll],
[
tokenId,
sortedConditions.map(abstractContractModelToLegacy),
resetClaimEligibilityForAll,
],
),
);
} else if (this.isNewSinglePhaseDrop(this.contractWrapper)) {
encoded.push(
this.contractWrapper.readContract.interface.encodeFunctionData(
"setClaimConditions",
[
tokenId,
abstractContractModelToNew(sortedConditions[0]),
resetClaimEligibilityForAll,
],
),
);
} else if (this.isNewMultiphaseDrop(this.contractWrapper)) {
encoded.push(
this.contractWrapper.readContract.interface.encodeFunctionData(
"setClaimConditions",
[
tokenId,
sortedConditions.map(abstractContractModelToNew),
resetClaimEligibilityForAll,
],
),
);
} else {
@@ -610,21 +772,142 @@ export class DropErc1155ClaimConditions<
this.contractWrapper,
this.storage,
checkERC20Allowance,
this.getSnapshotFormatVersion(),
);
}
private isSinglePhaseDropContract(
public async getClaimArguments(
tokenId: BigNumberish,
destinationAddress: string,
quantity: BigNumberish,
claimVerification: ClaimVerification,
): Promise<any[]> {
const activeClaimCondition = await this.getActive(tokenId);
if (this.isLegacyMultiPhaseDrop(this.contractWrapper)) {
return [
destinationAddress,
tokenId,
quantity,
activeClaimCondition.currencyAddress,
activeClaimCondition.price,
claimVerification.proofs,
claimVerification.maxClaimable,
];
} else if (this.isLegacySinglePhaseDrop(this.contractWrapper)) {
return [
destinationAddress,
tokenId,
quantity,
activeClaimCondition.currencyAddress,
activeClaimCondition.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxClaimable,
} as IDropSinglePhase_V1.AllowlistProofStruct,
ethers.utils.toUtf8Bytes(""),
];
}
return [
destinationAddress,
tokenId,
quantity,
activeClaimCondition.currencyAddress,
activeClaimCondition.price,
{
proof: claimVerification.proofs,
quantityLimitPerWallet: claimVerification.maxClaimable,
pricePerToken: claimVerification.price,
currency: claimVerification.currencyAddress,
} as IDropSinglePhase.AllowlistProofStruct,
ethers.utils.toUtf8Bytes(""),
];
}
/**
* Construct a claim transaction without executing it.
* This is useful for estimating the gas cost of a claim transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param destinationAddress - Address you want to send the token to
* @param tokenId - Id of the token you want to claim
* @param quantity - Quantity of the tokens you want to claim
*/
public async getClaimTransaction(
destinationAddress: string,
tokenId: BigNumberish,
quantity: BigNumberish,
options?: ClaimOptions,
): Promise<TransactionTask> {
if (options?.pricePerToken) {
throw new Error(
"Price per token should be set via claim conditions by calling `contract.erc1155.claimConditions.set()`",
);
}
const claimVerification = await this.prepareClaim(
tokenId,
quantity,
options?.checkERC20Allowance || true,
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: await this.getClaimArguments(
tokenId,
destinationAddress,
quantity,
claimVerification,
),
overrides: claimVerification.overrides,
});
}
isNewSinglePhaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropSinglePhase1155> {
return !hasFunction<DropSinglePhase1155>(
"getClaimConditionById",
contractWrapper,
return (
!hasFunction<DropSinglePhase1155>(
"getClaimConditionById",
contractWrapper,
) &&
hasFunction<DropSinglePhase1155>(
"getSupplyClaimedByWallet",
contractWrapper,
)
);
}
private isMultiPhaseDropContract(
isNewMultiphaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropERC1155> {
return hasFunction<DropERC1155>("getClaimConditionById", contractWrapper);
): contractWrapper is ContractWrapper<Drop1155> {
return (
hasFunction<Drop1155>("getClaimConditionById", contractWrapper) &&
hasFunction<Drop1155>("getSupplyClaimedByWallet", contractWrapper)
);
}
isLegacySinglePhaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropSinglePhase1155_V1> {
return (
!hasFunction<DropSinglePhase1155_V1>(
"getClaimConditionById",
contractWrapper,
) &&
hasFunction<DropSinglePhase1155_V1>("getClaimTimestamp", contractWrapper)
);
}
isLegacyMultiPhaseDrop(
contractWrapper: ContractWrapper<any>,
): contractWrapper is ContractWrapper<DropERC1155_V2> {
return (
hasFunction<DropERC1155_V2>("getClaimConditionById", contractWrapper) &&
hasFunction<DropERC1155_V2>("setWalletClaimCount", contractWrapper)
);
}
private getSnapshotFormatVersion(): SnapshotFormatVersion {
return this.isLegacyMultiPhaseDrop(this.contractWrapper) ||
this.isLegacySinglePhaseDrop(this.contractWrapper)
? SnapshotFormatVersion.V1
: SnapshotFormatVersion.V2;
}
}
@@ -1,5 +1,5 @@
import { PrebuiltEditionDrop } from "../../types/eips";
import { ContractEvents } from "./contract-events";
import type { DropERC1155 } from "@thirdweb-dev/contracts-js";
import { BigNumber, BigNumberish } from "ethers";
/**
@@ -9,7 +9,7 @@ import { BigNumber, BigNumberish } from "ethers";
export class DropErc1155History {
private events;
constructor(events: ContractEvents<DropERC1155>) {
constructor(events: ContractEvents<PrebuiltEditionDrop>) {
this.events = events;
}
@@ -1,20 +1,14 @@
import { hasFunction } from "../../common";
import { FEATURE_EDITION_CLAIMABLE_WITH_CONDITIONS } from "../../constants/erc1155-features";
import { CustomContractSchema } from "../../schema/contracts/custom";
import {
ClaimOptions,
ClaimVerification,
} from "../../types/claim-conditions/claim-conditions";
import { ClaimOptions } from "../../types/claim-conditions/claim-conditions";
import { BaseClaimConditionERC1155 } from "../../types/eips";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult } from "../types";
import { TransactionTask } from "./TransactionTask";
import { ContractMetadata } from "./contract-metadata";
import { ContractWrapper } from "./contract-wrapper";
import { DropErc1155ClaimConditions } from "./drop-erc1155-claim-conditions";
import type { DropERC1155 } from "@thirdweb-dev/contracts-js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumberish, ethers } from "ethers";
import { BigNumberish } from "ethers";
/**
* Configure and claim ERC1155 NFTs
@@ -52,42 +46,6 @@ export class Erc1155ClaimableWithConditions implements DetectableFeature {
);
}
/**
* Construct a claim transaction without executing it.
* This is useful for estimating the gas cost of a claim transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param destinationAddress - Address you want to send the token to
* @param tokenId - Id of the token you want to claim
* @param quantity - Quantity of the tokens you want to claim
*/
public async getClaimTransaction(
destinationAddress: string,
tokenId: BigNumberish,
quantity: BigNumberish,
options?: ClaimOptions,
): Promise<TransactionTask> {
if (options?.pricePerToken) {
throw new Error(
"In ERC1155ClaimableWithConditions, price per token is be set via claim conditions by calling `contract.erc1155.claimConditions.set()`",
);
}
const claimVerification = await this.conditions.prepareClaim(
tokenId,
quantity,
options?.checkERC20Allowance || true,
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: await this.getArgs(
tokenId,
destinationAddress,
quantity,
claimVerification,
),
overrides: claimVerification.overrides,
});
}
/**
* Claim NFTs to a specific Wallet
*
@@ -115,7 +73,7 @@ export class Erc1155ClaimableWithConditions implements DetectableFeature {
quantity: BigNumberish,
options?: ClaimOptions,
): Promise<TransactionResult> {
const tx = await this.getClaimTransaction(
const tx = await this.conditions.getClaimTransaction(
destinationAddress,
tokenId,
quantity,
@@ -123,51 +81,4 @@ export class Erc1155ClaimableWithConditions implements DetectableFeature {
);
return await tx.execute();
}
private async getArgs(
tokenId: BigNumberish,
destinationAddress: string,
quantity: BigNumberish,
claimVerification: ClaimVerification,
): Promise<any[]> {
const isLegacyNFTContract = await this.isLegacyNFTContract();
if (isLegacyNFTContract) {
return [
destinationAddress,
tokenId,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
];
}
return [
destinationAddress,
tokenId,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxQuantityPerTransaction,
},
ethers.utils.toUtf8Bytes(""),
];
}
private async isLegacyNFTContract() {
if (hasFunction<DropERC1155>("contractType", this.contractWrapper)) {
try {
const contractType = ethers.utils.toUtf8String(
await this.contractWrapper.readContract.contractType(),
);
return contractType.includes("DropERC1155");
} catch (e) {
return false;
}
} else {
return false;
}
}
}
@@ -3,6 +3,7 @@ import {
detectContractFeature,
hasFunction,
} from "../../common/feature-detection";
import { getPrebuiltInfo } from "../../common/legacy";
import { uploadOrExtractURIs } from "../../common/nft";
import {
FEATURE_EDITION_LAZY_MINTABLE,
@@ -21,10 +22,7 @@ import { DelayedReveal } from "./delayed-reveal";
import { Erc1155 } from "./erc-1155";
import { ERC1155Claimable } from "./erc-1155-claimable";
import { Erc1155ClaimableWithConditions } from "./erc-1155-claimable-with-conditions";
import type {
IClaimableERC1155,
TokenERC721,
} from "@thirdweb-dev/contracts-js";
import type { IClaimableERC1155 } from "@thirdweb-dev/contracts-js";
import { TokensLazyMintedEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/LazyMint";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { ethers } from "ethers";
@@ -232,17 +230,10 @@ export class Erc1155LazyMintable implements DetectableFeature {
}
private async isLegacyEditionDropContract() {
if (hasFunction<TokenERC721>("contractType", this.contractWrapper)) {
try {
const contractType = ethers.utils.toUtf8String(
await this.contractWrapper.readContract.contractType(),
);
return contractType.includes("DropERC1155");
} catch (e) {
return false;
}
} else {
return false;
}
const info = await getPrebuiltInfo(
this.contractWrapper.readContract.address,
this.contractWrapper.getProvider(),
);
return info && info.type === "DropERC1155" && info.version < 3;
}
}
@@ -172,7 +172,7 @@ export class Erc1155SignatureMintable implements DetectableFeature {
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* metadata: nftMetadata, // The NFT to mint
* to: {{wallet_address}}, // Who will receive the NFT (or AddressZero for anyone)
* to: {{wallet_address}}, // Who will receive the NFT
* quantity: 2, // the quantity of NFTs to mint
* price: 0.5, // the price per NFT
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
@@ -214,7 +214,7 @@ export class Erc1155SignatureMintable implements DetectableFeature {
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* metadata: nftMetadata, // The NFT to mint
* to: {{wallet_address}}, // Who will receive the NFT (or AddressZero for anyone)
* to: {{wallet_address}}, // Who will receive the NFT
* quantity: 2, // the quantity of NFTs to mint
* price: 0.5, // the price per NFT
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
@@ -793,7 +793,7 @@ export class Erc1155<
const claimWithConditions = this.lazyMintable?.claimWithConditions;
const claim = this.lazyMintable?.claim;
if (claimWithConditions) {
return claimWithConditions.getClaimTransaction(
return claimWithConditions.conditions.getClaimTransaction(
destinationAddress,
tokenId,
quantity,
@@ -1,6 +1,6 @@
import { FEATURE_TOKEN_CLAIMABLE_WITH_CONDITIONS } from "../../constants/erc20-features";
import { CustomContractSchema } from "../../schema/contracts/custom";
import { ClaimVerification } from "../../types";
import { ClaimOptions } from "../../types";
import { Amount } from "../../types/currency";
import { BaseDropERC20 } from "../../types/eips";
import { DetectableFeature } from "../interfaces/DetectableFeature";
@@ -20,7 +20,7 @@ import { ThirdwebStorage } from "@thirdweb-dev/storage";
* await contract.token.drop.claim.to("0x...", quantity);
* ```
*/
export class Erc20Claimable implements DetectableFeature {
export class Erc20ClaimableWithConditions implements DetectableFeature {
featureName = FEATURE_TOKEN_CLAIMABLE_WITH_CONDITIONS.name;
/**
* Configure claim conditions
@@ -93,37 +93,14 @@ export class Erc20Claimable implements DetectableFeature {
public async to(
destinationAddress: string,
amount: Amount,
checkERC20Allowance = true,
claimData?: ClaimVerification,
options?: ClaimOptions,
): Promise<TransactionResult> {
const quantity = await this.erc20.normalizeAmount(amount);
let claimVerification = claimData;
if (this.conditions && !claimData) {
claimVerification = await this.conditions.prepareClaim(
quantity,
checkERC20Allowance,
await this.contractWrapper.readContract.decimals(),
);
}
if (!claimVerification) {
throw new Error(
"Claim verification Data is required - either pass it in as 'claimData' or set claim conditions via 'conditions.set()'",
);
}
const receipt = await this.contractWrapper.sendTransaction(
"claim",
[
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
],
claimVerification.overrides,
const task = await this.conditions.getClaimTransaction(
destinationAddress,
quantity,
options,
);
return { receipt };
return await task.execute();
}
}
@@ -1,7 +1,7 @@
import { BaseDropERC20 } from "../../types/eips";
import { ContractWrapper } from "./contract-wrapper";
import { Erc20 } from "./erc-20";
import { Erc20Claimable } from "./erc-20-claimable";
import { Erc20ClaimableWithConditions } from "./erc-20-claimable-with-conditions";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
/**
@@ -36,7 +36,7 @@ export class Erc20Droppable {
* await contract.nft.drop.claim.conditions.set(claimConditions);
* ```
*/
public claim: Erc20Claimable;
public claim: Erc20ClaimableWithConditions;
private contractWrapper: ContractWrapper<BaseDropERC20>;
private erc20: Erc20;
private storage: ThirdwebStorage;
@@ -50,7 +50,7 @@ export class Erc20Droppable {
this.contractWrapper = contractWrapper;
this.storage = storage;
this.claim = new Erc20Claimable(
this.claim = new Erc20ClaimableWithConditions(
this.erc20,
this.contractWrapper,
this.storage,
@@ -123,7 +123,7 @@ export class Erc20SignatureMintable implements DetectableFeature {
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* quantity: 4.2, // The quantity of tokens to be minted
* to: {{wallet_address}}, // Who will receive the tokens (or AddressZero for anyone)
* to: {{wallet_address}}, // Who will receive the tokens
* price: 0.5, // the price to pay for minting those tokens
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
* mintStartTime: startTime, // can mint anytime from now
@@ -156,7 +156,7 @@ export class Erc20SignatureMintable implements DetectableFeature {
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* quantity: 4.2, // The quantity of tokens to be minted
* to: {{wallet_address}}, // Who will receive the tokens (or AddressZero for anyone)
* to: {{wallet_address}}, // Who will receive the tokens
* price: 0.5, // the price to pay for minting those tokens
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
* mintStartTime: startTime, // can mint anytime from now
+5 -13
View File
@@ -13,12 +13,7 @@ import {
FEATURE_TOKEN_CLAIMABLE_WITH_CONDITIONS,
} from "../../constants/erc20-features";
import { TokenMintInput, AmountSchema } from "../../schema";
import {
Currency,
CurrencyValue,
Amount,
ClaimVerification,
} from "../../types";
import { Currency, CurrencyValue, Amount, ClaimOptions } from "../../types";
import {
BaseERC20,
BaseSignatureMintERC20,
@@ -483,14 +478,12 @@ export class Erc20<
*/
public async claim(
amount: Amount,
checkERC20Allowance = true,
claimData?: ClaimVerification,
options?: ClaimOptions,
): Promise<TransactionResult> {
return this.claimTo(
await this.contractWrapper.getSignerAddress(),
amount,
checkERC20Allowance,
claimData,
options,
);
}
@@ -518,13 +511,12 @@ export class Erc20<
public async claimTo(
destinationAddress: string,
amount: Amount,
checkERC20Allowance = true,
claimData?: ClaimVerification,
options?: ClaimOptions,
): Promise<TransactionResult> {
return assertEnabled(
this.droppable?.claim,
FEATURE_TOKEN_CLAIMABLE_WITH_CONDITIONS,
).to(destinationAddress, amount, checkERC20Allowance, claimData);
).to(destinationAddress, amount, options);
}
/**
@@ -1,20 +1,17 @@
import { NFT } from "../../../core/schema/nft";
import { hasFunction } from "../../common";
import { FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS } from "../../constants/erc721-features";
import { FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2 } from "../../constants/erc721-features";
import { CustomContractSchema } from "../../schema/contracts/custom";
import { ClaimOptions, ClaimVerification } from "../../types";
import { ClaimOptions } from "../../types";
import { BaseClaimConditionERC721 } from "../../types/eips";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResultWithId } from "../types";
import { TransactionTask } from "./TransactionTask";
import { ContractMetadata } from "./contract-metadata";
import { ContractWrapper } from "./contract-wrapper";
import { DropClaimConditions } from "./drop-claim-conditions";
import { Erc721 } from "./erc-721";
import type { DropERC721 } from "@thirdweb-dev/contracts-js";
import { TokensClaimedEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/Drop";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, ethers } from "ethers";
import { BigNumber, BigNumberish } from "ethers";
/**
* Configure and claim ERC721 NFTs
@@ -27,7 +24,7 @@ import { BigNumber, BigNumberish, ethers } from "ethers";
* ```
*/
export class Erc721ClaimableWithConditions implements DetectableFeature {
featureName = FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS.name;
featureName = FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2.name;
/**
* Configure claim conditions
@@ -77,38 +74,6 @@ export class Erc721ClaimableWithConditions implements DetectableFeature {
);
}
/**
* Construct a claim transaction without executing it.
* This is useful for estimating the gas cost of a claim transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param destinationAddress
* @param quantity
* @param options
*/
public async getClaimTransaction(
destinationAddress: string,
quantity: BigNumberish,
options?: ClaimOptions,
): Promise<TransactionTask> {
if (options?.pricePerToken) {
throw new Error(
"In ERC721ClaimableWithConditions, price per token is be set via claim conditions by calling `contract.erc721.claimConditions.set()`",
);
}
const claimVerification = await this.conditions.prepareClaim(
quantity,
options?.checkERC20Allowance === undefined
? true
: options.checkERC20Allowance,
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "claim",
args: await this.getArgs(destinationAddress, quantity, claimVerification),
overrides: claimVerification.overrides,
});
}
/**
* Claim unique NFTs to a specific Wallet
*
@@ -127,6 +92,7 @@ export class Erc721ClaimableWithConditions implements DetectableFeature {
*
* @param destinationAddress - Address you want to send the token to
* @param quantity - Quantity of the tokens you want to claim
* @param options
* @returns - an array of results containing the id of the token claimed, the transaction receipt and a promise to optionally fetch the nft metadata
*/
public async to(
@@ -134,7 +100,7 @@ export class Erc721ClaimableWithConditions implements DetectableFeature {
quantity: BigNumberish,
options?: ClaimOptions,
): Promise<TransactionResultWithId<NFT>[]> {
const task = await this.getClaimTransaction(
const task = await this.conditions.getClaimTransaction(
destinationAddress,
quantity,
options,
@@ -156,48 +122,4 @@ export class Erc721ClaimableWithConditions implements DetectableFeature {
}
return results;
}
private async getArgs(
destinationAddress: string,
quantity: BigNumberish,
claimVerification: ClaimVerification,
): Promise<any[]> {
const isLegacyNFTContract = await this.isLegacyNFTContract();
if (isLegacyNFTContract) {
return [
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
claimVerification.proofs,
claimVerification.maxQuantityPerTransaction,
];
}
return [
destinationAddress,
quantity,
claimVerification.currencyAddress,
claimVerification.price,
{
proof: claimVerification.proofs,
maxQuantityInAllowlist: claimVerification.maxQuantityPerTransaction,
},
ethers.utils.toUtf8Bytes(""),
];
}
private async isLegacyNFTContract() {
if (hasFunction<DropERC721>("contractType", this.contractWrapper)) {
try {
const contractType = ethers.utils.toUtf8String(
await this.contractWrapper.readContract.contractType(),
);
return contractType.includes("DropERC721");
} catch (e) {
return false;
}
} else {
return false;
}
}
}
@@ -194,7 +194,11 @@ export class Erc721LazyMintable implements DetectableFeature {
if (
detectContractFeature<BaseClaimConditionERC721>(
this.contractWrapper,
"ERC721ClaimableWithConditions",
"ERC721ClaimableWithConditionsV1",
) ||
detectContractFeature<BaseClaimConditionERC721>(
this.contractWrapper,
"ERC721ClaimableWithConditionsV2",
)
) {
return new Erc721ClaimableWithConditions(
@@ -0,0 +1,384 @@
import {
CommonNFTInput,
NFT,
NFTMetadata,
NFTMetadataInput,
NFTMetadataOrUri,
} from "../../../core/schema/nft";
import { normalizePriceValue, setErc20Allowance } from "../../common/currency";
import { getBaseUriFromBatch, uploadOrExtractURIs } from "../../common/nft";
import { FEATURE_NFT_TIERED_DROP } from "../../constants/erc721-features";
import { GenericRequest } from "../../schema";
import {
TieredDropPayloadInput,
TieredDropPayloadOutput,
TieredDropPayloadSchema,
TieredDropPayloadWithSignature,
} from "../../schema/contracts/tiered-drop";
import { UploadProgressEvent } from "../../types/events";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult, TransactionResultWithId } from "../types";
import { ContractWrapper } from "./contract-wrapper";
import { Erc721 } from "./erc-721";
import type { TieredDrop, ISignatureAction } from "@thirdweb-dev/contracts-js";
import { TokensLazyMintedEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/TieredDrop";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumberish, ethers } from "ethers";
import invariant from "tiny-invariant";
export class Erc721TieredDrop implements DetectableFeature {
featureName = FEATURE_NFT_TIERED_DROP.name;
private contractWrapper: ContractWrapper<TieredDrop>;
private erc721: Erc721;
private storage: ThirdwebStorage;
constructor(
erc721: Erc721,
contractWrapper: ContractWrapper<TieredDrop>,
storage: ThirdwebStorage,
) {
this.erc721 = erc721;
this.contractWrapper = contractWrapper;
this.storage = storage;
}
public async getMetadataInTier(
tier: string,
): Promise<Omit<NFTMetadata, "id">[]> {
const batches = await this.contractWrapper.readContract.getMetadataInTier(
tier,
);
const nfts = await Promise.all(
batches.tokens
.map((range, i) => {
const nftsInRange = [];
const baseUri = batches.baseURIs[i];
for (
let j = range.startIdInclusive.toNumber();
j < range.endIdNonInclusive.toNumber();
j++
) {
const uri = `${baseUri}/${j}`;
const metadata = this.storage.downloadJSON(uri);
nftsInRange.push(metadata);
}
return nftsInRange;
})
.flat(),
);
return nfts;
}
public async getTokensInTier(tier: string): Promise<NFT[]> {
const endIndex =
await this.contractWrapper.readContract.getTokensInTierLen();
if (endIndex.eq(0)) {
return [];
}
const ranges = await this.contractWrapper.readContract.getTokensInTier(
tier,
0,
endIndex,
);
const nfts = await Promise.all(
ranges
.map((range) => {
const nftsInRange = [];
for (
let i = range.startIdInclusive.toNumber();
i < range.endIdNonInclusive.toNumber();
i++
) {
nftsInRange.push(this.erc721.get(i));
}
return nftsInRange;
})
.flat(),
);
return nfts;
}
public async createBatchWithTier(
metadatas: NFTMetadataOrUri[],
tier: string,
options?: {
onProgress: (event: UploadProgressEvent) => void;
},
): Promise<TransactionResultWithId<NFTMetadata>[]> {
// TODO: Change this to on extension
const startFileNumber = await this.erc721.nextTokenIdToMint();
const batch = await uploadOrExtractURIs(
metadatas,
this.storage,
startFileNumber.toNumber(),
options,
);
const baseUri = getBaseUriFromBatch(batch);
const receipt = await this.contractWrapper.sendTransaction("lazyMint", [
batch.length,
baseUri.endsWith("/") ? baseUri : `${baseUri}/`,
tier,
ethers.utils.toUtf8Bytes(""),
]);
const event = this.contractWrapper.parseLogs<TokensLazyMintedEvent>(
"TokensLazyMinted",
receipt?.logs,
);
const startingIndex = event[0].args.startTokenId;
const endingIndex = event[0].args.endTokenId;
const results: TransactionResultWithId<NFTMetadata>[] = [];
for (let id = startingIndex; id.lte(endingIndex); id = id.add(1)) {
results.push({
id,
receipt,
data: () => this.erc721.getTokenMetadata(id),
});
}
return results;
}
public async createDelayedRevealBatchWithTier(
placeholder: NFTMetadataInput,
metadatas: NFTMetadataInput[],
password: string,
tier: string,
options?: {
onProgress: (event: UploadProgressEvent) => void;
},
): Promise<TransactionResultWithId<NFTMetadata>[]> {
if (!password) {
throw new Error("Password is required");
}
const placeholderUris = await this.storage.uploadBatch(
[CommonNFTInput.parse(placeholder)],
{
rewriteFileNames: {
fileStartNumber: 0,
},
},
);
const placeholderUri = getBaseUriFromBatch(placeholderUris);
const startFileNumber = await this.erc721.nextTokenIdToMint();
const uris = await this.storage.uploadBatch(
metadatas.map((m) => CommonNFTInput.parse(m)),
{
onProgress: options?.onProgress,
rewriteFileNames: {
fileStartNumber: startFileNumber.toNumber(),
},
},
);
const baseUri = getBaseUriFromBatch(uris);
const baseUriId = await this.contractWrapper.readContract.getBaseURICount();
const chainId = await this.contractWrapper.getChainID();
const hashedPassword = ethers.utils.solidityKeccak256(
["string", "uint256", "uint256", "address"],
[password, chainId, baseUriId, this.contractWrapper.readContract.address],
);
const encryptedBaseUri =
await this.contractWrapper.readContract.encryptDecrypt(
ethers.utils.toUtf8Bytes(baseUri),
hashedPassword,
);
let data: string;
const provenanceHash = ethers.utils.solidityKeccak256(
["bytes", "bytes", "uint256"],
[ethers.utils.toUtf8Bytes(baseUri), hashedPassword, chainId],
);
data = ethers.utils.defaultAbiCoder.encode(
["bytes", "bytes32"],
[encryptedBaseUri, provenanceHash],
);
const receipt = await this.contractWrapper.sendTransaction("lazyMint", [
uris.length,
placeholderUri.endsWith("/") ? placeholderUri : `${placeholderUri}/`,
tier,
data,
]);
const event = this.contractWrapper.parseLogs<TokensLazyMintedEvent>(
"TokensLazyMinted",
receipt?.logs,
);
const startingIndex = event[0].args.startTokenId;
const endingIndex = event[0].args.endTokenId;
const results: TransactionResultWithId<NFTMetadata>[] = [];
for (let id = startingIndex; id.lte(endingIndex); id = id.add(1)) {
results.push({
id,
receipt,
data: () => this.erc721.getTokenMetadata(id),
});
}
return results;
}
public async reveal(
batchId: BigNumberish,
password: string,
): Promise<TransactionResult> {
if (!password) {
throw new Error("Password is required");
}
const chainId = await this.contractWrapper.getChainID();
const key = ethers.utils.solidityKeccak256(
["string", "uint256", "uint256", "address"],
[password, chainId, batchId, this.contractWrapper.readContract.address],
);
// performing the reveal locally to make sure it'd succeed before sending the transaction
try {
const decryptedUri = await this.contractWrapper
.callStatic()
.reveal(batchId, key);
// basic sanity check for making sure decryptedUri is valid
// this is optional because invalid decryption key would result in non-utf8 bytes and
// ethers would throw when trying to decode it
if (!decryptedUri.includes("://") || !decryptedUri.endsWith("/")) {
throw new Error("invalid password");
}
} catch (e) {
throw new Error("invalid password");
}
return {
receipt: await this.contractWrapper.sendTransaction("reveal", [
batchId,
key,
]),
};
}
public async generate(
payloadToSign: TieredDropPayloadInput,
): Promise<TieredDropPayloadWithSignature> {
const [payload] = await this.generateBatch([payloadToSign]);
return payload;
}
public async generateBatch(
payloadsToSign: TieredDropPayloadInput[],
): Promise<TieredDropPayloadWithSignature[]> {
const parsedPayloads = payloadsToSign.map((payload) =>
TieredDropPayloadSchema.parse(payload),
);
const chainId = await this.contractWrapper.getChainID();
const signer = this.contractWrapper.getSigner();
invariant(signer, "No signer available");
return await Promise.all(
parsedPayloads.map(async (payload) => {
const signature = await this.contractWrapper.signTypedData(
signer,
{
name: "SignatureAction",
version: "1",
chainId,
verifyingContract: this.contractWrapper.readContract.address,
},
{ GenericRequest: GenericRequest },
await this.mapPayloadToContractStruct(payload),
);
return { payload, signature: signature.toString() };
}),
);
}
public async verify(
signedPayload: TieredDropPayloadWithSignature,
): Promise<boolean> {
const message = await this.mapPayloadToContractStruct(
signedPayload.payload,
);
const verification = await this.contractWrapper.readContract.verify(
message,
signedPayload.signature,
);
return verification[0];
}
public async claimWithSignature(
signedPayload: TieredDropPayloadWithSignature,
): Promise<TransactionResult> {
const message = await this.mapPayloadToContractStruct(
signedPayload.payload,
);
const pricePerToken = await normalizePriceValue(
this.contractWrapper.getProvider(),
signedPayload.payload.price,
signedPayload.payload.currencyAddress,
);
const price = pricePerToken.mul(signedPayload.payload.quantity);
const overrides = await this.contractWrapper.getCallOverrides();
await setErc20Allowance(
this.contractWrapper,
price,
signedPayload.payload.currencyAddress,
overrides,
);
const receipt = await this.contractWrapper.sendTransaction(
"claimWithSignature",
[message, signedPayload.signature],
overrides,
);
// TODO: Listen for TokensClaimed event once it gets emitted
return { receipt };
}
private async mapPayloadToContractStruct(
payload: TieredDropPayloadOutput,
): Promise<ISignatureAction.GenericRequestStruct> {
const normalizedPricePerToken = await normalizePriceValue(
this.contractWrapper.getProvider(),
payload.price,
payload.currencyAddress,
);
const data = ethers.utils.defaultAbiCoder.encode(
[
"string[]",
"address",
"address",
"uint256",
"address",
"uint256",
"uint256",
"address",
],
[
payload.tierPriority,
payload.to,
payload.royaltyRecipient,
payload.royaltyBps,
payload.primarySaleRecipient,
payload.quantity,
normalizedPricePerToken,
payload.currencyAddress,
],
);
return {
uid: payload.uid,
validityStartTimestamp: payload.mintStartTime,
validityEndTimestamp: payload.mintEndTime,
data,
} as ISignatureAction.GenericRequestStruct;
}
}
@@ -190,7 +190,7 @@ export class Erc721WithQuantitySignatureMintable implements DetectableFeature {
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* metadata: nftMetadata, // The NFT to mint
* to: {{wallet_address}}, // Who will receive the NFT (or AddressZero for anyone)
* to: {{wallet_address}}, // Who will receive the NFT
* quantity: 2, // the quantity of NFTs to mint
* price: 0.5, // the price per NFT
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
@@ -247,7 +247,7 @@ export class Erc721WithQuantitySignatureMintable implements DetectableFeature {
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* metadata: nftMetadata, // The NFT to mint
* to: {{wallet_address}}, // Who will receive the NFT (or AddressZero for anyone)
* to: {{wallet_address}}, // Who will receive the NFT
* quantity: 2, // the quantity of NFTs to mint
* price: 0.5, // the price per NFT
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
+30 -3
View File
@@ -13,12 +13,13 @@ import {
FEATURE_NFT_BATCH_MINTABLE,
FEATURE_NFT_BURNABLE,
FEATURE_NFT_CLAIMABLE,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2,
FEATURE_NFT_LAZY_MINTABLE,
FEATURE_NFT_MINTABLE,
FEATURE_NFT_REVEALABLE,
FEATURE_NFT_SIGNATURE_MINTABLE,
FEATURE_NFT_SUPPLY,
FEATURE_NFT_TIERED_DROP,
} from "../../constants/erc721-features";
import { BaseDropERC721, BaseERC721 } from "../../types/eips";
import { ClaimOptions, UploadProgressEvent } from "../../types/index";
@@ -34,6 +35,7 @@ import { Erc721Burnable } from "./erc-721-burnable";
import { Erc721LazyMintable } from "./erc-721-lazymintable";
import { Erc721Mintable } from "./erc-721-mintable";
import { Erc721Supply } from "./erc-721-supply";
import { Erc721TieredDrop } from "./erc-721-tiered-drop";
import { Erc721WithQuantitySignatureMintable } from "./erc-721-with-quantity-signature-mintable";
import type {
DropERC721,
@@ -43,6 +45,7 @@ import type {
ISignatureMintERC721,
Multiwrap,
SignatureDrop,
TieredDrop,
TokenERC721,
} from "@thirdweb-dev/contracts-js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
@@ -72,6 +75,7 @@ export class Erc721<
private mintable: Erc721Mintable | undefined;
private burnable: Erc721Burnable | undefined;
private lazyMintable: Erc721LazyMintable | undefined;
private tieredDropable: Erc721TieredDrop | undefined;
private signatureMintable: Erc721WithQuantitySignatureMintable | undefined;
protected contractWrapper: ContractWrapper<T>;
protected storage: ThirdwebStorage;
@@ -92,6 +96,7 @@ export class Erc721<
this.mintable = this.detectErc721Mintable();
this.burnable = this.detectErc721Burnable();
this.lazyMintable = this.detectErc721LazyMintable();
this.tieredDropable = this.detectErc721TieredDrop();
this.signatureMintable = this.detectErc721SignatureMintable();
this._chainId = chainId;
}
@@ -613,7 +618,7 @@ export class Erc721<
const claimWithConditions = this.lazyMintable?.claimWithConditions;
const claim = this.lazyMintable?.claim;
if (claimWithConditions) {
return claimWithConditions.getClaimTransaction(
return claimWithConditions.conditions.getClaimTransaction(
destinationAddress,
quantity,
options,
@@ -682,10 +687,20 @@ export class Erc721<
get claimConditions() {
return assertEnabled(
this.lazyMintable?.claimWithConditions,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS,
FEATURE_NFT_CLAIMABLE_WITH_CONDITIONS_V2,
).conditions;
}
////// ERC721 Tiered Drop Extension //////
/**
* Tiered Drop
* @remarks Drop lazy minted NFTs using a tiered drop mechanism.
*/
get tieredDrop() {
return assertEnabled(this.tieredDropable, FEATURE_NFT_TIERED_DROP);
}
////// ERC721 SignatureMint Extension //////
/**
@@ -828,6 +843,18 @@ export class Erc721<
return undefined;
}
private detectErc721TieredDrop(): Erc721TieredDrop | undefined {
if (
detectContractFeature<TieredDrop>(
this.contractWrapper,
"ERC721TieredDrop",
)
) {
return new Erc721TieredDrop(this, this.contractWrapper, this.storage);
}
return undefined;
}
private detectErc721SignatureMintable():
| Erc721WithQuantitySignatureMintable
| undefined {
+63 -31
View File
@@ -1,8 +1,8 @@
import { TransactionError } from "../../common";
import { CONTRACT_ADDRESSES, SUPPORTED_CHAIN_IDS } from "../../constants";
import {
EditionDropInitializer,
EditionInitializer,
getContractName,
MarketplaceInitializer,
MultiwrapInitializer,
NFTCollectionInitializer,
@@ -41,6 +41,22 @@ import { z } from "zod";
export class ContractFactory extends ContractWrapper<TWFactory> {
private storage: ThirdwebStorage;
// Map from contract type to version to deploy specific versions by default
private DEFAULT_VERSION_MAP: Record<PrebuiltContractType, number> = {
[NFTDropInitializer.contractType]: 3,
[NFTCollectionInitializer.contractType]: 1,
[SignatureDropInitializer.contractType]: 4,
[MultiwrapInitializer.contractType]: 1,
[EditionDropInitializer.contractType]: 2,
[EditionInitializer.contractType]: 1,
[TokenDropInitializer.contractType]: 2,
[TokenInitializer.contractType]: 1,
[VoteInitializer.contractType]: 1,
[SplitInitializer.contractType]: 1,
[MarketplaceInitializer.contractType]: 2,
[PackInitializer.contractType]: 2,
};
constructor(
factoryAddr: string,
network: NetworkOrSignerOrProvider,
@@ -56,6 +72,7 @@ export class ContractFactory extends ContractWrapper<TWFactory> {
contractMetadata: z.input<
DeploySchemaForPrebuiltContractType<TContractType>
>,
version?: number,
): Promise<string> {
const contract = PREBUILT_CONTRACTS_MAP[contractType];
const metadata = contract.schema.deploy.parse(contractMetadata);
@@ -63,42 +80,35 @@ export class ContractFactory extends ContractWrapper<TWFactory> {
// TODO: is there any special pre-processing we need to do before uploading?
const contractURI = await this.storage.upload(metadata);
const ABI = await contract.getAbi();
const implementationAddress = await this.getImplementation(
contract,
version,
);
if (
!implementationAddress ||
implementationAddress === constants.AddressZero
) {
throw new Error(`No implementation found for ${contractType}`);
}
const ABI = await contract.getAbi(
implementationAddress,
this.getProvider(),
);
const encodedFunc = Contract.getInterface(ABI).encodeFunctionData(
"initialize",
await this.getDeployArguments(contractType, metadata, contractURI),
);
const encodedType = ethers.utils.formatBytes32String(contract.name);
let receipt;
try {
receipt = await this.sendTransaction("deployProxy", [
encodedType,
encodedFunc,
]);
} catch (e) {
console.log(e);
// if the error is caused by user cancelling the transaction, just re-throw it
if (
(e as TransactionError).message
.toLowerCase()
.includes("user rejected transaction") ||
(e as TransactionError).reason
.toLowerCase()
.includes("user rejected transaction")
) {
throw e;
}
// deploy might fail due to salt already used, fallback to deterministic deploy
const blockNumber = await this.getProvider().getBlockNumber();
receipt = await this.sendTransaction("deployProxyDeterministic", [
encodedType,
encodedFunc,
ethers.utils.formatBytes32String(blockNumber.toString()),
]);
}
const blockNumber = await this.getProvider().getBlockNumber();
const salt = ethers.utils.formatBytes32String(blockNumber.toString());
const receipt = await this.sendTransaction("deployProxyByImplementation", [
implementationAddress,
encodedFunc,
salt,
]);
const events = this.parseLogs<ProxyDeployedEvent>(
"ProxyDeployed",
@@ -285,4 +295,26 @@ export class ContractFactory extends ContractWrapper<TWFactory> {
? [openzeppelinForwarder, biconomyForwarder]
: [openzeppelinForwarder];
}
private async getImplementation(
contract: typeof PREBUILT_CONTRACTS_MAP[PrebuiltContractType],
version?: number,
) {
const encodedType = ethers.utils.formatBytes32String(contract.name);
return this.readContract.getImplementation(
encodedType,
version !== undefined
? version
: this.DEFAULT_VERSION_MAP[contract.contractType],
);
}
public async getLatestVersion(contractType: PrebuiltContractType) {
const name = getContractName(contractType);
if (!name) {
throw new Error(`Invalid contract type ${contractType}`);
}
const encodedType = ethers.utils.formatBytes32String(name);
return this.readContract.currentVersion(encodedType);
}
}
+1 -1
View File
@@ -401,7 +401,7 @@ export class ThirdwebSDK extends RPCConnectionHandler {
// otherwise if it's a prebuilt contract we can just use the contract type
const contractAbi = await PREBUILT_CONTRACTS_MAP[
resolvedContractType
].getAbi();
].getAbi(address, this.getProvider());
newContract = await this.getContractFromAbi(address, contractAbi);
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ export * from "./constants";
export * from "./contracts";
// export integration things
export * from "./integrations/paper-xyz";
export * from "./integrations/checkout";
// explcitly export the *TYPES* of prebuilt contracts
export type { Edition } from "./contracts/prebuilt-implementations/edition";
@@ -1,8 +1,7 @@
import { ChainId } from "../constants";
import { ContractWrapper } from "../core/classes/contract-wrapper";
import { SignedPayload721WithQuantitySignature } from "../schema/contracts/common/signature";
import { DropERC721 } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC721";
import { DropERC1155 } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC1155";
import { PrebuiltEditionDrop, PrebuiltNFTDrop } from "../types/eips";
import { SignatureDrop } from "@thirdweb-dev/contracts-js/dist/declarations/src/SignatureDrop";
import fetch from "cross-fetch";
import invariant from "tiny-invariant";
@@ -133,9 +132,9 @@ export type PaperCreateCheckoutLinkShardParams = {
* @internal
*/
export type PaperCreateCheckoutLinkIntentParams<
TContract extends DropERC721 | DropERC1155 | SignatureDrop,
TContract extends PrebuiltNFTDrop | PrebuiltEditionDrop | SignatureDrop,
> = PaperCreateCheckoutLinkShardParams &
(TContract extends DropERC1155
(TContract extends PrebuiltEditionDrop
? {
contractArgs: { tokenId: string };
}
@@ -143,7 +142,7 @@ export type PaperCreateCheckoutLinkIntentParams<
? {
contractArgs: SignedPayload721WithQuantitySignature;
}
: TContract extends DropERC721
: TContract extends PrebuiltNFTDrop
? {}
: never);
@@ -165,7 +164,7 @@ const DEFAULT_PARAMS: Partial<PaperCreateCheckoutLinkShardParams> = {
* @internal
*/
export async function createCheckoutLinkIntent<
TContract extends DropERC721 | DropERC1155 | SignatureDrop,
TContract extends PrebuiltNFTDrop | PrebuiltEditionDrop | SignatureDrop,
>(
contractId: string,
params: PaperCreateCheckoutLinkIntentParams<TContract>,
@@ -203,7 +202,7 @@ export async function createCheckoutLinkIntent<
* @internal
*/
export class PaperCheckout<
TContract extends DropERC721 | DropERC1155 | SignatureDrop,
TContract extends PrebuiltNFTDrop | PrebuiltEditionDrop | SignatureDrop,
> {
private contractWrapper;
@@ -9,7 +9,7 @@ import {
} from "../../shared";
import { CurrencyValueSchema } from "./currency";
import { SnapshotInputSchema } from "./snapshots";
import { BigNumber, utils } from "ethers";
import { BigNumber, BigNumberish, utils } from "ethers";
import { z } from "zod";
/**
@@ -19,11 +19,12 @@ export const ClaimConditionInputSchema = z.object({
startTime: StartDateSchema,
currencyAddress: z.string().default(NATIVE_TOKEN_ADDRESS),
price: AmountSchema.default(0),
maxQuantity: QuantitySchema,
quantityLimitPerTransaction: QuantitySchema,
maxClaimableSupply: QuantitySchema,
maxClaimablePerWallet: QuantitySchema,
waitInSeconds: BigNumberishSchema.default(0),
merkleRootHash: BytesLikeSchema.default(utils.hexZeroPad([0], 32)),
snapshot: z.optional(SnapshotInputSchema).nullable(),
metadata: z.union([z.string(), z.object({})]).optional(),
});
/**
@@ -54,4 +55,17 @@ export const ClaimConditionOutputSchema = ClaimConditionInputSchema.extend({
waitInSeconds: BigNumberSchema,
startTime: BigNumberSchema.transform((n) => new Date(n.toNumber() * 1000)),
snapshot: SnapshotInputSchema.optional().nullable(),
metadata: z.string().optional(),
});
export type AbstractClaimConditionContractStruct = {
startTimestamp: BigNumberish;
maxClaimableSupply: BigNumberish;
supplyClaimed: BigNumberish;
maxClaimablePerWallet: BigNumberish;
merkleRoot: string;
pricePerToken: BigNumberish;
currency: string;
waitTimeInSecondsBetweenClaims?: BigNumberish; // only in legacy claim conditions
metadata?: string; // only for new claim conditions
};
@@ -16,7 +16,11 @@ import { z } from "zod";
* @internal
*/
export const BaseSignaturePayloadInput = z.object({
to: z.string().default(constants.AddressZero),
to: z
.string()
.refine((address) => address.toLowerCase() !== constants.AddressZero, {
message: "Cannot create payload to mint to zero address",
}),
price: AmountSchema.default(0),
currencyAddress: z.string().default(NATIVE_TOKEN_ADDRESS),
mintStartTime: StartDateSchema,
@@ -255,3 +259,10 @@ export const MintRequest721withQuantity = [
{ name: "validityEndTimestamp", type: "uint128" },
{ name: "uid", type: "bytes32" },
];
export const GenericRequest = [
{ name: "validityStartTimestamp", type: "uint128" },
{ name: "validityEndTimestamp", type: "uint128" },
{ name: "uid", type: "bytes32" },
{ name: "data", type: "bytes" },
];
@@ -1,4 +1,6 @@
import { AddressSchema, AmountSchema } from "../../shared";
import { QuantitySchema } from "../../../../core/schema/shared";
import { AddressSchema } from "../../shared";
import { ethers } from "ethers";
import { z } from "zod";
/**
@@ -8,12 +10,11 @@ export const MerkleSchema = z.object({
merkle: z.record(z.string()).default({}),
});
/**
* @internal
*/
export const SnapshotEntryInput = z.object({
address: AddressSchema,
maxClaimable: AmountSchema.default(0),
maxClaimable: QuantitySchema.default(0), // defaults to 0
price: QuantitySchema, // defaults to unlimited
currencyAddress: AddressSchema.default(ethers.constants.AddressZero),
});
export type SnapshotEntry = z.output<typeof SnapshotEntryInput>;
@@ -49,7 +50,7 @@ export const SnapshotInputSchema = z.union([
z.array(SnapshotEntryInput),
]);
const SnapshotEntryWithProofSchema = SnapshotEntryInput.extend({
export const SnapshotEntryWithProofSchema = SnapshotEntryInput.extend({
proof: z.array(z.string()),
});
/**
@@ -0,0 +1,51 @@
import { BasisPointsSchema, BigNumberSchema } from "../shared";
import { BaseSignaturePayloadInput } from "./common";
import { constants } from "ethers";
import { z } from "zod";
/**
* @internal
*/
export const TieredDropPayloadSchema = BaseSignaturePayloadInput.extend({
tierPriority: z.array(z.string()),
royaltyRecipient: z.string().default(constants.AddressZero),
royaltyBps: BasisPointsSchema.default(0),
quantity: BigNumberSchema.default(1),
});
/**
* @public
*/
export type TieredDropPayloadInput = z.input<typeof TieredDropPayloadSchema>;
/**
* @internal
*/
export type TieredDropPayloadOutput = z.output<typeof TieredDropPayloadSchema>;
/**
* @internal
*/
export type TieredDropPayloadWithSignature = {
payload: TieredDropPayloadOutput;
signature: string;
};
export const GenericRequest = [
{
name: "validityStartTimestamp",
type: "uint128",
},
{
name: "validityEndTimestamp",
type: "uint128",
},
{
name: "uid",
type: "bytes32",
},
{
name: "data",
type: "bytes",
},
];
@@ -53,7 +53,7 @@ export type FilledConditionInput = z.output<typeof ClaimConditionInputSchema>;
export type ClaimVerification = {
overrides: CallOverrides;
proofs: BytesLike[];
maxQuantityPerTransaction: BigNumber;
maxClaimable: BigNumber;
price: BigNumber;
currencyAddress: string;
};
+10
View File
@@ -13,6 +13,12 @@ import type {
ISignatureMintERC721,
LazyMint,
DropSinglePhase1155,
DropERC1155,
DropERC20_V2,
DropERC20,
DropERC721,
DropERC721_V3,
DropERC1155_V2,
} from "@thirdweb-dev/contracts-js";
export type BaseERC20 = IERC20 & IERC20Metadata;
@@ -28,3 +34,7 @@ export type BaseSignatureMintERC721 = BaseERC721 & ISignatureMintERC721;
export type BaseSignatureMintERC20 = BaseERC20 & ISignatureMintERC20;
export type BaseSignatureMintERC1155 = BaseERC1155 & ISignatureMintERC1155;
export type BaseDelayedRevealERC1155 = BaseDropERC1155 & DelayedReveal;
export type PrebuiltTokenDrop = DropERC20 | DropERC20_V2;
export type PrebuiltNFTDrop = DropERC721 | DropERC721_V3;
export type PrebuiltEditionDrop = DropERC1155 | DropERC1155_V2;
+34 -27
View File
@@ -24,14 +24,18 @@ import {
ContractPublisher,
ContractPublisher__factory,
DropERC1155__factory,
DropERC1155_V2__factory,
DropERC20__factory,
DropERC20_V2__factory,
DropERC721__factory,
DropERC721_V3__factory,
Marketplace__factory,
MockContractPublisher,
MockContractPublisher__factory,
Multiwrap__factory,
Pack__factory,
SignatureDrop__factory,
SignatureDrop_V4__factory,
Split__factory,
TokenERC1155__factory,
TokenERC20__factory,
@@ -154,64 +158,67 @@ export const mochaHooks = {
if (contractType === "custom") {
continue;
}
let factory;
let factories = [];
switch (contractType) {
case TokenInitializer.contractType:
factory = TokenERC20__factory;
factories.push(TokenERC20__factory);
break;
case TokenDropInitializer.contractType:
factory = DropERC20__factory;
factories.push(DropERC20_V2__factory, DropERC20__factory);
break;
case NFTCollectionInitializer.contractType:
factory = TokenERC721__factory;
factories.push(TokenERC721__factory);
break;
case NFTDropInitializer.contractType:
factory = DropERC721__factory;
factories.push(DropERC721_V3__factory, DropERC721__factory);
break;
case SignatureDropInitializer.contractType:
factory = SignatureDrop__factory;
factories.push(SignatureDrop_V4__factory, SignatureDrop__factory);
break;
case EditionInitializer.contractType:
factory = TokenERC1155__factory;
factories.push(TokenERC1155__factory);
break;
case EditionDropInitializer.contractType:
factory = DropERC1155__factory;
factories.push(DropERC1155_V2__factory, DropERC1155__factory);
break;
case SplitInitializer.contractType:
factory = Split__factory;
factories.push(Split__factory);
break;
case VoteInitializer.contractType:
factory = VoteERC20__factory;
factories.push(VoteERC20__factory);
break;
case MarketplaceInitializer.contractType:
factory = Marketplace__factory;
factories.push(Marketplace__factory);
break;
case PackInitializer.contractType:
factory = Pack__factory;
factories.push(Pack__factory);
break;
case MultiwrapInitializer.contractType:
factory = Multiwrap__factory;
factories.push(Multiwrap__factory);
break;
default:
throw new Error(`No factory for contract: ${contractType}`);
}
const contractFactory = new ethers.ContractFactory(
factory.abi,
factory.bytecode,
).connect(signer);
for (const factory of factories) {
const contractFactory = new ethers.ContractFactory(
factory.abi,
factory.bytecode,
).connect(signer);
const deployedContract: ethers.Contract = await deployContract(
contractFactory,
contractType as ContractType,
);
const deployedContract: ethers.Contract = await deployContract(
contractFactory,
contractType as ContractType,
);
await deployedContract.deployed();
const tx = await thirdwebFactoryDeployer.addImplementation(
deployedContract.address,
);
await tx.wait();
implementations[contractType as ContractType] = deployedContract.address;
await deployedContract.deployed();
const tx = await thirdwebFactoryDeployer.addImplementation(
deployedContract.address,
);
await tx.wait();
implementations[contractType as ContractType] =
deployedContract.address;
}
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
@@ -0,0 +1,818 @@
import {
ClaimEligibility,
EditionDrop,
EditionDropInitializer,
NATIVE_TOKEN_ADDRESS,
TokenInitializer,
} from "../../src/evm";
import { expectError, sdk, signers, storage } from "./before-setup";
import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { DropERC1155_V2__factory } from "@thirdweb-dev/contracts-js";
import { assert, expect, use } from "chai";
import { BigNumber, ethers } from "ethers";
import invariant from "tiny-invariant";
global.fetch = require("cross-fetch");
const deepEqualInAnyOrder = require("deep-equal-in-any-order");
use(deepEqualInAnyOrder);
describe("Edition Drop Contract (V2)", async () => {
let bdContract: EditionDrop;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
abbyWallet: SignerWithAddress,
bobWallet: SignerWithAddress,
w1: SignerWithAddress,
w2: SignerWithAddress,
w3: SignerWithAddress,
w4: SignerWithAddress;
before(() => {
[adminWallet, samWallet, bobWallet, abbyWallet, w1, w2, w3, w4] = signers;
});
beforeEach(async () => {
sdk.updateSignerOrProvider(adminWallet);
const address = await sdk.deployer.deployBuiltInContract(
EditionDropInitializer.contractType,
{
name: `Testing edition drop from SDK`,
description: "Test contract from tests",
image:
"https://pbs.twimg.com/profile_images/1433508973215367176/XBCfBn3g_400x400.jpg",
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 500,
fee_recipient: AddressZero,
platform_fee_basis_points: 10,
platform_fee_recipient: adminWallet.address,
},
2,
);
bdContract = await sdk.getEditionDrop(address);
});
it("should estimate gas cost", async () => {
const cost = await bdContract.estimator.gasCostOf("lazyMint", [
1000,
"mock://12398172398172389/0",
]);
expect(parseFloat(cost)).gt(0);
});
it("comprehensive test", async () => {
const metadata = [];
for (let i = 0; i < 3; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
await bdContract.createBatch(metadata);
// claiming with default conditions
await bdContract.claimConditions.set(0, [{}]);
await bdContract.claim(0, 1);
// claiming with max supply
await bdContract.claimConditions.set(0, [
{
maxClaimableSupply: 2,
},
]);
try {
await bdContract.claim(0, 2);
expect.fail();
} catch (e) {
expectError(e, "exceed max mint supply");
}
await bdContract.claim(0, 1);
// claiming with max per wallet
await bdContract.claimConditions.set(0, [
{
maxClaimablePerWallet: 1,
},
]);
try {
await bdContract.claim(0, 2);
expect.fail();
} catch (e) {
expectError(e, "invalid quantity claimed");
}
await bdContract.claim(0, 1);
expect((await bdContract.totalSupply(0)).toString()).eq("3");
});
it("comprehensive test with allowlist", async () => {
const metadata = [];
for (let i = 0; i < 6; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
await bdContract.createBatch(metadata);
// claiming with default conditions
await bdContract.claimConditions.set(0, [
{
snapshot: [adminWallet.address],
},
]);
try {
sdk.updateSignerOrProvider(bobWallet);
await bdContract.claim(0, 1);
expect.fail();
} catch (e) {
expectError(e, "No claim found");
}
sdk.updateSignerOrProvider(adminWallet);
await bdContract.claim(0, 1);
// claiming with max supply
await bdContract.claimConditions.set(1, [
{
snapshot: [adminWallet.address],
maxClaimableSupply: 1,
},
]);
try {
await bdContract.claim(1, 2);
expect.fail();
} catch (e) {
expectError(e, "exceed max mint supply");
}
await bdContract.claim(1, 1);
// claiming with max per wallet
await bdContract.claimConditions.set(2, [
{
snapshot: [adminWallet.address],
maxClaimablePerWallet: 1,
},
]);
try {
await bdContract.claim(2, 2);
expect.fail();
} catch (e) {
expectError(e, "invalid quantity claimed");
}
await bdContract.claim(2, 1);
// claiming with max per wallet in snapshot
await bdContract.claimConditions.set(3, [
{
snapshot: [{ address: adminWallet.address, maxClaimable: 1 }],
maxClaimablePerWallet: 0,
},
]);
try {
await bdContract.claim(3, 2);
expect.fail();
} catch (e) {
expectError(e, "invalid quantity proof");
}
await bdContract.claim(3, 1);
try {
await bdContract.claim(3, 1);
expect.fail();
} catch (e) {
expectError(e, "proof claimed");
}
});
it("should allow you to set claim conditions", async () => {
await bdContract.createBatch([
{ name: "test", description: "test" },
{ name: "test", description: "test" },
]);
await bdContract.claimConditions.set(BigNumber.from("0"), [{}]);
const conditions = await bdContract.claimConditions.getAll(0);
assert.lengthOf(conditions, 1);
});
it("should get all", async () => {
await bdContract.createBatch([
{ name: "test", description: "test" },
{ name: "test", description: "test" },
]);
const all = await bdContract.getAll();
expect(all.length).to.eq(2);
});
it("allow all addresses in the merkle tree to claim", async () => {
await bdContract.createBatch([
{
name: "test",
description: "test",
},
]);
const testWallets: SignerWithAddress[] = [
bobWallet,
samWallet,
abbyWallet,
w1,
w2,
w3,
w4,
];
const members = testWallets.map((w) => w.address);
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 1000,
snapshot: members,
},
]);
for (const member of testWallets) {
await sdk.updateSignerOrProvider(member);
await bdContract.claim("0", 1);
}
const bundle = await bdContract.get("0");
assert(bundle.supply === testWallets.length);
const claimers = await bdContract.history.getAllClaimerAddresses("0");
expect(claimers.length).to.eq(testWallets.length);
expect(claimers).to.include(bobWallet.address);
});
it("allow all addresses in the merkle tree to claim using useSnapshot", async () => {
await bdContract.createBatch([
{
name: "test",
description: "test",
},
]);
const testWallets: SignerWithAddress[] = [
bobWallet,
samWallet,
abbyWallet,
w1,
w2,
w3,
];
const members = testWallets.map((w) => w.address);
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 1000,
snapshot: members,
},
]);
testWallets.push(w4);
for (const member of testWallets) {
try {
sdk.updateSignerOrProvider(member);
await bdContract.claim("0", 1);
} catch (e) {
if (member !== w4) {
throw e;
}
}
}
const bundle = await bdContract.get("0");
assert(bundle.supply === testWallets.length - 1);
});
it("should return the newly minted tokens", async () => {
const tokens = [
{
name: "test 0",
},
{
name: "test 1",
},
{
name: "test 2",
},
{
name: "test 3",
},
{
name: "test 4",
},
];
const result = await bdContract.createBatch(tokens);
assert.lengthOf(result, tokens.length);
for (const token of tokens) {
const found = result.find(
async (t) => (await t.data()).name === token.name,
);
assert.isDefined(found);
}
const moreTokens = [
{
name: "test 5",
},
{
name: "test 6",
},
];
const moreResult = await bdContract.createBatch(moreTokens);
assert.lengthOf(moreResult, moreTokens.length);
for (const token of moreTokens) {
const found = moreResult.find(
async (t) => (await t.data()).name === token.name,
);
assert.isDefined(found);
}
});
it("should allow setting max claims per wallet", async () => {
await bdContract.createBatch([
{ name: "name", description: "description" },
]);
await bdContract.claimConditions.set(0, [
{
snapshot: [
{ address: w1.address, maxClaimable: 2 },
{ address: w2.address, maxClaimable: 1 },
],
},
]);
await sdk.updateSignerOrProvider(w1);
await bdContract.claim(0, 2);
try {
await sdk.updateSignerOrProvider(w2);
await bdContract.claim(0, 2);
} catch (e) {
expectError(e, "invalid quantity proof");
}
});
it("should allow a default claim condition to be used to claim", async () => {
await bdContract.createBatch([
{
name: "test 0",
},
{
name: "test 1",
},
{
name: "test 2",
},
]);
await bdContract.claimConditions.set("0", [{}]);
await bdContract.claim("0", 1);
});
it("should return addresses of all the claimers", async () => {
await bdContract.createBatch([
{
name: "test 0",
},
{
name: "test 1",
},
]);
await bdContract.claimConditions.set("0", [{}]);
await bdContract.claimConditions.set("1", [{}]);
await bdContract.claim("0", 1);
await sdk.updateSignerOrProvider(samWallet);
await bdContract.claim("0", 1);
// TODO some asserts
// const claimers = await bdContract.getAllClaimerAddresses("0");
// expect(claimers).to.deep.equalInAnyOrder([
// samWallet.address,
// adminWallet.address,
// ]);
await sdk.updateSignerOrProvider(w1);
await bdContract.claim("1", 1);
await sdk.updateSignerOrProvider(w2);
await bdContract.claim("1", 1);
const ownedW1 = await bdContract.getOwned(w1.address);
assert(ownedW1.length === 1);
const ownedW2 = await bdContract.getOwned(w2.address);
assert(ownedW2.length === 1);
});
it("should return the correct status if a token can be claimed", async () => {
await bdContract.claimConditions.set("0", [
{
snapshot: [w1.address],
},
]);
await sdk.updateSignerOrProvider(w1);
const canClaimW1 = await bdContract.claimConditions.canClaim("0", 1);
assert.isTrue(canClaimW1, "w1 should be able to claim");
await sdk.updateSignerOrProvider(w2);
const canClaimW2 = await bdContract.claimConditions.canClaim("0", 1);
assert.isFalse(canClaimW2, "w2 should not be able to claim");
});
it("Platform fees", async () => {
const fees = await bdContract.platformFees.get();
expect(fees.platform_fee_recipient).to.eq(adminWallet.address);
expect(fees.platform_fee_basis_points).to.eq(10);
await bdContract.platformFees.set({
platform_fee_recipient: samWallet.address,
platform_fee_basis_points: 500,
});
const fees2 = await bdContract.platformFees.get();
expect(fees2.platform_fee_recipient).to.eq(samWallet.address);
expect(fees2.platform_fee_basis_points).to.eq(500);
});
it("should allow custom overrides", async () => {
bdContract.interceptor.overrideNextTransaction(() => ({
nonce: 5000,
}));
try {
await bdContract.createBatch([
{
name: "test",
description: "test",
},
]);
} catch (e) {
expectError(e, "Expected nonce to be");
}
});
it("canClaim: 1 address", async () => {
await bdContract.claimConditions.set("0", [
{
snapshot: [w1.address],
},
]);
assert.isTrue(
await bdContract.claimConditions.canClaim("0", 1, w1.address),
"can claim",
);
assert.isFalse(
await bdContract.claimConditions.canClaim("0", 1, w2.address),
"!can claim",
);
});
it("canClaim: 3 address", async () => {
await bdContract.claimConditions.set("0", [
{
snapshot: [
w1.address.toUpperCase().replace("0X", "0x"),
w2.address.toLowerCase(),
w3.address,
],
},
]);
assert.isTrue(
await bdContract.claimConditions.canClaim("0", 1, w1.address),
"can claim",
);
assert.isTrue(
await bdContract.claimConditions.canClaim("0", 1, w2.address),
"can claim",
);
assert.isTrue(
await bdContract.claimConditions.canClaim("0", 1, w3.address),
"can claim",
);
assert.isFalse(
await bdContract.claimConditions.canClaim("0", 1, bobWallet.address),
"!can claim",
);
});
it("should work when the token has a price", async () => {
await bdContract.createBatch([
{
name: "test",
description: "test",
},
]);
await bdContract.claimConditions.set("0", [
{
price: 1,
},
]);
await bdContract.claim("0", 1);
});
it("should set multiple claim conditions at once", async () => {
await bdContract.createBatch([
{
name: "test1",
description: "test1",
},
{
name: "test2",
description: "test2",
},
]);
await bdContract.claimConditions.setBatch([
{
tokenId: 0,
claimConditions: [
{
price: 1,
},
],
},
{
tokenId: 1,
claimConditions: [
{
price: 0.1,
maxClaimableSupply: 1,
},
],
},
]);
await bdContract.claim("0", 2);
try {
await bdContract.claim("1", 2);
} catch (e) {
expectError(e, "exceed max mint supply");
}
});
describe("eligibility", () => {
beforeEach(async () => {
await bdContract.createBatch([
{
name: "test",
description: "test",
},
]);
});
it("should return false if there isn't an active claim condition", async () => {
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"0",
bobWallet.address,
);
expect(reasons).to.include(ClaimEligibility.NoClaimConditionSet);
assert.lengthOf(reasons, 1);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
w1.address,
);
assert.isFalse(canClaim);
});
it("set claim condition in the future should not be claimable now", async () => {
await bdContract.claimConditions.set(0, [
{
startTime: new Date(Date.now() + 60 * 60 * 24 * 1000),
},
]);
const canClaim = await bdContract.claimConditions.canClaim(0, 1);
expect(canClaim).to.eq(false);
});
it("should check for the total supply", async () => {
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 1,
},
]);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"2",
w1.address,
);
expect(reasons).to.include(ClaimEligibility.NotEnoughSupply);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"2",
w1.address,
);
assert.isFalse(canClaim);
});
it("should check if an address has valid merkle proofs", async () => {
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 1,
snapshot: [w2.address, adminWallet.address],
},
]);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
w1.address,
);
expect(reasons).to.include(ClaimEligibility.AddressNotAllowed);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
w1.address,
);
assert.isFalse(canClaim);
});
it("should check if its been long enough since the last claim", async () => {
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 10,
waitInSeconds: 24 * 60 * 60,
},
]);
await sdk.updateSignerOrProvider(bobWallet);
await bdContract.claim("0", 1);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
bobWallet.address,
);
expect(reasons).to.include(
ClaimEligibility.WaitBeforeNextClaimTransaction,
);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
bobWallet.address,
);
assert.isFalse(canClaim);
});
it("should check if an address has enough native currency", async () => {
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: NATIVE_TOKEN_ADDRESS,
},
]);
await sdk.updateSignerOrProvider(bobWallet);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
bobWallet.address,
);
expect(reasons).to.include(ClaimEligibility.NotEnoughTokens);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
w1.address,
);
assert.isFalse(canClaim);
});
it("should check if an address has enough erc20 currency", async () => {
const currency = await sdk.getToken(
await sdk.deployer.deployBuiltInContract(
TokenInitializer.contractType,
{
name: "test",
symbol: "test",
primary_sale_recipient: adminWallet.address,
},
),
);
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: currency.getAddress(),
},
]);
await sdk.updateSignerOrProvider(bobWallet);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
bobWallet.address,
);
expect(reasons).to.include(ClaimEligibility.NotEnoughTokens);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
w1.address,
);
assert.isFalse(canClaim);
});
it("should return nothing if the claim is eligible", async () => {
await bdContract.claimConditions.set("0", [
{
maxClaimableSupply: 10,
price: "100",
currencyAddress: NATIVE_TOKEN_ADDRESS,
snapshot: [w1.address, w2.address, w3.address],
},
]);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
w1.address,
);
assert.lengthOf(reasons, 0);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
w1.address,
);
assert.isTrue(canClaim);
});
});
it("should allow you to update claim conditions", async () => {
await bdContract.createBatch([
{ name: "test", description: "test" },
{ name: "test", description: "test" },
]);
await bdContract.claimConditions.set(BigNumber.from("0"), [{}]);
await bdContract.claimConditions.update(BigNumber.from("0"), 0, {});
const conditions = await bdContract.claimConditions.getAll(0);
assert.lengthOf(conditions, 1);
});
it("should return snapshot data on claim conditions", async () => {
await bdContract.createBatch([
{ name: "test", description: "test" },
{ name: "test", description: "test" },
]);
await bdContract.claimConditions.set(0, [
{
snapshot: [samWallet.address],
},
]);
const conditions = await bdContract.claimConditions.getAll(0, {
withAllowList: true,
});
assert.lengthOf(conditions, 1);
invariant(conditions[0].snapshot);
expect(conditions[0].snapshot[0].address).to.eq(samWallet.address);
});
it("should be able to use claim as function expected", async () => {
await bdContract.createBatch([
{
name: "test",
},
]);
await bdContract.claimConditions.set("0", [{}]);
await bdContract.claim("0", 1);
assert((await bdContract.getOwned()).length > 0);
});
it("should be able to use claimTo function as expected", async () => {
await bdContract.createBatch([
{
name: "test",
},
]);
await bdContract.claimConditions.set("0", [{}]);
await bdContract.claimTo(samWallet.address, "0", 3);
assert((await bdContract.getOwned(samWallet.address)).length === 1);
assert(
(await bdContract.getOwned(samWallet.address))[0].owner ===
samWallet.address,
);
assert(
(await bdContract.getOwned(samWallet.address))[0].quantityOwned === 3,
);
});
describe("setting merkle claim conditions", () => {
it("should not overwrite existing merkle keys in the metadata", async () => {
await bdContract.createBatch([
{ name: "test", description: "test" },
{ name: "test", description: "test" },
]);
await bdContract.claimConditions.set("1", [
{
snapshot: [w1.address, w2.address, bobWallet.address],
},
]);
await bdContract.claimConditions.set("2", [
{
snapshot: [w3.address, w1.address, w2.address, adminWallet.address],
},
]);
const metadata = await bdContract.metadata.get();
const merkle: { [key: string]: string } = metadata.merkle;
assert.lengthOf(Object.keys(merkle), 2);
});
});
});
@@ -1,6 +1,7 @@
import {
ClaimEligibility,
EditionDrop,
EditionDropInitializer,
NATIVE_TOKEN_ADDRESS,
TokenInitializer,
} from "../../src/evm";
@@ -17,7 +18,7 @@ const deepEqualInAnyOrder = require("deep-equal-in-any-order");
use(deepEqualInAnyOrder);
describe("Edition Drop Contract", async () => {
describe("Edition Drop Contract (V4)", async () => {
let bdContract: EditionDrop;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
@@ -34,24 +35,73 @@ describe("Edition Drop Contract", async () => {
beforeEach(async () => {
sdk.updateSignerOrProvider(adminWallet);
const address = await sdk.deployer.deployEditionDrop({
name: `Testing bundle drop from SDK`,
description: "Test contract from tests",
image:
"https://pbs.twimg.com/profile_images/1433508973215367176/XBCfBn3g_400x400.jpg",
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 500,
fee_recipient: AddressZero,
platform_fee_basis_points: 10,
platform_fee_recipient: adminWallet.address,
});
const address = await sdk.deployer.deployBuiltInContract(
EditionDropInitializer.contractType,
{
name: `Testing edition drop from SDK`,
description: "Test contract from tests",
image:
"https://pbs.twimg.com/profile_images/1433508973215367176/XBCfBn3g_400x400.jpg",
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 500,
fee_recipient: AddressZero,
platform_fee_basis_points: 10,
platform_fee_recipient: adminWallet.address,
},
await sdk.deployer.getLatestBuiltInContractVersion(
EditionDropInitializer.contractType,
),
);
bdContract = await sdk.getEditionDrop(address);
});
it("should allow a snapshot to be set", async () => {
await bdContract.createBatch([
{
name: "Test1",
},
]);
await bdContract.claimConditions.set(0, [
{
startTime: new Date(Date.now() / 2),
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
price: 1,
},
{
startTime: new Date(),
snapshot: [bobWallet.address],
},
]);
let proof = await bdContract.claimConditions.getClaimerProofs(
0,
bobWallet.address,
);
expect(proof.address).to.eq(bobWallet.address);
proof = await bdContract.claimConditions.getClaimerProofs(
0,
bobWallet.address,
0,
);
expect(proof.address).to.eq(bobWallet.address);
proof = await bdContract.claimConditions.getClaimerProofs(
0,
samWallet.address,
);
expect(proof).to.eq(null);
proof = await bdContract.claimConditions.getClaimerProofs(
0,
samWallet.address,
0,
);
expect(proof.address).to.eq(samWallet.address);
});
it("should estimate gas cost", async () => {
const cost = await bdContract.estimator.gasCostOf("lazyMint", [
1000,
"mock://12398172398172389/0",
"0x00",
]);
expect(parseFloat(cost)).gt(0);
});
@@ -96,7 +146,7 @@ describe("Edition Drop Contract", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 1000,
maxClaimableSupply: 1000,
snapshot: members,
},
]);
@@ -129,10 +179,14 @@ describe("Edition Drop Contract", async () => {
w2,
w3,
];
const members = testWallets.map((w) => w.address);
const members = testWallets.map((w) => ({
address: w.address,
maxClaimable: 1,
}));
await bdContract.claimConditions.set("0", [
{
maxQuantity: 1000,
maxClaimablePerWallet: 0,
maxClaimableSupply: 1000,
snapshot: members,
},
]);
@@ -215,7 +269,7 @@ describe("Edition Drop Contract", async () => {
await sdk.updateSignerOrProvider(w2);
await bdContract.claim(0, 2);
} catch (e) {
expectError(e, "invalid quantity proof");
expectError(e, "!Qty");
}
});
@@ -236,6 +290,20 @@ describe("Edition Drop Contract", async () => {
await bdContract.claim("0", 1);
});
it("should allow claiming with a price", async () => {
await bdContract.createBatch([
{ name: "name", description: "description" },
]);
await bdContract.claimConditions.set(0, [
{
price: 0.1,
},
]);
await bdContract.claim(0, 2);
const balance = await bdContract.balance(0);
expect(balance).to.deep.equal(BigNumber.from(2));
});
it("should return addresses of all the claimers", async () => {
await bdContract.createBatch([
{
@@ -274,7 +342,8 @@ describe("Edition Drop Contract", async () => {
it("should return the correct status if a token can be claimed", async () => {
await bdContract.claimConditions.set("0", [
{
snapshot: [w1.address],
maxClaimablePerWallet: 0,
snapshot: [{ address: w1.address, maxClaimable: 1 }],
},
]);
@@ -320,7 +389,8 @@ describe("Edition Drop Contract", async () => {
it("canClaim: 1 address", async () => {
await bdContract.claimConditions.set("0", [
{
snapshot: [w1.address],
maxClaimablePerWallet: 0,
snapshot: [{ address: w1.address, maxClaimable: 1 }],
},
]);
@@ -335,13 +405,18 @@ describe("Edition Drop Contract", async () => {
});
it("canClaim: 3 address", async () => {
const members = [
{
address: w1.address.toUpperCase().replace("0X", "0x"),
maxClaimable: 1,
},
{ address: w2.address.toLowerCase(), maxClaimable: 1 },
{ address: w3.address.toLowerCase(), maxClaimable: 1 },
];
await bdContract.claimConditions.set("0", [
{
snapshot: [
w1.address.toUpperCase().replace("0X", "0x"),
w2.address.toLowerCase(),
w3.address,
],
maxClaimablePerWallet: 0,
snapshot: members,
},
]);
@@ -403,7 +478,7 @@ describe("Edition Drop Contract", async () => {
claimConditions: [
{
price: 0.1,
maxQuantity: 1,
maxClaimableSupply: 1,
},
],
},
@@ -412,7 +487,7 @@ describe("Edition Drop Contract", async () => {
try {
await bdContract.claim("1", 2);
} catch (e) {
expectError(e, "exceed max mint supply");
expectError(e, "!MaxSupply");
}
});
@@ -457,7 +532,7 @@ describe("Edition Drop Contract", async () => {
it("should check for the total supply", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 1,
maxClaimableSupply: 1,
},
]);
@@ -476,61 +551,26 @@ describe("Edition Drop Contract", async () => {
assert.isFalse(canClaim);
});
it("should check if an address has valid merkle proofs", async () => {
it("should get default values with default snapshot", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 1,
snapshot: [w2.address, adminWallet.address],
maxClaimableSupply: 1,
snapshot: [w1.address],
},
]);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
w1.address,
);
expect(reasons).to.include(ClaimEligibility.AddressNotAllowed);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
w1.address,
);
assert.isFalse(canClaim);
});
it("should check if its been long enough since the last claim", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 10,
waitInSeconds: 24 * 60 * 60,
},
]);
await sdk.updateSignerOrProvider(bobWallet);
await bdContract.claim("0", 1);
const reasons =
await bdContract.claimConditions.getClaimIneligibilityReasons(
"0",
"1",
bobWallet.address,
);
expect(reasons).to.include(
ClaimEligibility.WaitBeforeNextClaimTransaction,
);
const canClaim = await bdContract.claimConditions.canClaim(
"0",
"1",
bobWallet.address,
);
assert.isFalse(canClaim);
assert.isTrue(canClaim);
});
it("should check if an address has enough native currency", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: NATIVE_TOKEN_ADDRESS,
},
@@ -567,7 +607,7 @@ describe("Edition Drop Contract", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: currency.getAddress(),
},
@@ -593,7 +633,7 @@ describe("Edition Drop Contract", async () => {
it("should return nothing if the claim is eligible", async () => {
await bdContract.claimConditions.set("0", [
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "100",
currencyAddress: NATIVE_TOKEN_ADDRESS,
snapshot: [w1.address, w2.address, w3.address],
@@ -1,23 +1,21 @@
import { NFTMetadataInput } from "../../src/core/schema/nft";
import {
ClaimEligibility,
createSnapshot,
NATIVE_TOKEN_ADDRESS,
NFTDrop,
NFTDropInitializer,
TokenInitializer,
} from "../../src/evm";
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { expectError, sdk, signers, storage } from "./before-setup";
import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { assert, expect } from "chai";
import { BigNumber, ethers } from "ethers";
import { MerkleTree } from "merkletreejs";
import invariant from "tiny-invariant";
global.fetch = require("cross-fetch");
describe("NFT Drop Contract", async () => {
describe("NFT Drop Contract (v3)", async () => {
let dropContract: NFTDrop;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
@@ -30,18 +28,21 @@ describe("NFT Drop Contract", async () => {
beforeEach(async () => {
[adminWallet, samWallet, bobWallet, abbyWallet, w1, w2, w3, w4] = signers;
sdk.updateSignerOrProvider(adminWallet);
const address = await sdk.deployer.deployNFTDrop({
name: `Testing drop from SDK`,
description: "Test contract from tests",
image:
"https://pbs.twimg.com/profile_images/1433508973215367176/XBCfBn3g_400x400.jpg",
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 500,
fee_recipient: AddressZero,
platform_fee_basis_points: 10,
platform_fee_recipient: AddressZero,
});
const address = await sdk.deployer.deployBuiltInContract(
NFTDropInitializer.contractType,
{
name: `Testing drop from SDK`,
description: "Test contract from tests",
image:
"https://pbs.twimg.com/profile_images/1433508973215367176/XBCfBn3g_400x400.jpg",
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 500,
fee_recipient: AddressZero,
platform_fee_basis_points: 10,
platform_fee_recipient: AddressZero,
},
3,
);
dropContract = await sdk.getNFTDrop(address);
});
@@ -55,6 +56,116 @@ describe("NFT Drop Contract", async () => {
assert.equal(nft.metadata.name, "Test1");
});
it("comprehensive test", async () => {
const metadata = [];
for (let i = 0; i < 10; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
await dropContract.createBatch(metadata);
// claiming with default conditions
await dropContract.claimConditions.set([{}]);
await dropContract.claim(1);
// claiming with max supply
await dropContract.claimConditions.set([
{
maxClaimableSupply: 2,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim 2 - maxSupply");
} catch (e) {
expectError(e, "exceed max claimable supply");
}
await dropContract.claim(1);
// claiming with max per wallet
await dropContract.claimConditions.set([
{
maxClaimablePerWallet: 1,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim 2 - maxClaimablePerWallet");
} catch (e) {
expectError(e, "invalid quantity");
}
await dropContract.claim(1);
expect((await dropContract.totalClaimedSupply()).toString()).eq("3");
});
it("comprehensive test with allowlist", async () => {
const metadata = [];
for (let i = 0; i < 10; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
await dropContract.createBatch(metadata);
// claiming with default conditions
await dropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
},
]);
try {
sdk.updateSignerOrProvider(bobWallet);
await dropContract.claim(1);
expect.fail("should not be able to claim - not in allowlist");
} catch (e) {
expectError(e, "No claim found");
}
sdk.updateSignerOrProvider(adminWallet);
await dropContract.claim(1);
// claiming with max supply
await dropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
maxClaimableSupply: 2,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim - maxClaimableSupply");
} catch (e) {
expectError(e, "exceed max claimable supply");
}
await dropContract.claim(1);
// claiming with max per wallet
await dropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
maxClaimablePerWallet: 1,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim - maxClaimablePerWallet");
} catch (e) {
expectError(e, "invalid quantity");
}
await dropContract.claim(1);
// claiming with max per wallet in snapshot
await dropContract.claimConditions.set([
{
snapshot: [{ address: adminWallet.address, maxClaimable: 1 }],
maxClaimablePerWallet: 0,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim - proof maxClaimable");
} catch (e) {
expectError(e, "invalid quantity proof");
}
await dropContract.claim(1);
try {
await dropContract.claim(1);
expect.fail("should not be able to claim - proof used");
} catch (e) {
expectError(e, "proof claimed");
}
});
it("should get and execute transaction task", async () => {
await dropContract.createBatch([
{
@@ -85,23 +196,36 @@ describe("NFT Drop Contract", async () => {
const metadata = await dropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
expect(merkles).have.property(
"0xb1a60ad68b77609a455696695fbdd02b850d03ec285e7fe1f4c4093797457b24",
);
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
const roots = (await dropContract.claimConditions.getAll()).map(
(c) => c.merkleRootHash,
);
expect(roots).length(2);
const proof = await dropContract.claimConditions.getClaimerProofs(
let proof = await dropContract.claimConditions.getClaimerProofs(
bobWallet.address,
);
expect(proof.address).to.eq(bobWallet.address);
proof = await dropContract.claimConditions.getClaimerProofs(
bobWallet.address,
0,
);
expect(proof.address).to.eq(bobWallet.address);
proof = await dropContract.claimConditions.getClaimerProofs(
samWallet.address,
);
expect(proof).to.eq(null);
proof = await dropContract.claimConditions.getClaimerProofs(
samWallet.address,
0,
);
expect(proof.address).to.eq(samWallet.address);
});
it("should return snapshot data on claim conditions", async () => {
@@ -139,13 +263,12 @@ describe("NFT Drop Contract", async () => {
const metadata = await dropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
expect(merkles).have.property(
"0xb1a60ad68b77609a455696695fbdd02b850d03ec285e7fe1f4c4093797457b24",
);
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
const roots = (await dropContract.claimConditions.getAll()).map(
(c) => c.merkleRootHash,
@@ -268,14 +391,14 @@ describe("NFT Drop Contract", async () => {
await dropContract.createBatch(metadatas);
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
},
]);
await dropContract.claim(5);
await dropContract.claimConditions.set(
[
{
maxQuantity: 10,
maxClaimableSupply: 10,
},
],
true,
@@ -365,52 +488,6 @@ describe("NFT Drop Contract", async () => {
}
});
it("should generate valid proofs", async () => {
const members = [
bobWallet.address,
samWallet.address,
abbyWallet.address,
w1.address,
w2.address,
w3.address,
w4.address,
];
const hashedLeafs = members.map((l) =>
ethers.utils.solidityKeccak256(["address", "uint256"], [l, 0]),
);
const tree = new MerkleTree(hashedLeafs, ethers.utils.keccak256, {
sort: true,
sortLeaves: true,
sortPairs: true,
});
const input = members.map((address) => ({
address,
maxClaimable: 0,
}));
const snapshot = await createSnapshot(input, 0, storage);
for (const leaf of members) {
const expectedProof = tree.getHexProof(
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
);
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(leaf);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
const verified = tree.verify(
actualProof?.proof as string[],
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
tree.getHexRoot(),
);
expect(verified).to.eq(true);
}
});
it("should return the newly claimed token", async () => {
await dropContract.claimConditions.set([{}]);
await dropContract.createBatch([
@@ -472,7 +549,7 @@ describe("NFT Drop Contract", async () => {
});
it("should check for the total supply", async () => {
await dropContract.claimConditions.set([{ maxQuantity: 1 }]);
await dropContract.claimConditions.set([{ maxClaimableSupply: 1 }]);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
@@ -490,7 +567,7 @@ describe("NFT Drop Contract", async () => {
it("should check if an address has valid merkle proofs", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 1,
maxClaimableSupply: 1,
snapshot: [w2.address, adminWallet.address],
},
]);
@@ -511,7 +588,7 @@ describe("NFT Drop Contract", async () => {
it("should check if its been long enough since the last claim", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
waitInSeconds: 24 * 60 * 60,
},
]);
@@ -534,7 +611,7 @@ describe("NFT Drop Contract", async () => {
it("should check if an address has enough native currency", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: NATIVE_TOKEN_ADDRESS,
},
@@ -564,7 +641,7 @@ describe("NFT Drop Contract", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress,
},
@@ -585,7 +662,7 @@ describe("NFT Drop Contract", async () => {
it("should return nothing if the claim is eligible", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "100",
currencyAddress: NATIVE_TOKEN_ADDRESS,
snapshot: [w1.address, w2.address, w3.address],
@@ -802,14 +879,18 @@ describe("NFT Drop Contract", async () => {
it("set claim condition and update claim condition", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2), maxQuantity: 1, price: 0.15 },
{
startTime: new Date(Date.now() / 2),
maxClaimableSupply: 1,
price: 0.15,
},
{ startTime: new Date(), waitInSeconds: 60 },
]);
expect((await dropContract.claimConditions.getAll()).length).to.be.equal(2);
await dropContract.claimConditions.update(0, { waitInSeconds: 10 });
let updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxQuantity).to.be.deep.equal("1");
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("1");
expect(updatedConditions[0].price).to.be.deep.equal(
ethers.utils.parseUnits("0.15"),
);
@@ -821,12 +902,12 @@ describe("NFT Drop Contract", async () => {
);
await dropContract.claimConditions.update(1, {
maxQuantity: 10,
maxClaimableSupply: 10,
waitInSeconds: 10,
});
updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxQuantity).to.be.deep.equal("1");
expect(updatedConditions[1].maxQuantity).to.be.deep.equal("10");
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("1");
expect(updatedConditions[1].maxClaimableSupply).to.be.deep.equal("10");
expect(updatedConditions[1].waitInSeconds).to.be.deep.equal(
BigNumber.from(10),
);
@@ -834,8 +915,8 @@ describe("NFT Drop Contract", async () => {
it("set claim condition and update claim condition with diff timestamps should reorder", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2), maxQuantity: 1 },
{ startTime: new Date(), maxQuantity: 2 },
{ startTime: new Date(Date.now() / 2), maxClaimableSupply: 1 },
{ startTime: new Date(), maxClaimableSupply: 2 },
]);
expect((await dropContract.claimConditions.getAll()).length).to.be.equal(2);
@@ -844,8 +925,8 @@ describe("NFT Drop Contract", async () => {
});
// max quantities should be inverted now
const updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxQuantity).to.be.deep.equal("2");
expect(updatedConditions[1].maxQuantity).to.be.deep.equal("1");
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("2");
expect(updatedConditions[1].maxClaimableSupply).to.be.deep.equal("1");
});
it("set claim condition in the future should not be claimable now", async () => {
@@ -1000,14 +1081,14 @@ describe("NFT Drop Contract", async () => {
(await adminWallet.provider?.getBlockWithTransactions("latest"))
?.transactions || [];
const { index, _key } = dropContract.encoder.decode(
const [index, key] = dropContract.encoder.decode(
"reveal",
transactions[0].data,
);
// re-using broadcasted _key to decode :)
try {
await dropContract.revealer.reveal(index.add(1), _key);
await dropContract.revealer.reveal(index.add(1), key);
assert.fail("should not be able to re-used published password");
} catch (e) {
expect((e as Error).message).to.be.equal("invalid password");
File diff suppressed because it is too large Load Diff
+18 -6
View File
@@ -9,6 +9,7 @@ import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
DropERC721__factory,
DropERC721_V3__factory,
TokenERC721__factory,
} from "@thirdweb-dev/contracts-js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
@@ -83,15 +84,26 @@ describe("Publishing", async () => {
).to.eq(true);
// Drop
expect(isFeatureEnabled(DropERC721__factory.abi, "ERC721Enumerable")).to.eq(
true,
);
expect(
isFeatureEnabled(
DropERC721__factory.abi,
"ERC721ClaimableWithConditionsV2",
),
).to.eq(true);
expect(isFeatureEnabled(DropERC721__factory.abi, "ERC721Supply")).to.eq(
true,
);
expect(isFeatureEnabled(DropERC721__factory.abi, "ERC721Mintable")).to.eq(
false,
);
// Drop legacy
expect(
isFeatureEnabled(
DropERC721_V3__factory.abi,
"ERC721ClaimableWithConditionsV1",
),
).to.eq(true);
});
it("should update bio", async () => {
@@ -359,7 +371,7 @@ describe("Publishing", async () => {
});
it("ERC721Dropable multiphase feature detection", async () => {
const ipfsUri = "ipfs://QmWaidQMSYHPzYYZCxMc2nSk2vrD28mS43Xc9k7QFyAGja/0";
const ipfsUri = "ipfs://Qmbu57WNPmmGuNZEiEAVi9yeXxGK2GkJRBbRMaPxs9KS5b";
const addr = await sdk.deployer.deployContractFromUri(ipfsUri, []);
const c = await sdk.getContract(addr);
@@ -402,7 +414,7 @@ describe("Publishing", async () => {
await c.erc721.claimConditions.set([
{
price: "0",
maxQuantity: 2,
maxClaimableSupply: 2,
startTime: new Date(0),
},
]);
@@ -440,7 +452,7 @@ describe("Publishing", async () => {
await c.erc1155.claimConditions.set(0, [
{
price: "0",
maxQuantity: 2,
maxClaimableSupply: 2,
startTime: new Date(0),
},
]);
@@ -0,0 +1,873 @@
import { NFTMetadataInput } from "../../src/core/schema/nft";
import {
NATIVE_TOKEN_ADDRESS,
PayloadToSign721withQuantity,
SignatureDrop,
SignatureDropInitializer,
SignedPayload721WithQuantitySignature,
Token,
TokenInitializer,
} from "../../src/evm";
import { expectError, sdk, signers, storage } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { assert, expect } from "chai";
import { BigNumber } from "ethers";
import invariant from "tiny-invariant";
global.fetch = require("cross-fetch");
describe("Signature drop tests (v4)", async () => {
let signatureDropContract: SignatureDrop;
let customTokenContract: Token;
let tokenAddress: string;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
abbyWallet: SignerWithAddress,
bobWallet: SignerWithAddress,
w1: SignerWithAddress,
w2: SignerWithAddress,
w3: SignerWithAddress,
w4: SignerWithAddress;
let meta: PayloadToSign721withQuantity;
before(() => {
[adminWallet, samWallet, bobWallet, abbyWallet, w1, w2, w3, w4] = signers;
});
beforeEach(async () => {
sdk.updateSignerOrProvider(adminWallet);
signatureDropContract = await sdk.getSignatureDrop(
await sdk.deployer.deployBuiltInContract(
SignatureDropInitializer.contractType,
{
name: "OUCH VOUCH",
symbol: "VOUCH",
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 0,
},
4,
),
);
meta = {
currencyAddress: NATIVE_TOKEN_ADDRESS,
metadata: {
name: "OUCH VOUCH",
},
price: "1",
quantity: 1,
to: samWallet.address,
};
customTokenContract = await sdk.getToken(
await sdk.deployer.deployBuiltInContract(TokenInitializer.contractType, {
name: "Test",
symbol: "TEST",
primary_sale_recipient: adminWallet.address,
}),
);
await customTokenContract.mintBatchTo([
{
toAddress: samWallet.address,
amount: 1000,
},
{
toAddress: adminWallet.address,
amount: 1000,
},
]);
tokenAddress = customTokenContract.getAddress();
});
describe("Generating Signatures", () => {
let goodPayload: SignedPayload721WithQuantitySignature;
let badPayload: SignedPayload721WithQuantitySignature;
beforeEach(async () => {
goodPayload = await signatureDropContract.signature.generate(meta);
badPayload = await signatureDropContract.signature.generate(meta);
badPayload.payload.price = "0";
});
it("should generate a valid signature", async () => {
const valid = await signatureDropContract.signature.verify(goodPayload);
assert.isTrue(valid, "This voucher should be valid");
});
it("should reject invalid signatures", async () => {
const invalid = await signatureDropContract.signature.verify(badPayload);
assert.isFalse(
invalid,
"This voucher should be invalid because the signature is invalid",
);
});
it("should reject invalid vouchers", async () => {
goodPayload.payload.price = "0";
const invalidModified = await signatureDropContract.signature.verify(
goodPayload,
);
assert.isFalse(
invalidModified,
"This voucher should be invalid because the price was changed",
);
});
it("should generate a valid batch of signatures", async () => {
const input = [
{
...meta,
metadata: {
name: "OUCH VOUCH 0",
},
},
{
...meta,
metadata: {
name: "OUCH VOUCH 1",
},
},
{
...meta,
metadata: {
name: "OUCH VOUCH 2",
},
},
];
await signatureDropContract.createBatch([
{
name: "OUCH VOUCH 0",
},
{
name: "OUCH VOUCH 1",
},
{
name: "OUCH VOUCH 2",
},
]);
const batch = await signatureDropContract.signature.generateBatch(input);
for (let i = 0; i < batch.length; i++) {
const entry = batch[i];
const tx = await signatureDropContract.signature.mint(entry);
const mintedId = (await signatureDropContract.get(tx.id)).metadata.id;
const nft = await signatureDropContract.get(mintedId);
assert.equal(input[i].metadata.name, nft.metadata.name);
}
});
});
describe("Claiming", async () => {
let v1: SignedPayload721WithQuantitySignature,
v2: SignedPayload721WithQuantitySignature;
beforeEach(async () => {
v1 = await signatureDropContract.signature.generate(meta);
v2 = await signatureDropContract.signature.generate(meta);
await signatureDropContract.createBatch([
{
name: "Test1",
},
{
name: "Test2",
},
]);
});
it("comprehensive test", async () => {
const metadata = [];
for (let i = 0; i < 10; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
await signatureDropContract.createBatch(metadata);
// claiming with default conditions
await signatureDropContract.claimConditions.set([{}]);
await signatureDropContract.claim(1);
// claiming with max supply
await signatureDropContract.claimConditions.set([
{
maxClaimableSupply: 1,
},
]);
try {
await signatureDropContract.claim(2);
expect.fail("should not be able to claim 2 - maxSupply");
} catch (e) {
expectError(e, "exceeds max supply");
}
await signatureDropContract.claim(1);
// claiming with max per wallet
await signatureDropContract.claimConditions.set([
{
maxClaimablePerWallet: 1,
},
]);
try {
await signatureDropContract.claim(2);
expect.fail("should not be able to claim 2 - maxClaimablePerWallet");
} catch (e) {
expectError(e, "Invalid quantity");
}
await signatureDropContract.claim(1);
expect((await signatureDropContract.totalClaimedSupply()).toString()).eq(
"3",
);
});
it("comprehensive test with allowlist", async () => {
const metadata = [];
for (let i = 0; i < 10; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
await signatureDropContract.createBatch(metadata);
// claiming with default conditions
await signatureDropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
},
]);
try {
sdk.updateSignerOrProvider(bobWallet);
await signatureDropContract.claim(1);
expect.fail("should not be able to claim - not in allowlist");
} catch (e) {
expectError(e, "No claim found");
}
sdk.updateSignerOrProvider(adminWallet);
await signatureDropContract.claim(1);
// claiming with max supply
await signatureDropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
maxClaimableSupply: 1,
},
]);
try {
await signatureDropContract.claim(2);
expect.fail("should not be able to claim - maxClaimableSupply");
} catch (e) {
expectError(e, "exceeds max supply");
}
await signatureDropContract.claim(1);
// claiming with max per wallet
await signatureDropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
maxClaimablePerWallet: 1,
},
]);
try {
await signatureDropContract.claim(2);
expect.fail("should not be able to claim - maxClaimablePerWallet");
} catch (e) {
expectError(e, "Invalid quantity");
}
await signatureDropContract.claim(1);
// claiming with max per wallet in snapshot
await signatureDropContract.claimConditions.set([
{
snapshot: [{ address: adminWallet.address, maxClaimable: 1 }],
maxClaimablePerWallet: 0,
},
]);
try {
await signatureDropContract.claim(2);
expect.fail("should not be able to claim - proof maxClaimable");
} catch (e) {
expectError(e, "Invalid qty proof");
}
await signatureDropContract.claim(1);
try {
await signatureDropContract.claim(1);
expect.fail("should not be able to claim - proof used");
} catch (e) {
expectError(e, "proof claimed");
}
});
it("should mint with URI", async () => {
const uri = await storage.upload({
name: "Test1",
});
const toSign = {
to: adminWallet.address,
metadata: uri,
quantity: 1,
};
const payload = await signatureDropContract.signature.generate(toSign);
const tx = await signatureDropContract.signature.mint(payload);
const nft = await signatureDropContract.get(tx.id);
assert.equal(nft.metadata.name, "Test1");
});
it("should mint batch with URI", async () => {
const uri1 = await storage.upload({
name: "Test1",
});
const uri2 = await storage.upload({
name: "Test2",
});
const toSign1 = {
to: adminWallet.address,
metadata: uri1,
quantity: 1,
};
const toSign2 = {
to: adminWallet.address,
metadata: uri2,
quantity: 1,
};
const payloads = await signatureDropContract.signature.generateBatch([
toSign1,
toSign2,
]);
const tx = await signatureDropContract.signature.mintBatch(payloads);
const nft1 = await signatureDropContract.get(tx[0].id);
assert.equal(nft1.metadata.name, "Test1");
const nft2 = await signatureDropContract.get(tx[1].id);
assert.equal(nft2.metadata.name, "Test2");
});
it("should allow a valid voucher to mint", async () => {
await sdk.updateSignerOrProvider(samWallet);
const tx = await signatureDropContract.signature.mint(v1);
const newId = (await signatureDropContract.get(tx.id)).metadata.id;
assert.equal(newId.toString(), "0");
await sdk.updateSignerOrProvider(samWallet);
const tx2 = await signatureDropContract.signature.mint(v2);
const newId2 = (await signatureDropContract.get(tx2.id)).metadata.id;
assert.equal(newId2.toString(), "1");
});
it("should mint the right metadata", async () => {
const tx = await signatureDropContract.signature.mint(v1);
const nft = await signatureDropContract.get(tx.id);
assert.equal(nft.metadata.name, "Test1");
const tx2 = await signatureDropContract.signature.mint(v2);
const nft2 = await signatureDropContract.get(tx2.id);
assert.equal(nft2.metadata.name, "Test2");
});
it("should mint the right custom token price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await signatureDropContract.signature.generate({
to: samWallet.address,
price: 1,
currencyAddress: tokenAddress,
metadata: {
name: "custom token test",
},
quantity: 1,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(),
});
await sdk.updateSignerOrProvider(samWallet);
await signatureDropContract.signature.mint(payload);
const newBalance = await samWallet.getBalance();
assert(
oldBalance.sub(newBalance).gte(BigNumber.from(1)),
"balance doesn't match",
);
});
it("should mint the right native price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await signatureDropContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test",
},
quantity: 1,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(),
});
await sdk.updateSignerOrProvider(samWallet);
await signatureDropContract.signature.mint(payload);
const newBalance = await samWallet.getBalance();
assert(
oldBalance.sub(newBalance).gte(BigNumber.from(1)),
"balance doesn't match",
);
});
it("should mint the right native price with multiple tokens", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await signatureDropContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test with quantity",
},
quantity: 2,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(),
});
await sdk.updateSignerOrProvider(samWallet);
await signatureDropContract.signature.mint(payload);
const newBalance = await samWallet.getBalance();
assert(
oldBalance.sub(newBalance).gte(BigNumber.from(2)),
"balance doesn't match",
);
});
});
describe("Delay Reveal", () => {
it("metadata should reveal correctly", async () => {
await signatureDropContract.revealer.createDelayedRevealBatch(
{
name: "Placeholder #1",
},
[{ name: "NFT #1" }, { name: "NFT #2" }],
"my secret password",
);
expect((await signatureDropContract.get("0")).metadata.name).to.be.equal(
"Placeholder #1",
);
await signatureDropContract.revealer.reveal(0, "my secret password");
expect((await signatureDropContract.get("0")).metadata.name).to.be.equal(
"NFT #1",
);
});
it("different reveal order and should return correct unreveal list", async () => {
await signatureDropContract.revealer.createDelayedRevealBatch(
{
name: "Placeholder #1",
},
[
{
name: "NFT #1",
},
{
name: "NFT #2",
},
],
"my secret key",
);
await signatureDropContract.revealer.createDelayedRevealBatch(
{
name: "Placeholder #2",
},
[
{
name: "NFT #3",
},
{
name: "NFT #4",
},
],
"my secret key",
);
await signatureDropContract.createBatch([
{
name: "NFT #00",
},
{
name: "NFT #01",
},
]);
await signatureDropContract.revealer.createDelayedRevealBatch(
{
name: "Placeholder #3",
},
[
{
name: "NFT #5",
},
{
name: "NFT #6",
},
{
name: "NFT #7",
},
],
"my secret key",
);
let unrevealList =
await signatureDropContract.revealer.getBatchesToReveal();
expect(unrevealList.length).to.be.equal(3);
expect(unrevealList[0].batchId.toNumber()).to.be.equal(0);
expect(unrevealList[0].placeholderMetadata.name).to.be.equal(
"Placeholder #1",
);
expect(unrevealList[1].batchId.toNumber()).to.be.equal(1);
expect(unrevealList[1].placeholderMetadata.name).to.be.equal(
"Placeholder #2",
);
// skipped 2 because it is a revealed batch
expect(unrevealList[2].batchId.toNumber()).to.be.equal(3);
expect(unrevealList[2].placeholderMetadata.name).to.be.equal(
"Placeholder #3",
);
await signatureDropContract.revealer.reveal(
unrevealList[0].batchId,
"my secret key",
);
unrevealList = await signatureDropContract.revealer.getBatchesToReveal();
expect(unrevealList.length).to.be.equal(2);
expect(unrevealList[0].batchId.toNumber()).to.be.equal(1);
expect(unrevealList[0].placeholderMetadata.name).to.be.equal(
"Placeholder #2",
);
expect(unrevealList[1].batchId.toNumber()).to.be.equal(3);
expect(unrevealList[1].placeholderMetadata.name).to.be.equal(
"Placeholder #3",
);
await signatureDropContract.revealer.reveal(
unrevealList[unrevealList.length - 1].batchId,
"my secret key",
);
unrevealList = await signatureDropContract.revealer.getBatchesToReveal();
expect(unrevealList.length).to.be.equal(1);
expect(unrevealList[0].batchId.toNumber()).to.be.equal(1);
expect(unrevealList[0].placeholderMetadata.name).to.be.equal(
"Placeholder #2",
);
});
it("should not be able to re-used published password for next batch", async () => {
await signatureDropContract.revealer.createDelayedRevealBatch(
{
name: "Placeholder #1",
},
[{ name: "NFT #1" }, { name: "NFT #2" }],
"my secret password",
);
await signatureDropContract.revealer.createDelayedRevealBatch(
{
name: "Placeholder #2",
},
[{ name: "NFT #3" }, { name: "NFT #4" }],
"my secret password",
);
await signatureDropContract.revealer.reveal(0, "my secret password");
const transactions =
(await adminWallet.provider?.getBlockWithTransactions("latest"))
?.transactions || [];
const { _index, _key } = signatureDropContract.encoder.decode(
"reveal",
transactions[0].data,
);
// re-using broadcasted _key to decode :)
try {
await signatureDropContract.revealer.reveal(_index.add(1), _key);
assert.fail("should not be able to re-used published password");
} catch (e) {
expect((e as Error).message).to.be.equal("invalid password");
}
// original password should work
await signatureDropContract.revealer.reveal(1, "my secret password");
});
});
describe("Claim Conditions", () => {
it("should allow a snapshot to be set", async () => {
await signatureDropContract.createBatch([
{ name: "test1", description: "test" },
{ name: "test2", description: "test" },
]);
await signatureDropContract.claimConditions.set([
{
snapshot: [samWallet.address],
},
]);
const conditions = await signatureDropContract.claimConditions.getActive({
withAllowList: true,
});
invariant(conditions.snapshot);
expect(conditions.snapshot[0].address).to.eq(samWallet.address);
});
it("should remove merkles from the metadata when claim conditions are removed", async () => {
await signatureDropContract.claimConditions.set([
{
startTime: new Date(),
waitInSeconds: 10,
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
},
]);
const metadata = await signatureDropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xb1a60ad68b77609a455696695fbdd02b850d03ec285e7fe1f4c4093797457b24",
);
const roots = await signatureDropContract.claimConditions.getActive();
expect(roots.merkleRootHash.length > 0);
await signatureDropContract.claimConditions.set([{}]);
const newMetadata = await signatureDropContract.metadata.get();
const newMerkles = newMetadata.merkle;
expect(JSON.stringify(newMerkles)).to.eq("{}");
});
it("allow all addresses in the merkle tree to claim", async () => {
const testWallets: SignerWithAddress[] = [
bobWallet,
samWallet,
abbyWallet,
w1,
w2,
w3,
w4,
];
const members = testWallets.map((w, i) =>
i % 3 === 0
? w.address.toLowerCase()
: i % 3 === 1
? w.address.toUpperCase().replace("0X", "0x")
: w.address,
);
await signatureDropContract.claimConditions.set([
{
snapshot: members,
},
]);
const metadata = [] as NFTMetadataInput[];
for (let i = 0; i < 10; i++) {
metadata.push({
name: `test ${i}`,
});
}
await signatureDropContract.createBatch(metadata);
/**
* Claiming 1 tokens with proofs: 0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9,0xb1a5bda84b83f7f014abcf0cf69cab5a4de1c3ececa8123a5e4aaacb01f63f83
*/
for (const member of testWallets) {
await sdk.updateSignerOrProvider(member);
await signatureDropContract.claim(1);
}
});
it("allow one address in the merkle tree to claim", async () => {
const testWallets: SignerWithAddress[] = [bobWallet];
const members = testWallets.map((w) => w.address);
await signatureDropContract.claimConditions.set([
{
snapshot: members,
},
]);
const metadata = [] as NFTMetadataInput[];
for (let i = 0; i < 2; i++) {
metadata.push({
name: `test ${i}`,
});
}
await signatureDropContract.createBatch(metadata);
/**
* Claiming 1 tokens with proofs: 0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9,0xb1a5bda84b83f7f014abcf0cf69cab5a4de1c3ececa8123a5e4aaacb01f63f83
*/
for (const member of testWallets) {
await sdk.updateSignerOrProvider(member);
await signatureDropContract.claim(1);
}
try {
await sdk.updateSignerOrProvider(samWallet);
await signatureDropContract.claim(1);
assert.fail("should have thrown");
} catch (e) {
// expected
}
});
it("should query total claimed supply even after claim reset", async () => {
const metadatas = [] as NFTMetadataInput[];
for (let i = 0; i < 100; i++) {
metadatas.push({
name: `test ${i}`,
});
}
await signatureDropContract.createBatch(metadatas);
await signatureDropContract.claimConditions.set([
{
maxClaimableSupply: 10,
},
]);
await signatureDropContract.claim(5);
await signatureDropContract.claimConditions.set(
[
{
maxClaimableSupply: 10,
},
],
true,
);
await signatureDropContract.claim(10);
expect(
(await signatureDropContract.totalClaimedSupply()).toNumber(),
).to.eq(15);
expect((await signatureDropContract.getAllClaimed()).length).to.eq(15);
expect(
(await signatureDropContract.totalUnclaimedSupply()).toNumber(),
).to.eq(85);
expect((await signatureDropContract.getAllUnclaimed()).length).to.eq(85);
});
it("should correctly upload metadata for each nft", async () => {
const metadatas = [] as NFTMetadataInput[];
for (let i = 0; i < 100; i++) {
metadatas.push({
name: `test ${i}`,
});
}
await signatureDropContract.createBatch(metadatas);
const all = await signatureDropContract.getAll();
let owned = await signatureDropContract.getOwned();
expect(all.length).to.eq(100);
expect(owned.length).to.eq(0);
await signatureDropContract.claimConditions.set([{}]);
await signatureDropContract.claim(1);
owned = await signatureDropContract.getOwned();
expect(owned.length).to.eq(1);
expect(owned[0].owner).to.eq(adminWallet.address);
const claimed = await signatureDropContract.totalClaimedSupply();
const unclaimed = await signatureDropContract.totalUnclaimedSupply();
expect(claimed.toNumber()).to.eq(1);
expect(unclaimed.toNumber()).to.eq(99);
});
it("should correctly update total supply after burning", async () => {
const metadatas = [] as NFTMetadataInput[];
for (let i = 0; i < 20; i++) {
metadatas.push({
name: `test ${i}`,
});
}
await signatureDropContract.createBatch(metadatas);
await signatureDropContract.claimConditions.set([{}]);
await signatureDropContract.claim(10);
const ts = await signatureDropContract.totalSupply();
expect(ts.toNumber()).to.eq(20);
await signatureDropContract.burn(0);
const ts2 = await signatureDropContract.totalSupply();
expect(ts2.toNumber()).to.eq(20);
});
it("should not allow claiming to someone not in the merkle tree", async () => {
await signatureDropContract.claimConditions.set([
{
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
},
]);
await signatureDropContract.createBatch([
{ name: "name", description: "description" },
]);
await sdk.updateSignerOrProvider(w1);
try {
await signatureDropContract.claim(1);
} catch (err: any) {
expect(err).to.have.property(
"message",
"No claim found for this address",
"",
);
return;
}
assert.fail("should not reach this point, claim should have failed");
});
it("should allow claims with default settings", async () => {
await signatureDropContract.createBatch([
{ name: "name", description: "description" },
]);
await signatureDropContract.claimConditions.set([{}]);
await signatureDropContract.claim(1);
});
it("should allow setting max claims per wallet", async () => {
await signatureDropContract.createBatch([
{ name: "name", description: "description" },
{ name: "name2", description: "description" },
{ name: "name3", description: "description" },
{ name: "name4", description: "description" },
]);
await signatureDropContract.claimConditions.set([
{
snapshot: [
{ address: w1.address, maxClaimable: 2 },
{ address: w2.address, maxClaimable: 1 },
],
},
]);
await sdk.updateSignerOrProvider(w1);
const tx = await signatureDropContract.claim(2);
expect(tx.length).to.eq(2);
try {
await sdk.updateSignerOrProvider(w2);
await signatureDropContract.claim(2);
} catch (e) {
expectError(e, "Invalid qty proof");
}
});
it("should return the newly claimed token", async () => {
await signatureDropContract.claimConditions.set([{}]);
await signatureDropContract.createBatch([
{
name: "test 0",
},
{
name: "test 1",
},
{
name: "test 2",
},
]);
try {
await signatureDropContract.createBatch([
{
name: "test 0",
},
{
name: "test 1",
},
{
name: "test 2",
},
]);
} catch (err) {
expect(err).to.have.property("message", "Batch already created!", "");
}
const _token = await signatureDropContract.claim(2);
assert.lengthOf(_token, 2);
});
});
});
@@ -1,6 +1,5 @@
import { NFTMetadataInput } from "../../src/core/schema/nft";
import {
createSnapshot,
NATIVE_TOKEN_ADDRESS,
PayloadToSign721withQuantity,
SignatureDrop,
@@ -9,18 +8,15 @@ import {
Token,
TokenInitializer,
} from "../../src/evm";
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { expectError, sdk, signers, storage } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { assert, expect } from "chai";
import { BigNumber, ethers } from "ethers";
import { keccak256 } from "ethers/lib/utils";
import { MerkleTree } from "merkletreejs";
import { BigNumber } from "ethers";
import invariant from "tiny-invariant";
global.fetch = require("cross-fetch");
describe("Signature drop tests", async () => {
describe("Signature drop tests (v5)", async () => {
let signatureDropContract: SignatureDrop;
let customTokenContract: Token;
let tokenAddress: string;
@@ -52,6 +48,7 @@ describe("Signature drop tests", async () => {
primary_sale_recipient: adminWallet.address,
seller_fee_basis_points: 0,
},
5,
),
);
@@ -81,6 +78,10 @@ describe("Signature drop tests", async () => {
toAddress: adminWallet.address,
amount: 1000,
},
{
toAddress: bobWallet.address,
amount: 1000,
},
]);
tokenAddress = customTokenContract.getAddress();
});
@@ -188,6 +189,7 @@ describe("Signature drop tests", async () => {
name: "Test1",
});
const toSign = {
to: adminWallet.address,
metadata: uri,
quantity: 1,
};
@@ -205,10 +207,12 @@ describe("Signature drop tests", async () => {
name: "Test2",
});
const toSign1 = {
to: adminWallet.address,
metadata: uri1,
quantity: 1,
};
const toSign2 = {
to: adminWallet.address,
metadata: uri2,
quantity: 1,
};
@@ -248,6 +252,7 @@ describe("Signature drop tests", async () => {
it("should mint the right custom token price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await signatureDropContract.signature.generate({
to: samWallet.address,
price: 1,
currencyAddress: tokenAddress,
metadata: {
@@ -269,6 +274,7 @@ describe("Signature drop tests", async () => {
it("should mint the right native price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await signatureDropContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test",
@@ -289,6 +295,7 @@ describe("Signature drop tests", async () => {
it("should mint the right native price with multiple tokens", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await signatureDropContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test with quantity",
@@ -493,18 +500,10 @@ describe("Signature drop tests", async () => {
await signatureDropContract.claimConditions.set([
{
startTime: new Date(),
waitInSeconds: 10,
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
},
]);
const metadata = await signatureDropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xb1a60ad68b77609a455696695fbdd02b850d03ec285e7fe1f4c4093797457b24",
);
const roots = await signatureDropContract.claimConditions.getActive();
expect(roots.merkleRootHash.length > 0);
@@ -524,16 +523,22 @@ describe("Signature drop tests", async () => {
w3,
w4,
];
const members = testWallets.map((w, i) =>
i % 3 === 0
? w.address.toLowerCase()
: i % 3 === 1
? w.address.toUpperCase().replace("0X", "0x")
: w.address,
);
const members = testWallets
.map((w, i) =>
i % 3 === 0
? w.address.toLowerCase()
: i % 3 === 1
? w.address.toUpperCase().replace("0X", "0x")
: w.address,
)
.map((a) => ({
address: a,
maxClaimable: 1,
}));
await signatureDropContract.claimConditions.set([
{
maxClaimablePerWallet: 0,
snapshot: members,
},
]);
@@ -557,10 +562,16 @@ describe("Signature drop tests", async () => {
it("allow one address in the merkle tree to claim", async () => {
const testWallets: SignerWithAddress[] = [bobWallet];
const members = testWallets.map((w) => w.address);
const members = testWallets.map((w) => ({
address: w.address,
maxClaimable: 1,
}));
await signatureDropContract.claimConditions.set([
{
maxClaimablePerWallet: 0,
currencyAddress: tokenAddress,
price: 0.1,
snapshot: members,
},
]);
@@ -585,9 +596,9 @@ describe("Signature drop tests", async () => {
try {
await sdk.updateSignerOrProvider(samWallet);
await signatureDropContract.claim(1);
assert.fail("should have thrown");
} catch (e) {
// expected
expectError(e, "!Qty");
}
});
@@ -601,14 +612,14 @@ describe("Signature drop tests", async () => {
await signatureDropContract.createBatch(metadatas);
await signatureDropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
},
]);
await signatureDropContract.claim(5);
await signatureDropContract.claimConditions.set(
[
{
maxQuantity: 10,
maxClaimableSupply: 10,
},
],
true,
@@ -667,7 +678,12 @@ describe("Signature drop tests", async () => {
it("should not allow claiming to someone not in the merkle tree", async () => {
await signatureDropContract.claimConditions.set([
{
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
maxClaimablePerWallet: 0,
snapshot: [
bobWallet.address,
samWallet.address,
abbyWallet.address,
].map((a) => ({ address: a, maxClaimable: 1 })),
},
]);
await signatureDropContract.createBatch([
@@ -678,14 +694,8 @@ describe("Signature drop tests", async () => {
try {
await signatureDropContract.claim(1);
} catch (err: any) {
expect(err).to.have.property(
"message",
"No claim found for this address",
"",
);
return;
expectError(err, "!Qty");
}
assert.fail("should not reach this point, claim should have failed");
});
it("should allow claims with default settings", async () => {
@@ -718,53 +728,7 @@ describe("Signature drop tests", async () => {
await sdk.updateSignerOrProvider(w2);
await signatureDropContract.claim(2);
} catch (e) {
expectError(e, "Invalid qty proof");
}
});
it("should generate valid proofs", async () => {
const members = [
bobWallet.address,
samWallet.address,
abbyWallet.address,
w1.address,
w2.address,
w3.address,
w4.address,
];
const hashedLeafs = members.map((l) =>
ethers.utils.solidityKeccak256(["address", "uint256"], [l, 0]),
);
const tree = new MerkleTree(hashedLeafs, keccak256, {
sort: true,
sortLeaves: true,
sortPairs: true,
});
const input = members.map((address) => ({
address,
maxClaimable: 0,
}));
const snapshot = await createSnapshot(input, 0, storage);
for (const leaf of members) {
const expectedProof = tree.getHexProof(
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
);
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(leaf);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
const verified = tree.verify(
actualProof?.proof as string[],
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
tree.getHexRoot(),
);
expect(verified).to.eq(true);
expectError(e, "!Qty");
}
});
@@ -177,6 +177,7 @@ describe("Edition sig minting", async () => {
name: "Test1",
});
const toSign = {
to: adminWallet.address,
metadata: uri,
quantity: 10,
};
@@ -194,10 +195,12 @@ describe("Edition sig minting", async () => {
name: "Test2",
});
const toSign1 = {
to: adminWallet.address,
metadata: uri1,
quantity: 10,
};
const toSign2 = {
to: adminWallet.address,
metadata: uri2,
quantity: 10,
};
@@ -233,6 +236,7 @@ describe("Edition sig minting", async () => {
it("should mint additional supply", async () => {
const tx = await editionContract.signature.mint(v1);
const additional = await editionContract.signature.generateFromTokenId({
to: adminWallet.address,
tokenId: tx.id,
quantity: 100,
});
@@ -251,6 +255,7 @@ describe("Edition sig minting", async () => {
supply: 0,
});
const payload = await editionContract.signature.generateFromTokenId({
to: samWallet.address,
tokenId: "0",
quantity: "1",
metadata: "",
@@ -267,6 +272,7 @@ describe("Edition sig minting", async () => {
it("should mint the right custom token price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await editionContract.signature.generate({
to: samWallet.address,
price: 1,
currencyAddress: tokenAddress,
metadata: {
@@ -288,6 +294,7 @@ describe("Edition sig minting", async () => {
it("should mint the right native price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await editionContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test",
@@ -308,6 +315,7 @@ describe("Edition sig minting", async () => {
it("should mint the right native price with multiple tokens", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await editionContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test with quantity",
@@ -144,6 +144,7 @@ describe("Token sig minting", async () => {
it("should mint the right custom token price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await contract.signature.generate({
to: samWallet.address,
price: 1,
quantity: 10,
currencyAddress: tokenAddress,
@@ -160,6 +161,7 @@ describe("Token sig minting", async () => {
it("should mint the right native price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await contract.signature.generate({
to: samWallet.address,
price: 1,
quantity: 0.23,
});
@@ -195,6 +195,7 @@ describe("NFT sig minting", async () => {
name: "Test1",
});
const toSign = {
to: adminWallet.address,
metadata: uri,
};
const payload = await nftContract.signature.generate(toSign);
@@ -211,9 +212,11 @@ describe("NFT sig minting", async () => {
name: "Test2",
});
const toSign1 = {
to: adminWallet.address,
metadata: uri1,
};
const toSign2 = {
to: adminWallet.address,
metadata: uri2,
};
const payloads = await nftContract.signature.generateBatch([
@@ -230,6 +233,7 @@ describe("NFT sig minting", async () => {
it("should mint the right custom token price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await nftContract.signature.generate({
to: samWallet.address,
price: 1,
currencyAddress: tokenAddress,
metadata: {
@@ -250,6 +254,7 @@ describe("NFT sig minting", async () => {
it("should mint the right native price", async () => {
const oldBalance = await samWallet.getBalance();
const payload = await nftContract.signature.generate({
to: samWallet.address,
price: 1,
metadata: {
name: "native token test",
+144 -37
View File
@@ -1,8 +1,12 @@
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { createSnapshot, Snapshot } from "../../src/evm/index";
import { MockStorage } from "./mock/MockStorage";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber } from "ethers";
import {
ShardedMerkleTree,
SnapshotFormatVersion,
} from "../../src/evm/common/sharded-merkle-tree";
import { createSnapshot, SnapshotEntryInput } from "../../src/evm/index";
import { sdk, signers, storage } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { BigNumber, ethers } from "ethers";
import { MerkleTree } from "merkletreejs";
const chai = require("chai");
const deepEqualInAnyOrder = require("deep-equal-in-any-order");
@@ -14,56 +18,159 @@ const { expect, assert } = chai;
global.fetch = require("cross-fetch");
describe("Snapshots", async () => {
let merkleRoot: string;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
abbyWallet: SignerWithAddress,
bobWallet: SignerWithAddress,
w1: SignerWithAddress,
w2: SignerWithAddress,
w3: SignerWithAddress,
w4: SignerWithAddress;
const leafs = [
"0xE79ee09bD47F4F5381dbbACaCff2040f2FbC5803",
"0x99703159fbE079e1a48B53039a5e52e7b2d9E559",
"0x38641f11406E513A187d40600a13C9F921db23c2",
"0x14fb3a9B317612ddc6d6Cc3c907CD9F2Aa091eE7",
];
const input = leafs.map((address, i) => ({
address,
maxClaimable: BigNumber.from(i + 1).toString(),
}));
let storage: ThirdwebStorage;
let members;
beforeEach(async () => {
storage = MockStorage();
[adminWallet, samWallet, bobWallet, abbyWallet, w1, w2, w3, w4] = signers;
members = [
adminWallet.address,
bobWallet.address,
samWallet.address,
abbyWallet.address,
w1.address,
w2.address,
w3.address,
w4.address,
];
});
beforeEach(async () => {
const result = await createSnapshot(input, 0, storage);
merkleRoot = result.merkleRoot;
});
it("should shard merkle tree", async () => {
const result = await ShardedMerkleTree.buildAndUpload(input, 0, storage);
const sm = await ShardedMerkleTree.fromUri(result.uri, storage);
const proofs = await sm?.getProof(
"0xE79ee09bD47F4F5381dbbACaCff2040f2FbC5803",
);
expect(proofs?.maxClaimable).to.equal("1");
expect(proofs?.proof).length(2);
});
[SnapshotFormatVersion.V1, SnapshotFormatVersion.V2].forEach(
(snapshotVersion) => {
it("should shard merkle tree: " + snapshotVersion, async () => {
const input = members.map((address, i) => ({
address,
maxClaimable: BigNumber.from(i + 1).toString(),
}));
const result = await ShardedMerkleTree.buildAndUpload(
input,
0,
sdk.getProvider(),
storage,
snapshotVersion,
);
const sm = await ShardedMerkleTree.fromUri(result.uri, storage);
const proofs = await sm?.getProof(
adminWallet.address,
sdk.getProvider(),
snapshotVersion,
);
expect(proofs?.maxClaimable).to.equal("1");
expect(proofs?.proof).length.gt(0);
});
},
);
it("should generate a valid merkle root from a list of addresses", async () => {
const input = members.map((address) => ({
address,
}));
const result = await createSnapshot(
input,
0,
sdk.getProvider(),
storage,
SnapshotFormatVersion.V1,
);
const merkleRoot = result.merkleRoot;
assert.equal(
merkleRoot,
"0x8ef5aa32e8d987fe1e5b93155939ebfbb897746f476f0a97ad5354aa5317c751",
"0x7c55d9d851d24cc0a32d49d22cda5e0162ce54cdde1773e9d8fb31e02c8818f4",
);
});
[SnapshotFormatVersion.V1, SnapshotFormatVersion.V2].forEach(
(snapshotVersion) => {
it("should generate valid proofs:" + snapshotVersion, async () => {
const hashedLeafs = members.map((l) =>
ShardedMerkleTree.hashEntry(
SnapshotEntryInput.parse({
address: l,
}),
0,
18,
snapshotVersion,
),
);
const tree = new MerkleTree(hashedLeafs, ethers.utils.keccak256, {
sort: true,
});
const input = members.map((address) => ({
address,
}));
const snapshot = await createSnapshot(
input,
0,
sdk.getProvider(),
storage,
snapshotVersion,
);
for (const leaf of members) {
const expectedProof = tree.getHexProof(
ShardedMerkleTree.hashEntry(
SnapshotEntryInput.parse({
address: leaf,
}),
0,
18,
snapshotVersion,
),
);
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(
leaf,
sdk.getProvider(),
snapshotVersion,
);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
const verified = tree.verify(
actualProof?.proof,
ShardedMerkleTree.hashEntry(
{ ...actualProof },
0,
18,
snapshotVersion,
),
tree.getHexRoot(),
);
expect(verified).to.eq(true);
}
});
},
);
it("should warn about duplicate leafs", async () => {
const input = members.map((address, i) => ({
address,
maxClaimable: BigNumber.from(i + 1).toString(),
}));
const duplicateLeafs = input.concat({
address: "0xE79ee09bD47F4F5381dbbACaCff2040f2FbC5803",
address: adminWallet.address,
maxClaimable: "0",
});
try {
await createSnapshot(duplicateLeafs, 0, storage);
await createSnapshot(
duplicateLeafs,
0,
sdk.getProvider(),
storage,
SnapshotFormatVersion.V1,
);
} catch (error) {
expect(error).to.have.property("message", "DUPLICATE_LEAFS", "");
return;
+212
View File
@@ -0,0 +1,212 @@
import { NATIVE_TOKEN_ADDRESS, ThirdwebSDK } from "../../src/evm";
import { SmartContract } from "../../src/evm/contracts/smart-contract";
import { signers } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect, assert } from "chai";
describe("Tiered Drop Contract", async () => {
let contract: SmartContract;
let sdk: ThirdwebSDK;
let adminWallet: SignerWithAddress;
let claimerWallet: SignerWithAddress;
async function deployTieredDrop() {
// This needs to match the release for the currently used ABI
const releaseUri =
"ipfs://QmTK2dpcDMiePacs6bNh6LhQdPB9RkxXHmx9CtrPLPRBuS/0";
const address = await sdk.deployer.deployContractFromUri(releaseUri, [], {
forceDirectDeploy: true,
});
const tieredDrop = await sdk.getContract(address);
const walletAddress = await sdk.wallet.getAddress();
await tieredDrop.call(
"initialize",
walletAddress, // defaultAdmin
"Tiered Drop #1", // name
"TD", // symbol
"ipfs://QmUj5kNz7Xe5AEhV2YvHiCKfMSL5YZpD4E18QLLYEsGBcd/0", // contractUri
[], // trustedForwarders
walletAddress, // saleRecipient
walletAddress, // royaltyRecipient
0, // royaltyBps
);
return tieredDrop;
}
before(async () => {
[adminWallet, claimerWallet] = signers;
sdk = new ThirdwebSDK(adminWallet);
contract = await deployTieredDrop();
});
beforeEach(() => {
sdk.updateSignerOrProvider(adminWallet);
});
it("Should lazy mint NFTs", async () => {
const metadata = [
{
name: "NFT #1",
description: "My first NFT",
},
{
name: "NFT #2",
description: "My second NFT",
},
];
const txs = await contract.erc721.tieredDrop.createBatchWithTier(
metadata,
"tier1",
);
expect(txs.length).to.equal(2);
const nfts = await contract.erc721.tieredDrop.getMetadataInTier("tier1");
expect(nfts.length).to.equal(2);
expect(nfts[0].name).to.equal("NFT #1");
});
it("Should reject invalid payload", async () => {
const payload = {
currencyAddress: NATIVE_TOKEN_ADDRESS,
price: 0,
quantity: 1,
tierPriority: ["tier1"],
to: claimerWallet.address,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(),
};
const signedPayload = await contract.erc721.tieredDrop.generate(payload);
signedPayload.payload.price = "1";
sdk.updateSignerOrProvider(claimerWallet);
const isValid = await contract.erc721.tieredDrop.verify(signedPayload);
assert.isFalse(isValid);
});
it("Should claim NFTs", async () => {
const payload = {
currencyAddress: NATIVE_TOKEN_ADDRESS,
price: 0,
quantity: 1,
tierPriority: ["tier1"],
to: claimerWallet.address,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(Date.now() - 1000),
};
const signedPayload = await contract.erc721.tieredDrop.generate(payload);
const isValid = await contract.erc721.tieredDrop.verify(signedPayload);
assert.isTrue(isValid);
sdk.updateSignerOrProvider(claimerWallet);
await contract.erc721.tieredDrop.claimWithSignature(signedPayload);
});
it("Should get tokens in tier", async () => {
const payload = {
currencyAddress: NATIVE_TOKEN_ADDRESS,
price: 0,
quantity: 1,
tierPriority: ["tier1"],
to: claimerWallet.address,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(Date.now() - 1000),
};
const signedPayload = await contract.erc721.tieredDrop.generate(payload);
await contract.erc721.tieredDrop.claimWithSignature(signedPayload);
const nfts = await contract.erc721.tieredDrop.getTokensInTier("tier1");
expect(nfts.length).to.equal(2);
expect(nfts[0].metadata.name).to.equal("NFT #1");
expect(nfts[1].metadata.name).to.equal("NFT #2");
});
it("Should claim from multiple tiers", async () => {
let metadata = [
{
name: "NFT #3",
description: "My first NFT",
},
];
await contract.erc721.tieredDrop.createBatchWithTier(metadata, "tier1");
metadata = [
{
name: "NFT #4",
description: "My fourth NFT",
},
];
await contract.erc721.tieredDrop.createBatchWithTier(metadata, "tier2");
metadata = [
{
name: "NFT #5",
description: "My fifth NFT",
},
];
await contract.erc721.tieredDrop.createBatchWithTier(metadata, "tier3");
const payload = {
currencyAddress: NATIVE_TOKEN_ADDRESS,
price: 0,
quantity: 3,
tierPriority: ["tier1", "tier2", "tier3"],
to: claimerWallet.address,
mintEndTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
mintStartTime: new Date(Date.now() - 1000),
};
const signedPayload = await contract.erc721.tieredDrop.generate(payload);
await contract.erc721.tieredDrop.claimWithSignature(signedPayload);
let nfts = await contract.erc721.tieredDrop.getTokensInTier("tier1");
expect(nfts.length).to.equal(3);
expect(nfts[2].metadata.name).to.equal("NFT #3");
nfts = await contract.erc721.tieredDrop.getTokensInTier("tier2");
expect(nfts.length).to.equal(1);
expect(nfts[0].metadata.name).to.equal("NFT #4");
nfts = await contract.erc721.tieredDrop.getTokensInTier("tier3");
expect(nfts.length).to.equal(1);
expect(nfts[0].metadata.name).to.equal("NFT #5");
});
it("metadata should reveal correctly", async () => {
contract = await deployTieredDrop();
const placeholder = {
name: "Placeholder",
description: "This is a placeholder",
};
const metadata = [
{
name: "NFT #1",
description: "My first NFT",
},
{
name: "NFT #2",
description: "My second NFT",
},
];
await contract.erc721.tieredDrop.createDelayedRevealBatchWithTier(
placeholder,
metadata,
"my secret password",
"tier1",
);
let nfts = await contract.erc721.tieredDrop.getMetadataInTier("tier1");
expect(nfts.length).to.equal(2);
expect(nfts[0].name).to.equal("Placeholder");
await contract.erc721.tieredDrop.reveal(0, "my secret password");
nfts = await contract.erc721.tieredDrop.getMetadataInTier("tier1");
expect(nfts.length).to.equal(2);
expect(nfts[0].name).to.equal("NFT #1");
});
});
@@ -1,23 +1,20 @@
import {
ClaimEligibility,
createSnapshot,
NATIVE_TOKEN_ADDRESS,
TokenDrop,
TokenDropInitializer,
TokenInitializer,
} from "../../src/evm";
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { expectError, sdk, signers, storage } from "./before-setup";
import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { assert, expect } from "chai";
import { BigNumber, ethers } from "ethers";
import { MerkleTree } from "merkletreejs";
import { BigNumber } from "ethers";
import invariant from "tiny-invariant";
global.fetch = require("cross-fetch");
describe("Token Drop Contract", async () => {
describe("Token Drop Contract (v2)", async () => {
let dropContract: TokenDrop;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
@@ -42,10 +39,111 @@ describe("Token Drop Contract", async () => {
platform_fee_basis_points: 10,
platform_fee_recipient: AddressZero,
},
2,
);
dropContract = await sdk.getTokenDrop(address);
});
it("comprehensive test", async () => {
// claiming with default conditions
await dropContract.claimConditions.set([{}]);
await dropContract.claim(1);
// claiming with max supply
await dropContract.claimConditions.set([
{
maxClaimableSupply: 2,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim 2 - maxSupply");
} catch (e) {
expectError(e, "exceed max mint supply");
}
await dropContract.claim(1);
// claiming with max per wallet
await dropContract.claimConditions.set([
{
maxClaimablePerWallet: 1,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim 2 - maxClaimablePerWallet");
} catch (e) {
expectError(e, "invalid quantity");
}
await dropContract.claim(1);
expect((await dropContract.totalSupply()).displayValue).eq("3.0");
});
it("comprehensive test with allowlist", async () => {
// claiming with default conditions
await dropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
},
]);
try {
sdk.updateSignerOrProvider(bobWallet);
await dropContract.claim(1);
expect.fail("should not be able to claim - not in allowlist");
} catch (e) {
expectError(e, "No claim found");
}
sdk.updateSignerOrProvider(adminWallet);
await dropContract.claim(1);
// claiming with max supply
await dropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
maxClaimableSupply: 2,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim - maxClaimableSupply");
} catch (e) {
expectError(e, "exceed max mint supply");
}
await dropContract.claim(1);
// claiming with max per wallet
await dropContract.claimConditions.set([
{
snapshot: [adminWallet.address],
maxClaimablePerWallet: 1,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim - maxClaimablePerWallet");
} catch (e) {
expectError(e, "invalid quantity");
}
await dropContract.claim(1);
// claiming with max per wallet in snapshot
await dropContract.claimConditions.set([
{
snapshot: [{ address: adminWallet.address, maxClaimable: 1 }],
maxClaimablePerWallet: 0,
},
]);
try {
await dropContract.claim(2);
expect.fail("should not be able to claim - proof maxClaimable");
} catch (e) {
expectError(e, "invalid quantity proof");
}
await dropContract.claim(1);
try {
await dropContract.claim(1);
expect.fail("should not be able to claim - proof used");
} catch (e) {
expectError(e, "proof claimed");
}
});
it("should allow a snapshot to be set", async () => {
await dropContract.claimConditions.set([
{
@@ -62,13 +160,12 @@ describe("Token Drop Contract", async () => {
const metadata = await dropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
expect(merkles).have.property(
"0xb1a60ad68b77609a455696695fbdd02b850d03ec285e7fe1f4c4093797457b24",
);
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
const roots = (await dropContract.claimConditions.getAll()).map(
(c) => c.merkleRootHash,
@@ -106,13 +203,12 @@ describe("Token Drop Contract", async () => {
const metadata = await dropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
expect(merkles).have.property(
"0xb1a60ad68b77609a455696695fbdd02b850d03ec285e7fe1f4c4093797457b24",
);
expect(merkles).have.property(
"0xd89eb21bf7ee4dd07d88e8f90a513812d9d38ac390a58722762c9f3afc4e0feb",
);
const roots = (await dropContract.claimConditions.getAll()).map(
(c) => c.merkleRootHash,
@@ -242,52 +338,6 @@ describe("Token Drop Contract", async () => {
}
});
it("should generate valid proofs", async () => {
const members = [
bobWallet.address,
samWallet.address,
abbyWallet.address,
w1.address,
w2.address,
w3.address,
w4.address,
];
const hashedLeafs = members.map((l) =>
ethers.utils.solidityKeccak256(["address", "uint256"], [l, 0]),
);
const tree = new MerkleTree(hashedLeafs, ethers.utils.keccak256, {
sort: true,
sortLeaves: true,
sortPairs: true,
});
const input = members.map((address) => ({
address,
maxClaimable: 0,
}));
const snapshot = await createSnapshot(input, 18, storage);
for (const leaf of members) {
const expectedProof = tree.getHexProof(
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
);
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(leaf);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
const verified = tree.verify(
actualProof?.proof as string[],
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
tree.getHexRoot(),
);
expect(verified).to.eq(true);
}
});
describe("eligibility", () => {
it("should return false if there isn't an active claim condition", async () => {
const reasons =
@@ -306,7 +356,7 @@ describe("Token Drop Contract", async () => {
});
it("should check for the total supply", async () => {
await dropContract.claimConditions.set([{ maxQuantity: 1.2 }]);
await dropContract.claimConditions.set([{ maxClaimableSupply: 1.2 }]);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
@@ -324,7 +374,7 @@ describe("Token Drop Contract", async () => {
it("should check if an address has valid merkle proofs", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 1,
maxClaimableSupply: 1,
snapshot: [w2.address, adminWallet.address],
},
]);
@@ -345,19 +395,19 @@ describe("Token Drop Contract", async () => {
it("should transform qunatities back to readable values", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: "10.8",
quantityLimitPerTransaction: "1.2",
maxClaimableSupply: "10.8",
maxClaimablePerWallet: "1.2",
},
]);
const active = await dropContract.claimConditions.getActive();
expect(active.maxQuantity).to.be.equal("10.8");
expect(active.quantityLimitPerTransaction).to.be.equal("1.2");
expect(active.maxClaimableSupply).to.be.equal("10.8");
expect(active.maxClaimablePerWallet).to.be.equal("1.2");
});
it("should check if its been long enough since the last claim", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: "10.8",
maxClaimableSupply: "10.8",
waitInSeconds: 24 * 60 * 60,
},
]);
@@ -383,7 +433,7 @@ describe("Token Drop Contract", async () => {
it("should check if an address has enough native currency", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: NATIVE_TOKEN_ADDRESS,
},
@@ -416,7 +466,7 @@ describe("Token Drop Contract", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress,
},
@@ -440,7 +490,7 @@ describe("Token Drop Contract", async () => {
it("should return nothing if the claim is eligible", async () => {
await dropContract.claimConditions.set([
{
maxQuantity: 10,
maxClaimableSupply: 10,
price: "100",
currencyAddress: NATIVE_TOKEN_ADDRESS,
snapshot: [w1.address, w2.address, w3.address],
@@ -582,14 +632,14 @@ describe("Token Drop Contract", async () => {
it("set claim condition and update claim condition", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2), maxQuantity: 1.2 },
{ startTime: new Date(Date.now() / 2), maxClaimableSupply: 1.2 },
{ startTime: new Date(), waitInSeconds: 60 },
]);
const oldConditions = await dropContract.claimConditions.getAll();
expect(oldConditions.length).to.be.equal(2);
await dropContract.claimConditions.update(0, { waitInSeconds: 10 });
let updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxQuantity).to.be.deep.equal("1.2");
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("1.2");
expect(updatedConditions[0].waitInSeconds).to.be.deep.equal(
BigNumber.from(10),
);
@@ -598,12 +648,12 @@ describe("Token Drop Contract", async () => {
);
await dropContract.claimConditions.update(1, {
maxQuantity: 10,
maxClaimableSupply: 10,
waitInSeconds: 10,
});
updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxQuantity).to.be.deep.equal("1.2");
expect(updatedConditions[1].maxQuantity).to.be.deep.equal("10.0");
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("1.2");
expect(updatedConditions[1].maxClaimableSupply).to.be.deep.equal("10.0");
expect(updatedConditions[1].waitInSeconds).to.be.deep.equal(
BigNumber.from(10),
);
@@ -611,8 +661,8 @@ describe("Token Drop Contract", async () => {
it("set claim condition and update claim condition with diff timestamps should reorder", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2), maxQuantity: 1 },
{ startTime: new Date(), maxQuantity: 2 },
{ startTime: new Date(Date.now() / 2), maxClaimableSupply: 1 },
{ startTime: new Date(), maxClaimableSupply: 2 },
]);
expect((await dropContract.claimConditions.getAll()).length).to.be.equal(2);
@@ -621,8 +671,8 @@ describe("Token Drop Contract", async () => {
});
// max quantities should be inverted now
const updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxQuantity).to.be.deep.equal("2.0");
expect(updatedConditions[1].maxQuantity).to.be.deep.equal("1.0");
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("2.0");
expect(updatedConditions[1].maxClaimableSupply).to.be.deep.equal("1.0");
});
it("set claim condition in the future should not be claimable now", async () => {
+549
View File
@@ -0,0 +1,549 @@
import {
ClaimEligibility,
NATIVE_TOKEN_ADDRESS,
TokenDrop,
TokenDropInitializer,
TokenInitializer,
} from "../../src/evm";
import { expectError, sdk, signers } from "./before-setup";
import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { assert, expect } from "chai";
import invariant from "tiny-invariant";
global.fetch = require("cross-fetch");
describe("Token Drop Contract (v3)", async () => {
let dropContract: TokenDrop;
let adminWallet: SignerWithAddress,
samWallet: SignerWithAddress,
abbyWallet: SignerWithAddress,
bobWallet: SignerWithAddress,
w1: SignerWithAddress,
w2: SignerWithAddress,
w3: SignerWithAddress,
w4: SignerWithAddress;
beforeEach(async () => {
[adminWallet, samWallet, bobWallet, abbyWallet, w1, w2, w3, w4] = signers;
sdk.updateSignerOrProvider(adminWallet);
const address = await sdk.deployer.deployBuiltInContract(
TokenDropInitializer.contractType,
{
name: `Testing drop from SDK`,
description: "Test contract from tests",
image:
"https://pbs.twimg.com/profile_images/1433508973215367176/XBCfBn3g_400x400.jpg",
primary_sale_recipient: adminWallet.address,
platform_fee_basis_points: 10,
platform_fee_recipient: AddressZero,
},
await sdk.deployer.getLatestBuiltInContractVersion(
TokenDropInitializer.contractType,
),
);
dropContract = await sdk.getTokenDrop(address);
});
it("should allow a snapshot to be set", async () => {
await dropContract.claimConditions.set([
{
startTime: new Date(Date.now() / 2),
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
price: 1,
},
{
startTime: new Date(),
snapshot: [bobWallet.address],
},
]);
const metadata = await dropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xb2acf11f3694bfc1e9db18a7cd01083bc48db49bd1ae524a90f102914f3f7c2a",
);
expect(merkles).have.property(
"0xeed6db05cfdf0ef8cf5b98791d1e7d436862f346af8b0a7c8a5eb864dbb1c6fb",
);
const roots = (await dropContract.claimConditions.getAll()).map(
(c) => c.merkleRootHash,
);
expect(roots).length(2);
});
it("should return snapshot data on claim conditions", async () => {
await dropContract.claimConditions.set([
{
snapshot: [samWallet.address],
},
]);
const conditions = await dropContract.claimConditions.getAll({
withAllowList: true,
});
assert.lengthOf(conditions, 1);
invariant(conditions[0].snapshot);
expect(conditions[0].snapshot[0].address).to.eq(samWallet.address);
});
it("should remove merkles from the metadata when claim conditions are removed", async () => {
await dropContract.claimConditions.set([
{
startTime: new Date(),
waitInSeconds: 10,
snapshot: [bobWallet.address, samWallet.address, abbyWallet.address],
},
{
startTime: new Date(Date.now() + 60 * 60 * 1000),
snapshot: [bobWallet.address],
},
]);
const metadata = await dropContract.metadata.get();
const merkles = metadata.merkle;
expect(merkles).have.property(
"0xb2acf11f3694bfc1e9db18a7cd01083bc48db49bd1ae524a90f102914f3f7c2a",
);
expect(merkles).have.property(
"0xeed6db05cfdf0ef8cf5b98791d1e7d436862f346af8b0a7c8a5eb864dbb1c6fb",
);
const roots = (await dropContract.claimConditions.getAll()).map(
(c) => c.merkleRootHash,
);
expect(roots).length(2);
await dropContract.claimConditions.set([{}]);
const newMetadata = await dropContract.metadata.get();
const newMerkles = newMetadata.merkle;
expect(JSON.stringify(newMerkles)).to.eq("{}");
});
it("allow all addresses in the merkle tree to claim", async () => {
const testWallets: SignerWithAddress[] = [
bobWallet,
samWallet,
abbyWallet,
w1,
w2,
w3,
w4,
];
const members = testWallets.map((w, i) =>
i % 3 === 0
? w.address.toLowerCase()
: i % 3 === 1
? w.address.toUpperCase().replace("0X", "0x")
: w.address,
);
await dropContract.claimConditions.set([
{
snapshot: members,
},
]);
for (const member of testWallets) {
await sdk.updateSignerOrProvider(member);
await dropContract.claim(1.2);
const balance = await dropContract.balanceOf(member.address);
expect(balance.displayValue).to.eq("1.2");
}
});
it("allow one address in the merkle tree to claim", async () => {
const testWallets: SignerWithAddress[] = [bobWallet];
const members = testWallets.map((w) => w.address);
await dropContract.claimConditions.set([
{
snapshot: members,
},
]);
for (const member of testWallets) {
await sdk.updateSignerOrProvider(member);
await dropContract.claim(1);
}
try {
await sdk.updateSignerOrProvider(samWallet);
await dropContract.claim(1);
assert.fail("should have thrown");
} catch (e) {
// expected
}
});
it("should not allow claiming to someone not in the merkle tree", async () => {
await dropContract.claimConditions.set(
[
{
maxClaimablePerWallet: 0,
snapshot: [
bobWallet.address,
samWallet.address,
abbyWallet.address,
].map((a) => ({ address: a, maxClaimable: 1 })),
},
],
false,
);
await sdk.updateSignerOrProvider(w1);
try {
await dropContract.claim(1);
} catch (err: any) {
expectError(err, "!Qty");
}
});
it("should allow claims with default settings", async () => {
await dropContract.claimConditions.set([{}]);
await dropContract.claim(142.69);
const balance = await dropContract.balance();
expect(balance.displayValue).to.eq("142.69");
});
it("should allow claiming with a price", async () => {
await dropContract.claimConditions.set([
{
price: 0.1,
},
]);
await dropContract.claim(100);
const balance = await dropContract.balance();
expect(balance.displayValue).to.eq("100.0");
});
it("should allow setting max claims per wallet", async () => {
await dropContract.claimConditions.set([
{
snapshot: [
{ address: w1.address, maxClaimable: "2.1" },
{ address: w2.address, maxClaimable: 1 },
],
},
]);
await sdk.updateSignerOrProvider(w1);
await dropContract.claim(2);
const balance = await dropContract.balance();
expect(balance.displayValue).to.eq("2.0");
try {
await sdk.updateSignerOrProvider(w2);
await dropContract.claim(2);
} catch (e) {
expectError(e, "!Qty");
}
});
describe("eligibility", () => {
it("should return false if there isn't an active claim condition", async () => {
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
bobWallet.address,
);
expect(reasons).to.include(ClaimEligibility.NoClaimConditionSet);
assert.lengthOf(reasons, 1);
const canClaim = await dropContract.claimConditions.canClaim(
1,
w1.address,
);
assert.isFalse(canClaim);
});
it("should check for the total supply", async () => {
await dropContract.claimConditions.set([{ maxClaimableSupply: 1.2 }]);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1.8",
w1.address,
);
expect(reasons).to.include(ClaimEligibility.NotEnoughSupply);
const canClaim = await dropContract.claimConditions.canClaim(
1.8,
w1.address,
);
assert.isFalse(canClaim);
});
it("should transform qunatities back to readable values", async () => {
await dropContract.claimConditions.set([
{
maxClaimableSupply: "10.8",
maxClaimablePerWallet: "1.2",
},
]);
const active = await dropContract.claimConditions.getActive();
expect(active.maxClaimableSupply).to.be.equal("10.8");
expect(active.maxClaimablePerWallet).to.be.equal("1.2");
});
it("should check if an address has enough native currency", async () => {
await dropContract.claimConditions.set([
{
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress: NATIVE_TOKEN_ADDRESS,
},
]);
await sdk.updateSignerOrProvider(bobWallet);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
bobWallet.address,
);
expect(reasons).to.include(ClaimEligibility.NotEnoughTokens);
const canClaim = await dropContract.claimConditions.canClaim(
1,
bobWallet.address,
);
assert.isFalse(canClaim);
});
it("should check if an address has enough erc20 currency", async () => {
const currencyAddress = await sdk.deployer.deployBuiltInContract(
TokenInitializer.contractType,
{
name: "test",
symbol: "test",
primary_sale_recipient: adminWallet.address,
},
);
await dropContract.claimConditions.set([
{
maxClaimableSupply: 10,
price: "1000000000000000",
currencyAddress,
},
]);
await sdk.updateSignerOrProvider(bobWallet);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
bobWallet.address,
);
expect(reasons).to.include(ClaimEligibility.NotEnoughTokens);
const canClaim = await dropContract.claimConditions.canClaim(
1,
bobWallet.address,
);
assert.isFalse(canClaim);
});
it("should return nothing if the claim is eligible", async () => {
await dropContract.claimConditions.set([
{
maxClaimableSupply: 10,
price: "100",
currencyAddress: NATIVE_TOKEN_ADDRESS,
snapshot: [w1.address, w2.address, w3.address],
},
]);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
w1.address,
);
assert.lengthOf(reasons, 0);
const canClaim = await dropContract.claimConditions.canClaim(
"1",
w1.address,
);
assert.isTrue(canClaim);
});
});
it("should verify claim correctly after resetting claim conditions", async () => {
await dropContract.claimConditions.set([
{
maxClaimablePerWallet: 0,
snapshot: [{ address: w1.address, maxClaimable: 1 }],
},
]);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
w2.address,
);
expect(reasons).to.contain(ClaimEligibility.AddressNotAllowed);
await dropContract.claimConditions.set([{}]);
const reasons2 =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
w2.address,
);
expect(reasons2.length).to.eq(0);
});
it("should verify claim correctly after updating claim conditions", async () => {
await dropContract.claimConditions.set([
{
maxClaimablePerWallet: 0,
snapshot: [{ address: w1.address, maxClaimable: 1 }],
},
]);
const reasons =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
w2.address,
);
expect(reasons).to.contain(ClaimEligibility.AddressNotAllowed);
await dropContract.claimConditions.update(0, {
maxClaimablePerWallet: 0,
snapshot: [
{ address: w1.address, maxClaimable: 1 },
{ address: w2.address, maxClaimable: 1 },
],
});
const reasons2 =
await dropContract.claimConditions.getClaimIneligibilityReasons(
"1",
w2.address,
);
expect(reasons2.length).to.eq(0);
});
it("should allow you to update claim conditions", async () => {
await dropContract.claimConditions.set([{}]);
const conditions = await dropContract.claimConditions.getAll();
await dropContract.claimConditions.set([{}]);
assert.lengthOf(conditions, 1);
});
it("should be able to use claimTo function as expected", async () => {
await dropContract.claimConditions.set([{}]);
await dropContract.claimTo(samWallet.address, 1);
assert(
(await dropContract.balanceOf(samWallet.address)).displayValue === "1.0",
);
});
it("canClaim: 1 address", async () => {
await dropContract.claimConditions.set([
{
maxClaimablePerWallet: 0,
snapshot: [{ address: w1.address, maxClaimable: 1 }],
},
]);
assert.isTrue(
await dropContract.claimConditions.canClaim(1, w1.address),
"can claim",
);
assert.isFalse(
await dropContract.claimConditions.canClaim(1, w2.address),
"!can claim",
);
});
it("canClaim: 3 address", async () => {
const members = [
{
address: w1.address.toUpperCase().replace("0X", "0x"),
maxClaimable: 1,
},
{ address: w2.address.toLowerCase(), maxClaimable: 1 },
{ address: w3.address.toLowerCase(), maxClaimable: 1 },
];
await dropContract.claimConditions.set([
{
maxClaimablePerWallet: 0,
snapshot: members,
},
]);
assert.isTrue(
await dropContract.claimConditions.canClaim(1, w1.address),
"can claim",
);
assert.isTrue(
await dropContract.claimConditions.canClaim(1, w2.address),
"can claim",
);
assert.isTrue(
await dropContract.claimConditions.canClaim(1, w3.address),
"can claim",
);
assert.isFalse(
await dropContract.claimConditions.canClaim(1, bobWallet.address),
"!can claim",
);
});
it("set claim condition and reset claim condition", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2) },
{ startTime: new Date() },
]);
expect((await dropContract.claimConditions.getAll()).length).to.be.equal(2);
await dropContract.claimConditions.set([]);
expect((await dropContract.claimConditions.getAll()).length).to.be.equal(0);
});
it("set claim condition and update claim condition", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2), maxClaimableSupply: 1.2 },
{ startTime: new Date(), maxClaimablePerWallet: 60 },
]);
const oldConditions = await dropContract.claimConditions.getAll();
expect(oldConditions.length).to.be.equal(2);
await dropContract.claimConditions.update(0, { maxClaimablePerWallet: 10 });
let updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("1.2");
expect(updatedConditions[0].maxClaimablePerWallet).to.be.deep.equal("10.0");
expect(updatedConditions[1].maxClaimablePerWallet).to.be.deep.equal("60.0");
await dropContract.claimConditions.update(1, {
maxClaimableSupply: 10,
maxClaimablePerWallet: 10,
});
updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("1.2");
expect(updatedConditions[1].maxClaimableSupply).to.be.deep.equal("10.0");
expect(updatedConditions[1].maxClaimablePerWallet).to.be.deep.equal("10.0");
});
it("set claim condition and update claim condition with diff timestamps should reorder", async () => {
await dropContract.claimConditions.set([
{ startTime: new Date(Date.now() / 2), maxClaimableSupply: 1 },
{ startTime: new Date(), maxClaimableSupply: 2 },
]);
expect((await dropContract.claimConditions.getAll()).length).to.be.equal(2);
await dropContract.claimConditions.update(0, {
startTime: new Date(Date.now() + 24 * 60 * 60 * 1000),
});
// max quantities should be inverted now
const updatedConditions = await dropContract.claimConditions.getAll();
expect(updatedConditions[0].maxClaimableSupply).to.be.deep.equal("2.0");
expect(updatedConditions[1].maxClaimableSupply).to.be.deep.equal("1.0");
});
it("set claim condition in the future should not be claimable now", async () => {
await dropContract.claimConditions.set([
{
startTime: new Date(Date.now() + 60 * 60 * 24 * 1000 * 1000),
},
]);
const canClaim = await dropContract.claimConditions.canClaim(1);
expect(canClaim).to.eq(false);
});
});
+4 -4
View File
@@ -4467,10 +4467,10 @@
"@tanstack/query-core" "4.3.4"
use-sync-external-store "^1.2.0"
"@thirdweb-dev/contracts@3.1.8-0":
version "3.1.8-0"
resolved "https://registry.yarnpkg.com/@thirdweb-dev/contracts/-/contracts-3.1.8-0.tgz#847af3fe9a53368e7be04b7b6e6afe076d3d1676"
integrity sha512-PiQjdRcAUBlu3poc4g6dN9KvjTgYsA1Vi+B/IgryptCgcJeMY4cldMabKCgPaCg9dPVpUq4O+SOM00P6mBNvGw==
"@thirdweb-dev/contracts@3.2.0-4":
version "3.2.0-4"
resolved "https://registry.yarnpkg.com/@thirdweb-dev/contracts/-/contracts-3.2.0-4.tgz#b696bdaae1dee7cf448549ac10bb78e55c622272"
integrity sha512-Mc3VvBB6rqTMKKH04UQbDabAlVPT7PiKBQrJDuhAPjOxBYWwn9pHi7amXJt2tJOkmHAj+w+oT76q7IqWv3QHhw==
"@tootallnate/once@1":
version "1.1.2"