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
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
78566babaa Version Packages (#354)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-28 10:29:57 -07:00
Joaquim VergesandGitHub 65fc7ba6ff [SDK] Expose claimCondtions.getClaimerProofs(address) (#357) 2022-10-28 10:25:36 -07:00
Nacho IacovinoandGitHub 1c24c3c3c4 [Multi] Add useUpdateCreator hook, fix CreatorInput schema (#353) 2022-10-26 18:19:32 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
408c5a81fa Version Packages (#347)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-26 08:38:25 -07:00
Joaquim VergesandGitHub 352291791e [SDK, React] expose marketplace.auction.executeSale (#352) 2022-10-26 00:19:01 +00:00
Jonas DanielsandGitHub aabbb149e8 [React] - fix wallet connection during hot reload (#351) 2022-10-25 22:48:39 +00:00
Jonas DanielsandGitHub d59450f814 do not release e2e tests (#350) 2022-10-25 20:29:22 +00:00
7d9a4c6abc Update name of useRoyalty, and update (#348)
Co-authored-by: Jonas Daniels <[email protected]>
2022-10-25 19:56:09 +00:00
Jonas DanielsandGitHub 99806e595d [VARIOUS] - exclude sizes of test files etc (#349) 2022-10-25 18:39:30 +00:00
Nacho IacovinoandGitHub 21c21c1adf resolveReleasesFromAddress -> resolveContractUriFromAddress (#344) 2022-10-24 23:43:43 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
4b10719047 Version Packages (#345)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-24 16:18:44 -07:00
Joaquim VergesandGitHub 989cf7347c [React, SDK] Small fixes for useOffers and makeOffers (#346) 2022-10-24 23:09:32 +00:00
Joaquim VergesandGitHub 72227b2e16 [SDK, React] Expose new useOffers and useMakeOffer hooks (#343) 2022-10-24 15:28:34 -07:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
ed6c54f20d Version Packages (#336)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-21 13:26:24 -07:00
Joaquim VergesandGitHub 26116a6f63 [SDK] Update to new univeral RPC endpoint (#342) 2022-10-21 13:21:11 -07:00
Jonas DanielsandGitHub f11a2719ff [workflow] github hosted runners (#340) 2022-10-20 22:02:58 -07:00
Jonas DanielsandGitHub ef27aad0aa [Various, React] - add initial e2e testing setup (#327) 2022-10-21 04:19:04 +00:00
Nacho IacovinoandGitHub b03a902145 [React] Docs update (#339) 2022-10-20 20:17:01 -07:00
Adam MajmudarandGitHub 340605b507 Clean native token address in changeset (#337) 2022-10-20 23:57:55 +00:00
Adam MajmudarandGitHub 15f8006e1f Export smart contract (#335) 2022-10-20 23:46:02 +00:00
Adam MajmudarandGitHub 811c5a1166 Update code snippets for signature minting (#334) 2022-10-20 23:29:07 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c41a80d86d Version Packages (#331)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-20 15:35:12 -07:00
Joaquim VergesandGitHub ad851f7b4d [SDK] Fix for finding addresses in legacy merkle tree format (#332) 2022-10-20 21:35:52 +00:00
Joaquim VergesandGitHub 6218c88584 [SDK] Fix for deploy byte parameters (#330) 2022-10-20 04:47:55 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c9b564feaf Version Packages (#323)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2022-10-19 21:19:06 -07:00
Adam MajmudarandGitHub 87fd6ab14e Update storage download on fail (#324) 2022-10-19 12:44:36 -07:00
Joaquim VergesandGitHub df74340fa5 [SDK] Import cbor-x fully for es builds (#328) 2022-10-19 17:46:54 +00:00
Joaquim VergesandGitHub a57b4f01f3 [SDK] Implement sharded merkle trees for lightweight allowlist checks (#321) 2022-10-19 16:54:09 +00:00
Jonas DanielsandGitHub 126752d7d0 [Various] - clean up extraneous dependencies (#326) 2022-10-18 23:56:04 -07:00
89ff921a7d Fix vite (#315)
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2022-10-18 15:23:57 -07:00
173 changed files with 13759 additions and 4492 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"ignore": ["e2e-cra-5", "e2e-next-12", "e2e-vite-3"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"updateInternalDependents": "always",
"onlyUpdatePeerDependentsWhenOutOfRange": true
+2 -2
View File
@@ -9,10 +9,10 @@ runs:
with:
node-version: 16
registry-url: "https://registry.npmjs.org"
# cache: "yarn"
cache: "yarn"
- name: Install dependencies
shell: bash
run: yarn install --frozen-lockfile
run: yarn install
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
+1 -2
View File
@@ -14,8 +14,7 @@ jobs:
build:
name: Build and Test
# timeout-minutes: 15
# try out buildjet to speed up build and test
runs-on: buildjet-32vcpu-ubuntu-2204
runs-on: ubuntu-latest-32
# To use Remote Caching, uncomment the next lines and follow the steps below.
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+1 -8
View File
@@ -13,8 +13,7 @@ concurrency:
jobs:
integration-tests:
timeout-minutes: 60
# try out buildjet to speed up build and test
runs-on: buildjet-32vcpu-ubuntu-2204
runs-on: ubuntu-latest-32
steps:
- uses: actions/checkout@v3
@@ -28,9 +27,3 @@ jobs:
run: yarn e2e
env:
NODE_OPTIONS: "--max_old_space_size=4096"
- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: packages/react/playwright-report/
retention-days: 30
+1 -2
View File
@@ -40,8 +40,7 @@ jobs:
comment_body: ${{ github.event.comment.body }}
deploy:
# try out buildjet to speed up build and test
runs-on: buildjet-32vcpu-ubuntu-2204
runs-on: ubuntu-latest-32
needs: deploy-check
if: needs.deploy-check.outputs.triggered == 'true'
steps:
+1 -2
View File
@@ -14,8 +14,7 @@ concurrency:
jobs:
release:
name: Release
# try out buildjet to speed up build and test
runs-on: buildjet-32vcpu-ubuntu-2204
runs-on: ubuntu-latest-32
steps:
- name: Checkout branch
uses: actions/checkout@v3
+3 -2
View File
@@ -4,8 +4,7 @@ on: [pull_request]
jobs:
build:
# try out buildjet to speed up build and test
runs-on: buildjet-32vcpu-ubuntu-2204
runs-on: ubuntu-latest-32
steps:
- uses: actions/checkout@v3
@@ -16,3 +15,5 @@ jobs:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
strip-hash: "\\-(\\w{8})(?:\\.esm|\\.cjs)"
minimum-change-threshold: 100
# exclude source maps, node modules and any test related files
exclude: "{**/*.map,**/node_modules/**,**/test/**}"
+5 -1
View File
@@ -12,4 +12,8 @@ server/dist
public/dist
.turbo
./packages/contracts-js/abis
.idea/
.idea/
.yalc/
yalc.lock
build/
playwright-report/
-1
View File
@@ -1,4 +1,3 @@
lib
node_modules
dist
contracts-js
+2 -6
View File
@@ -8,7 +8,8 @@
"private": true,
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"test/**/*"
]
},
"scripts": {
@@ -29,11 +30,6 @@
"release:nightly": "node ./scripts/pre-release.mjs && changeset publish --tag nightly",
"push": "turbo run push"
},
"preconstruct": {
"packages": [
"packages/*"
]
},
"packageManager": "[email protected]",
"dependencies": {
"@changesets/changelog-github": "^0.4.6",
+22
View File
@@ -1,5 +1,27 @@
# @thirdweb-dev/auth
## 2.0.27
## 2.0.26
## 2.0.25
## 2.0.24
## 2.0.23
## 2.0.22
## 2.0.21
### Patch Changes
- [#327](https://github.com/thirdweb-dev/js/pull/327) [`ef27aad`](https://github.com/thirdweb-dev/js/commit/ef27aad0aafc4577e85f44dc77dfbe880bd239b5) Thanks [@jnsdls](https://github.com/jnsdls)! - enable e2e testing
## 2.0.20
## 2.0.19
## 2.0.18
## 2.0.17
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "2.0.18",
"version": "2.0.27",
"exports": {
"./next": {
"module": "./next/dist/thirdweb-dev-auth-next.esm.js",
+67
View File
@@ -1,5 +1,72 @@
# 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
- Updated dependencies [[`1c24c3c`](https://github.com/thirdweb-dev/js/commit/1c24c3c3c48476a824f817e09e7bf0fbe67c1db5), [`65fc7ba`](https://github.com/thirdweb-dev/js/commit/65fc7ba6fff093a971b53cb0979c39768e090ba1)]:
- @thirdweb-dev/sdk@3.4.3
## 0.8.9
### Patch Changes
- Updated dependencies [[`21c21c1`](https://github.com/thirdweb-dev/js/commit/21c21c1adfd09b60ad563bfd4c308597140de33c), [`3522917`](https://github.com/thirdweb-dev/js/commit/352291791ee900e1500f84f095290497934d2f60)]:
- @thirdweb-dev/sdk@3.4.2
## 0.8.8
### Patch Changes
- Updated dependencies [[`72227b2`](https://github.com/thirdweb-dev/js/commit/72227b2e166a3a68bbb41cf2b389322f5b7547a2)]:
- @thirdweb-dev/sdk@3.4.1
## 0.8.7
### Patch Changes
- [#327](https://github.com/thirdweb-dev/js/pull/327) [`ef27aad`](https://github.com/thirdweb-dev/js/commit/ef27aad0aafc4577e85f44dc77dfbe880bd239b5) Thanks [@jnsdls](https://github.com/jnsdls)! - enable e2e testing
- Updated dependencies [[`340605b`](https://github.com/thirdweb-dev/js/commit/340605b507f384fbd2999b9c16542af3c53e84a9), [`26116a6`](https://github.com/thirdweb-dev/js/commit/26116a6f637ee845f7bd17f20ffe17caf184658e), [`15f8006`](https://github.com/thirdweb-dev/js/commit/15f8006e1fb22333b7ee239b45e7b1b12d6dccc8), [`ef27aad`](https://github.com/thirdweb-dev/js/commit/ef27aad0aafc4577e85f44dc77dfbe880bd239b5)]:
- @thirdweb-dev/sdk@3.4.0
## 0.8.6
### Patch Changes
- Updated dependencies [[`ad851f7`](https://github.com/thirdweb-dev/js/commit/ad851f7b4d6abb5ab9364ef3dec229f913e18ce5), [`6218c88`](https://github.com/thirdweb-dev/js/commit/6218c885842e6b4e44ec38ec92a9211e56c49bf4)]:
- @thirdweb-dev/sdk@3.3.1
## 0.8.5
### Patch Changes
- [#326](https://github.com/thirdweb-dev/js/pull/326) [`126752d`](https://github.com/thirdweb-dev/js/commit/126752d7d02c9a808a63fb9e67a9df6658b5682b) Thanks [@jnsdls](https://github.com/jnsdls)! - remove tons of unused dependencies, use SDK function for extracting ipfs has from bytecode
- Updated dependencies [[`126752d`](https://github.com/thirdweb-dev/js/commit/126752d7d02c9a808a63fb9e67a9df6658b5682b), [`df74340`](https://github.com/thirdweb-dev/js/commit/df74340fa51323304c32419761d6f18628b060fa), [`89ff921`](https://github.com/thirdweb-dev/js/commit/89ff921a7d9f0e42b4e6707c8b56d0aab95c1aa0), [`a57b4f0`](https://github.com/thirdweb-dev/js/commit/a57b4f01f3a49590b897101f2730582e4124a554)]:
- @thirdweb-dev/sdk@3.3.0
## 0.8.4
### Patch Changes
+1 -8
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.8.4",
"version": "0.8.13",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -31,24 +31,17 @@
"dependencies": {
"@thirdweb-dev/sdk": "*",
"async-retry": "^1.3.3",
"cbor": "^8.1.0",
"commander": "^9.1.0",
"cpy": "^9.0.1",
"enquirer": "^2.3.6",
"ethers": "^5.7.0",
"form-data": "^4.0.0",
"got": "11.8.5",
"inquirer": "^8.2.3",
"ipfs-unixfs": "^6.0.7",
"ipld-dag-pb": "^0.22.3",
"multihashes": "^4.0.3",
"open": "^8.4.0",
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
"solc": "0.8.13",
"tar": "^6.1.11",
"tslog": "^3.3.3",
"update-check": "^1.5.4",
"update-notifier": "^6.0.2",
"validate-npm-package-name": "^4.0.0",
"yaml": "^2.1.1"
+3 -3
View File
@@ -40,7 +40,7 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$
.version(cliVersion, "-v, --version");
program
.command("create")
.command("create [projectPath]")
.description(
"Create a web3 app from any of our official templates: https://github.com/thirdweb-example/",
)
@@ -66,8 +66,8 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$
"-t, --template [name]",
`A template to start your project from. You can use an template repository name from the official thirdweb-example org.`,
)
.action(async (options) => {
await twCreate(options);
.action(async (path, options) => {
await twCreate(path, options);
});
program
@@ -1,7 +1,7 @@
import { extractIPFSHashFromBytecode } from "../helpers/ipfs";
import { logger } from "../helpers/logger";
import { CompileOptions, IBuilder } from "../interfaces/Builder";
import { ContractPayload } from "../interfaces/ContractPayload";
import { extractIPFSHashFromBytecode } from "@thirdweb-dev/sdk/evm";
export abstract class BaseBuilder implements IBuilder {
abstract compile(
-66
View File
@@ -1,66 +0,0 @@
import { decodeFirstSync } from "cbor";
import { UnixFS } from "ipfs-unixfs";
import { DAGNode } from "ipld-dag-pb";
import { toB58String } from "multihashes";
/**
* Derives IPFS hash of string
* @param {String} str
* @return {String} IPFS hash (ex: "Qm")
*/
export async function getIPFSHash(str: string) {
const file = new UnixFS({
type: "file",
data: Buffer.from(str),
});
const node = new DAGNode(file.marshal());
const metadataLink = await node.toDAGLink();
return toB58String(metadataLink.Hash.multihash);
}
/**
* Extract the IPFS hash from the given bytecode
* @param bytecode
* @returns
*/
export function extractIPFSHashFromBytecode(
bytecode: string,
): string | undefined {
const numericBytecode = hexToBytes(bytecode);
const cborLength: number =
numericBytecode[numericBytecode.length - 2] * 0x100 +
numericBytecode[numericBytecode.length - 1];
const bytecodeBuffer = Buffer.from(
numericBytecode.slice(numericBytecode.length - 2 - cborLength, -2),
);
const cborData = decodeFirstSync(bytecodeBuffer);
if (cborData["ipfs"]) {
const uri = toB58String(cborData["ipfs"]);
return uri;
}
return undefined;
}
function hexToBytes(hex: string | number) {
hex = hex.toString(16);
if (!hex.startsWith("0x")) {
hex = `0x${hex}`;
}
if (!isHexStrict(hex)) {
throw new Error('Given value "' + hex + '" is not a valid hex string.');
}
hex = hex.replace(/^0x/i, "");
for (var bytes = [], c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.slice(c, c + 2), 16));
}
return bytes;
}
function isHexStrict(hex: string | number) {
return (
(typeof hex === "string" || typeof hex === "number") &&
/^(-)?0x[0-9a-f]*$/i.test(hex.toString())
);
}
+3 -3
View File
@@ -17,9 +17,9 @@ let language: string = "";
let baseContract: string = "";
/* let createType: string = "app"; */
export async function twCreate(options: any) {
if (typeof projectPath === "string") {
projectPath = projectPath.trim();
export async function twCreate(pPath: string = "", options: any) {
if (typeof pPath === "string") {
projectPath = pPath;
}
if (options.app) {
+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",
+79
View File
@@ -1,5 +1,84 @@
# @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
- [#353](https://github.com/thirdweb-dev/js/pull/353) [`1c24c3c`](https://github.com/thirdweb-dev/js/commit/1c24c3c3c48476a824f817e09e7bf0fbe67c1db5) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Add useUpdateCreators hook, fix CreatorInput schema
## 3.4.2
### Patch Changes
- [#348](https://github.com/thirdweb-dev/js/pull/348) [`7d9a4c6`](https://github.com/thirdweb-dev/js/commit/7d9a4c6abcad1b4f43e431ce7b0b38db9016ea9a) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - [SOL] - add `useUpdateRoyaltySettings()` hook
[SOL] - change `useRoyalty()` hook -> `useRoyaltySettings()` for consistency
- [#352](https://github.com/thirdweb-dev/js/pull/352) [`3522917`](https://github.com/thirdweb-dev/js/commit/352291791ee900e1500f84f095290497934d2f60) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - expose useExecuteAuctionSale and useAcceptDirectListingOffer
- [#351](https://github.com/thirdweb-dev/js/pull/351) [`aabbb14`](https://github.com/thirdweb-dev/js/commit/aabbb149e81d6824015a4797b2b6c929ca359545) Thanks [@jnsdls](https://github.com/jnsdls)! - [EVM] - fix wallet connection during dev (hot reload)
## 3.4.1
### Patch Changes
- [#343](https://github.com/thirdweb-dev/js/pull/343) [`72227b2`](https://github.com/thirdweb-dev/js/commit/72227b2e166a3a68bbb41cf2b389322f5b7547a2) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose new useOffers and useMakeOffer hooks
## 3.4.0
### Minor Changes
- [#327](https://github.com/thirdweb-dev/js/pull/327) [`ef27aad`](https://github.com/thirdweb-dev/js/commit/ef27aad0aafc4577e85f44dc77dfbe880bd239b5) Thanks [@jnsdls](https://github.com/jnsdls)! - Gnosis Safe and Magic Link connectors are no longer included in the default export. They are now available as named exports instead.
## Gnosis Safe
### Connector
```diff
- import { GnosisSafeConnector } from "@thirdweb-dev/react";
+ import { GnosisSafeConnector } from "@thirdweb-dev/react/evm/connectors/gnosis-safe";
```
### Hook
```diff
- import { useGnosis } from "@thirdweb-dev/react";
+ import { useGnosis } from "@thirdweb-dev/react/evm/connectors/gnosis-safe";
```
## Magic Link
### Connector
```diff
- import { MagicLink } from "@thirdweb-dev/react";
+ import { MagicConnector } from "@thirdweb-dev/react/evm/connectors/magic";
```
### Hook
```diff
- import { useMagic } from "@thirdweb-dev/react";
+ import { useMagic } from "@thirdweb-dev/react/evm/connectors/magic";
```
### Patch Changes
- [#339](https://github.com/thirdweb-dev/js/pull/339) [`b03a902`](https://github.com/thirdweb-dev/js/commit/b03a9021451b79f802f682f66e5ae8e9355d7e6f) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Fix docs for some functions
## 3.3.1
## 3.3.0
## 3.2.6
### Patch Changes
+2 -2
View File
@@ -26,11 +26,11 @@ The `useContractCall` hook to call a smart contract function has been renamed to
### Prebuilt Contract Hooks
Hooks to connect to prebuilt contracts such as `useNFTDrop` and `useMarketplace` are now deprecated in favour of using the generic `useContract` hook:
Hooks to connect to prebuilt contracts such as `useNFTDrop` and `useMarketplace` are now deprecated in favour of using the generic `useContract` hook followed by the contract type:
```diff
- const contract = useNFTDrop("0x...")
+ const { contract } = useContract("0x...")
+ const { contract } = useContract("0x...", "nft-drop")
```
You can currently still continue to use the prebuilt hooks, however they provide a warning in the console informing you of their deprecation. We will likely remove these hooks in a future version of the SDK.
+1 -1
View File
@@ -191,7 +191,7 @@ export default function Home() {
<div>
{nfts.map((nft) => (
<ThirdwebNftMedia
key={nft.tokenId}
key={nft.metadata.id}
metadata={nft.metadata}
height={200}
/>
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-react-evm-connectors-gnosis-safe.cjs.js",
"module": "dist/thirdweb-dev-react-evm-connectors-gnosis-safe.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-react-evm-connectors-magic.cjs.js",
"module": "dist/thirdweb-dev-react-evm-connectors-magic.esm.js"
}
+19 -18
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.2.6",
"version": "3.5.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -20,6 +20,14 @@
"module": "./solana/dist/thirdweb-dev-react-solana.esm.js",
"default": "./solana/dist/thirdweb-dev-react-solana.cjs.js"
},
"./evm/connectors/magic": {
"module": "./evm/connectors/magic/dist/thirdweb-dev-react-evm-connectors-magic.esm.js",
"default": "./evm/connectors/magic/dist/thirdweb-dev-react-evm-connectors-magic.cjs.js"
},
"./evm/connectors/gnosis-safe": {
"module": "./evm/connectors/gnosis-safe/dist/thirdweb-dev-react-evm-connectors-gnosis-safe.esm.js",
"default": "./evm/connectors/gnosis-safe/dist/thirdweb-dev-react-evm-connectors-gnosis-safe.cjs.js"
},
"./package.json": "./package.json"
},
"files": [
@@ -31,7 +39,8 @@
"entrypoints": [
"index.ts",
"solana/index.ts",
"evm/index.ts"
"evm/index.ts",
"evm/connectors/**.ts"
],
"___experimentalFlags_WILL_CHANGE_IN_PATCH": {
"exports": true
@@ -49,8 +58,6 @@
"generate-docs:solana": "api-extractor run --local --config ./config/api-extractor-solana.json && api-documenter markdown -i ./temp -o ./docs/solana && rm -rf ./temp-solana && mv ./temp ./temp-solana",
"generate-snippets": "node ./scripts/generate-snippets-evm.mjs && node ./scripts/generate-snippets-solana.mjs && node ./scripts/generate-feature-snippets-evm.mjs",
"build": "tsc && preconstruct build",
"e2e": "yarn test-ct",
"test-ct": "playwright test -c playwright-ct.config.ts",
"push": "yalc push"
},
"sideEffects:": false,
@@ -63,15 +70,11 @@
"@microsoft/api-documenter": "^7.19.4",
"@microsoft/api-extractor": "^7.29.2",
"@microsoft/tsdoc": "^0.14.1",
"@playwright/experimental-ct-react": "1.25.1",
"@playwright/test": "1.25.1",
"@preconstruct/cli": "^2.2.1",
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/web3.js": "^1.63.0",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/sdk": "*",
"@thirdweb-dev/solana": "*",
"@types/color": "^3.0.3",
"@types/mime": "^3.0.1",
"@types/react": "^18.0.5",
@@ -93,13 +96,14 @@
"dependencies": {
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@gnosis.pm/safe-core-sdk": "2.1.0",
"@gnosis.pm/safe-ethers-adapters": "0.1.0-alpha.9",
"@gnosis.pm/safe-ethers-lib": "1.1.0",
"@gnosis.pm/safe-core-sdk": "^3.1.1",
"@gnosis.pm/safe-ethers-adapters": "0.1.0-alpha.11",
"@gnosis.pm/safe-ethers-lib": "^1.6.1",
"@react-icons/all-files": "^4.1.0",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@tanstack/react-query": "^4.0.10",
"@zag-js/menu": "0.1.14",
"@zag-js/react": "0.1.16",
"@zag-js/menu": "^0.1.16",
"@zag-js/react": "^0.1.18",
"buffer": "^6.0.3",
"color": "^4.2.3",
"copy-to-clipboard": "^3.3.2",
@@ -114,7 +118,7 @@
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/web3.js": "^1.63.0",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/sdk": "*",
"ethers": ">=5.5.1",
"react": ">=18.0.0",
@@ -136,9 +140,6 @@
},
"@solana/web3.js": {
"optional": true
},
"@thirdweb-dev/solana": {
"optional": true
}
}
}
-57
View File
@@ -1,57 +0,0 @@
import type { PlaywrightTestConfig } from "@playwright/experimental-ct-react";
import { devices } from "@playwright/experimental-ct-react";
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./",
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: "./__snapshots__",
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
// workers: process.env.CI ? 1 : undefined,
// we can use parallel tests because we have more cores now
workers: undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
],
};
export default config;
-4
View File
@@ -1,4 +0,0 @@
// Import styles, initialize component theme here.
// import '../src/common.css';
export {};
@@ -1,15 +0,0 @@
import { ConnectWallet } from ".";
import { ThirdwebProvider } from "../../providers/full";
import { test, expect } from "@playwright/experimental-ct-react";
test.use({ viewport: { width: 500, height: 500 } });
test("should render the connect wallet button", async ({ mount }) => {
const component = await mount(
<ThirdwebProvider desiredChainId={1}>
<ConnectWallet />
</ThirdwebProvider>,
);
await expect(component).toContainText("Connect Wallet");
});
@@ -144,7 +144,7 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = ({
},
connect,
] = useConnect();
const disconnect = useDisconnect({ reconnectAfterGnosis: false });
const disconnect = useDisconnect({ reconnectPrevious: false });
const supportedConnectors = connectors.filter((c) =>
SUPPORTED_CONNECTORS.includes(c.id as typeof SUPPORTED_CONNECTORS[number]),
@@ -13,12 +13,14 @@ export interface ThirdwebNftMediaProps extends SharedMediaProps {
}
/**
*
* This component can be used to render NFTs from the thirdweb SDK.
* Props: {@link ThirdwebNftMediaProps}
*
* @example
* ```jsx
* import { ThirdwebNftMedia, useNFTCollection, useNFT } from "@thirdweb-dev/react";
* import { ThirdwebNftMedia, useContract, useNFT } from "@thirdweb-dev/react";
* export default function NFTCollectionRender() {
* const contract = useNFTCollection(<your-contract-address>);
* const { contract } = useContract(<your-contract-address>);
* const { data: nft, isLoading } = useNFT(contract, 0);
*
* return (
@@ -32,11 +34,6 @@ export interface ThirdwebNftMediaProps extends SharedMediaProps {
* );
* }
* ```
*
* Use this to get the primary sales recipient of your {@link SmartContract}
* @param contract - an instance of a {@link SmartContract}
* @returns the wallet address of the primary sales recipient
* @beta
*/
export const ThirdwebNftMedia = React.forwardRef<
HTMLMediaElement,
@@ -1,16 +0,0 @@
import { Web3Button } from ".";
import { ThirdwebProvider } from "../../providers/full";
import { test, expect } from "@playwright/experimental-ct-react";
test.use({ viewport: { width: 500, height: 500 } });
test("should render the connect wallet button", async ({ mount }) => {
const component = await mount(
<ThirdwebProvider desiredChainId={1}>
<Web3Button contractAddress="0x123" action={() => undefined}>
Do the thing
</Web3Button>
</ThirdwebProvider>,
);
await expect(component).toContainText("Connect Wallet");
});
@@ -1,13 +1,23 @@
import { ChainId } from "@thirdweb-dev/sdk";
import { useConnect } from "../hooks/wagmi-required/useConnect";
import { Signer, ethers, utils } from "ethers";
import invariant from "tiny-invariant";
import { Chain, Connector, ConnectorData, normalizeChainId } from "wagmi";
import {
Chain,
Connector,
ConnectorData,
normalizeChainId,
useContext as useWagmiContext,
} from "wagmi";
const CHAIN_ID_TO_GNOSIS_SERVER_URL = {
[ChainId.Mainnet]: "https://safe-transaction.mainnet.gnosis.io",
[ChainId.Avalanche]: "https://safe-transaction.avalanche.gnosis.io",
[ChainId.Polygon]: "https://safe-transaction.polygon.gnosis.io",
[ChainId.Goerli]: "https://safe-transaction.goerli.gnosis.io",
// mainnet
1: "https://safe-transaction.mainnet.gnosis.io",
// avalanche
43114: "https://safe-transaction.avalanche.gnosis.io",
// polygon
137: "https://safe-transaction.polygon.gnosis.io",
// goerli
5: "https://safe-transaction.goerli.gnosis.io",
};
export interface GnosisConnectorArguments {
@@ -163,3 +173,67 @@ export class GnosisSafeConnector extends Connector {
this.config = config;
}
}
/**
* Hook for connecting to a Gnosis Safe. This enables multisig wallets to connect to your application and sing transactions.
*
* ```javascript
* import { useGnosis } from "@thirdweb-dev/react"
* ```
*
*
* @example
* ```javascript
* import { useGnosis } from "@thirdweb-dev/react"
*
* const App = () => {
* const connectWithGnosis = useGnosis()
*
* return (
* <button onClick={() => connectWithGnosis({ safeAddress: "0x...", safeChainId: 1 })}>
* Connect Gnosis Safe
* </button>
* )
* }
* ```
*
* @public
*/
export function useGnosis() {
const wagmiContext = useWagmiContext();
invariant(
wagmiContext,
`useGnosis() can only be used inside <ThirdwebProvider />. If you are using <ThirdwebSDKProvider /> you will have to use your own wallet-connection logic.`,
);
const [connectors, connect] = useConnect();
if (connectors.loading) {
return () => Promise.reject("Gnosis connector not ready to be used, yet");
}
const connector = connectors.data.connectors.find((c) => c.id === "gnosis");
invariant(
connector,
"Gnosis connector not found, please make sure it is provided to your <ThirdwebProvider />",
);
return async (config: GnosisConnectorArguments) => {
const previousConnector = connectors.data.connector;
const previousConnectorChain = await previousConnector?.getChainId();
invariant(
!!previousConnector,
"Cannot connect to Gnosis Safe without first being connected to a personal wallet.",
);
invariant(
previousConnectorChain === config.safeChainId,
"Gnosis safe chain id must match personal wallet chain id.",
);
invariant(
utils.isAddress(config.safeAddress),
"Gnosis safe address must be a valid address.",
);
(connector as GnosisSafeConnector).setConfiguration(
previousConnector,
config,
);
return connect(connector);
};
}
+93 -1
View File
@@ -1,3 +1,4 @@
import { useConnect } from "../hooks/wagmi-required/useConnect";
import { providers, utils } from "ethers";
import type {
LoginWithMagicLinkConfiguration,
@@ -5,7 +6,12 @@ import type {
MagicSDKAdditionalConfiguration,
} from "magic-sdk";
import invariant from "tiny-invariant";
import { Chain, Connector, normalizeChainId } from "wagmi";
import {
Chain,
Connector,
normalizeChainId,
useContext as useWagmiContext,
} from "wagmi";
export interface MagicConnectorArguments
extends MagicSDKAdditionalConfiguration {
@@ -186,3 +192,89 @@ export class MagicConnector extends Connector {
}
}
}
/**
* Hook for connecting to an email wallet using magic link.
* This enables users without their own wallets to connect to your application and sign transactions securely using their email.
*
* ```javascript
* import { useMagic } from "@thirdweb-dev/react"
* ```
*
*
* @example
* Before using this hook, you first need to set up the magic configuration in your `ThirdwebProvider`, including your magic API key.
*
* ```javascript
* // Add the magic configuration object to your wallet connectors
* const connectors = [
* "metamask",
* "walletConnect",
* "walletLink",
* {
* name: "magic",
* options: {
* apiKey: "your-magic-api-key",
* }
* }
* ]
*
* // Add the above to the walletConnectors prop of your <ThirdwebProvider />
* const Provider = ({ children }) => (
* return (
* <ThirdwebProvider
* walletConnectors={connectors}
* // Specify remaining parameters
* ...
* >
* {children}
* </ThirdwebProvider>
* )
* }
* ```
*
* In order to use the hook to connect users with magic link, you just need to provide the users email to the connect function.
*
* You can setup the hook with the following configuration:
* ```javascript
* import { useMagic } from "@thirdweb-dev/react"
* import { useState } from "react"
*
* const LoginWithMagicLink = () => {
* const connectWithMagic = useMagic()
* const [email, setEmail] = useState()
*
* return (
* <div>
* <input value={email} onChange={(e) => setEmail(e.target.value)} />
* <button onClick={() => connectWithMagic({ email })}>Login</button>
* </div>
* )
* }
* ```
*
* @public
*/
export function useMagic() {
const wagmiContext = useWagmiContext();
invariant(
wagmiContext,
`useMagic() can only be used inside <ThirdwebProvider />. If you are using <ThirdwebSDKProvider /> you will have to use your own wallet-connection logic.`,
);
const [connectors, connect] = useConnect();
if (connectors.loading) {
return () => Promise.reject("Magic connector not ready to be used, yet");
}
const connector = connectors.data.connectors.find((c) => c.id === "magic");
invariant(
connector,
"Magic connector not found, please make sure it is provided to your <ThirdwebProvider />",
);
return (configuration: LoginWithMagicLinkConfiguration) => {
(connector as MagicConnector).setConfiguration(configuration);
return connect(connector);
};
}
@@ -33,7 +33,17 @@ export const ThirdwebConnectedWalletProvider: React.FC<
const { rpcUrlMap } = useThirdwebConfigContext();
const [contextValue, setContextValue] =
useState<ThirdwebConnectedWalletContext>(INITIAL_CONTEXT_VALUE);
useState<ThirdwebConnectedWalletContext>({
...INITIAL_CONTEXT_VALUE,
signer: signer ? signer : undefined,
});
useEffect(() => {
setContextValue((val) => ({
...val,
signer: signer ? signer : undefined,
}));
}, [signer]);
useEffect(() => {
let s = signer;
@@ -10,7 +10,11 @@ import {
} from "../../utils/cache-keys";
import { useQueryWithNetwork } from "../query-utils/useQueryWithNetwork";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ClaimCondition, ClaimConditionInput } from "@thirdweb-dev/sdk";
import {
ClaimCondition,
ClaimConditionFetchOptions,
ClaimConditionInput,
} from "@thirdweb-dev/sdk";
import { BigNumberish } from "ethers";
import invariant from "tiny-invariant";
@@ -65,25 +69,30 @@ export type SetClaimConditionsParams = {
export function useActiveClaimCondition(
contract: RequiredParam<DropContract>,
tokenId?: BigNumberish,
options?: ClaimConditionFetchOptions,
) {
const contractAddress = contract?.getAddress();
const { erc1155, erc721, erc20 } = getErcs(contract);
return useQueryWithNetwork(
cacheKeys.extensions.claimConditions.getActive(contractAddress, tokenId),
cacheKeys.extensions.claimConditions.getActive(
contractAddress,
tokenId,
options,
),
() => {
if (erc1155) {
requiredParamInvariant(
tokenId,
"tokenId is required for ERC1155 claim conditions",
);
return erc1155.claimConditions.getActive(tokenId);
return erc1155.claimConditions.getActive(tokenId, options);
}
if (erc721) {
return erc721.claimConditions.getActive();
return erc721.claimConditions.getActive(options);
}
if (erc20) {
return erc20.claimConditions.getActive();
return erc20.claimConditions.getActive(options);
}
throw new Error("Contract must be ERC721, ERC1155 or ERC20");
},
@@ -121,25 +130,30 @@ export function useActiveClaimCondition(
export function useClaimConditions(
contract: RequiredParam<DropContract>,
tokenId?: BigNumberish,
options?: ClaimConditionFetchOptions,
) {
const contractAddress = contract?.getAddress();
const { erc1155, erc721, erc20 } = getErcs(contract);
return useQueryWithNetwork(
cacheKeys.extensions.claimConditions.getAll(contractAddress, tokenId),
cacheKeys.extensions.claimConditions.getAll(
contractAddress,
tokenId,
options,
),
() => {
if (erc1155) {
requiredParamInvariant(
tokenId,
"tokenId is required for ERC1155 claim conditions",
);
return erc1155.claimConditions.getAll(tokenId);
return erc1155.claimConditions.getAll(tokenId, options);
}
if (erc721) {
return erc721.claimConditions.getAll();
return erc721.claimConditions.getAll(options);
}
if (erc20) {
return erc20.claimConditions.getAll();
return erc20.claimConditions.getAll(options);
}
throw new Error("Contract must be ERC721, ERC1155 or ERC20");
},
@@ -350,12 +364,10 @@ export function useResetClaimConditions(
return useMutation(
async () => {
const cleanConditions = (conditions: ClaimCondition[]) => {
const cleanConditions = async (conditions: ClaimCondition[]) => {
return conditions.map((c) => ({
...c,
price: c.currencyMetadata.displayValue,
maxQuantity: c.maxQuantity.toString(),
quantityLimitPerTransaction: c.quantityLimitPerTransaction.toString(),
}));
};
@@ -364,24 +376,30 @@ export function useResetClaimConditions(
tokenId,
"tokenId is required for ERC1155 claim conditions",
);
const claimConditions = await erc1155.claimConditions.getAll(tokenId);
const claimConditions = await erc1155.claimConditions.getAll(tokenId, {
withAllowList: true,
});
return erc1155.claimConditions.set(
tokenId,
cleanConditions(claimConditions || []),
await cleanConditions(claimConditions || []),
true,
);
}
if (erc721) {
const claimConditions = await erc721.claimConditions.getAll();
const claimConditions = await erc721.claimConditions.getAll({
withAllowList: true,
});
return await erc721.claimConditions.set(
cleanConditions(claimConditions || []),
await cleanConditions(claimConditions || []),
true,
);
}
if (erc20) {
const claimConditions = await erc20.claimConditions.getAll();
const claimConditions = await erc20.claimConditions.getAll({
withAllowList: true,
});
return await erc20.claimConditions.set(
cleanConditions(claimConditions || []),
await cleanConditions(claimConditions || []),
true,
);
}
@@ -3,13 +3,20 @@ import {
requiredParamInvariant,
} from "../../../core/query-utils/required-param";
import { useSDKChainId } from "../../providers/base";
import { BuyNowParams, MakeBidParams } from "../../types";
import {
AcceptDirectOffer,
BuyNowParams,
ExecuteAuctionSale,
MakeBidParams,
MakeOfferParams,
} from "../../types";
import {
cacheKeys,
invalidateContractAndBalances,
} from "../../utils/cache-keys";
import { useQueryWithNetwork } from "../query-utils/useQueryWithNetwork";
import { useAddress } from "../wallet";
import { useContractEvents } from "./contracts";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type {
AuctionListing,
@@ -514,6 +521,203 @@ export function useMakeBid(contract: RequiredParam<Marketplace>) {
);
}
/**
* Use this to make an offer on direct or auction listing from your marketplace contract.
*
* @example
* ```jsx
* const Component = () => {
* const {
* mutate: makeOffer,
* isLoading,
* error,
* } = useMakeOffer(">>YourMarketplaceContractInstance<<");
*
* if (error) {
* console.error("failed to make a bid", error);
* }
*
* return (
* <button
* disabled={isLoading}
* onClick={() => makeOffer({ listingId: 1, pricePerToken: 0.5, quantity: 1 })}
* >
* Bid!
* </button>
* );
* };
* ```
*
* @param contract - an instance of a Marketplace contract
* @returns a mutation object that can be used to make a bid on an auction listing
* @beta
*/
export function useMakeOffer(contract: RequiredParam<Marketplace>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (data: MakeOfferParams) => {
invariant(walletAddress, "no wallet connected, cannot make bid");
requiredParamInvariant(contract, "No Contract instance provided");
return await contract.makeOffer(
data.listingId,
data.pricePerToken,
data.quantity,
);
},
{
onSettled: () =>
invalidateContractAndBalances(
queryClient,
contractAddress,
activeChainId,
),
},
);
}
/**
* Accept an offer on a direct listing from an offeror, will accept the latest offer by the given offeror.
*
* @example
* ```jsx
* const Component = () => {
* const {
* mutate: acceptOffer,
* isLoading,
* error,
* } = useAcceptDirectListingOffer(">>YourMarketplaceContractInstance<<");
*
* if (error) {
* console.error("failed to accept offer", error);
* }
*
* return (
* <button
* disabled={isLoading}
* onClick={() => acceptOffer({ listingId: 1, addressOfOfferor: "0x..." })}
* >
* Accept offer
* </button>
* );
* };
* ```
*
* @param contract - an instance of a Marketplace contract
* @returns a mutation object that can be used to accept an offer on a direct listing
* @beta
*/
export function useAcceptDirectListingOffer(
contract: RequiredParam<Marketplace>,
) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (data: AcceptDirectOffer) => {
invariant(walletAddress, "no wallet connected, cannot make bid");
requiredParamInvariant(contract?.direct, "No Direct instance provided");
return await contract.direct.acceptOffer(
data.listingId,
data.addressOfOfferor,
);
},
{
onSettled: () =>
invalidateContractAndBalances(
queryClient,
contractAddress,
activeChainId,
),
},
);
}
/**
* Execute an auction sale. Can only be executed once the auction has ended and the auction has a winning bid.
*
* @example
* ```jsx
* const Component = () => {
* const {
* mutate: executeAuctionSale,
* isLoading,
* error,
* } = useExecuteAuctionSale(">>YourMarketplaceContractInstance<<");
*
* if (error) {
* console.error("failed to execute sale", error);
* }
*
* return (
* <button
* disabled={isLoading}
* onClick={() => executeAuctionSale({ listingId: 1 })}
* >
* Execute sale
* </button>
* );
* };
* ```
*
* @param contract - an instance of a Marketplace contract
* @returns a mutation object that can be used to accept an offer on a direct listing
* @beta
*/
export function useExecuteAuctionSale(contract: RequiredParam<Marketplace>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (data: ExecuteAuctionSale) => {
invariant(walletAddress, "no wallet connected, cannot make bid");
requiredParamInvariant(
contract?.auction,
"No Auction marketplace instance provided",
);
return await contract.auction.executeSale(data.listingId);
},
{
onSettled: () =>
invalidateContractAndBalances(
queryClient,
contractAddress,
activeChainId,
),
},
);
}
/**
* Get all the offers for a listing
*
* @remarks Fetch all the offers for a specified direct or auction listing.
* @example
* ```javascript
* const { data: offers, isLoading, error } = useOffers(<YourMarketplaceContractInstance>, <listingId>);
* ```
*
* @param contract - an instance of a Marketplace contract
* @param listingId - the id of the listing to fetch offers for
* @beta
*/
export function useOffers(
contract: RequiredParam<Marketplace>,
listingId: RequiredParam<BigNumberish>,
) {
const result = useContractEvents(contract, "NewOffer");
return {
...result,
data: result.data
?.filter((ev) => ev.data.listingId.eq(listingId))
?.map((ev) => ev.data),
};
}
/**
* Use this to buy out an auction listing from your marketplace contract.
*
+1 -1
View File
@@ -542,7 +542,7 @@ export function useTransferNFT<TContract extends NFTContract>(
* @example
* ```jsx
* const Component = () => {
* const editionDrop = useEditionDrop(<ContractAddress>);
* const editionDrop = useContract(<ContractAddress>, "edition-drop");
* const {
* mutate: airdropNFT,
* isLoading,
+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");
},
@@ -1,72 +0,0 @@
import {
GnosisConnectorArguments,
GnosisSafeConnector,
} from "../../connectors/gnosis-safe";
import { useConnect } from "../wagmi-required/useConnect";
import { utils } from "ethers";
import invariant from "tiny-invariant";
import { useContext as useWagmiContext } from "wagmi";
/**
* Hook for connecting to a Gnosis Safe. This enables multisig wallets to connect to your application and sing transactions.
*
* ```javascript
* import { useGnosis } from "@thirdweb-dev/react"
* ```
*
*
* @example
* ```javascript
* import { useGnosis } from "@thirdweb-dev/react"
*
* const App = () => {
* const connectWithGnosis = useGnosis()
*
* return (
* <button onClick={() => connectWithGnosis({ safeAddress: "0x...", safeChainId: 1 })}>
* Connect Gnosis Safe
* </button>
* )
* }
* ```
*
* @public
*/
export function useGnosis() {
const wagmiContext = useWagmiContext();
invariant(
wagmiContext,
`useGnosis() can only be used inside <ThirdwebProvider />. If you are using <ThirdwebSDKProvider /> you will have to use your own wallet-connection logic.`,
);
const [connectors, connect] = useConnect();
if (connectors.loading) {
return () => Promise.reject("Gnosis connector not ready to be used, yet");
}
const connector = connectors.data.connectors.find((c) => c.id === "gnosis");
invariant(
connector,
"Gnosis connector not found, please make sure it is provided to your <ThirdwebProvider />",
);
return async (config: GnosisConnectorArguments) => {
const previousConnector = connectors.data.connector;
const previousConnectorChain = await previousConnector?.getChainId();
invariant(
!!previousConnector,
"Cannot connect to Gnosis Safe without first being connected to a personal wallet.",
);
invariant(
previousConnectorChain === config.safeChainId,
"Gnosis safe chain id must match personal wallet chain id.",
);
invariant(
utils.isAddress(config.safeAddress),
"Gnosis safe address must be a valid address.",
);
(connector as GnosisSafeConnector).setConfiguration(
previousConnector,
config,
);
return connect(connector);
};
}
@@ -1,91 +0,0 @@
import { MagicConnector } from "../../connectors/magic";
import { useConnect } from "../wagmi-required/useConnect";
import { LoginWithMagicLinkConfiguration } from "magic-sdk";
import invariant from "tiny-invariant";
import { useContext as useWagmiContext } from "wagmi";
/**
* Hook for connecting to an email wallet using magic link.
* This enables users without their own wallets to connect to your application and sign transactions securely using their email.
*
* ```javascript
* import { useMagic } from "@thirdweb-dev/react"
* ```
*
*
* @example
* Before using this hook, you first need to set up the magic configuration in your `ThirdwebProvider`, including your magic API key.
*
* ```javascript
* // Add the magic configuration object to your wallet connectors
* const connectors = [
* "metamask",
* "walletConnect",
* "walletLink",
* {
* name: "magic",
* options: {
* apiKey: "your-magic-api-key",
* }
* }
* ]
*
* // Add the above to the walletConnectors prop of your <ThirdwebProvider />
* const Provider = ({ children }) => (
* return (
* <ThirdwebProvider
* walletConnectors={connectors}
* // Specify remaining parameters
* ...
* >
* {children}
* </ThirdwebProvider>
* )
* }
* ```
*
* In order to use the hook to connect users with magic link, you just need to provide the users email to the connect function.
*
* You can setup the hook with the following configuration:
* ```javascript
* import { useMagic } from "@thirdweb-dev/react"
* import { useState } from "react"
*
* const LoginWithMagicLink = () => {
* const connectWithMagic = useMagic()
* const [email, setEmail] = useState()
*
* return (
* <div>
* <input value={email} onChange={(e) => setEmail(e.target.value)} />
* <button onClick={() => connectWithMagic({ email })}>Login</button>
* </div>
* )
* }
* ```
*
* @public
*/
export function useMagic() {
const wagmiContext = useWagmiContext();
invariant(
wagmiContext,
`useMagic() can only be used inside <ThirdwebProvider />. If you are using <ThirdwebSDKProvider /> you will have to use your own wallet-connection logic.`,
);
const [connectors, connect] = useConnect();
if (connectors.loading) {
return () => Promise.reject("Magic connector not ready to be used, yet");
}
const connector = connectors.data.connectors.find((c) => c.id === "magic");
invariant(
connector,
"Magic connector not found, please make sure it is provided to your <ThirdwebProvider />",
);
return (configuration: LoginWithMagicLinkConfiguration) => {
(connector as MagicConnector).setConfiguration(configuration);
return connect(connector);
};
}
@@ -1,10 +1,10 @@
import { useConnect } from "../wagmi-required/useConnect";
import { Buffer } from "buffer";
import invariant from "tiny-invariant";
import { useContext as useWagmiContext } from "wagmi";
if (!globalThis.Buffer) {
globalThis.Buffer = require("buffer/").Buffer;
}
globalThis.Buffer = Buffer;
/**
* Hook for connecting to a mobile wallet with Wallet Connect
*
@@ -1,10 +1,10 @@
import { useConnect } from "../wagmi-required/useConnect";
import { Buffer } from "buffer";
import invariant from "tiny-invariant";
import { useContext as useWagmiContext } from "wagmi";
if (!globalThis.Buffer) {
globalThis.Buffer = require("buffer/").Buffer;
}
globalThis.Buffer = Buffer;
/**
* Hook for connecting to a Coinbase wallet.
*
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const edition = await sdk.useEdition("0x1234...");
* + const edition = await sdk.useContract("0x1234...", "edition").contract;
* - const edition = useEdition("0x1234...");
* + const edition = useContract("0x1234...", "edition").contract;
* ```
*/
export function useEdition(contractAddress: RequiredParam<string>) {
@@ -27,8 +27,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const editionDrop = await sdk.useEditionDrop("0x1234...");
* + const editionDrop = await sdk.useContract("0x1234...", "edition-drop").contract;
* - const editionDrop = useEditionDrop("0x1234...");
* + const editionDrop = useContract("0x1234...", "edition-drop").contract;
* ```
*/
export function useEditionDrop(contractAddress: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const marketplace = await sdk.useMarketplace("0x1234...");
* + const marketplace = await sdk.useContract("0x1234...", "marketplace").contract;
* - const marketplace = useMarketplace("0x1234...");
* + const marketplace = useContract("0x1234...", "marketplace").contract;
* ```
*/
export function useMarketplace(contractAddress: RequiredParam<string>) {
@@ -27,8 +27,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const multiwrap = await sdk.useMultiwrap("0x1234...");
* + const multiwrap = await sdk.useContract("0x1234...", "multiwrap").contract;
* - const multiwrap = useMultiwrap("0x1234...");
* + const multiwrap = useContract("0x1234...", "multiwrap").contract;
* ```
*/
export function useMultiwrap(contractAddress: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const nftCollection = await sdk.useNFTCollection("0x1234...");
* + const nftCollection = await sdk.useContract("0x1234...", "nft-collection").contract;
* - const nftCollection = useNFTCollection("0x1234...");
* + const nftCollection = useContract("0x1234...", "nft-collection").contract;
* ```
*/
export function useNFTCollection(contractAddress: RequiredParam<string>) {
@@ -27,8 +27,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const nftDrop = await sdk.useNFTDrop("0x1234...");
* + const nftDrop = await sdk.useContract("0x1234...", "nft-drop").contract;
* - const nftDrop = useNFTDrop("0x1234...");
* + const nftDrop = useContract("0x1234...", "nft-drop").contract;
* ```
*/
export function useNFTDrop(contractAddress: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const pack = await sdk.usePack("0x1234...");
* + const pack = await sdk.useContract("0x1234...", "pack").contract;
* - const pack = usePack("0x1234...");
* + const pack = useContract("0x1234...", "pack").contract;
* ```
*/
export function usePack(contractAddress: RequiredParam<string>) {
@@ -27,8 +27,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const signatureDrop = await sdk.useSignatureDrop("0x1234...");
* + const signatureDrop = await sdk.useContract("0x1234...", "signature-drop").contract;
* - const signatureDrop = useSignatureDrop("0x1234...");
* + const signatureDrop = useContract("0x1234...", "signature-drop").contract;
* ```
*/
export function useSignatureDrop(contractAddress?: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const split = await sdk.useSplit("0x1234...");
* + const split = await sdk.useContract("0x1234...", "split").contract;
* - const split = useSplit("0x1234...");
* + const split = useContract("0x1234...", "split").contract;
* ```
*/
export function useSplit(contractAddress: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const token = await sdk.useToken("0x1234...");
* + const token = await sdk.useContract("0x1234...", "token").contract;
* - const token = useToken("0x1234...");
* + const token = useContract("0x1234...", "token").contract;
* ```
*/
export function useToken(contractAddress: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const token = await sdk.useTokenDrop("0x1234...");
* + const token = await sdk.useContract("0x1234...", "token-drop").contract;
* - const token = useTokenDrop("0x1234...");
* + const token = useContract("0x1234...", "token-drop").contract;
* ```
*/
export function useTokenDrop(contractAddress: RequiredParam<string>) {
@@ -28,8 +28,8 @@ import { useContract } from "../async/contracts";
* @deprecated
* This hook is deprecated and will be removed in a future major version. You should use {@link useContract} instead.
* ```diff
* - const vote = await sdk.useVote("0x1234...");
* + const vote = await sdk.useContract("0x1234...", "vote").contract;
* - const vote = useVote("0x1234...");
* + const vote = useContract("0x1234...", "vote").contract;
* ```
*/
export function useVote(contractAddress: RequiredParam<string>) {
@@ -1,7 +1,6 @@
import { GnosisSafeConnector } from "../../connectors/gnosis-safe";
import { useConnect } from "./useConnect";
import invariant from "tiny-invariant";
import { useAccount, useContext as useWagmiContext } from "wagmi";
import { Connector, useAccount, useContext as useWagmiContext } from "wagmi";
/**
* Hook for disconnecting the currently connected wallet
@@ -31,23 +30,22 @@ import { useAccount, useContext as useWagmiContext } from "wagmi";
*
* @public
*/
export function useDisconnect(options?: { reconnectAfterGnosis?: boolean }) {
export function useDisconnect(options?: { reconnectPrevious?: boolean }) {
const wagmiContext = useWagmiContext();
invariant(
wagmiContext,
`useDisconnect() can only be used inside <ThirdwebProvider />. If you are using <ThirdwebSDKProvider /> you will have to use your own connection logic.`,
);
const optsWithDefaults = { ...{ reconnectAfterGnosis: true }, ...options };
const optsWithDefaults = { ...{ reconnectPrevious: true }, ...options };
const [, connect] = useConnect();
const [data, disconnect] = useAccount();
return async () => {
const previousConnector =
(data.data?.connector instanceof GnosisSafeConnector &&
data.data.connector.previousConnector) ||
undefined;
// if there is a previous connector get it
const previousConnector: Connector | undefined =
(data?.data?.connector as any)?.previousConnector || undefined;
// if it's gnosis, just connect the previous connector
if (optsWithDefaults.reconnectAfterGnosis && previousConnector) {
if (optsWithDefaults.reconnectPrevious && previousConnector) {
try {
return await connect(previousConnector);
} catch (err) {
-2
View File
@@ -31,8 +31,6 @@ export * from "./hooks/wagmi-required/useConnect";
export * from "./hooks/connectors/useMetamask";
export * from "./hooks/connectors/useWalletConnect";
export * from "./hooks/connectors/useWalletLink";
export * from "./hooks/connectors/useGnosis";
export * from "./hooks/connectors/useMagic";
// re-exports
export { ChainId } from "@thirdweb-dev/sdk";
+76 -73
View File
@@ -1,90 +1,93 @@
import { useState, useCallback, useLayoutEffect, useEffect, useRef, PropsWithChildren } from "react";
import { createPortal } from "react-dom";
import {
useState,
useCallback,
useLayoutEffect,
useEffect,
useRef,
PropsWithChildren,
} from "react";
import { createPortal } from "react-dom";
function canUseDOM() {
function canUseDOM() {
return !!(
typeof window !== "undefined" &&
window.document &&
window.document.createElement
);
typeof window !== "undefined" &&
window.document &&
window.document.createElement
);
}
/**
* Forces a re-render, similar to `forceUpdate` in class components.
*/
function useForceUpdate() {
const [, dispatch] = useState<{}>(Object.create(null));
return useCallback(() => {
dispatch(Object.create(null));
}, []);
const [, dispatch] = useState<{}>(Object.create(null));
return useCallback(() => {
dispatch(Object.create(null));
}, []);
}
const useIsomorphicLayoutEffect = canUseDOM() ? useLayoutEffect : useEffect;
/**
* Portal from `@reach/portal`
*
* @see Docs https://reach.tech/portal#portal
*/
const PortalImpl: React.FC<PortalProps> = ({
children,
type = "reach-portal",
containerRef,
}) => {
const mountNode = useRef<HTMLDivElement | null>(null);
const portalNode = useRef<HTMLElement | null>(null);
const forceUpdate = useForceUpdate();
/**
* Portal from `@reach/portal`
*
* @see Docs https://reach.tech/portal#portal
*/
const PortalImpl: React.FC<PortalProps> = ({
children,
type = "reach-portal",
containerRef,
}) => {
const mountNode = useRef<HTMLDivElement | null>(null);
const portalNode = useRef<HTMLElement | null>(null);
const forceUpdate = useForceUpdate();
useIsomorphicLayoutEffect(() => {
// This ref may be null when a hot-loader replaces components on the page
if (!mountNode.current) {
return;
}
// It's possible that the content of the portal has, itself, been portaled.
// In that case, it's important to append to the correct document element.
const ownerDocument = mountNode.current.ownerDocument;
const body = containerRef?.current || ownerDocument.body;
portalNode.current = ownerDocument?.createElement(type);
body.appendChild(portalNode.current);
forceUpdate();
return () => {
if (portalNode.current && body) {
body.removeChild(portalNode.current);
}
};
}, [type, forceUpdate, containerRef]);
return portalNode.current ? (
createPortal(children, portalNode.current)
) : (
<span ref={mountNode} />
);
};
useIsomorphicLayoutEffect(() => {
// This ref may be null when a hot-loader replaces components on the page
if (!mountNode.current) {return;}
// It's possible that the content of the portal has, itself, been portaled.
// In that case, it's important to append to the correct document element.
const ownerDocument = mountNode.current.ownerDocument;
const body = containerRef?.current || ownerDocument.body;
portalNode.current = ownerDocument?.createElement(type);
body.appendChild(portalNode.current);
forceUpdate();
return () => {
if (portalNode.current && body) {
body.removeChild(portalNode.current);
}
};
}, [type, forceUpdate, containerRef]);
export const Portal: React.FC<PortalProps> = ({
unstable_skipInitialRender,
...props
}) => {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
if (unstable_skipInitialRender) {
setHydrated(true);
}
}, [unstable_skipInitialRender]);
if (unstable_skipInitialRender && !hydrated) {
return null;
}
return <PortalImpl {...props} />;
};
return portalNode.current ? (
createPortal(children, portalNode.current)
) : (
<span ref={mountNode} />
);
};
export const Portal: React.FC<PortalProps> = ({
unstable_skipInitialRender,
...props
}) => {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
if (unstable_skipInitialRender) {
setHydrated(true);
}
}, [unstable_skipInitialRender]);
if (unstable_skipInitialRender && !hydrated) {
return null;
}
return <PortalImpl {...props} />;
};
export type PortalProps = PropsWithChildren<{
type?: string;
containerRef?: React.RefObject<Node>;
unstable_skipInitialRender?: boolean;
}>;
Portal.displayName = "Portal";
export type PortalProps = PropsWithChildren<{
type?: string;
containerRef?: React.RefObject<Node>;
unstable_skipInitialRender?: boolean;
}>;
Portal.displayName = "Portal";
+21 -6
View File
@@ -21,7 +21,7 @@ import {
} from "@thirdweb-dev/sdk";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { Signer } from "ethers";
import { createContext, useContext, useEffect, useMemo } from "react";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import invariant from "tiny-invariant";
interface TWSDKContext {
@@ -60,21 +60,36 @@ export const WrappedThirdwebSDKProvider: ComponentWithChildren<
}) => {
const { signer } = useThirdwebConnectedWalletContext();
const sdk = useMemo(() => {
const [sdk, setSDK] = useState<ThirdwebSDK>();
useEffect(() => {
if (!desiredChainId || typeof window === "undefined") {
return undefined;
}
const _sdk = new ThirdwebSDK(provider, sdkOptions, storageInterface);
// if we already have a signer from the wallet context, use it immediately
if (signer) {
_sdk.updateSignerOrProvider(signer);
}
(_sdk as any)._constructedAt = Date.now();
(_sdk as any)._chainId = desiredChainId;
return _sdk;
setSDK(_sdk);
// explicitly *not* passing the signer, if we have it we use it if we don't we don't
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, sdkOptions, storageInterface, desiredChainId]);
useEffect(() => {
if (signer && sdk && (sdk as any)._chainId === desiredChainId) {
sdk.updateSignerOrProvider(signer);
if (sdk && (sdk as any)._chainId === desiredChainId) {
if (signer) {
sdk.updateSignerOrProvider(signer);
} else {
sdk.updateSignerOrProvider(provider);
}
}
}, [signer, sdk, desiredChainId]);
}, [signer, sdk, desiredChainId, provider]);
const ctxValue = useMemo(
() => ({
+9 -46
View File
@@ -1,8 +1,5 @@
import {
GnosisConnectorArguments,
GnosisSafeConnector,
} from "../connectors/gnosis-safe";
import { MagicConnector, MagicConnectorArguments } from "../connectors/magic";
import type { GnosisSafeConnector } from "../connectors/gnosis-safe";
import type { MagicConnector } from "../connectors/magic";
import {
Chain,
SupportedChain,
@@ -25,6 +22,7 @@ import {
ProviderProps as WagmiproviderProps,
useProvider,
useSigner,
Connector,
} from "wagmi";
import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet";
import { InjectedConnector } from "wagmi/connectors/injected";
@@ -56,26 +54,6 @@ export type WalletLinkConnectorType =
options: CoinbaseWalletConnector["options"];
};
/**
* @internal
*/
export type MagicConnectorType =
| "magic"
| {
name: "magic";
options: Omit<MagicConnectorArguments, "network">;
};
/**
* @internal
*/
export type GnosisConnectorType =
| "gnosis"
| {
name: "gnosis";
options: GnosisConnectorArguments;
};
/**
* @internal
*/
@@ -83,8 +61,8 @@ export type WalletConnector =
| InjectedConnectorType
| WalletConnectConnectorType
| WalletLinkConnectorType
| MagicConnectorType
| GnosisConnectorType;
| GnosisSafeConnector
| MagicConnector;
/**
* @internal
@@ -280,6 +258,9 @@ export const ThirdwebProvider = <
connectors: ({ chainId }: { chainId?: number }) => {
return walletConnectors
.map((connector) => {
if (connector instanceof Connector) {
return connector;
}
// injected connector
if (
(typeof connector === "string" &&
@@ -344,25 +325,7 @@ export const ThirdwebProvider = <
},
});
}
if (typeof connector === "object" && connector.name === "magic") {
const jsonRpcUrl = _rpcUrlMap[chainId || desiredChainId || 1];
return new MagicConnector({
chains: _supporrtedChains,
options: {
...connector.options,
network: { rpcUrl: jsonRpcUrl, chainId: desiredChainId || 1 },
rpcUrls: _rpcUrlMap,
},
});
}
if (
(typeof connector === "string" && connector === "gnosis") ||
(typeof connector === "object" && connector.name === "gnosis")
) {
return new GnosisSafeConnector({
chains: _supporrtedChains,
});
}
return null;
})
.filter((c) => c !== null);
+16
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">;
/**
@@ -212,6 +216,18 @@ export type ClaimNFTReturnType =
// MARKETPLACE //
export type MakeBidParams = { listingId: BigNumberish; bid: Price };
export type MakeOfferParams = {
listingId: BigNumberish;
pricePerToken: Price;
quantity?: Amount;
};
export type AcceptDirectOffer = {
listingId: BigNumberish;
addressOfOfferor: string;
};
export type ExecuteAuctionSale = {
listingId: BigNumberish;
};
export type BuyNowParams<TListingType = ListingType> =
TListingType extends ListingType.Direct
+7 -4
View File
@@ -3,6 +3,7 @@ import { SupportedChainId } from "../constants/chain";
import { ContractAddress, WalletAddress } from "../types";
import { QueryClient, QueryKey } from "@tanstack/react-query";
import type {
ClaimConditionFetchOptions,
MarketplaceFilter,
QueryAllParams,
SUPPORTED_CHAIN_ID,
@@ -267,22 +268,24 @@ export const cacheKeys = {
getActive: (
contractAddress: RequiredParam<ContractAddress>,
tokenId?: BigNumberish,
options?: ClaimConditionFetchOptions,
) =>
createContractCacheKey(
contractAddress,
tokenId
? ["claimConditions", "getActive", { tokenId }]
: ["claimConditions", "getActive"],
? ["claimConditions", "getActive", { tokenId }, options]
: ["claimConditions", "getActive", options],
),
getAll: (
contractAddress: RequiredParam<ContractAddress>,
tokenId?: BigNumberish,
options?: ClaimConditionFetchOptions,
) =>
createContractCacheKey(
contractAddress,
tokenId
? ["claimConditions", "getAll", { tokenId }]
: ["claimConditions", "getAll"],
? ["claimConditions", "getAll", { tokenId }, options]
: ["claimConditions", "getAll", options],
),
getClaimIneligibilityReasons: (
contractAddress: RequiredParam<ContractAddress>,
+1 -1
View File
@@ -1,4 +1,4 @@
// at the moment we'll re-export everything from the ethereum package (only!)
// at the moment we'll re-export everything from the ethereum package
export * from "./evm";
// core exports
@@ -30,7 +30,7 @@ export function nftRoyaltyQuery(
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { data: royalty, isLoading } = useRoyalty(program);
* const { data: royalty, isLoading } = useRoyaltySettings(program);
*
* return (
* <p>{royalty}</p>
@@ -40,6 +40,6 @@ export function nftRoyaltyQuery(
*
* @public
*/
export function useRoyalty(program: RequiredParam<NFTCollection | NFTDrop>) {
export function useRoyaltySettings(program: RequiredParam<NFTCollection | NFTDrop>) {
return useQuery(nftRoyaltyQuery(program));
}
@@ -0,0 +1,47 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { CreatorInput, NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
/**
* Update the creators for an NFT program
* @param program - The NFT program instance to update the creators for
*
* @example
* ```jsx
* import { useProgram, useUpdateCreators } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: updateCreators, isLoading, error } = useUpdateCreators(program);
*
* return (
* <button
* onClick={() => updateCreators([{ address: "0x...", share: 10 }])}
* >
* Update Creators
* </button>
* )
* }
* ```
*
* @public
*/
export function useUpdateCreators(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
const queryClient = useQueryClient();
return useMutation(
async (creators: CreatorInput[]) => {
requiredParamInvariant(program, "program is required");
return await program.updateCreators(creators);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
@@ -0,0 +1,47 @@
import { createSOLProgramQueryKey } from "../../../core/query-utils/query-key";
import {
requiredParamInvariant,
RequiredParam,
} from "../../../core/query-utils/required-param";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { NFTCollection, NFTDrop } from "@thirdweb-dev/sdk/solana";
/**
* Update the royalty for an NFT program
* @param program - The NFT program instance to update the royalty for
*
* @example
* ```jsx
* import { useProgram, useUpdateRoyaltySettings } from "@thirdweb-dev/react/solana";
*
* export default function Component() {
* const { program } = useProgram("{{program_address}}");
* const { mutateAsync: updateRoyalties, isLoading, error } = useUpdateRoyaltySettings(program);
*
* return (
* <button
* onClick={() => updateRoyalties(300)}
* >
* Update Royalties
* </button>
* )
* }
* ```
*
* @public
*/
export function useUpdateRoyaltySettings(
program: RequiredParam<NFTCollection | NFTDrop>,
) {
const queryClient = useQueryClient();
return useMutation(
async (sellerFeeBasisPoints: number) => {
requiredParamInvariant(program, "program is required");
return await program.updateRoyalty(sellerFeeBasisPoints);
},
{
onSettled: () =>
queryClient.invalidateQueries(createSOLProgramQueryKey(program)),
},
);
}
+4
View File
@@ -14,6 +14,10 @@ export * from "./hooks/program/useProgramAccountType";
export * from "./hooks/nft/useNFTs";
export * from "./hooks/nft/useTransferNFT";
export * from "./hooks/nft/useBurnNFT";
export * from "./hooks/nft/useRoyaltySettings";
export * from "./hooks/nft/useUpdateRoyaltySettings";
export * from "./hooks/nft/useCreators";
export * from "./hooks/nft/useUpdateCreators";
// NFT Collection
export * from "./hooks/nft/collection/useMintNFT";
+1 -1
View File
@@ -1,12 +1,12 @@
import { ThirdwebAuthConfig } from "../contexts/thirdweb-auth";
import { ThirdwebSDKProvider } from "./base";
import type { WalletAdapter } from "@solana/wallet-adapter-base";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-phantom";
import {
ConnectionProvider,
useWallet,
WalletProvider,
} from "@solana/wallet-adapter-react";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";
import { getUrlForNetwork, Network } from "@thirdweb-dev/sdk/solana";
import { PropsWithChildren } from "react";
+117
View File
@@ -1,5 +1,122 @@
# @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
- [#353](https://github.com/thirdweb-dev/js/pull/353) [`1c24c3c`](https://github.com/thirdweb-dev/js/commit/1c24c3c3c48476a824f817e09e7bf0fbe67c1db5) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Add useUpdateCreators hook, fix CreatorInput schema
- [#357](https://github.com/thirdweb-dev/js/pull/357) [`65fc7ba`](https://github.com/thirdweb-dev/js/commit/65fc7ba6fff093a971b53cb0979c39768e090ba1) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose claimConditions.getClaimerProofs(address) to get allowlist information for a given address
## 3.4.2
### Patch Changes
- [#344](https://github.com/thirdweb-dev/js/pull/344) [`21c21c1`](https://github.com/thirdweb-dev/js/commit/21c21c1adfd09b60ad563bfd4c308597140de33c) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Update internal function resolveContractUriFromAddress -> resolveContractUriFromAddress
- [#352](https://github.com/thirdweb-dev/js/pull/352) [`3522917`](https://github.com/thirdweb-dev/js/commit/352291791ee900e1500f84f095290497934d2f60) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose marketplace.auction.executeSale(listingId)
## 3.4.1
### Patch Changes
- [#343](https://github.com/thirdweb-dev/js/pull/343) [`72227b2`](https://github.com/thirdweb-dev/js/commit/72227b2e166a3a68bbb41cf2b389322f5b7547a2) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose general marketplace.makeOffer() function
## 3.4.0
### Patch Changes
- [#337](https://github.com/thirdweb-dev/js/pull/337) [`340605b`](https://github.com/thirdweb-dev/js/commit/340605b507f384fbd2999b9c16542af3c53e84a9) Thanks [@adam-maj](https://github.com/adam-maj)! - Clean native token address in listing
- [#342](https://github.com/thirdweb-dev/js/pull/342) [`26116a6`](https://github.com/thirdweb-dev/js/commit/26116a6f637ee845f7bd17f20ffe17caf184658e) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add new univeral RPC urls
- [#335](https://github.com/thirdweb-dev/js/pull/335) [`15f8006`](https://github.com/thirdweb-dev/js/commit/15f8006e1fb22333b7ee239b45e7b1b12d6dccc8) Thanks [@adam-maj](https://github.com/adam-maj)! - Export smart contract
- [#327](https://github.com/thirdweb-dev/js/pull/327) [`ef27aad`](https://github.com/thirdweb-dev/js/commit/ef27aad0aafc4577e85f44dc77dfbe880bd239b5) Thanks [@jnsdls](https://github.com/jnsdls)! - enable e2e testing
- Updated dependencies [[`ef27aad`](https://github.com/thirdweb-dev/js/commit/ef27aad0aafc4577e85f44dc77dfbe880bd239b5)]:
- @thirdweb-dev/storage@1.0.6
## 3.3.1
### Patch Changes
- [#332](https://github.com/thirdweb-dev/js/pull/332) [`ad851f7`](https://github.com/thirdweb-dev/js/commit/ad851f7b4d6abb5ab9364ef3dec229f913e18ce5) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix for finding addresses in legacy merkle tree format
- [#330](https://github.com/thirdweb-dev/js/pull/330) [`6218c88`](https://github.com/thirdweb-dev/js/commit/6218c885842e6b4e44ec38ec92a9211e56c49bf4) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix deploying contracts with non-32 bytes parameters like bytes4
## 3.3.0
### Minor Changes
- [#315](https://github.com/thirdweb-dev/js/pull/315) [`89ff921`](https://github.com/thirdweb-dev/js/commit/89ff921a7d9f0e42b4e6707c8b56d0aab95c1aa0) Thanks [@furqanrydhan](https://github.com/furqanrydhan)! - fixing vite, multihash moved to non lazy load + cbor-x instead of cbor-web
- [#321](https://github.com/thirdweb-dev/js/pull/321) [`a57b4f0`](https://github.com/thirdweb-dev/js/commit/a57b4f01f3a49590b897101f2730582e4124a554) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - [SDK] Implement sharded merkle trees for lightweight allowlist checks
**Behavior change**
We've made allowlists much more performant using sharded merkle trees. This allows us to process large allowlists (1M+) efficiently.
To support those large allowlists, fetching claim conditions does not fetch the allowlist data by default anymore. Instead, you can pass an options object to additionally fetch the allowlist data along with the rest of the claim conditions data.
This affects `ClaimConditions.getActive()` and `ClaimConditions.getAll()`
Examples:
```ts
const activeClaimCondition =
await contract.erc721.claimConditions.getActive();
// `activeClaimCondition.snapshot` is undefined
const activeclaimConditionWithtAllowList =
await contract.erc721.claimConditions.getActive({
withAllowList: true,
});
// `activeClaimCondition.snapshot` returns the allowlist data
```
### Patch Changes
- [#326](https://github.com/thirdweb-dev/js/pull/326) [`126752d`](https://github.com/thirdweb-dev/js/commit/126752d7d02c9a808a63fb9e67a9df6658b5682b) Thanks [@jnsdls](https://github.com/jnsdls)! - [EVM] - expose fn to get ipfs hash from bytecote publicly
- [#328](https://github.com/thirdweb-dev/js/pull/328) [`df74340`](https://github.com/thirdweb-dev/js/commit/df74340fa51323304c32419761d6f18628b060fa) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update cbor-x import to work with es2018 builds (embeds)
- Updated dependencies [[`87fd6ab`](https://github.com/thirdweb-dev/js/commit/87fd6ab14e1a67a1b12e72bd397fb21769537307)]:
- @thirdweb-dev/[email protected]
## 3.2.6
### Patch Changes
+9 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.2.6",
"version": "3.5.0",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
@@ -65,9 +65,8 @@
"push": "yalc push"
},
"devDependencies": {
"@gmrchk/cli-testing-library": "^0.1.2",
"@metaplex-foundation/amman": "^0.10.0",
"@metaplex-foundation/amman-client": "^0.2.1",
"@metaplex-foundation/amman": "0.10.0",
"@metaplex-foundation/amman-client": "0.2.1",
"@microsoft/api-documenter": "^7.19.4",
"@microsoft/api-extractor": "^7.29.2",
"@microsoft/tsdoc": "^0.14.1",
@@ -82,7 +81,6 @@
"@types/uuid": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"cbor": "^8.1.0",
"chai": "^4.3.6",
"deep-equal-in-any-order": "^1.1.18",
"eslint": "^8.21.0",
@@ -91,11 +89,9 @@
"eslint-plugin-inclusive-language": "^2.2.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-tsdoc": "^0.2.16",
"esm": "^3.2.25",
"ethers": "^5.7.0",
"hardhat": "^2.9.3",
"mocha": "^10.0.0",
"node-stdlib-browser": "^1.2.0",
"prettier": "^2.7.1",
"typescript": "^4.7.4"
},
@@ -103,22 +99,20 @@
"ethers": "^5"
},
"dependencies": {
"@metaplex-foundation/js": "^0.15.0",
"@metaplex-foundation/mpl-token-metadata": "^2.2.2",
"@project-serum/anchor": "^0.25.0",
"@solana/spl-token": "^0.2.0",
"@solana/web3.js": "^1.50.1",
"@metaplex-foundation/js": "0.15.0",
"@metaplex-foundation/mpl-token-metadata": "2.2.2",
"@project-serum/anchor": "0.25.0",
"@solana/spl-token": "0.2.0",
"@solana/web3.js": "1.62.0",
"@thirdweb-dev/contracts-js": "*",
"@thirdweb-dev/storage": "*",
"bn.js": "^5.2.1",
"bs58": "^5.0.0",
"cbor-web": "^8.1.0",
"cbor-x": "^1.4.1",
"cross-fetch": "^3.1.5",
"eventemitter3": "^4.0.7",
"fast-deep-equal": "^3.1.3",
"form-data": "^4.0.0",
"merkletreejs": "^0.2.24",
"multihashes": "^4.0.3",
"tiny-invariant": "^1.2.0",
"tweetnacl": "^1.0.3",
"uuid": "^9.0.0",
+6
View File
@@ -0,0 +1,6 @@
export const DEFAULT_API_KEY =
"c6634ad2d97b74baf15ff556016830c251050e6c36b9da508ce3ec80095d3dc1";
export function getRpcUrl(network: string) {
return `https://${network}.rpc.thirdweb.com/${DEFAULT_API_KEY}`;
}
+4 -2
View File
@@ -72,12 +72,12 @@ export const BigNumberTransformSchema = z
export const BasisPointsSchema = z
.number()
.max(MAX_BPS, "Cannot exeed 100%")
.max(MAX_BPS, "Cannot exceed 100%")
.min(0, "Cannot be below 0%");
export const PercentSchema = z
.number()
.max(100, "Cannot exeed 100%")
.max(100, "Cannot exceed 100%")
.min(0, "Cannot be below 0%");
export const HexColor = z.union([
@@ -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));
});
+324 -101
View File
@@ -1,12 +1,15 @@
import { Quantity } from "../../core/schema/shared";
import { NATIVE_TOKEN_ADDRESS } from "../constants";
import { ContractWrapper } from "../core/classes/contract-wrapper";
import {
AbstractClaimConditionContractStruct,
ClaimConditionInputArray,
ClaimConditionInputSchema,
ClaimConditionOutputSchema,
} from "../schema/contracts/common/claim-conditions";
import {
SnapshotInputSchema,
SnapshotEntry,
SnapshotEntryWithProof,
SnapshotSchema,
} from "../schema/contracts/common/snapshots";
import {
@@ -23,17 +26,22 @@ import {
isNativeToken,
normalizePriceValue,
} from "./currency";
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,
BigNumberish,
ethers,
CallOverrides,
constants,
ethers,
providers,
utils,
CallOverrides,
} from "ethers";
/**
@@ -49,31 +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 claims = await fetchSnapshot(
const snapshotEntry = await fetchSnapshotEntryForAddress(
addressToClaim,
activeClaimCondition.merkleRootHash.toString(),
await merkleMetadataFetcher(),
contractWrapper.getProvider(),
storage,
snapshotFormatVersion,
);
const item =
claims &&
claims.find(
(c) => c.address.toLowerCase() === addressToClaim.toLowerCase(),
);
if (item === undefined) {
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 = item.proof;
maxClaimable = ethers.utils.parseUnits(item.maxClaimable, tokenDecimals);
}
} catch (e) {
// have to handle the valid error case that we *do* want to throw on
@@ -88,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,
);
@@ -108,20 +151,12 @@ export async function prepareClaim(
return {
overrides,
proofs,
maxQuantityPerTransaction: maxClaimable,
price,
currencyAddress,
maxClaimable,
price: priceInProof,
currencyAddress: currencyAddressInProof,
};
}
type Snapshot =
| {
address: string;
proof: string[];
maxClaimable: string;
}[]
| undefined;
/**
* @internal
* @param merkleRoot
@@ -132,20 +167,67 @@ export async function fetchSnapshot(
merkleRoot: string,
merkleMetadata: Record<string, string> | undefined,
storage: ThirdwebStorage,
): Promise<Snapshot> {
): Promise<SnapshotEntry[] | null> {
if (!merkleMetadata) {
return undefined;
return null;
}
const snapshotUri = merkleMetadata[merkleRoot];
let snapshot: Snapshot = undefined;
if (snapshotUri) {
const raw = await storage.downloadJSON(snapshotUri);
const snapshotData = SnapshotSchema.parse(raw);
if (merkleRoot === snapshotData.merkleRoot) {
snapshot = snapshotData.claims;
if (raw.isShardedMerkleTree && raw.merkleRoot === merkleRoot) {
const smt = await ShardedMerkleTree.fromUri(snapshotUri, storage);
return smt?.getAllEntries() || null;
} else {
const snapshotData = SnapshotSchema.parse(raw);
if (merkleRoot === snapshotData.merkleRoot) {
return snapshotData.claims.map((claim) => ({
address: claim.address,
maxClaimable: claim.maxClaimable,
price: claim.price,
currencyAddress: claim.currencyAddress,
}));
}
}
}
return snapshot;
return null;
}
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;
}
const snapshotUri = merkleMetadata[merkleRoot];
if (snapshotUri) {
const raw = await storage.downloadJSON(snapshotUri);
if (raw.isShardedMerkleTree && raw.merkleRoot === merkleRoot) {
const merkleTree = await ShardedMerkleTree.fromShardedMerkleTreeInfo(
raw,
storage,
);
return await merkleTree.getProof(
address,
provider,
snapshotFormatVersion,
);
}
// legacy non-sharded, just fetch it all and filter out
const snapshotData = SnapshotSchema.parse(raw);
if (merkleRoot === snapshotData.merkleRoot) {
return (
snapshotData.claims.find(
(c) => c.address.toLowerCase() === address.toLowerCase(),
) || null
);
}
}
return null;
}
/**
@@ -203,7 +285,13 @@ export async function updateExistingClaimConditions(
/**
* Fetches the proof for the current signer for a particular wallet.
*
* @param addressToClaim
* @param merkleRoot - The merkle root of the condition to check.
* @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(
@@ -212,47 +300,59 @@ export async function getClaimerProofs(
tokenDecimals: number,
merkleMetadata: Record<string, string>,
storage: ThirdwebStorage,
): Promise<{ maxClaimable: BigNumber; proof: string[] }> {
const claims: Snapshot = await fetchSnapshot(
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 (claims === undefined) {
return {
proof: [],
maxClaimable: BigNumber.from(0),
};
}
const item = claims.find(
(c) => c.address.toLowerCase() === addressToClaim?.toLowerCase(),
);
if (item === undefined) {
return {
proof: [],
maxClaimable: BigNumber.from(0),
};
if (!claim) {
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: item.proof,
maxClaimable: ethers.utils.parseUnits(item.maxClaimable, tokenDecimals),
proof: claim.proof,
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(
@@ -260,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;
@@ -274,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 };
}
@@ -303,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,
};
}
@@ -348,24 +543,25 @@ 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,
storage: ThirdwebStorage,
shouldDownloadSnapshot: boolean,
): Promise<ClaimCondition> {
const cv = await fetchCurrencyValue(provider, pm.currency, pm.pricePerToken);
const claims = await fetchSnapshot(pm.merkleRoot, merkleMetadata, storage);
const maxClaimableSupply = convertToReadableQuantity(
pm.maxClaimableSupply,
tokenDecimals,
);
const quantityLimitPerTransaction = convertToReadableQuantity(
pm.quantityLimitPerTransaction,
const maxClaimablePerWallet = convertToReadableQuantity(
pm.maxClaimablePerWallet,
tokenDecimals,
);
const availableSupply = convertToReadableQuantity(
@@ -378,21 +574,32 @@ 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,
currencyMetadata: cv,
merkleRootHash: pm.merkleRoot,
snapshot: claims,
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 {
@@ -400,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,
+7
View File
@@ -26,6 +26,13 @@ export function isNativeToken(tokenAddress: string): boolean {
);
}
export function cleanCurrencyAddress(currencyAddress: string): string {
if (isNativeToken(currencyAddress)) {
return NATIVE_TOKEN_ADDRESS;
}
return currencyAddress;
}
export async function normalizePriceValue(
provider: providers.Provider,
inputPrice: Price,
@@ -22,6 +22,8 @@ import {
} from "../schema/contracts/custom";
import { ExtensionNotImplementedError } from "./error";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import bs58 from "bs58";
import { decode } from "cbor-x";
import { BaseContract, BigNumber, ethers } from "ethers";
import { z } from "zod";
@@ -282,9 +284,9 @@ export async function resolveContractUriFromAddress(
* @internal
* @param bytecode
*/
async function extractIPFSHashFromBytecode(
export function extractIPFSHashFromBytecode(
bytecode: string,
): Promise<string | undefined> {
): string | undefined {
const numericBytecode = hexToBytes(bytecode);
const cborLength: number =
numericBytecode[numericBytecode.length - 2] * 0x100 +
@@ -293,15 +295,13 @@ async function extractIPFSHashFromBytecode(
numericBytecode.slice(numericBytecode.length - 2 - cborLength, -2),
);
// load these lazily to avoid loading them when they are not needed
const [cbor, multiHashes] = await Promise.all([
import("cbor-web"),
import("multihashes"),
]);
const cborData = cbor.decodeFirstSync(bytecodeBuffer);
if (cborData["ipfs"]) {
return `ipfs://${multiHashes.toB58String(cborData["ipfs"])}`;
const cborData = decode(bytecodeBuffer);
if ("ipfs" in cborData && cborData["ipfs"]) {
try {
return `ipfs://${bs58.encode(cborData["ipfs"])}`;
} catch (e) {
console.warn("feature-detection ipfs cbor failed", e);
}
}
return undefined;
}
+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,
});
}
@@ -0,0 +1,295 @@
import {
ShardData,
ShardedMerkleTreeInfo,
ShardedSnapshot,
SnapshotEntry,
SnapshotEntryWithProof,
SnapshotEntryWithProofSchema,
SnapshotInputSchema,
} from "../schema/contracts/common/snapshots";
import { SnapshotInput } from "../types";
import { convertQuantityToBigNumber } from "./claim-conditions";
import { fetchCurrencyMetadata } from "./currency";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { ethers, utils } from "ethers";
import { MerkleTree } from "merkletreejs";
// shard using the first 2 hex character of the address
// this splits the merkle tree into 256 shards
// 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>;
private trees: Record<string, MerkleTree>;
private storage: ThirdwebStorage;
private baseUri: string;
private originalEntriesUri: string;
private tokenDecimals: number;
constructor(
storage: ThirdwebStorage,
baseUri: string,
originalEntriesUri: string,
shardNybbles: number,
tokenDecimals: number,
) {
this.storage = storage;
this.shardNybbles = shardNybbles;
this.baseUri = baseUri;
this.originalEntriesUri = originalEntriesUri;
this.tokenDecimals = tokenDecimals;
this.shards = {};
this.trees = {};
}
static async fromUri(
uri: string,
storage: ThirdwebStorage,
): Promise<ShardedMerkleTree | undefined> {
try {
const shardedMerkleTreeInfo =
await storage.downloadJSON<ShardedMerkleTreeInfo>(uri);
if (shardedMerkleTreeInfo.isShardedMerkleTree) {
return ShardedMerkleTree.fromShardedMerkleTreeInfo(
shardedMerkleTreeInfo,
storage,
);
}
} catch (e) {
return undefined;
}
}
static async fromShardedMerkleTreeInfo(
info: ShardedMerkleTreeInfo,
storage: ThirdwebStorage,
): Promise<ShardedMerkleTree> {
return new ShardedMerkleTree(
storage,
info.baseUri,
info.originalEntriesUri,
info.shardNybbles,
info.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);
// TODO Could also derive shardNybbles from input size
const shards: Record<string, SnapshotEntry[]> = {};
for (const snapshotEntry of inputs) {
const shard = snapshotEntry.address
.slice(2, 2 + shardNybbles)
.toLowerCase();
if (shards[shard] === undefined) {
shards[shard] = [];
}
shards[shard].push(snapshotEntry);
}
const currencyDecimalMap: Record<string, number> = {};
// create shard => subtree root map
const subTrees = await Promise.all(
Object.entries(shards).map(async ([shard, entries]) => [
shard,
new MerkleTree(
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,
{
sort: true,
},
).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,
});
const shardsToUpload = [];
for (const [shardId, entries] of Object.entries(shards)) {
const data: ShardData = {
proofs: tree
.getProof(roots[shardId])
.map((value) => "0x" + value.data.toString("hex")),
entries,
};
shardsToUpload.push({
data: JSON.stringify(data),
name: `${shardId}.json`,
});
}
const uris = await storage.uploadBatch(shardsToUpload);
const baseUri = uris[0].slice(0, uris[0].lastIndexOf("/"));
const originalEntriesUri = await storage.upload(inputs);
const shardedMerkleInfo: ShardedMerkleTreeInfo = {
merkleRoot: tree.getHexRoot(),
baseUri,
originalEntriesUri,
shardNybbles,
tokenDecimals,
isShardedMerkleTree: true,
};
const masterUri = await storage.upload(shardedMerkleInfo);
return {
shardedMerkleInfo,
uri: masterUri,
};
}
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`,
);
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;
}
}
const entry = shard.entries.find(
(i) => i.address.toLowerCase() === address.toLowerCase(),
);
if (!entry) {
return null;
}
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 SnapshotEntryWithProofSchema.parse({
...entry,
proof: proof.concat(shard.proofs),
});
}
public async getAllEntries(): Promise<SnapshotEntry[]> {
try {
return await this.storage.downloadJSON<SnapshotEntry[]>(
this.originalEntriesUri,
);
} catch (e) {
console.warn("Could not fetch original snapshot entries", e);
return [];
}
}
}
+18 -46
View File
@@ -1,27 +1,32 @@
import {
SnapshotInputSchema,
SnapshotSchema,
} from "../schema/contracts/common/snapshots";
import { SnapshotInputSchema } from "../schema";
import {
SnapshotInfo,
SnapshotInput,
} from "../types/claim-conditions/claim-conditions";
import { DuplicateLeafsError } from "./error";
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);
@@ -29,48 +34,15 @@ export async function createSnapshot(
if (hasDuplicates) {
throw new DuplicateLeafsError();
}
const hashedLeafs = input.map((i) =>
hashLeafNode(i.address, utils.parseUnits(i.maxClaimable, tokenDecimals)),
const tree = await ShardedMerkleTree.buildAndUpload(
input,
tokenDecimals,
provider,
storage,
snapshotFormatVersion,
);
// import dynamically to avoid bloating bundle size for non-merkle tree users
const MerkleTree = (await import("merkletreejs")).MerkleTree;
const tree = new MerkleTree(hashedLeafs, utils.keccak256, {
sort: true,
});
const snapshot = SnapshotSchema.parse({
merkleRoot: tree.getHexRoot(),
claims: input.map((i, index) => {
const proof = tree.getHexProof(hashedLeafs[index]);
return {
address: i.address,
maxClaimable: i.maxClaimable,
proof,
};
}),
});
const uri = await storage.upload(snapshot);
return {
merkleRoot: tree.getHexRoot(),
snapshotUri: uri,
snapshot,
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 -2
View File
@@ -24,13 +24,12 @@ export const NATIVE_TOKENS: Record<
symbol: "WETH",
},
},
[ChainId.Goerli]: {
name: "Görli Ether",
symbol: "GOR",
decimals: 18,
wrapped: {
address: "0x0bb7509324ce409f7bbc4b701f932eaca9736ab7",
address: "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6",
name: "Wrapped Ether",
symbol: "WETH",
},
@@ -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;
+24 -58
View File
@@ -1,3 +1,4 @@
import { getRpcUrl } from "../../core/constants/urls";
import { SignerOrProvider } from "../core/types";
import { StaticJsonRpcBatchProvider } from "../lib/static-batch-rpc";
import { ChainId, SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "./chains";
@@ -7,39 +8,16 @@ import { ethers, providers } from "ethers";
* @internal
*/
export const DEFAULT_IPFS_GATEWAY = "https://gateway.ipfscdn.io/ipfs/";
/**
* @internal
*/
export const PUBLIC_GATEWAYS = [
"https://gateway.ipfscdn.io/ipfs/",
"https://cloudflare-ipfs.com/ipfs/",
"https://ipfs.io/ipfs/",
];
/**
* @internal
*/
export const TW_IPFS_SERVER_URL = "https://upload.nftlabs.co";
/**
* @internal
*/
export const PINATA_IPFS_URL = `https://api.pinata.cloud/pinning/pinFileToIPFS`;
/**
* @internal
* This is a community API key that is subject to rate limiting. Please use your own key.
*/
const DEFAULT_API_KEY = "_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC";
type ChainNames =
| "mumbai"
| "polygon"
// common alias for `polygon`
| "matic"
| "goerli"
| "mainnet"
// common alias for `mainnet`
| "ethereum"
| "goerli"
| "polygon"
// common alias for `polygon`
| "matic"
| "mumbai"
| "fantom"
| "fantom-testnet"
| "avalanche"
@@ -105,49 +83,37 @@ export function getProviderForNetwork(network: ChainOrRpc | SignerOrProvider) {
return network;
}
switch (network) {
case "mumbai":
return `https://polygon-mumbai.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
case "rinkeby":
return `https://eth-rinkeby.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
case "goerli":
return `https://eth-goerli.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
case "polygon":
case "matic":
return `https://polygon-mainnet.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
case "mainnet":
case "ethereum":
return `https://eth-mainnet.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
return getRpcUrl("ethereum");
case "goerli":
return getRpcUrl("goerli");
case "polygon":
case "matic":
return getRpcUrl("polygon");
case "mumbai":
return getRpcUrl("mumbai");
case "optimism":
// TODO test this RPC
return `https://opt-mainnet.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
case "optimism-kovan":
// alchemy optimism kovan rpc doesn't link to the testnet sequencer...
return "https://kovan.optimism.io";
return getRpcUrl("optimism");
case "optimism-goerli":
// TODO test this RPC
return `https://opt-goerli.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
return getRpcUrl("optimism-goerli");
case "arbitrum":
// TODO test this RPC
return `https://arb-mainnet.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
case "arbitrum-rinkeby":
// TODO test this RPC
return `https://arb-rinkeby.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
return getRpcUrl("arbitrum");
case "arbitrum-goerli":
// TODO test this RPC
return `https://arb-goerli.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
return getRpcUrl("arbitrum-goerli");
case "fantom":
return "https://rpc.ftm.tools";
return getRpcUrl("fantom");
case "fantom-testnet":
return "https://rpc.testnet.fantom.network/";
return getRpcUrl("fantom-testnet");
case "avalanche":
return "https://api.avax.network/ext/bc/C/rpc";
return getRpcUrl("avalanche");
case "avalanche-testnet":
case "avalanche-fuji":
return "https://api.avax-test.network/ext/bc/C/rpc";
return getRpcUrl("avalanche-fuji");
case "binance":
return "https://bsc-dataseed1.binance.org";
return getRpcUrl("binance");
case "binance-testnet":
return "https://data-seed-prebsc-1-s1.binance.org:8545";
return getRpcUrl("binance-testnet");
default:
if (network.startsWith("http") || network.startsWith("ws")) {
return network;
+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,
@@ -1,7 +1,12 @@
import { DEFAULT_QUERY_ALL_COUNT } from "../../../core/schema/QueryParams";
import { ListingNotFoundError } from "../../common";
import { isNativeToken } from "../../common/currency";
import { mapOffer } from "../../common/marketplace";
import { getRoleHash } from "../../common/role";
import {
NATIVE_TOKENS,
SUPPORTED_CHAIN_ID,
} from "../../constants";
import { ContractEncoder } from "../../core/classes/contract-encoder";
import { ContractEvents } from "../../core/classes/contract-events";
import { ContractInterceptor } from "../../core/classes/contract-interceptor";
@@ -17,6 +22,7 @@ import { NetworkOrSignerOrProvider, TransactionResult } from "../../core/types";
import { ListingType } from "../../enums";
import { MarketplaceContractSchema } from "../../schema/contracts/marketplace";
import { SDKOptions } from "../../schema/sdk-options";
import { Price } from "../../types/currency";
import { AuctionListing, DirectListing, Offer } from "../../types/marketplace";
import { MarketplaceFilter } from "../../types/marketplace/MarketPlaceFilter";
import { UnmappedOffer } from "../../types/marketplace/UnmappedOffer";
@@ -324,22 +330,22 @@ export class Marketplace implements UpdateableNetwork {
// get only the events for this listing id
const listingEvents = events.filter((e) => e.data.listingId.eq(listingId));
// derive the offers from the events
const offers = await Promise.all(
return await Promise.all(
listingEvents.map(async (e): Promise<Offer> => {
const offer = await mapOffer(
return await mapOffer(
this.contractWrapper.getProvider(),
BigNumber.from(listingId),
{
quantityWanted: e.data.quantityWanted,
pricePerToken: e.data.totalOfferAmount.div(e.data.quantityWanted),
pricePerToken: e.data.quantityWanted.gt(0)
? e.data.totalOfferAmount.div(e.data.quantityWanted)
: e.data.totalOfferAmount,
currency: e.data.currency,
offeror: e.data.offeror,
} as UnmappedOffer,
);
return offer;
}),
);
return offers;
}
/** ******************************
@@ -391,6 +397,60 @@ export class Marketplace implements UpdateableNetwork {
}
}
/**
* Make an offer for a Direct or Auction Listing
*
* @remarks Make an offer on a direct or auction listing
*
* @example
* ```javascript
* // The listing ID of the asset you want to offer on
* const listingId = 0;
* // The price you are willing to offer per token
* const pricePerToken = 0.5;
* // The quantity of tokens you want to receive for this offer
* const quantity = 1;
*
* await contract.makeOffer(
* listingId,
* pricePerToken,
* quantity,
* );
* ```
*/
public async makeOffer(
listingId: BigNumberish,
pricePerToken: Price,
quantity?: BigNumberish,
): Promise<TransactionResult> {
const listing = await this.contractWrapper.readContract.listings(listingId);
if (listing.listingId.toString() !== listingId.toString()) {
throw new ListingNotFoundError(this.getAddress(), listingId.toString());
}
const chainId = await this.contractWrapper.getChainID();
switch (listing.listingType) {
case ListingType.Direct: {
invariant(
quantity,
"quantity is required when making an offer on a direct listing",
);
return await this.direct.makeOffer(
listingId,
quantity,
isNativeToken(listing.currency)
? NATIVE_TOKENS[chainId as SUPPORTED_CHAIN_ID].wrapped.address
: listing.currency,
pricePerToken,
);
}
case ListingType.Auction: {
return await this.auction.makeBid(listingId, pricePerToken);
}
default:
throw Error(`Unknown listing type: ${listing.listingType}`);
}
}
/**
* Set the Auction bid buffer
* @remarks A percentage (e.g. 5%) in basis points (5% = 500, 100% = 10000). A new bid is considered to be a winning bid only if its bid amount is at least the bid buffer (e.g. 5%) greater than the previous winning bid. This prevents buyers from making very slightly higher bids to win the auctioned items.
@@ -4,8 +4,6 @@ import {
} from "../../../core/schema/QueryParams";
import { NFT, NFTMetadata, NFTMetadataOrUri } from "../../../core/schema/nft";
import { getRoleHash } from "../../common";
import { prepareClaim } from "../../common/claim-conditions";
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";
@@ -28,22 +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 { ClaimVerification } from "../../types";
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.
@@ -59,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
@@ -95,7 +88,7 @@ export class NFTDrop extends StandardErc721<DropERC721> {
* ```
*/
public royalties: ContractRoyalty<
DropERC721,
PrebuiltNFTDrop,
typeof DropErc721ContractSchema
>;
/**
@@ -120,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.
@@ -152,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,
@@ -170,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,
@@ -197,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,
@@ -385,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);
}
/**
@@ -623,27 +579,6 @@ export class NFTDrop extends StandardErc721<DropERC721> {
/** ******************************
* PRIVATE FUNCTIONS
*******************************/
/**
* Returns proofs and the overrides required for the transaction.
*
* @returns - `overrides` and `proofs` as an object.
*/
private async prepareClaim(
quantity: BigNumberish,
checkERC20Allowance: boolean,
): Promise<ClaimVerification> {
return prepareClaim(
quantity,
await this.claimConditions.getActive(),
async () => (await this.metadata.get()).merkle,
0,
this.contractWrapper,
this.storage,
checkERC20Allowance,
);
}
/**
* @internal
*/
@@ -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);
}
/**
@@ -712,7 +725,7 @@ export class ContractDeployer extends RPCConnectionHandler {
ethers.utils.isHexString(constructorParamValues[index]),
`Could not parse bytes value. Expected valid hex string but got "${constructorParamValues[index]}".`,
);
return ethers.utils.toUtf8Bytes(constructorParamValues[index]);
return constructorParamValues[index];
}
if (p.startsWith("uint") || p.startsWith("int")) {
return BigNumber.from(constructorParamValues[index].toString());
@@ -195,13 +195,13 @@ export class ContractPublisher extends RPCConnectionHandler {
* @internal
* TODO clean this up (see method above, too)
*/
public async resolveReleasesFromAddress(address: string) {
public async resolveContractUriFromAddress(address: string) {
const contractUri = await resolveContractUriFromAddress(
address,
this.getProvider(),
);
invariant(contractUri, "Could not resolve contract URI from address");
return await this.resolvePublishMetadataFromCompilerMetadata(contractUri);
return contractUri;
}
/**
@@ -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,6 +1,12 @@
import { includesErrorMessage } from "../../common";
import {
abstractContractModelToLegacy,
abstractContractModelToNew,
convertQuantityToBigNumber,
fetchSnapshotEntryForAddress,
getClaimerProofs,
legacyContractModelToAbstract,
newContractModelToAbstract,
prepareClaim,
processClaimConditionInputs,
transformResultToClaimCondition,
@@ -11,31 +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 } 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";
@@ -46,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;
@@ -75,7 +96,9 @@ export class DropClaimConditions<
*
* @returns the claim condition metadata
*/
public async getActive(): Promise<ClaimCondition> {
public async getActive(
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition> {
const cc = await this.get();
const metadata = await this.metadata.get();
return await transformResultToClaimCondition(
@@ -84,16 +107,41 @@ export class DropClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle || {},
this.storage,
options?.withAllowList || false,
);
}
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");
}
@@ -104,8 +152,13 @@ export class DropClaimConditions<
*
* @returns the claim conditions metadata
*/
public async getAll(): Promise<ClaimCondition[]> {
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
public async getAll(
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition[]> {
if (
this.isLegacyMultiPhaseDrop(this.contractWrapper) ||
this.isNewMultiphaseDrop(this.contractWrapper)
) {
const claimCondition =
(await this.contractWrapper.readContract.claimCondition()) as {
currentStartId: BigNumber;
@@ -113,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();
@@ -129,11 +180,12 @@ export class DropClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle,
this.storage,
options?.withAllowList || false,
),
),
);
} else {
return [await this.getActive()];
return [await this.getActive(options)];
}
}
@@ -226,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);
}
}
}
@@ -332,6 +483,33 @@ export class DropClaimConditions<
return reasons;
}
/**
* Returns allow list information and merkle proofs for the given address.
* @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.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,
merkleRoot.toString(),
metadata.merkle,
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
} else {
return null;
}
}
/** ***************************************
* WRITE FUNCTIONS
*****************************************/
@@ -376,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: [],
@@ -404,6 +585,7 @@ export class DropClaimConditions<
await this.getTokenDecimals(),
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
const merkleInfo: { [key: string]: string } = {};
@@ -424,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",
@@ -444,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,
]),
);
@@ -517,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,6 +1,11 @@
import { includesErrorMessage } from "../../common";
import {
abstractContractModelToLegacy,
abstractContractModelToNew,
fetchSnapshotEntryForAddress,
getClaimerProofs,
legacyContractModelToAbstract,
newContractModelToAbstract,
prepareClaim,
processClaimConditionInputs,
transformResultToClaimCondition,
@@ -8,27 +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 {
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";
@@ -38,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;
@@ -63,7 +82,10 @@ export class DropErc1155ClaimConditions<
*
* @returns the claim condition metadata
*/
public async getActive(tokenId: BigNumberish): Promise<ClaimCondition> {
public async getActive(
tokenId: BigNumberish,
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition> {
const mc = await this.get(tokenId);
const metadata = await this.metadata.get();
return await transformResultToClaimCondition(
@@ -72,25 +94,51 @@ export class DropErc1155ClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle,
this.storage,
options?.withAllowList || false,
);
}
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");
}
@@ -101,8 +149,14 @@ export class DropErc1155ClaimConditions<
*
* @returns the claim conditions metadata
*/
public async getAll(tokenId: BigNumberish): Promise<ClaimCondition[]> {
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
public async getAll(
tokenId: BigNumberish,
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition[]> {
if (
this.isLegacyMultiPhaseDrop(this.contractWrapper) ||
this.isNewMultiphaseDrop(this.contractWrapper)
) {
const claimCondition =
(await this.contractWrapper.readContract.claimCondition(tokenId)) as {
currentStartId: BigNumber;
@@ -110,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(
@@ -128,11 +177,12 @@ export class DropErc1155ClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle,
this.storage,
options?.withAllowList || false,
),
),
);
} else {
return [await this.getActive(tokenId)];
return [await this.getActive(tokenId, options)];
}
}
@@ -221,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;
}
}
@@ -275,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,
@@ -286,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,
@@ -333,6 +473,35 @@ export class DropErc1155ClaimConditions<
return reasons;
}
/**
* Returns allow list information and merkle proofs for the given address.
* @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.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,
merkleRoot.toString(),
metadata.merkle,
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
} else {
return null;
}
}
/** ***************************************
* WRITE FUNCTIONS
*****************************************/
@@ -431,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: [],
@@ -459,6 +628,7 @@ export class DropErc1155ClaimConditions<
0,
this.contractWrapper.getProvider(),
this.storage,
this.getSnapshotFormatVersion(),
);
snapshotInfos.forEach((s) => {
@@ -510,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 {
@@ -572,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;
}
}

Some files were not shown because too many files have changed in this diff Show More