Compare commits

..
Author SHA1 Message Date
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
124 changed files with 7434 additions and 3408 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",
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/auth
## 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.23",
"exports": {
"./next": {
"module": "./next/dist/thirdweb-dev-auth-next.esm.js",
+39
View File
@@ -1,5 +1,44 @@
# thirdweb
## 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.9",
"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) {
+63
View File
@@ -1,5 +1,68 @@
# @thirdweb-dev/react
## 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.4.2",
"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,7 +364,7 @@ export function useResetClaimConditions(
return useMutation(
async () => {
const cleanConditions = (conditions: ClaimCondition[]) => {
const cleanConditions = async (conditions: ClaimCondition[]) => {
return conditions.map((c) => ({
...c,
price: c.currencyMetadata.displayValue,
@@ -364,24 +378,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,
@@ -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);
+12
View File
@@ -212,6 +212,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 { 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)),
},
);
}
+2
View File
@@ -14,6 +14,8 @@ 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";
// 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";
+75
View File
@@ -1,5 +1,80 @@
# @thirdweb-dev/sdk
## 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.4.2",
"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}`;
}
+69 -42
View File
@@ -6,6 +6,8 @@ import {
ClaimConditionOutputSchema,
} from "../schema/contracts/common/claim-conditions";
import {
SnapshotEntry,
SnapshotEntryWithProof,
SnapshotInputSchema,
SnapshotSchema,
} from "../schema/contracts/common/snapshots";
@@ -23,17 +25,18 @@ import {
isNativeToken,
normalizePriceValue,
} from "./currency";
import { ShardedMerkleTree } from "./sharded-merkle-tree";
import { createSnapshot } from "./snapshots";
import { IDropClaimCondition } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC20";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import {
BigNumber,
BigNumberish,
ethers,
CallOverrides,
constants,
ethers,
providers,
utils,
CallOverrides,
} from "ethers";
/**
@@ -59,21 +62,17 @@ export async function prepareClaim(
.toString()
.startsWith(constants.AddressZero)
) {
const claims = await fetchSnapshot(
const claim = await fetchSnapshotEntryForAddress(
addressToClaim,
activeClaimCondition.merkleRootHash.toString(),
await merkleMetadataFetcher(),
storage,
);
const item =
claims &&
claims.find(
(c) => c.address.toLowerCase() === addressToClaim.toLowerCase(),
);
if (item === undefined) {
if (!claim) {
throw new Error("No claim found for this address");
}
proofs = item.proof;
maxClaimable = ethers.utils.parseUnits(item.maxClaimable, tokenDecimals);
proofs = claim.proof;
maxClaimable = ethers.utils.parseUnits(claim.maxClaimable, tokenDecimals);
}
} catch (e) {
// have to handle the valid error case that we *do* want to throw on
@@ -114,14 +113,6 @@ export async function prepareClaim(
};
}
type Snapshot =
| {
address: string;
proof: string[];
maxClaimable: string;
}[]
| undefined;
/**
* @internal
* @param merkleRoot
@@ -132,20 +123,59 @@ 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,
}));
}
}
}
return snapshot;
return null;
}
export async function fetchSnapshotEntryForAddress(
address: string,
merkleRoot: string,
merkleMetadata: Record<string, string> | undefined,
storage: ThirdwebStorage,
): 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);
}
// 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 +233,11 @@ 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
* @returns - The proof for the current signer for the specified condition.
*/
export async function getClaimerProofs(
@@ -213,30 +247,21 @@ export async function getClaimerProofs(
merkleMetadata: Record<string, string>,
storage: ThirdwebStorage,
): Promise<{ maxClaimable: BigNumber; proof: string[] }> {
const claims: Snapshot = await fetchSnapshot(
const claim = await fetchSnapshotEntryForAddress(
addressToClaim,
merkleRoot,
merkleMetadata,
storage,
);
if (claims === undefined) {
return {
proof: [],
maxClaimable: BigNumber.from(0),
};
}
const item = claims.find(
(c) => c.address.toLowerCase() === addressToClaim?.toLowerCase(),
);
if (item === undefined) {
if (!claim) {
return {
proof: [],
maxClaimable: BigNumber.from(0),
};
}
return {
proof: item.proof,
maxClaimable: ethers.utils.parseUnits(item.maxClaimable, tokenDecimals),
proof: claim.proof,
maxClaimable: ethers.utils.parseUnits(claim.maxClaimable, tokenDecimals),
};
}
@@ -356,10 +381,10 @@ export async function transformResultToClaimCondition(
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,
@@ -388,7 +413,9 @@ export async function transformResultToClaimCondition(
currencyAddress: pm.currency,
currencyMetadata: cv,
merkleRootHash: pm.merkleRoot,
snapshot: claims,
snapshot: shouldDownloadSnapshot
? await fetchSnapshot(pm.merkleRoot, merkleMetadata, storage)
: undefined,
});
}
+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;
}
@@ -0,0 +1,216 @@
import {
ShardData,
ShardedMerkleTreeInfo,
ShardedSnapshot,
SnapshotEntry,
SnapshotEntryWithProof,
SnapshotInputSchema,
} from "../schema";
import { SnapshotInput } from "../types";
import { hashLeafNode } from "./snapshots";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { 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 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): string {
return hashLeafNode(
entry.address,
utils.parseUnits(entry.maxClaimable, tokenDecimals),
);
}
static async buildAndUpload(
snapshotInput: SnapshotInput,
tokenDecimals: number,
storage: ThirdwebStorage,
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);
}
// create shard => subtree root map
const roots = Object.fromEntries(
Object.entries(shards).map(([shard, entries]) => [
shard,
new MerkleTree(
entries.map((entry) =>
ShardedMerkleTree.hashEntry(entry, tokenDecimals),
),
utils.keccak256,
{
sort: true,
},
).getHexRoot(),
]),
);
// 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,
): Promise<SnapshotEntryWithProof | null> {
const shardId = address.slice(2, 2 + this.shardNybbles).toLowerCase();
let shard = this.shards[shardId];
if (shard === undefined) {
try {
shard = this.shards[shardId] =
await this.storage.downloadJSON<ShardData>(
`${this.baseUri}/${shardId}.json`,
);
this.trees[shardId] = new MerkleTree(
shard.entries.map((entry) =>
ShardedMerkleTree.hashEntry(entry, this.tokenDecimals),
),
utils.keccak256,
{ sort: true },
);
} 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 leaf = ShardedMerkleTree.hashEntry(entry, this.tokenDecimals);
const proof = this.trees[shardId]
.getProof(leaf)
.map((i) => "0x" + i.data.toString("hex"));
return {
address,
proof: proof.concat(shard.proofs),
maxClaimable: entry.maxClaimable,
};
}
public async getAllEntries(): Promise<SnapshotEntry[]> {
try {
const entries = await this.storage.downloadJSON<SnapshotEntry[]>(
this.originalEntriesUri,
);
return entries;
} catch (e) {
console.warn("Could not fetch original snapshot entries", e);
return [];
}
}
private computeAllShardIds(shardNybbles: number) {
const max = parseInt(`0x${"f".repeat(shardNybbles)}`, 16);
const allShards = [];
for (let i = 0; i <= max; i++) {
allShards.push(i.toString(16).padStart(shardNybbles, "0"));
}
return allShards;
}
}
+8 -29
View File
@@ -1,12 +1,10 @@
import {
SnapshotInputSchema,
SnapshotSchema,
} from "../schema/contracts/common/snapshots";
import { SnapshotInputSchema } from "../schema/contracts/common/snapshots";
import {
SnapshotInfo,
SnapshotInput,
} from "../types/claim-conditions/claim-conditions";
import { DuplicateLeafsError } from "./error";
import { ShardedMerkleTree } from "./sharded-merkle-tree";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, utils } from "ethers";
@@ -29,33 +27,14 @@ 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,
storage,
);
// 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,
};
}
+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",
},
+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;
@@ -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,7 +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";
@@ -31,7 +30,6 @@ import {
import { PaperCheckout } from "../../integrations/paper-xyz";
import { DropErc721ContractSchema } from "../../schema/contracts/drop-erc721";
import { SDKOptions } from "../../schema/sdk-options";
import { ClaimVerification } from "../../types";
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";
@@ -623,27 +621,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
*/
@@ -712,7 +712,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,6 +18,7 @@ import { AmountSchema } from "../../schema";
import {
Amount,
ClaimCondition,
ClaimConditionFetchOptions,
ClaimConditionInput,
ClaimVerification,
} from "../../types";
@@ -75,7 +76,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,6 +87,7 @@ export class DropClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle || {},
this.storage,
options?.withAllowList || false,
);
}
@@ -104,7 +108,9 @@ export class DropClaimConditions<
*
* @returns the claim conditions metadata
*/
public async getAll(): Promise<ClaimCondition[]> {
public async getAll(
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition[]> {
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
const claimCondition =
(await this.contractWrapper.readContract.claimCondition()) as {
@@ -129,11 +135,12 @@ export class DropClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle,
this.storage,
options?.withAllowList || false,
),
),
);
} else {
return [await this.getActive()];
return [await this.getActive(options)];
}
}
@@ -13,6 +13,7 @@ import { NATIVE_TOKEN_ADDRESS } from "../../constants/index";
import { ClaimEligibility } from "../../enums";
import {
ClaimCondition,
ClaimConditionFetchOptions,
ClaimConditionInput,
ClaimConditionsForToken,
ClaimVerification,
@@ -63,7 +64,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,6 +76,7 @@ export class DropErc1155ClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle,
this.storage,
options?.withAllowList || false,
);
}
@@ -101,7 +106,10 @@ export class DropErc1155ClaimConditions<
*
* @returns the claim conditions metadata
*/
public async getAll(tokenId: BigNumberish): Promise<ClaimCondition[]> {
public async getAll(
tokenId: BigNumberish,
options?: ClaimConditionFetchOptions,
): Promise<ClaimCondition[]> {
if (this.isMultiPhaseDropContract(this.contractWrapper)) {
const claimCondition =
(await this.contractWrapper.readContract.claimCondition(tokenId)) as {
@@ -128,6 +136,7 @@ export class DropErc1155ClaimConditions<
this.contractWrapper.getProvider(),
metadata.merkle,
this.storage,
options?.withAllowList || false,
),
),
);
@@ -159,6 +159,34 @@ export class Erc1155SignatureMintable implements DetectableFeature {
* Verify that a payload is correctly signed
* @param signedPayload - the payload to verify
* @twfeature ERC1155SignatureMintable
*
* @example
* ```javascript
* const nftMetadata = {
* name: "Cool NFT #1",
* description: "This is a cool NFT",
* image: fs.readFileSync("path/to/image.png"), // This can be an image url or file
* };
*
* const startTime = new Date();
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* metadata: nftMetadata, // The NFT to mint
* to: {{wallet_address}}, // Who will receive the NFT (or AddressZero for anyone)
* quantity: 2, // the quantity of NFTs to mint
* price: 0.5, // the price per NFT
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
* mintStartTime: startTime, // can mint anytime from now
* mintEndTime: endTime, // to 24h from now
* royaltyRecipient: "0x...", // custom royalty recipient for this NFT
* royaltyBps: 100, // custom royalty fees for this NFT (in bps)
* primarySaleRecipient: "0x...", // custom sale recipient for this NFT
* };
*
* const signedPayload = contract.erc1155.signature.generate(payload);
* // Now you can verify that the payload is valid
* const isValid = await contract.erc1155.signature.verify(signedPayload);
* ```
*/
public async verify(signedPayload: SignedPayload1155): Promise<boolean> {
const mintRequest = signedPayload.payload;
@@ -197,7 +225,7 @@ export class Erc1155SignatureMintable implements DetectableFeature {
* primarySaleRecipient: "0x...", // custom sale recipient for this NFT
* };
*
* const signedPayload = contract.erc1155.signature.generate(payload);
* const signedPayload = await contract.erc1155.signature.generate(payload);
* // now anyone can use these to mint the NFT using `contract.erc1155.signature.mint(signedPayload)`
* ```
* @param payloadToSign - the payload to sign
@@ -242,7 +270,7 @@ export class Erc1155SignatureMintable implements DetectableFeature {
* primarySaleRecipient: "0x...", // custom sale recipient for this NFT
* };
*
* const signedPayload = contract.erc1155.signature.generate(payload);
* const signedPayload = await contract.erc1155.signature.generateFromTokenId(payload);
* // now anyone can use these to mint the NFT using `contract.erc1155.signature.mint(signedPayload)`
* ```
* @param payloadToSign - the payload to sign
@@ -1,7 +1,15 @@
import { normalizePriceValue, setErc20Allowance } from "../../common/currency";
import { FEATURE_TOKEN_SIGNATURE_MINTABLE } from "../../constants/erc20-features";
import type { TokenInitializer } from "../../contracts";
import { FilledSignaturePayload20, MintRequest20, PayloadToSign20, PayloadWithUri20, Signature20PayloadInput, Signature20PayloadOutput, SignedPayload20 } from "../../schema/contracts/common/signature";
import {
FilledSignaturePayload20,
MintRequest20,
PayloadToSign20,
PayloadWithUri20,
Signature20PayloadInput,
Signature20PayloadOutput,
SignedPayload20,
} from "../../schema/contracts/common/signature";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult } from "../types";
import { ContractRoles } from "./contract-roles";
@@ -10,7 +18,6 @@ import type { ITokenERC20, TokenERC20 } from "@thirdweb-dev/contracts-js";
import { BigNumber, ethers } from "ethers";
import invariant from "tiny-invariant";
/**
* Enables generating ERC20 Tokens with rules and an associated signature, which can then be minted by anyone securely
* @public
@@ -110,6 +117,24 @@ export class Erc20SignatureMintable implements DetectableFeature {
* Verify that a payload is correctly signed
* @param signedPayload - the payload to verify
* @twfeature ERC20SignatureMintable
*
* ```javascript
* const startTime = new Date();
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* quantity: 4.2, // The quantity of tokens to be minted
* to: {{wallet_address}}, // Who will receive the tokens (or AddressZero for anyone)
* price: 0.5, // the price to pay for minting those tokens
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
* mintStartTime: startTime, // can mint anytime from now
* mintEndTime: endTime, // to 24h from now,
* primarySaleRecipient: "0x...", // custom sale recipient for this token mint
* };
*
* const signedPayload = await contract.erc20.signature.generate(payload);
* // Now you can verify if the signed payload is valid
* const isValid = await contract.erc20.signature.verify(signedPayload);
* ```
*/
public async verify(signedPayload: SignedPayload20): Promise<boolean> {
const mintRequest = signedPayload.payload;
@@ -139,7 +164,7 @@ export class Erc20SignatureMintable implements DetectableFeature {
* primarySaleRecipient: "0x...", // custom sale recipient for this token mint
* };
*
* const signedPayload = contract.erc20.signature.generate(payload);
* const signedPayload = await contract.erc20.signature.generate(payload);
* // now anyone can use these to mint the NFT using `contract.erc20.signature.mint(signedPayload)`
* ```
* @param mintRequest - the payload to sign
@@ -237,4 +262,4 @@ export class Erc20SignatureMintable implements DetectableFeature {
uid: mintRequest.uid,
} as ITokenERC20.MintRequestStructOutput;
}
}
}
@@ -177,6 +177,34 @@ export class Erc721WithQuantitySignatureMintable implements DetectableFeature {
* Verify that a payload is correctly signed
* @param signedPayload - the payload to verify
* @twfeature ERC721SignatureMint
*
* @example
* ```javascript
* const nftMetadata = {
* name: "Cool NFT #1",
* description: "This is a cool NFT",
* image: fs.readFileSync("path/to/image.png"), // This can be an image url or file
* };
*
* const startTime = new Date();
* const endTime = new Date(Date.now() + 60 * 60 * 24 * 1000);
* const payload = {
* metadata: nftMetadata, // The NFT to mint
* to: {{wallet_address}}, // Who will receive the NFT (or AddressZero for anyone)
* quantity: 2, // the quantity of NFTs to mint
* price: 0.5, // the price per NFT
* currencyAddress: NATIVE_TOKEN_ADDRESS, // the currency to pay with
* mintStartTime: startTime, // can mint anytime from now
* mintEndTime: endTime, // to 24h from now
* royaltyRecipient: "0x...", // custom royalty recipient for this NFT
* royaltyBps: 100, // custom royalty fees for this NFT (in bps)
* primarySaleRecipient: "0x...", // custom sale recipient for this NFT
* };
*
* const signedPayload = await contract.erc721.signature.generate(payload);
* // Now you can verify if the signed payload is valid
* const isValid = await contract.erc721.signature.verify(signedPayload);
* ```
*/
public async verify(
signedPayload: SignedPayload721WithQuantitySignature,
@@ -230,7 +258,7 @@ export class Erc721WithQuantitySignatureMintable implements DetectableFeature {
* primarySaleRecipient: "0x...", // custom sale recipient for this NFT
* };
*
* const signedPayload = contract.erc721.signature.generate(payload);
* const signedPayload = await contract.erc721.signature.generate(payload);
* // now anyone can use these to mint the NFT using `contract.erc721.signature.mint(signedPayload)`
* ```
* @param mintRequest - the payload to sign
@@ -5,6 +5,7 @@ import {
WrongListingTypeError,
} from "../../common";
import {
cleanCurrencyAddress,
fetchCurrencyMetadata,
fetchCurrencyValue,
normalizePriceValue,
@@ -25,9 +26,13 @@ import {
Offer,
} from "../../types/marketplace";
import { TransactionResult, TransactionResultWithId } from "../types";
import { ContractEncoder } from "./contract-encoder";
import { ContractWrapper } from "./contract-wrapper";
import type { IMarketplace, Marketplace } from "@thirdweb-dev/contracts-js";
import { ListingAddedEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/Marketplace";
import {
ListingAddedEvent,
Marketplace as MarketplaceContract,
} from "@thirdweb-dev/contracts-js/dist/declarations/src/Marketplace";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, ethers, constants } from "ethers";
import invariant from "tiny-invariant";
@@ -39,6 +44,7 @@ import invariant from "tiny-invariant";
export class MarketplaceAuction {
private contractWrapper: ContractWrapper<Marketplace>;
private storage: ThirdwebStorage;
public encoder: ContractEncoder<MarketplaceContract>;
constructor(
contractWrapper: ContractWrapper<Marketplace>,
@@ -46,6 +52,7 @@ export class MarketplaceAuction {
) {
this.contractWrapper = contractWrapper;
this.storage = storage;
this.encoder = new ContractEncoder<Marketplace>(contractWrapper);
}
getAddress(): string {
@@ -233,7 +240,9 @@ export class MarketplaceAuction {
assetContract: listing.assetContractAddress,
tokenId: listing.tokenId,
buyoutPricePerToken: normalizedPricePerToken,
currencyToAccept: listing.currencyContractAddress,
currencyToAccept: cleanCurrencyAddress(
listing.currencyContractAddress,
),
listingType: ListingType.Auction,
quantityToList: listing.quantity,
reservePricePerToken: normalizedReservePrice,
@@ -398,9 +407,9 @@ export class MarketplaceAuction {
}
/**
* Close the Auction
* Close the Auction for the buyer or the seller
*
* @remarks Closes the Auction and executes the sale.
* @remarks Closes the Auction and executes the sale for the buyer or the seller.
*
* @example
* ```javascript
@@ -439,6 +448,49 @@ export class MarketplaceAuction {
}
}
/**
* Execute the Auction Sale
*
* @remarks Closes the Auction and executes the sale for both parties.
*
* @example
* ```javascript
* // The listing ID of the auction listing you want to close
* const listingId = "0";
* await contract.auction.executeSale(listingId);
* ```
*
* @param listingId - the auction listing ud to close
*/
public async executeSale(listingId: BigNumberish) {
const listing = await this.validateListing(BigNumber.from(listingId));
try {
const winningBid = await this.getWinningBid(listingId);
invariant(winningBid, "No winning bid found");
const closeForSeller = this.encoder.encode("closeAuction", [
listingId,
listing.sellerAddress,
]);
const closeForBuyer = this.encoder.encode("closeAuction", [
listingId,
winningBid.buyerAddress,
]);
return await this.contractWrapper.multiCall([
closeForSeller,
closeForBuyer,
]);
} catch (err: any) {
if (err.message.includes("cannot close auction before it has ended")) {
throw new AuctionHasNotEndedError(
listingId.toString(),
listing.endTimeInEpochSeconds.toString(),
);
} else {
throw err;
}
}
}
/**
* Update an Auction listing with new metadata
* @param listing - the listing id to update
@@ -1,5 +1,6 @@
import { ListingNotFoundError, WrongListingTypeError } from "../../common";
import {
cleanCurrencyAddress,
fetchCurrencyValue,
isNativeToken,
normalizePriceValue,
@@ -188,7 +189,9 @@ export class MarketplaceDirect {
assetContract: listing.assetContractAddress,
tokenId: listing.tokenId,
buyoutPricePerToken: normalizedPricePerToken,
currencyToAccept: listing.currencyContractAddress,
currencyToAccept: cleanCurrencyAddress(
listing.currencyContractAddress,
),
listingType: ListingType.Direct,
quantityToList: listing.quantity,
reservePricePerToken: normalizedPricePerToken,
+1
View File
@@ -44,3 +44,4 @@ export type { Split } from "./contracts/prebuilt-implementations/split";
export type { Token } from "./contracts/prebuilt-implementations/token";
export type { TokenDrop } from "./contracts/prebuilt-implementations/token-drop";
export type { Vote } from "./contracts/prebuilt-implementations/vote";
export type { SmartContract } from "./contracts/smart-contract";
@@ -23,7 +23,7 @@ export const ClaimConditionInputSchema = z.object({
quantityLimitPerTransaction: QuantitySchema,
waitInSeconds: BigNumberishSchema.default(0),
merkleRootHash: BytesLikeSchema.default(utils.hexZeroPad([0], 32)),
snapshot: z.optional(SnapshotInputSchema),
snapshot: z.optional(SnapshotInputSchema).nullable(),
});
/**
@@ -53,4 +53,5 @@ export const ClaimConditionOutputSchema = ClaimConditionInputSchema.extend({
price: BigNumberSchema,
waitInSeconds: BigNumberSchema,
startTime: BigNumberSchema.transform((n) => new Date(n.toNumber() * 1000)),
snapshot: SnapshotInputSchema.optional().nullable(),
});
@@ -11,25 +11,47 @@ export const MerkleSchema = z.object({
/**
* @internal
*/
export const SnapshotAddressInput = z.object({
export const SnapshotEntryInput = z.object({
address: AddressSchema,
maxClaimable: AmountSchema.default(0),
});
export type SnapshotEntry = z.output<typeof SnapshotEntryInput>;
export type ShardData = {
proofs: string[];
entries: SnapshotEntry[];
};
export type ShardedMerkleTreeInfo = {
merkleRoot: string;
baseUri: string;
originalEntriesUri: string;
shardNybbles: number;
tokenDecimals: number;
isShardedMerkleTree: true;
};
export type ShardedSnapshot = {
shardedMerkleInfo: ShardedMerkleTreeInfo;
uri: string;
};
/**
* @internal
*/
export const SnapshotInputSchema = z.union([
z.array(z.string()).transform((strings) =>
strings.map((address) =>
SnapshotAddressInput.parse({
SnapshotEntryInput.parse({
address,
}),
),
),
z.array(SnapshotAddressInput),
z.array(SnapshotEntryInput),
]);
const SnapshotEntryWithProofSchema = SnapshotEntryInput.extend({
proof: z.array(z.string()),
});
/**
* @internal
*/
@@ -38,12 +60,14 @@ export const SnapshotSchema = z.object({
* The merkle root
*/
merkleRoot: z.string(),
claims: z.array(
SnapshotAddressInput.extend({
proof: z.array(z.string()),
}),
),
claims: z.array(SnapshotEntryWithProofSchema),
});
/**
* @internal
*/
export type SnapshotEntryWithProof = z.output<
typeof SnapshotEntryWithProofSchema
>;
/**
* @internal
@@ -51,5 +75,4 @@ export const SnapshotSchema = z.object({
export const SnapshotInfoSchema = z.object({
merkleRoot: z.string(),
snapshotUri: z.string(),
snapshot: SnapshotSchema,
});
@@ -77,3 +77,7 @@ export type ClaimOptions = {
*/
checkERC20Allowance?: boolean;
};
export type ClaimConditionFetchOptions = {
withAllowList?: boolean;
};
-21
View File
@@ -1,21 +0,0 @@
/**
* @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`;
+3 -8
View File
@@ -1,13 +1,8 @@
import { getRpcUrl } from "../../core/constants/urls";
import { Network } from "../types/index";
import { Metaplex } from "@metaplex-foundation/js";
import { Cluster, clusterApiUrl } from "@solana/web3.js";
/**
* @internal
* This is a community API key that is subject to rate limiting. Please use your own key.
*/
const DEFAULT_API_KEY = "_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC";
/**
* @internal
*/
@@ -18,9 +13,9 @@ export function getUrlForNetwork(network: Network) {
case "testnet":
return clusterApiUrl(network as Cluster);
case "mainnet-beta":
return `https://solana-mainnet.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
return getRpcUrl("solana");
case "devnet":
return `https://solana-devnet.g.alchemy.com/v2/${DEFAULT_API_KEY}`;
return getRpcUrl("solana-devnet");
default:
return network;
}
+3 -1
View File
@@ -639,7 +639,9 @@ describe("Edition Drop Contract", async () => {
snapshot: [samWallet.address],
},
]);
const conditions = await bdContract.claimConditions.getAll(0);
const conditions = await bdContract.claimConditions.getAll(0, {
withAllowList: true,
});
assert.lengthOf(conditions, 1);
invariant(conditions[0].snapshot);
expect(conditions[0].snapshot[0].address).to.eq(samWallet.address);
+35 -49
View File
@@ -200,7 +200,24 @@ describe("Marketplace Contract", async () => {
});
const id = tx.id;
sdk.updateSignerOrProvider(samWallet);
await marketplaceContract.auction.makeBid(id, 0.1);
await marketplaceContract.makeOffer(id, 0.1);
});
it("Should list with native currency address", async () => {
const { id: listingId } = await marketplaceContract.direct.createListing({
assetContractAddress: dummyNftContract.getAddress(),
buyoutPricePerToken: 0.1,
currencyContractAddress: "0x0000000000000000000000000000000000000000",
startTimestamp: new Date(0), // start date can be in the past
listingDurationInSeconds: 60 * 60 * 24,
tokenId: 0,
quantity: 1,
});
const listing = await marketplaceContract.getListing(listingId);
expect(listing.currencyContractAddress.toLowerCase()).to.equal(
NATIVE_TOKEN_ADDRESS.toLowerCase(),
);
});
it("should list direct listings with 1155s", async () => {
@@ -412,13 +429,7 @@ describe("Marketplace Contract", async () => {
"The buyer should start with no tokens",
);
await marketplaceContract.direct.makeOffer(
directListingId,
10,
tokenAddress,
0.034,
new Date(Date.now() + 60 * 60 * 24 * 10 * 1000),
);
await marketplaceContract.makeOffer(directListingId, 0.034, 10);
await sdk.updateSignerOrProvider(adminWallet);
await marketplaceContract.direct.acceptOffer(
@@ -465,12 +476,7 @@ describe("Marketplace Contract", async () => {
"0",
"The buyer should start with no tokens",
);
await marketplaceContract.direct.makeOffer(
directListingId,
1,
customTokenContract.getAddress(),
0.05,
);
await marketplaceContract.makeOffer(directListingId, 0.05, 1);
await marketplaceContract.buyoutListing(directListingId, 1);
const balance = await dummyNftContract.balanceOf(bobWallet.address);
assert.equal(
@@ -500,12 +506,7 @@ describe("Marketplace Contract", async () => {
it("should allow offers to be made on direct listings", async () => {
sdk.updateSignerOrProvider(bobWallet);
await marketplaceContract.direct.makeOffer(
directListingId,
1,
tokenAddress,
"1",
);
await marketplaceContract.makeOffer(directListingId, 1, "1");
const offer = (await marketplaceContract.direct.getActiveOffer(
directListingId,
@@ -520,12 +521,7 @@ describe("Marketplace Contract", async () => {
assert.equal(offer.listingId.toString(), directListingId.toString());
sdk.updateSignerOrProvider(samWallet);
await marketplaceContract.direct.makeOffer(
directListingId,
1,
tokenAddress,
"1",
);
await marketplaceContract.makeOffer(directListingId, 1, "1");
const secondOffer = (await marketplaceContract.direct.getActiveOffer(
directListingId,
@@ -550,8 +546,8 @@ describe("Marketplace Contract", async () => {
it("should allow bids by the same person", async () => {
await sdk.updateSignerOrProvider(bobWallet);
await marketplaceContract.auction.makeBid(auctionListingId, 0.06);
await marketplaceContract.auction.makeBid(auctionListingId, 0.08);
await marketplaceContract.makeOffer(auctionListingId, 0.06);
await marketplaceContract.makeOffer(auctionListingId, 0.08);
const winningBid = (await marketplaceContract.auction.getWinningBid(
auctionListingId,
@@ -566,7 +562,7 @@ describe("Marketplace Contract", async () => {
it("should allow bids to be made on auction listings", async () => {
await sdk.updateSignerOrProvider(bobWallet);
await marketplaceContract.auction.makeBid(auctionListingId, 0.06);
await marketplaceContract.makeOffer(auctionListingId, 0.06);
let winningBid = (await marketplaceContract.auction.getWinningBid(
auctionListingId,
@@ -584,7 +580,7 @@ describe("Marketplace Contract", async () => {
// Make a higher winning bid
await sdk.updateSignerOrProvider(samWallet);
await marketplaceContract.auction.makeBid(auctionListingId, 0.09);
await marketplaceContract.makeOffer(auctionListingId, 0.09);
winningBid = (await marketplaceContract.auction.getWinningBid(
auctionListingId,
@@ -603,21 +599,11 @@ describe("Marketplace Contract", async () => {
it("should return all offers for a listing when queried", async () => {
// make an offer as bob
sdk.updateSignerOrProvider(bobWallet);
await marketplaceContract.direct.makeOffer(
directListingId,
1,
tokenAddress,
"0.5",
);
await marketplaceContract.makeOffer(directListingId, "0.5", 1);
// make an offer as sam
sdk.updateSignerOrProvider(samWallet);
await marketplaceContract.direct.makeOffer(
directListingId,
1,
tokenAddress,
"1",
);
await marketplaceContract.makeOffer(directListingId, "1", 1);
// fetch all offers for the listing
sdk.updateSignerOrProvider(adminWallet);
@@ -708,7 +694,7 @@ describe("Marketplace Contract", async () => {
"0",
"The buyer should start with no tokens",
);
await marketplaceContract.auction.makeBid(auctionListingId, "20");
await marketplaceContract.makeOffer(auctionListingId, "20");
const balance = await dummyNftContract.balanceOf(bobWallet.address);
assert.equal(
@@ -731,7 +717,7 @@ describe("Marketplace Contract", async () => {
"0",
"The buyer should start with no tokens",
);
await marketplaceContract.auction.makeBid(auctionListingId, "2");
await marketplaceContract.makeOffer(auctionListingId, "2");
const winningBid = (await marketplaceContract.auction.getWinningBid(
auctionListingId,
@@ -765,9 +751,9 @@ describe("Marketplace Contract", async () => {
"0",
"The buyer should start with no tokens",
);
await marketplaceContract.auction.makeBid(auctionListingId, "2");
await marketplaceContract.makeOffer(auctionListingId, "2");
try {
await marketplaceContract.auction.makeBid(auctionListingId, "2.01");
await marketplaceContract.makeOffer(auctionListingId, "2.01");
// eslint-disable-next-line no-empty
} catch (err) {}
});
@@ -843,7 +829,7 @@ describe("Marketplace Contract", async () => {
});
it("should throw an error when trying to close an auction that already started (with bids)", async () => {
await marketplaceContract.auction.makeBid(auctionListingId, 0.06);
await marketplaceContract.makeOffer(auctionListingId, 0.06);
try {
await marketplaceContract.auction.cancelListing(auctionListingId);
assert.fail("should have thrown an error");
@@ -916,7 +902,7 @@ describe("Marketplace Contract", async () => {
await sdk.updateSignerOrProvider(bobWallet);
await marketplaceContract.auction.makeBid(listingId, 2);
await marketplaceContract.makeOffer(listingId, 2);
await fastForwardTime(60 * 60 * 24);
@@ -1168,7 +1154,7 @@ describe("Marketplace Contract", async () => {
// place a bid
sdk.updateSignerOrProvider(samWallet);
await marketplaceContract.auction.makeBid(listingId, 0.06);
await marketplaceContract.makeOffer(listingId, 0.06);
const minimumNextBid =
await marketplaceContract.auction.getMinimumNextBid(listingId);
+8 -3
View File
@@ -6,6 +6,7 @@ import {
NFTDrop,
TokenInitializer,
} from "../../src/evm";
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { expectError, sdk, signers, storage } from "./before-setup";
import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
@@ -109,7 +110,9 @@ describe("NFT Drop Contract", async () => {
snapshot: [samWallet.address],
},
]);
const conditions = await dropContract.claimConditions.getAll();
const conditions = await dropContract.claimConditions.getAll({
withAllowList: true,
});
assert.lengthOf(conditions, 1);
invariant(conditions[0].snapshot);
expect(conditions[0].snapshot[0].address).to.eq(samWallet.address);
@@ -386,9 +389,11 @@ describe("NFT Drop Contract", async () => {
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
);
const actualProof = snapshot.snapshot.claims.find(
(c) => c.address === leaf,
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(leaf);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
+10
View File
@@ -477,4 +477,14 @@ describe("Publishing", async () => {
});
expect(tx).to.not.eq(undefined);
});
it("bytes4 test", async () => {
const ipfsUri = "ipfs://QmTTdsRTtxQXHfUwCGK6epXhvuEwjBwLFcCjGUYDRUQc95";
const addr = await sdk.deployer.deployContractFromUri(ipfsUri, [
"test",
"tt",
"0x01551220", // bytes4 param
]);
expect(addr).to.not.eq(undefined);
});
});
+8 -4
View File
@@ -9,6 +9,7 @@ import {
Token,
TokenInitializer,
} from "../../src/evm";
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { expectError, sdk, signers, storage } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { assert, expect } from "chai";
@@ -481,8 +482,9 @@ describe("Signature drop tests", async () => {
snapshot: [samWallet.address],
},
]);
const conditions =
await signatureDropContract.claimConditions.getActive();
const conditions = await signatureDropContract.claimConditions.getActive({
withAllowList: true,
});
invariant(conditions.snapshot);
expect(conditions.snapshot[0].address).to.eq(samWallet.address);
});
@@ -749,9 +751,11 @@ describe("Signature drop tests", async () => {
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
);
const actualProof = snapshot.snapshot.claims.find(
(c) => c.address === leaf,
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(leaf);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
+16 -34
View File
@@ -1,6 +1,8 @@
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { createSnapshot, Snapshot } from "../../src/evm/index";
import { MockStorage } from "./mock/MockStorage";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber } from "ethers";
const chai = require("chai");
const deepEqualInAnyOrder = require("deep-equal-in-any-order");
@@ -12,8 +14,6 @@ const { expect, assert } = chai;
global.fetch = require("cross-fetch");
describe("Snapshots", async () => {
let snapshot: Snapshot;
let uri: string;
let merkleRoot: string;
const leafs = [
@@ -23,9 +23,9 @@ describe("Snapshots", async () => {
"0x14fb3a9B317612ddc6d6Cc3c907CD9F2Aa091eE7",
];
const input = leafs.map((address) => ({
const input = leafs.map((address, i) => ({
address,
maxClaimable: 0,
maxClaimable: BigNumber.from(i + 1).toString(),
}));
let storage: ThirdwebStorage;
@@ -36,22 +36,30 @@ describe("Snapshots", async () => {
beforeEach(async () => {
const result = await createSnapshot(input, 0, storage);
snapshot = result.snapshot;
uri = result.snapshotUri;
merkleRoot = result.merkleRoot;
});
it("should shard merkle tree", async () => {
const result = await ShardedMerkleTree.buildAndUpload(input, 0, storage);
const sm = await ShardedMerkleTree.fromUri(result.uri, storage);
const proofs = await sm?.getProof(
"0xE79ee09bD47F4F5381dbbACaCff2040f2FbC5803",
);
expect(proofs?.maxClaimable).to.equal("1");
expect(proofs?.proof).length(2);
});
it("should generate a valid merkle root from a list of addresses", async () => {
assert.equal(
merkleRoot,
"0xe0c95ec2a9cc03bb25cdf2f3c9092a00698716373e4e34715498a68167fe4acd",
"0x8ef5aa32e8d987fe1e5b93155939ebfbb897746f476f0a97ad5354aa5317c751",
);
});
it("should warn about duplicate leafs", async () => {
const duplicateLeafs = input.concat({
address: "0xE79ee09bD47F4F5381dbbACaCff2040f2FbC5803",
maxClaimable: 0,
maxClaimable: "0",
});
try {
@@ -65,30 +73,4 @@ describe("Snapshots", async () => {
"should not reach this point, exception should have been thrown",
);
});
it("should contain the same number of claims as there are leafs", () => {
assert.lengthOf(snapshot.claims, leafs.length);
});
it("should contain a proof for every claim", () => {
assert.lengthOf(snapshot.claims, leafs.length);
snapshot.claims.forEach((claim) => {
assert.isNotEmpty(claim.proof);
});
});
it("should contain a claim for each leaf", () => {
leafs.forEach((leaf) => {
assert.notEqual(
snapshot.claims.find((c) => c.address === leaf),
undefined,
);
});
});
it("should upload the snapshot to storage", async () => {
const rawSnapshotJson = await storage.downloadJSON(uri);
expect(rawSnapshotJson).to.deep.equalInAnyOrder(snapshot);
});
});
+8 -3
View File
@@ -6,6 +6,7 @@ import {
TokenDropInitializer,
TokenInitializer,
} from "../../src/evm";
import { ShardedMerkleTree } from "../../src/evm/common/sharded-merkle-tree";
import { expectError, sdk, signers, storage } from "./before-setup";
import { AddressZero } from "@ethersproject/constants";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
@@ -81,7 +82,9 @@ describe("Token Drop Contract", async () => {
snapshot: [samWallet.address],
},
]);
const conditions = await dropContract.claimConditions.getAll();
const conditions = await dropContract.claimConditions.getAll({
withAllowList: true,
});
assert.lengthOf(conditions, 1);
invariant(conditions[0].snapshot);
expect(conditions[0].snapshot[0].address).to.eq(samWallet.address);
@@ -268,9 +271,11 @@ describe("Token Drop Contract", async () => {
ethers.utils.solidityKeccak256(["address", "uint256"], [leaf, 0]),
);
const actualProof = snapshot.snapshot.claims.find(
(c) => c.address === leaf,
const smt = await ShardedMerkleTree.fromUri(
snapshot.snapshotUri,
storage,
);
const actualProof = await smt?.getProof(leaf);
assert.isDefined(actualProof);
expect(actualProof?.proof).to.include.ordered.members(expectedProof);
-4
View File
@@ -1,4 +0,0 @@
declare module "cbor-web" {
import { decodeFirstSync } from "cbor";
export { decodeFirstSync };
}
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/storage
## 1.0.6
### 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
## 1.0.5
### Patch Changes
- [#324](https://github.com/thirdweb-dev/js/pull/324) [`87fd6ab`](https://github.com/thirdweb-dev/js/commit/87fd6ab14e1a67a1b12e72bd397fb21769537307) Thanks [@adam-maj](https://github.com/adam-maj)! - Update storage download
## 1.0.4
### Patch Changes

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