Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
9919026f38 Version Packages (#554)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-02-08 13:04:43 -08:00
YashandGitHub c349f44d58 [SDK] Fix tests marketplace-v3 (#557) 2023-02-08 11:21:56 -08:00
Joaquim VergesandGitHub fc7c6b6211 [SDK] Extract util test functions (#556) 2023-02-08 10:13:10 -08:00
Joaquim VergesandGitHub 55b00eff26 [SDK] Add gas estimators to marketplace plugins (#555) 2023-02-07 16:22:55 -08:00
Jonas DanielsandGitHub 76d219e6be [contracts-js, storage] - force release (#553) 2023-02-07 20:00:00 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
f13408a33d Version Packages (#552)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-02-07 11:30:03 -08:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
74b19012c2 Version Packages (#543)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-02-06 21:02:00 -08:00
Joaquim VergesandGitHub 8f85868324 Fix contracts-js turbo caching issue (#551) 2023-02-07 01:14:11 +00:00
Adam MajmudarandGitHub 448b357ed4 [SDK] Add solidity stack traces to error messages (#550) 2023-02-06 19:21:17 -05:00
YashandGitHub 912738bd0a [CLI] Add staking bases (#549) 2023-02-06 12:01:22 -08:00
f1aecc3049 [SDK] Contracts version update - Marketplace V3 (#544)
Co-authored-by: Joaquim Verges <[email protected]>
2023-02-06 05:40:23 +00:00
Adam MajmudarandGitHub 0b38dac852 [SDK] Add proper error catching for when user has no funds (#547) 2023-02-05 21:28:44 -08:00
Nacho IacovinoandGitHub 440a4ade95 [React] Add more marketplace v3 hooks (#546) 2023-02-04 04:01:35 +00:00
Adam MajmudarandGitHub 29d0c62610 [SDK] Add error info fields to TransactionError (#545) 2023-02-04 03:09:34 +00:00
Adam MajmudarandGitHub f295ec67b4 [SDK] Upgraded error messages on failed transactions (#539) 2023-02-04 00:35:10 +00:00
1686fb4b2c [SDK] marketplace v3 integration (#474)
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: Nacho Iacovino <[email protected]>
Co-authored-by: Nacho Iacovino <[email protected]>
Co-authored-by: Jonas Daniels <[email protected]>
2023-02-03 13:19:09 -08:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
85dd18e8f0 Version Packages (#541)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-02-03 10:53:16 -08:00
Joaquim VergesandGitHub ec1f2324c7 [SDK] Solana - Expose getTransaction pagination options + transactions for given wallet (#542) 2023-02-03 10:39:40 -08:00
b59a31c365 Add support for rendering 3D NFTs (#470)
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: Jarrod Watts <[email protected]>
Co-authored-by: Nacho Iacovino <[email protected]>
Co-authored-by: Nacho Iacovino <[email protected]>
2023-02-03 16:38:59 +01:00
Joaquim VergesandGitHub d29f7e78f0 [SDK] expose getMintTransaction() for fine grain control on mint calls (#540) 2023-02-02 19:32:29 -08:00
github-actions[bot]andGitHub 3e2b392b6b Version Packages (#538) 2023-02-02 09:21:36 -08:00
Adam MajmudarandGitHub cf96333fa2 Fix cache error on mkdir (#537) 2023-02-02 17:12:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4213cfee77 Bump http-cache-semantics from 4.1.0 to 4.1.1 (#536)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-02 00:26:26 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
25ef4be50f Version Packages (#534)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-02-01 16:16:07 -08:00
ab1f8bb6f6 [CLI] Modify auto-update process with flag and caching (#528)
Co-authored-by: Jonas Daniels <[email protected]>
2023-02-02 00:04:50 +00:00
Jonas DanielsandGitHub 5f8ec45af7 [SDK] - add burnBatch and getTransactions in solana (#535) 2023-02-01 15:35:22 -08:00
Adam MajmudarandGitHub b45d761f8f [SDK] add creator and royalty settings update for drop (#410) 2023-02-01 15:47:49 -05:00
Adam MajmudarandGitHub 04de5b2783 [SDK] expose getSnapshotEntryForAddress (#471) 2023-02-01 17:34:20 +00:00
Adam MajmudarandGitHub dee4596549 [Auth] Update peerDependencies (#533) 2023-02-01 12:21:16 -05:00
Nacho IacovinoandGitHub 3547901ddd Add all available hooks to React readme (#530) 2023-02-01 02:21:47 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
fe7ab1c5d3 Version Packages (#526)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-31 15:06:05 -08:00
Jonas DanielsandGitHub bdd9ed2298 [react] - fix zag-js/react issues (#527) 2023-01-31 14:57:55 -08:00
ciaranightingaleandGitHub 0977454d45 update readme (#529) 2023-01-31 14:49:24 -08:00
Joaquim VergesandGitHub 19b8e18b6b [SDK] Fix getClaimTransaction().estimateGasCostInEther() for claims costing native currency (#525) 2023-01-31 09:21:00 -08:00
Adam MajmudarandGitHub 7c0060b7fd Fix useMagic and useGnosis documentation (#523) 2023-01-30 22:34:25 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
0a07d7a248 Version Packages (#521)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-28 21:52:12 -08:00
Adam MajmudarandGitHub 8c81ca5c30 Update react-core dependencies (#520) 2023-01-29 05:25:55 +00:00
95 changed files with 12521 additions and 3669 deletions
+1
View File
@@ -38,6 +38,7 @@
"@manypkg/get-packages": "^1.1.3",
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"eslint": "^8.21.0",
"mocha": "^10.2.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1",
"turbo": "^1.4.6",
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/auth
## 3.0.3
### Patch Changes
- [#533](https://github.com/thirdweb-dev/js/pull/533) [`dee4596`](https://github.com/thirdweb-dev/js/commit/dee45965496d5d0298944031dd13a4345f9e1683) Thanks [@adam-maj](https://github.com/adam-maj)! - Remove next major version pin on auth
## 3.0.2
## 3.0.1
### Patch Changes
- [#520](https://github.com/thirdweb-dev/js/pull/520) [`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1) Thanks [@adam-maj](https://github.com/adam-maj)! - Update auth and react-core dependencies
## 3.0.0
### Major Changes
+6 -2
View File
@@ -16,9 +16,13 @@
Install the latest version of the SDK with either `npm` or `yarn`:
```shell
npm install @thirdweb-dev/auth @thirdweb-dev/sdk ethers
npm install @thirdweb-dev/auth
```
```shell
yarn add @thirdweb-dev/auth @thirdweb-dev/sdk ethers
yarn add @thirdweb-dev/auth
```
## Documentation
- [Auth Docs](https://portal.thirdweb.com/auth)
+5 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "3.0.0",
"version": "3.0.3",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"exports": {
@@ -66,7 +66,6 @@
"@solana/web3.js": "^1.73.0",
"@swc-node/register": "^1.5.4",
"@swc/core": "^1.3.23",
"@thirdweb-dev/sdk": "*",
"@thirdweb-dev/wallets": "*",
"@types/bs58": "^4.0.1",
"@types/chai": "^4.3.4",
@@ -97,13 +96,12 @@
"peerDependencies": {
"@noble/ed25519": "^1.7.1",
"@solana/web3.js": "^1.73.0",
"@thirdweb-dev/sdk": "*",
"bs58": "^5.0.0",
"cookie-parser": "^1.4.6",
"ethers": ">=5.5.1",
"express": "^4.18.1",
"next": "^12.2.0",
"next-auth": "^4.10.3",
"ethers": "^5",
"express": "^4",
"next": "^12 || ^13",
"next-auth": "^4",
"tweetnacl": "^1.0.3"
},
"peerDependenciesMeta": {
@@ -138,7 +136,6 @@
"dependencies": {
"cookie": "^0.5.0",
"uuid": "^9.0.0",
"yarn": "^1.22.19",
"zod": "^3.20.2"
}
}
+3 -3
View File
@@ -56,11 +56,11 @@ export default async function handler(
token = await ctx.auth.generate(payload, generateOptions);
} catch (err: any) {
if (err.message) {
return res.status(403).json({ error: err.message });
return res.status(400).json({ error: err.message });
} else if (typeof err === "string") {
return res.status(403).json({ error: err });
return res.status(400).json({ error: err });
} else {
return res.status(403).json({ error: "Invalid login payload" });
return res.status(400).json({ error: "Invalid login payload" });
}
}
+3 -3
View File
@@ -56,11 +56,11 @@ export default async function handler(
token = await ctx.auth.generate(payload, generateOptions);
} catch (err: any) {
if (err.message) {
return res.status(403).json({ error: err.message });
return res.status(400).json({ error: err.message });
} else if (typeof err === "string") {
return res.status(403).json({ error: err });
return res.status(400).json({ error: err });
} else {
return res.status(403).json({ error: "Invalid login payload" });
return res.status(400).json({ error: "Invalid login payload" });
}
}
+59
View File
@@ -1,5 +1,64 @@
# thirdweb
## 0.9.19
### Patch Changes
- Updated dependencies [[`55b00ef`](https://github.com/thirdweb-dev/js/commit/55b00eff2668eb751b9f907e035b3a0c122d2183)]:
- @thirdweb-dev/sdk@3.8.2
## 0.9.18
### Patch Changes
- Updated dependencies [[`448b357`](https://github.com/thirdweb-dev/js/commit/448b357ed4b864e05e531837ca3bc97fbcab3291)]:
- @thirdweb-dev/sdk@3.8.1
## 0.9.17
### Patch Changes
- [#549](https://github.com/thirdweb-dev/js/pull/549) [`912738b`](https://github.com/thirdweb-dev/js/commit/912738bd0afdf81e118c720811911ba3d1979ac0) Thanks [@kumaryash90](https://github.com/kumaryash90)! - add staking bases to CLI
- Updated dependencies [[`0b38dac`](https://github.com/thirdweb-dev/js/commit/0b38dac852e54d0bccd263631f4342aee39d7c29), [`29d0c62`](https://github.com/thirdweb-dev/js/commit/29d0c62610cdb88f75d19574bddcb028b5c40393), [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`f295ec6`](https://github.com/thirdweb-dev/js/commit/f295ec67b4cc422c51beda94cbc4d54a31ce9566), [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`f1aecc3`](https://github.com/thirdweb-dev/js/commit/f1aecc30499e39fb23a205bade939bd939f0d0e4)]:
- @thirdweb-dev/sdk@3.8.0
## 0.9.16
### Patch Changes
- Updated dependencies [[`ec1f232`](https://github.com/thirdweb-dev/js/commit/ec1f2324c7a0e58c2d2a209cfe0d194807ba5b70), [`d29f7e7`](https://github.com/thirdweb-dev/js/commit/d29f7e78f09063630e812dd41164d8a41d4f97dd)]:
- @thirdweb-dev/sdk@3.7.4
## 0.9.15
### Patch Changes
- [#537](https://github.com/thirdweb-dev/js/pull/537) [`cf96333`](https://github.com/thirdweb-dev/js/commit/cf96333fa2f46a4a73a822a0b1248891cc890267) Thanks [@adam-maj](https://github.com/adam-maj)! - Fix cache error on windows OS
## 0.9.14
### Patch Changes
- [#528](https://github.com/thirdweb-dev/js/pull/528) [`ab1f8bb`](https://github.com/thirdweb-dev/js/commit/ab1f8bb6f658dadbca2cb53decbc893050217407) Thanks [@adam-maj](https://github.com/adam-maj)! - Added --skip-update-check flag to CLI to prevent checking for new versions and automatically updating before running commands. Also, only check for check for updates every 24 hours.
- Updated dependencies [[`b45d761`](https://github.com/thirdweb-dev/js/commit/b45d761f8f2695b48312ec6eff15e0549e6a8a41), [`5f8ec45`](https://github.com/thirdweb-dev/js/commit/5f8ec45af7c81d075a84846098f38cad90d03fb9), [`04de5b2`](https://github.com/thirdweb-dev/js/commit/04de5b278362573991140c3a496de7451e5c770d)]:
- @thirdweb-dev/sdk@3.7.3
## 0.9.13
### Patch Changes
- Updated dependencies [[`19b8e18`](https://github.com/thirdweb-dev/js/commit/19b8e18b6bf68e374c817fefbf2fd29ac5573052)]:
- @thirdweb-dev/sdk@3.7.2
## 0.9.12
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@3.7.1
## 0.9.11
### Patch Changes
+11 -2
View File
@@ -101,8 +101,17 @@ $ npx thirdweb@latest release
The simplest way to work on the CLI locally is to:
1. Install the package locally
2. Run the `build:watch` command to compile any changes in realtime
1. Install the package locally by running
```bash
git clone https://github.com/thirdweb-dev/js.git
```
2. From the project root run
```bash
yarn && yarn build
```
3. From inside `js/packages/cli` Run the `build:watch` command to compile any changes in real time
```bash
$ npm install -g ./
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.9.11",
"version": "0.9.19",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -41,6 +41,7 @@
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
"solc": "0.8.17",
"sync-disk-cache": "^2.1.0",
"tar": "^6.1.11",
"tslog": "^3.3.3",
"update-notifier": "^6.0.2",
+165 -129
View File
@@ -3,6 +3,7 @@ import { detectExtensions } from "../common/feature-detector";
import { processProject } from "../common/processor";
import { cliVersion, pkg } from "../constants/urls";
import { info, logger, spinner } from "../core/helpers/logger";
import { CacheEntry } from "../core/types/cache";
import { twCreate } from "../create/command";
import { deploy } from "../deploy";
import { findPackageInstallation } from "../helpers/detect-local-packages";
@@ -13,12 +14,14 @@ import { exec, spawn } from "child_process";
import { Command } from "commander";
import open from "open";
import prompts from "prompts";
import Cache from "sync-disk-cache";
const main = async () => {
// eslint-disable-next-line turbo/no-undeclared-env-vars
const skipIntro = process.env.THIRDWEB_CLI_SKIP_INTRO === "true";
const program = new Command();
const cache = new Cache("thirdweb:cli");
// yes this has to look like this, eliminates whitespace
if (!skipIntro) {
@@ -37,7 +40,168 @@ const main = async () => {
program
.name("thirdweb-cli")
.description("Official thirdweb command line interface")
.version(cliVersion, "-v, --version");
.version(cliVersion, "-v, --version")
.option("--skip-update-check", "Skip check for auto updates")
.hook("preAction", async () => {
if (skipIntro || program.opts().skipUpdateCheck) {
return;
}
let shouldCheckVersion = true;
try {
const lastCheckCache: CacheEntry = cache.get("last-version-check");
if (lastCheckCache.isCached) {
const lastVersionCheck = new Date(lastCheckCache.value);
// Don't check for updates if already checked within past 24 hours
if (
new Date().getTime() - lastVersionCheck.getTime() <
1000 * 60 * 60 * 24
) {
shouldCheckVersion = false;
}
}
} catch {
// no-op
}
if (!shouldCheckVersion) {
return;
}
const versionSpinner = spinner("Checking for updates...");
await import("update-notifier").then(
async ({ default: updateNotifier }) => {
const notifier = updateNotifier({
pkg,
shouldNotifyInNpmScript: true,
// check every time while we're still building the CLI
updateCheckInterval: 0,
});
const versionInfo = await notifier.fetchInfo();
versionSpinner.stop();
try {
// Set cache to prevent checking for updates again for 24 hours
cache.set("last-version-check", new Date().toISOString());
} catch {
// no-op
}
if (versionInfo.type !== "latest") {
const res = await prompts({
type: "toggle",
name: "upgrade",
message: `A new version of the CLI is available. Would you like to upgrade?`,
initial: true,
active: "yes",
inactive: "no",
});
if (res.upgrade) {
const updateSpinner = spinner(
`Upgrading CLI to version ${versionInfo.latest}...`,
);
const clonedEnvironment = { ...process.env };
clonedEnvironment.THIRDWEB_CLI_SKIP_INTRO = "true";
const installation = await findPackageInstallation();
// If the package isn't installed anywhere, just defer to npx thirdweb@latest
if (!installation) {
updateSpinner.succeed(
`Now using CLI version ${versionInfo.latest}. Continuing execution...`,
);
await new Promise((done, failed) => {
const shell = spawn(
`npx --yes thirdweb@latest ${process.argv
.slice(2)
.join(" ")}`,
[],
{ stdio: "inherit", shell: true, env: clonedEnvironment },
);
shell.on("close", (code) => {
if (code === 0) {
done("");
} else {
failed();
}
});
});
return process.exit(0);
}
// Otherwise, get the correct command based on package manager and local vs. global
let command = "";
switch (installation.packageManager) {
case "npm":
command = installation.isGlobal
? `npm install -g thirdweb`
: `npm install thirdweb`;
break;
case "yarn":
command = installation.isGlobal
? `yarn global add thirdweb`
: `yarn add thirdweb`;
break;
case "pnpm":
command = installation.isGlobal
? `pnpm add -g thirdweb@latest`
: `pnpm add thirdweb@latest`;
break;
default:
console.error(
`Could not detect package manager in use, aborting automatic upgrade.\nIf you want to upgrade the CLI, please do it manually with your package manager.`,
);
process.exit(1);
}
await new Promise((done, failed) => {
exec(command, (err, stdout, stderr) => {
if (err) {
failed(err);
return;
}
done({ stdout, stderr });
});
});
updateSpinner.succeed(
`Successfully upgraded CLI to version ${versionInfo.latest}. Continuing execution...`,
);
// If the package is installed globally with yarn or pnpm, then npx won't recognize it
// So we need to make sure to run the command directly
const executionCommand =
!installation.isGlobal || installation.packageManager === "npm"
? `npx thirdweb`
: `thirdweb`;
await new Promise((done, failed) => {
const shell = spawn(
`${executionCommand} ${process.argv.slice(2).join(" ")}`,
[],
{ stdio: "inherit", shell: true, env: clonedEnvironment },
);
shell.on("close", (code) => {
if (code === 0) {
done("");
} else {
failed();
}
});
});
process.exit(0);
}
}
},
);
});
program
.command("create [projectType] [projectPath]")
@@ -201,134 +365,6 @@ const main = async () => {
await detectExtensions(options);
});
if (!skipIntro) {
const versionSpinner = spinner("Checking for updates...");
await import("update-notifier").then(
async ({ default: updateNotifier }) => {
const notifier = updateNotifier({
pkg,
shouldNotifyInNpmScript: true,
// check every time while we're still building the CLI
updateCheckInterval: 0,
});
const versionInfo = await notifier.fetchInfo();
versionSpinner.stop();
if (versionInfo.type !== "latest") {
const res = await prompts({
type: "toggle",
name: "upgrade",
message: `A new version of the CLI is available. Would you like to upgrade?`,
initial: true,
active: "yes",
inactive: "no",
});
if (res.upgrade) {
const updateSpinner = spinner(
`Upgrading CLI to version ${versionInfo.latest}...`,
);
const clonedEnvironment = { ...process.env };
clonedEnvironment.THIRDWEB_CLI_SKIP_INTRO = "true";
const installation = await findPackageInstallation();
// If the package isn't installed anywhere, just defer to npx thirdweb@latest
if (!installation) {
updateSpinner.succeed(
`Now using CLI version ${versionInfo.latest}. Continuing execution...`,
);
await new Promise((done, failed) => {
const shell = spawn(
`npx --yes thirdweb@latest ${process.argv
.slice(2)
.join(" ")}`,
[],
{ stdio: "inherit", shell: true, env: clonedEnvironment },
);
shell.on("close", (code) => {
if (code === 0) {
done("");
} else {
failed();
}
});
});
return process.exit(0);
}
// Otherwise, get the correct command based on package manager and local vs. global
let command = "";
switch (installation.packageManager) {
case "npm":
command = installation.isGlobal
? `npm install -g thirdweb`
: `npm install thirdweb`;
break;
case "yarn":
command = installation.isGlobal
? `yarn global add thirdweb`
: `yarn add thirdweb`;
break;
case "pnpm":
command = installation.isGlobal
? `pnpm add -g thirdweb@latest`
: `pnpm add thirdweb@latest`;
break;
default:
console.error(
`Could not detect package manager in use, aborting automatic upgrade.\nIf you want to upgrade the CLI, please do it manually with your package manager.`,
);
process.exit(1);
}
await new Promise((done, failed) => {
exec(command, (err, stdout, stderr) => {
if (err) {
failed(err);
return;
}
done({ stdout, stderr });
});
});
updateSpinner.succeed(
`Successfully upgraded CLI to version ${versionInfo.latest}. Continuing execution...`,
);
// If the package is installed globally with yarn or pnpm, then npx won't recognize it
// So we need to make sure to run the command directly
const executionCommand =
!installation.isGlobal || installation.packageManager === "npm"
? `npx thirdweb`
: `thirdweb`;
await new Promise((done, failed) => {
const shell = spawn(
`${executionCommand} ${process.argv.slice(2).join(" ")}`,
[],
{ stdio: "inherit", shell: true, env: clonedEnvironment },
);
shell.on("close", (code) => {
if (code === 0) {
done("");
} else {
failed();
}
});
});
process.exit(0);
}
}
},
);
}
await program.parseAsync();
};
+2 -2
View File
@@ -39,9 +39,9 @@ export async function processProject(
if (projectType === "none") {
if (command === "deploy") {
info(
"No contracts detected in this directory. Redirecting to the thirdweb contract deployment page.",
"No contracts detected in this directory. Redirecting to the thirdweb contract explore page.",
);
return "https://thirdweb.com/contracts";
return "https://thirdweb.com/explore";
}
error("No detected contracts in this directory.");
@@ -112,6 +112,28 @@ contract Contract is ERC1155Base {
)
{}
}`,
Staking721Base: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@thirdweb-dev/contracts/base/Staking721Base.sol";
contract Contract is Staking721Base {
constructor(
uint256 _timeUnit,
uint256 _rewardsPerUnitTime,
address _nftCollection,
address _rewardToken,
address _nativeTokenWrapper
)
Staking721Base(
_timeUnit,
_rewardsPerUnitTime,
_nftCollection,
_rewardToken,
_nativeTokenWrapper
)
{}
}`,
ERC1155SignatureMint: `// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
@@ -182,6 +204,28 @@ contract Contract is ERC1155Drop {
_primarySaleRecipient
)
{}
}`,
Staking1155Base: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@thirdweb-dev/contracts/base/Staking1155Base.sol";
contract Contract is Staking1155Base {
constructor(
uint256 _defaultTimeUnit,
uint256 _defaultRewardsPerUnitTime,
address _stakingToken,
address _rewardToken,
address _nativeTokenWrapper
)
Staking1155Base(
_defaultTimeUnit,
_defaultRewardsPerUnitTime,
_stakingToken,
_rewardToken,
_nativeTokenWrapper
)
{}
}`,
ERC20Base: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
@@ -288,6 +332,30 @@ contract Contract is ERC20DropVote {
_primarySaleRecipient
)
{}
}`,
Staking20Base: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@thirdweb-dev/contracts/base/Staking20Base.sol";
contract Contract is Staking20Base {
constructor(
uint256 _timeUnit,
uint256 _rewardRatioNumerator,
uint256 _rewardRatioDenominator,
address _stakingToken,
address _rewardToken,
address _nativeTokenWrapper
)
Staking20Base(
_timeUnit,
_rewardRatioNumerator,
_rewardRatioDenominator,
_stakingToken,
_rewardToken,
_nativeTokenWrapper
)
{}
}`,
};
+5
View File
@@ -0,0 +1,5 @@
export type CacheEntry = {
isCached: boolean;
key: string;
value: string;
};
+2
View File
@@ -0,0 +1,2 @@
declare module 'sync-disk-cache';
declare module "solc";
-1
View File
@@ -1 +0,0 @@
declare module "solc";
+18
View File
@@ -1,5 +1,23 @@
# @thirdweb-dev/contracts-js
## 1.3.1
### Patch Changes
- [#553](https://github.com/thirdweb-dev/js/pull/553) [`76d219e`](https://github.com/thirdweb-dev/js/commit/76d219e6bea1496b45623b3081152854ce1eaa6e) Thanks [@jnsdls](https://github.com/jnsdls)! - force release
## 1.3.0
### Minor Changes
- [#474](https://github.com/thirdweb-dev/js/pull/474) [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Update contracts to 3.3.3
### Patch Changes
- [#549](https://github.com/thirdweb-dev/js/pull/549) [`912738b`](https://github.com/thirdweb-dev/js/commit/912738bd0afdf81e118c720811911ba3d1979ac0) Thanks [@kumaryash90](https://github.com/kumaryash90)! - add staking bases to CLI
- [#544](https://github.com/thirdweb-dev/js/pull/544) [`f1aecc3`](https://github.com/thirdweb-dev/js/commit/f1aecc30499e39fb23a205bade939bd939f0d0e4) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Contracts v3.4.0
## 1.2.3
### Patch Changes
+5 -4
View File
@@ -1,12 +1,12 @@
{
"name": "@thirdweb-dev/contracts-js",
"version": "1.2.3",
"version": "1.3.1",
"main": "dist/thirdweb-dev-contracts-js.cjs.js",
"module": "dist/thirdweb-dev-contracts-js.esm.js",
"license": "Apache-2.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/contracts-js",
"dependencies": {
"@thirdweb-dev/contracts": "3.3.0"
"@thirdweb-dev/contracts": "3.4.1"
},
"devDependencies": {
"@babel/preset-env": "^7.18.10",
@@ -34,6 +34,7 @@
"clean": "rm -rf dist/",
"typechain": "rm -rf ./src && typechain --target ethers-v5 --out-dir src '../../node_modules/@thirdweb-dev/contracts/abi/*.json'",
"extract-abi": "node ./scripts/extract-abis.mjs",
"build": "yarn typechain && tsc && yarn preconstruct build && yarn extract-abi"
"build": "yarn typechain && tsc && yarn preconstruct build && yarn extract-abi",
"push": "yalc push"
}
}
}
+35
View File
@@ -1,5 +1,40 @@
# @thirdweb-dev/react-core
## 3.8.2
## 3.8.1
## 3.8.0
### Patch Changes
- [#546](https://github.com/thirdweb-dev/js/pull/546) [`440a4ad`](https://github.com/thirdweb-dev/js/commit/440a4ade95874e696c589eaa7aae9f0fecc862be) Thanks [@nachoiacovino](https://github.com/nachoiacovino)! - Add missing marketplace v3 hooks
## 3.7.4
## 3.7.3
### Patch Changes
- Updated dependencies [[`dee4596`](https://github.com/thirdweb-dev/js/commit/dee45965496d5d0298944031dd13a4345f9e1683)]:
- @thirdweb-dev/auth@3.0.3
## 3.7.2
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/auth@3.0.2
## 3.7.1
### Patch Changes
- [#520](https://github.com/thirdweb-dev/js/pull/520) [`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1) Thanks [@adam-maj](https://github.com/adam-maj)! - Update auth and react-core dependencies
- Updated dependencies [[`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1)]:
- @thirdweb-dev/auth@3.0.1
## 3.7.0
### Minor Changes
+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "3.7.0",
"version": "3.8.2",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -80,13 +80,13 @@
},
"dependencies": {
"@tanstack/react-query": "^4.0.10",
"tiny-invariant": "^1.2.0"
"tiny-invariant": "^1.2.0",
"@thirdweb-dev/auth": "*"
},
"peerDependencies": {
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/sdk": "*",
"@thirdweb-dev/auth": "*",
"ethers": ">=5.5.1",
"react": ">=18.0.0"
},
@@ -100,9 +100,6 @@
},
"@solana/wallet-adapter-react": {
"optional": true
},
"@thirdweb-dev/auth": {
"optional": true
}
}
}
@@ -21,14 +21,17 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import type {
AuctionListing,
DirectListing,
Marketplace,
MarketplaceFilter,
MarketplaceV3,
NewAuctionListing,
NewDirectListing,
} from "@thirdweb-dev/sdk";
import { ListingType } from "@thirdweb-dev/sdk";
import { Marketplace } from "@thirdweb-dev/sdk/dist/declarations/src/evm/contracts/prebuilt-implementations/marketplace";
import { BigNumberish } from "ethers";
import { BigNumber, BigNumberish } from "ethers";
import invariant from "tiny-invariant";
import { DirectListingInputParams } from "@thirdweb-dev/sdk/dist/declarations/src/evm/schema/marketplacev3/direct-listings";
import { EnglishAuctionInputParams } from "@thirdweb-dev/sdk/dist/declarations/src/evm/schema/marketplacev3/english-auctions";
/** **********************/
/** READ HOOKS **/
@@ -44,7 +47,7 @@ import invariant from "tiny-invariant";
*
* @param contract - an instance of a marketplace contract
* @param listingId - the listing id to check
* @returns a response object that includes an array of listings
* @returns a response object that includes the desired listing
* @beta
*/
export function useListing(
@@ -66,6 +69,76 @@ export function useListing(
);
}
/**
* Use this to get a specific direct listing from the marketplace v3.
*
* @example
* ```javascript
* const { data: directListing, isLoading, error } = useListing(<YourMarketplaceV3ContractInstance>, <listingId>);
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @param listingId - the listing id to check
* @returns a response object that includes the desired direct listing
* @internal
*/
export function useDirectListing(
contract: RequiredParam<MarketplaceV3>,
listingId: RequiredParam<BigNumberish>,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.directListings.getListing(
contractAddress,
listingId,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(listingId, "No listing id provided");
return contract.directListings.getListing(listingId);
},
{
enabled: !!contract,
keepPreviousData: true,
},
);
}
/**
* Use this to get a specific english auction from the marketplace v3.
*
* @example
* ```javascript
* const { data: englishAuction, isLoading, error } = useEnglishAuction(<YourMarketplaceV3ContractInstance>, <auctionId>);
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @param auctionId - the auction id to check
* @returns a response object that includes the desired english auction
* @internal
*/
export function useEnglishAuction(
contract: RequiredParam<MarketplaceV3>,
auctionId: RequiredParam<BigNumberish>,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.englishAuctions.getAuction(
contractAddress,
auctionId,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(auctionId, "No auction id provided");
return contract.englishAuctions.getAuction(auctionId);
},
{
enabled: !!contract,
keepPreviousData: true,
},
);
}
/**
* Use this to get a list all listings from your marketplace contract.
*
@@ -97,16 +170,152 @@ export function useListings(
);
}
/**
* Use this to get a list all direct listings from your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: directListings, isLoading, error } = useDirectListings(<YourMarketplaceV3ContractInstance>, { start: 0, count: 100 });
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @param filter - filter to pass to the query for the sake of pagination & filtering
* @returns a response object that includes an array of direct listings
* @internal
*/
export function useDirectListings(
contract: RequiredParam<MarketplaceV3>,
filter?: MarketplaceFilter,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.directListings.getAll(
contractAddress,
filter,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.directListings.getAll(filter);
},
{
enabled: !!contract,
keepPreviousData: true,
},
);
}
/**
* Use this to get a list all valid direct listings from your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: directListings, isLoading, error } = useValidDirectListings(<YourMarketplaceV3ContractInstance>, { start: 0, count: 100 });
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @param filter - filter to pass to the query for the sake of pagination & filtering
* @returns a response object that includes an array of direct listings
* @internal
*/
export function useValidDirectListings(
contract: RequiredParam<MarketplaceV3>,
filter?: MarketplaceFilter,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.directListings.getAllValid(
contractAddress,
filter,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.directListings.getAllValid(filter);
},
{
enabled: !!contract,
keepPreviousData: true,
},
);
}
/**
* Use this to get a list all english auctions from your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: englishAuctions, isLoading, error } = useEnglishAuctions(<YourMarketplaceV3ContractInstance>, { start: 0, count: 100 });
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @param filter - filter to pass to the query for the sake of pagination & filtering
* @returns a response object that includes an array of english auctions
* @internal
*/
export function useEnglishAuctions(
contract: RequiredParam<MarketplaceV3>,
filter?: MarketplaceFilter,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.englishAuctions.getAll(
contractAddress,
filter,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.englishAuctions.getAll(filter);
},
{
enabled: !!contract,
keepPreviousData: true,
},
);
}
/**
* Use this to get a list all valid english auctions from your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: englishAuctions, isLoading, error } = useValidEnglishAuctions(<YourMarketplaceV3ContractInstance>, { start: 0, count: 100 });
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @param filter - filter to pass to the query for the sake of pagination & filtering
* @returns a response object that includes an array of english auctions
* @internal
*/
export function useValidEnglishAuctions(
contract: RequiredParam<MarketplaceV3>,
filter?: MarketplaceFilter,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.englishAuctions.getAllValid(
contractAddress,
filter,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.englishAuctions.getAllValid(filter);
},
{
enabled: !!contract,
keepPreviousData: true,
},
);
}
/**
* Use this to get a count of all listings on your marketplace contract.
*
* @example
* ```javascript
* const { data: listings, isLoading, error } = useListings(<YourMarketplaceContractInstance>);
* const { data: listingsCount, isLoading, error } = useListingsCount(<YourMarketplaceContractInstance>);
* ```
*
* @param contract - an instance of a marketplace contract
* @returns a response object that includes an array of listings
* @returns a response object that includes the listing count
* @beta
*/
export function useListingsCount(contract: RequiredParam<Marketplace>) {
@@ -123,6 +332,64 @@ export function useListingsCount(contract: RequiredParam<Marketplace>) {
);
}
/**
* Use this to get a count of all direct listings on your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: listingsCount, isLoading, error } = useListingsCount(<YourMarketplaceV3ContractInstance>);
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @returns a response object that includes the direct listings count
* @internal
*/
export function useDirectListingsCount(contract: RequiredParam<MarketplaceV3>) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.directListings.getTotalCount(
contractAddress,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.directListings.getTotalCount();
},
{
enabled: !!contract,
},
);
}
/**
* Use this to get a count of all direct listings on your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: englishAuctionsCount, isLoading, error } = useEnglishAuctionsCount(<YourMarketplaceV3ContractInstance>);
* ```
*
* @param contract - an instance of a marketplace v3 contract
* @returns a response object that includes the direct english actions count
* @internal
*/
export function useEnglishAuctionsCount(
contract: RequiredParam<MarketplaceV3>,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.englishAuctions.getTotalCount(
contractAddress,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.englishAuctions.getTotalCount();
},
{
enabled: !!contract,
},
);
}
/**
* Use this to get a list active listings from your marketplace contract.
*
@@ -181,6 +448,10 @@ export function useWinningBid(
() => {
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(listingId, "No listing id provided");
invariant(
contract.auction.getWinningBid,
"contract does not support auction.getWinningBid",
);
return contract.auction.getWinningBid(listingId);
},
{
@@ -189,6 +460,44 @@ export function useWinningBid(
);
}
/**
* Use this to get a the winning bid for an auction listing from your marketplace v3 contract.
*
* @example
* ```javascript
* const { data: winningBid, isLoading, error } = useWinningBid(<YourMarketplaceV3ContractInstance>, <listingId>);
* ```
*
* @param contract - an instance of a marketplace contract
* @param auctionId - the auction id to check
* @returns a response object that includes the {@link Bid} that is winning the auction
* @beta
*/
export function useEnglishAuctionWinningBid(
contract: RequiredParam<MarketplaceV3>,
auctionId: RequiredParam<BigNumberish>,
) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.auction.getWinningBid(
contractAddress,
auctionId,
),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(auctionId, "No auction id provided");
invariant(
contract.englishAuctions.getWinningBid,
"contract does not support englishAuctions.getWinningBid",
);
return contract.englishAuctions.getWinningBid(auctionId);
},
{
enabled: !!contract && auctionId !== undefined,
},
);
}
/**
* Use this to get the winner of an auction listing from your marketplace contract.
*
@@ -203,7 +512,7 @@ export function useWinningBid(
* @beta
*/
export function useAuctionWinner(
contract: RequiredParam<Marketplace>,
contract: RequiredParam<Marketplace | MarketplaceV3>,
listingId: RequiredParam<BigNumberish>,
) {
const contractAddress = contract?.getAddress();
@@ -212,18 +521,27 @@ export function useAuctionWinner(
contractAddress,
listingId,
),
async () => {
() => {
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(listingId, "No listing id provided");
let winner: string | undefined;
try {
winner = await contract.auction.getWinner(listingId);
} catch (err) {
if (!(err as Error)?.message?.includes("Could not find auction")) {
throw err;
}
const isV1 = isMarketplaceV1(contract);
if (isV1) {
invariant(
contract.auction.getWinner,
"contract does not support auction.getWinner",
);
return contract.auction.getWinner(listingId);
} else if (!isV1) {
invariant(
contract.englishAuctions.getWinner,
"contract does not support englishAuctions.getWinner",
);
return contract.englishAuctions.getWinner(listingId);
}
return winner;
invariant(false, "Contract is not a valid marketplace contract");
},
{
enabled: !!contract && listingId !== undefined,
@@ -240,17 +558,35 @@ export function useAuctionWinner(
* ```
*
* @param contract - an instance of a marketplace contract
* @param listingId - the listing id to check (only necessary for marketplace v3)
* @returns a response object that includes an array of listings
* @beta
*/
export function useBidBuffer(contract: RequiredParam<Marketplace>) {
export function useBidBuffer(contract: RequiredParam<Marketplace | MarketplaceV3>, listingId?: RequiredParam<BigNumberish>) {
const contractAddress = contract?.getAddress();
return useQueryWithNetwork(
cacheKeys.contract.marketplace.getBidBufferBps(contractAddress),
() => {
requiredParamInvariant(contract, "No Contract instance provided");
return contract.getBidBufferBps();
const isV1 = isMarketplaceV1(contract);
if (isV1) {
invariant(
contract.getBidBufferBps,
"contract does not support getBidBufferBps",
);
return contract.getBidBufferBps();
} else if (!isV1) {
invariant(
contract.englishAuctions.getBidBufferBps,
"contract does not support englishAuctions.getBidBufferBps",
);
requiredParamInvariant(listingId, "No listing id provided");
return BigNumber.from(contract.englishAuctions.getBidBufferBps(listingId));
}
invariant(false, "Contract is not a valid marketplace contract");
},
{
enabled: !!contract,
@@ -272,7 +608,7 @@ export function useBidBuffer(contract: RequiredParam<Marketplace>) {
* @beta
*/
export function useMinimumNextBid(
contract: RequiredParam<Marketplace>,
contract: RequiredParam<Marketplace | MarketplaceV3>,
listingId: RequiredParam<BigNumberish>,
) {
const contractAddress = contract?.getAddress();
@@ -284,7 +620,24 @@ export function useMinimumNextBid(
async () => {
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(listingId, "No listing id provided");
return await contract.auction.getMinimumNextBid(listingId);
const isV1 = isMarketplaceV1(contract);
if (isV1) {
invariant(
contract.auction.getMinimumNextBid,
"contract does not support auction.getMinimumNextBid",
);
return contract.auction.getMinimumNextBid(listingId);
} else if (!isV1) {
invariant(
contract.englishAuctions.getMinimumNextBid,
"contract does not support englishAuctions.getMinimumNextBid",
);
return contract.englishAuctions.getMinimumNextBid(listingId);
}
invariant(false, "Contract is not a valid marketplace contract");
},
{
enabled: !!contract && listingId !== undefined,
@@ -327,20 +680,32 @@ export function useMinimumNextBid(
* @returns a mutation object that can be used to create a new direct listing
* @beta
*/
export function useCreateDirectListing(contract: RequiredParam<Marketplace>) {
export function useCreateDirectListing<TMarketplace extends Marketplace | MarketplaceV3>(contract: RequiredParam<TMarketplace>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (data: NewDirectListing) => {
invariant(walletAddress, "no wallet connected, cannot create listing");
async (data: TMarketplace extends Marketplace ? NewDirectListing : DirectListingInputParams) => {
invariant(walletAddress, "No wallet connected, cannot create listing");
requiredParamInvariant(contract, "No Contract instance provided");
invariant(
contract.direct.createListing,
"contract does not support direct.createListing",
);
return await contract.direct.createListing(data);
const isV1 = isMarketplaceV1(contract);
if (isV1) {
invariant(
contract.direct.createListing,
"contract does not support direct.createListing",
);
return await contract.direct.createListing(data as NewDirectListing);
} else if (!isV1) {
invariant(
contract.directListings.createListing,
"contract does not support directListings.createListing",
);
return await contract.directListings.createListing(data as DirectListingInputParams);
}
invariant(false, "Contract is not a valid marketplace contract");
},
{
onSettled: () =>
@@ -384,20 +749,32 @@ export function useCreateDirectListing(contract: RequiredParam<Marketplace>) {
* @returns a mutation object that can be used to create a new auction listing
* @beta
*/
export function useCreateAuctionListing(contract: RequiredParam<Marketplace>) {
export function useCreateAuctionListing<TMarketplace extends Marketplace | MarketplaceV3>(contract: RequiredParam<TMarketplace>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (data: NewAuctionListing) => {
async (data: TMarketplace extends Marketplace ? NewAuctionListing : EnglishAuctionInputParams) => {
invariant(walletAddress, "no wallet connected, cannot create listing");
requiredParamInvariant(contract, "No Contract instance provided");
invariant(
contract.direct.createListing,
"contract does not support auction.createListing",
);
return await contract.auction.createListing(data);
const isV1 = isMarketplaceV1(contract);
if (isV1) {
invariant(
contract.auction.createListing,
"contract does not support auction.createListing",
);
return await contract.auction.createListing(data as NewAuctionListing);
} else if (!isV1) {
invariant(
contract.englishAuctions.createAuction,
"contract does not support englishAuctions.createAuction",
);
return await contract.englishAuctions.createAuction(data as EnglishAuctionInputParams);
}
invariant(false, "Contract is not a valid marketplace contract");
},
{
onSettled: () =>
@@ -431,26 +808,38 @@ export function useCreateAuctionListing(contract: RequiredParam<Marketplace>) {
* disabled={isLoading}
* onClick={() => cancelListing()}
* >
* Create Auction Listing!
* Cancel Auction Listing!
* </button>
* );
* };
* ```
*
* @param contract - an instance of a Marketplace contract
* @returns a mutation object that can be used to create a new auction listing
* @returns a mutation object that can be used to cancel a listing
* @beta
*/
export function useCancelListing(contract: RequiredParam<Marketplace>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (data: Pick<AuctionListing | DirectListing, "type" | "id">) => {
invariant(walletAddress, "no wallet connected, cannot create listing");
requiredParamInvariant(contract, "No Contract instance provided");
if (data.type === ListingType.Auction) {
return await contract?.auction.cancelListing(data.id);
invariant(
contract.auction.cancelListing,
"contract does not support auction.cancelListing",
);
return await contract.auction.cancelListing(data.id);
} else {
return await contract?.direct.cancelListing(data.id);
invariant(
contract.direct.cancelListing,
"contract does not support direct.cancelListing",
);
return await contract.direct.cancelListing(data.id);
}
},
{
@@ -464,6 +853,125 @@ export function useCancelListing(contract: RequiredParam<Marketplace>) {
);
}
/**
* Use this to cancel a direct listing on your marketplace v3 contract.
*
* @example
* ```jsx
* const Component = () => {
* const {
* mutate: cancelDirectListing,
* isLoading,
* error,
* } = useCancelDirectListing(">>YourMarketplaceV3ContractInstance<<");
*
* if (error) {
* console.error("failed to cancel direct listing", error);
* }
*
* return (
* <button
* disabled={isLoading}
* onClick={() => cancelListing()}
* >
* Cancel Direct Listing
* </button>
* );
* };
* ```
*
* @param contract - an instance of a Marketplace v3 contract
* @returns a mutation object that can be used to cancel a direct listing
* @internal
*/
export function useCancelDirectListing(contract: RequiredParam<MarketplaceV3>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (listingId: BigNumberish) => {
invariant(walletAddress, "no wallet connected, cannot create listing");
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(listingId, "No listing id provided");
invariant(
contract.directListings.cancelListing,
"contract does not support directListings.cancelListing",
);
return await contract.directListings.cancelListing(listingId);
},
{
onSettled: () =>
invalidateContractAndBalances(
queryClient,
contractAddress,
activeChainId,
),
},
);
}
/**
* Use this to cancel a direct listing on your marketplace v3 contract.
*
* @example
* ```jsx
* const Component = () => {
* const {
* mutate: cancelEnglishAuction,
* isLoading,
* error,
* } = useCancelEnglishAuction(">>YourMarketplaceV3ContractInstance<<");
*
* if (error) {
* console.error("failed to cancel english auction", error);
* }
*
* return (
* <button
* disabled={isLoading}
* onClick={() => cancelEnglishAuction()}
* >
* Cancel English Auction
* </button>
* );
* };
* ```
*
* @param contract - an instance of a Marketplace v3 contract
* @returns a mutation object that can be used to cancel an english auction
* @internal
*/
export function useCancelEnglishAuction(contract: RequiredParam<MarketplaceV3>) {
const activeChainId = useSDKChainId();
const contractAddress = contract?.getAddress();
const queryClient = useQueryClient();
const walletAddress = useAddress();
return useMutation(
async (auctionId: BigNumberish) => {
invariant(walletAddress, "no wallet connected, cannot create listing");
requiredParamInvariant(contract, "No Contract instance provided");
requiredParamInvariant(auctionId, "No auction id provided");
invariant(
contract.englishAuctions.cancelAuction,
"contract does not support englishAuctions.cancelAuction",
);
return await contract.englishAuctions.cancelAuction(auctionId);
},
{
onSettled: () =>
invalidateContractAndBalances(
queryClient,
contractAddress,
activeChainId,
),
},
);
}
/**
* Use this to place a bid on an auction listing from your marketplace contract.
*
@@ -786,3 +1294,7 @@ export function useBuyNow(contract: RequiredParam<Marketplace>) {
},
);
}
function isMarketplaceV1(contract: RequiredParam<Marketplace | MarketplaceV3>): contract is Marketplace {
return (contract as Marketplace).getAllListings !== undefined;
}
@@ -25,7 +25,7 @@ export function useLogin() {
invariant(authConfig.auth, "You need a connected wallet to login.");
const payload = await authConfig.auth.login(options);
await fetch(`${authConfig.authUrl}/login`, {
const res = await fetch(`${authConfig.authUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -33,6 +33,15 @@ export function useLogin() {
body: JSON.stringify({ payload }),
});
if (!res.ok) {
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
throw new Error(`Login request failed with status code ${res.status}`);
}
queryClient.invalidateQueries(cacheKeys.auth.user());
},
});
@@ -268,6 +268,72 @@ export const cacheKeys = {
{ listingId },
]),
},
directListings: {
getAll: (
contractAddress: RequiredParam<ContractAddress>,
params?: MarketplaceFilter,
) =>
createContractCacheKey(
contractAddress,
params ? ["getAllDirectListings", params] : ["getAllDirectListings"],
),
getAllValid: (
contractAddress: RequiredParam<ContractAddress>,
params?: MarketplaceFilter,
) =>
createContractCacheKey(
contractAddress,
params ? ["getAllValidDirectListings", params] : ["getAllValidDirectListings"],
),
getListing: (
contractAddress: RequiredParam<ContractAddress>,
listingId: RequiredParam<BigNumberish>,
) =>
createContractCacheKey(contractAddress, [
"getListing",
{ listingId },
]),
getTotalCount: (contractAddress: RequiredParam<ContractAddress>) =>
createContractCacheKey(contractAddress, ["getTotalDirectListingsCount"]),
},
englishAuctions: {
getAll: (
contractAddress: RequiredParam<ContractAddress>,
params?: MarketplaceFilter,
) =>
createContractCacheKey(
contractAddress,
params ? ["getAllEnglishAuctions", params] : ["getAllEnglishAuctions"],
),
getAllValid: (
contractAddress: RequiredParam<ContractAddress>,
params?: MarketplaceFilter,
) =>
createContractCacheKey(
contractAddress,
params ? ["getAllEValidEnglishAuctions", params] : ["getAllEValidEnglishAuctions"],
),
getAuction: (
contractAddress: RequiredParam<ContractAddress>,
auctionId: RequiredParam<BigNumberish>,
) =>
createContractCacheKey(contractAddress, [
"getAuction",
{ auctionId },
]),
getWinningBid: (
contractAddress: RequiredParam<ContractAddress>,
auctionId: RequiredParam<BigNumberish>,
) =>
createContractCacheKey(contractAddress, [
"getWinningBid",
{ auctionId },
]),
getTotalCount: (contractAddress: RequiredParam<ContractAddress>) =>
createContractCacheKey(contractAddress, ["getTotalEnglishAuctionsCount"]),
},
},
},
// extensions
@@ -37,7 +37,7 @@ export function useLogin() {
invariant(authConfig.auth, "You need a connected wallet to login.");
const payload = await authConfig.auth.login(options);
await fetch(`${authConfig.authUrl}/login`, {
const res = await fetch(`${authConfig.authUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -45,6 +45,15 @@ export function useLogin() {
body: JSON.stringify({ payload }),
});
if (!res.ok) {
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
throw new Error(`Login request failed with status code ${res.status}`);
}
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
},
});
+2
View File
@@ -9,4 +9,6 @@ etc/
temp/
temp-evm/
temp-solana/
temp-evm-gnosis-safe/
temp-evm-magic/
docs/
+55
View File
@@ -1,5 +1,60 @@
# @thirdweb-dev/react
## 3.8.2
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/react-core@3.8.2
## 3.8.1
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/react-core@3.8.1
## 3.8.0
### Patch Changes
- Updated dependencies [[`440a4ad`](https://github.com/thirdweb-dev/js/commit/440a4ade95874e696c589eaa7aae9f0fecc862be)]:
- @thirdweb-dev/react-core@3.8.0
## 3.7.4
### Patch Changes
- [#470](https://github.com/thirdweb-dev/js/pull/470) [`b59a31c`](https://github.com/thirdweb-dev/js/commit/b59a31c365380c4dc9aed8d7678d46b4b21a029d) Thanks [@JustinTime42](https://github.com/JustinTime42)! - Added support for displaying 3d model NFTs
- Updated dependencies []:
- @thirdweb-dev/react-core@3.7.4
## 3.7.3
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/react-core@3.7.3
## 3.7.2
### Patch Changes
- [#527](https://github.com/thirdweb-dev/js/pull/527) [`bdd9ed2`](https://github.com/thirdweb-dev/js/commit/bdd9ed22989f5baa511bec70ef1ae9d88bfb957e) Thanks [@jnsdls](https://github.com/jnsdls)! - fix dependency issues with @zag-js/react
- Updated dependencies []:
- @thirdweb-dev/react-core@3.7.2
## 3.7.1
### Patch Changes
- [#520](https://github.com/thirdweb-dev/js/pull/520) [`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1) Thanks [@adam-maj](https://github.com/adam-maj)! - Update auth and react-core dependencies
- Updated dependencies [[`8c81ca5`](https://github.com/thirdweb-dev/js/commit/8c81ca5c3033b04b1f64e3a1134a72e7e3ec03b1)]:
- @thirdweb-dev/react-core@3.7.1
## 3.7.0
### Minor Changes
+108
View File
@@ -305,3 +305,111 @@ const KitchenSinkExample = () => {
);
};
```
## Available hooks
### Main hooks
| Hook | Description |
| --- | --- |
| [`useSDK`](https://portal.thirdweb.com/react/react.usesdk) | Access the instance of the thirdweb SDK created by the ThirdwebProvider. |
| [`useContract`](https://portal.thirdweb.com/react/react.usecontract) | Use this resolve a contract address to a smart contract instance. |
| [`useContractWrite`](https://portal.thirdweb.com/react/react.usecontractwrite) | Use this to get data from a contract read-function call. |
| [`useContractRead`](https://portal.thirdweb.com/react/react.usecontractread) | Use this to get a function to make a write call to your contract. |
| [`useContractEvents`](https://portal.thirdweb.com/react/react.usecontractevents) | Use this to get the contract metadata for a (built-in or custom) contract. |
| [`useContractMetadata`](https://portal.thirdweb.com/react/react.usecontractmetadata) | Use this to get the contract metadata for a (built-in or custom) contract. |
### Wallet connection
| Hook | Description |
| --- | --- |
| [`useAddress`](https://portal.thirdweb.com/react/react.useaddress) | Hook for accessing the address of the connected wallet. |
| [`useMetamask`](https://portal.thirdweb.com/react/react.usemetamask) | Hook for connecting to a Metamask wallet. |
| [`useWalletConnect`](https://portal.thirdweb.com/react/react.usewalletconnect) | Hook for connecting to a mobile wallet with Wallet Connect. |
| [`useCoinbaseWallet`](https://portal.thirdweb.com/react/react.usecoinbasewallet) | Hook for connecting to a Coinbase wallet. |
| [`useMagic`](https://portal.thirdweb.com/react/react.usemagic) | Hook for connecting to an email wallet using magic link. |
| [`useGnosis`](https://portal.thirdweb.com/react/react.usegnosis) | Hook for connecting to a Gnosis Safe. |
| [`useDisconnect`](https://portal.thirdweb.com/react/react.usedisconnect) | Hook for disconnecting the currently connected wallet. |
### Network connection
| Hook | Description |
| --- | --- |
| [`useChainId`](https://portal.thirdweb.com/react/react.usechainid) | Hook for accessing the chain ID of the network the current wallet is connected to |
| [`useNetwork`](https://portal.thirdweb.com/react/react.usenetwork) | Hook for getting metadata about the network the current wallet is connected to and switching networks. |
| [`useNetworkMismatch`](https://portal.thirdweb.com/react/react.usenetworkmismatch) | Hook for checking whether the connected wallet is on the correct network specified by the desiredChainId passed to the <ThirdwebProvider />. |
### NFT
| Hook | Description |
| --- | --- |
| [`useNFT`](https://portal.thirdweb.com/react/react.usenft) | Use this to get an individual NFT token of your NFTContract. |
| [`useNFTs`](https://portal.thirdweb.com/react/react.usenfts) | Use this to get a list of NFT tokens of your NFTContract. |
| [`useTotalCirculatingSupply`](https://portal.thirdweb.com/react/react.usetotalcirculatingsupply) | Use this to get a the total (minted) supply of your NFTContract. |
| [`useOwnedNFTs`](https://portal.thirdweb.com/react/react.useownednfts) | Use this to get a the owned NFTs for a specific Erc721OrErc1155 and wallet address. |
| [`useNFTBalance`](https://portal.thirdweb.com/react/react.usenftbalance) | Use this to get a the total balance of a NFTContract and wallet address. |
| [`useTotalCount`](https://portal.thirdweb.com/react/react.usetotalcount) | Use this to get the total count of NFT tokens of your NFTContract. |
| [`useMintNFT`](https://portal.thirdweb.com/react/react.usemintnft) | Use this to mint a new NFT on your Erc721OrErc1155. |
| [`useMintNFTSupply`](https://portal.thirdweb.com/react/react.usemintnftsupply) | Use this mint extra supply on your Erc1155. |
| [`useTransferNFT`](https://portal.thirdweb.com/react/react.usetransfernft) | Use this to transfer tokens on your NFTContract. |
| [`useAirdropNFT`](https://portal.thirdweb.com/react/react.useairdropnft) | Use this to airdrop tokens on your Erc1155. |
| [`useBurnNFT`](https://portal.thirdweb.com/react/react.useburnnft) | Use this to burn an NFT on your Erc721OrErc1155. |
### Token
| Hook | Description |
| --- | --- |
| [`useTokenSupply`](https://portal.thirdweb.com/react/react.usetokensupply) | Use this to get a the total supply of your contract. |
| [`useTokenBalance`](https://portal.thirdweb.com/react/react.usetokenbalance) | Use this to get the balance of your contract for a given address. |
| [`useTokenDecimals`](https://portal.thirdweb.com/react/react.usetokendecimals) | Use this to get the decimals of your contract for a given address. |
| [`useMintToken`](https://portal.thirdweb.com/react/react.useminttoken) | Use this to mint new tokens on your contract. |
| [`useClaimToken`](https://portal.thirdweb.com/react/react.useclaimtoken) | Use this to claim tokens from your contract. |
| [`useTransferToken`](https://portal.thirdweb.com/react/react.usetransfertoken) | Use this to transfer tokens on your contract. |
| [`useTransferBatchToken`](https://portal.thirdweb.com/react/react.usetransferbatchtoken) | Use this to transfer batch tokens on your contract. |
| [`useBurnToken`](https://portal.thirdweb.com/react/react.useburntoken) | Use this to burn tokens on your contract. |
### Marketplace
| Hook | Description |
| --- | --- |
| [`useListing`](https://portal.thirdweb.com/react/react.uselisting) | Use this to get a specific listing from the marketplace. |
| [`useListings`](https://portal.thirdweb.com/react/react.uselistings) | Use this to get a list all listings from your marketplace contract. |
| [`useListingsCount`](https://portal.thirdweb.com/react/react.uselistingscount) | Use this to get a count of all listings on your marketplace contract. |
| [`useActiveListings`](https://portal.thirdweb.com/react/react.useactivelistings) | Use this to get a list active listings from your marketplace contract. |
| [`useWinningBid`](https://portal.thirdweb.com/react/react.usewinningbid) | Use this to get a the winning bid for an auction listing from your marketplace contract. |
| [`useAuctionWinner`](https://portal.thirdweb.com/react/react.useauctionwinner) | Use this to get the winner of an auction listing from your marketplace contract. |
| [`useBidBuffer`](https://portal.thirdweb.com/react/react.usebidbuffer) | Use this to get the buffer in basis points between offers from your marketplace contract. |
| [`useCreateDirectListing`](https://portal.thirdweb.com/react/react.usecreatedirectlisting) | Use this to create a new Direct Listing on your marketplace contract. |
| [`useCreateAuctionListing`](https://portal.thirdweb.com/react/react.usecreateauctionlisting) | Use this to create a new Auction Listing on your marketplace contract. |
| [`useCancelListing`](https://portal.thirdweb.com/react/react.usecancellisting) | Use this to cancel a listing on your marketplace contract. |
| [`useMakeBid`](https://portal.thirdweb.com/react/react.usemakebid) | Use this to place a bid on an auction listing from your marketplace contract. |
| [`useBuyNow`](https://portal.thirdweb.com/react/react.usebuynow) | Use this to buy out an auction listing from your marketplace contract. |
### Permissions Controls
| Hook | Description |
| --- | --- |
| [`useAllRoleMembers`](https://portal.thirdweb.com/react/react.useallrolemembers) | Use this to get the roles of all members. |
| [`useRoleMembers`](https://portal.thirdweb.com/react/react.userolemembers) | Use this to get the members of a role. |
| [`useIsAddressRole`](https://portal.thirdweb.com/react/react.useisaddressrole) | Use this to check if a WalletAddress is a member of a role. |
| [`useSetAllRoleMembers`](https://portal.thirdweb.com/react/react.usesetallrolemembers) | Use this to OVERWRITE the list of addresses that are members of specific roles. |
| [`useGrantRole`](https://portal.thirdweb.com/react/react.usegrantrole) | Use this to grant a WalletAddress a specific role. |
| [`useRevokeRole`](https://portal.thirdweb.com/react/react.userevokerole) | Use this to revoke a WalletAddress a specific role. |
### Drop
| Hook | Description |
| --- | --- |
| [`useUnclaimedNFTs`](https://portal.thirdweb.com/react/react.useunclaimednfts) | Use this to get a list of unclaimed NFT tokens of your ERC721 Drop contract. |
| [`useClaimedNFTs`](https://portal.thirdweb.com/react/react.useclaimednfts) | Use this to get a list of claimed (minted) NFT tokens of your ERC721 Drop contract.. |
| [`useUnclaimedNFTSupply`](https://portal.thirdweb.com/react/react.useunclaimednftsupply) | Use this to get the total unclaimed NFT supply of your ERC721 Drop contract. |
| [`useClaimedNFTSupply`](https://portal.thirdweb.com/react/react.useclaimednftsupply) | Use this to get the total claimed (minted) NFT supply of your ERC721 Drop contract. |
| [`useBatchesToReveal`](https://portal.thirdweb.com/react/react.usebatchestoreveal) | Use this to get a list of batches that are ready to be revealed. |
| [`useClaimNFT`](https://portal.thirdweb.com/react/react.useclaimnft) | Use this to claim a NFT on your DropContract |
| [`useLazyMint`](https://portal.thirdweb.com/react/react.uselazymint) | Use this to lazy mint a batch of NFTs on your DropContract. |
| [`useDelayedRevealLazyMint`](https://portal.thirdweb.com/react/react.usedelayedreveallazymint) | Use this to lazy mint a batch of delayed reveal NFTs on your DropContract. |
| [`useRevealLazyMint`](https://portal.thirdweb.com/react/react.usereveallazymint) | Use this to reveal a batch of delayed reveal NFTs on your RevealableContract. |
### Claim Conditions
| Hook | Description |
| --- | --- |
| [`useActiveClaimCondition`](https://portal.thirdweb.com/react/react.useactiveclaimcondition) | Use this to get the active claim condition for ERC20, ERC721 or ERC1155 based contracts. |
| [`useClaimConditions`](https://portal.thirdweb.com/react/react.useclaimconditions) | Use this to get all claim conditions for ERC20, ERC721 or ERC1155 based contracts. |
| [`useClaimIneligibilityReasons`](https://portal.thirdweb.com/react/react.useclaimineligibilityreasons) | Use this to check for reasons that prevent claiming for either ERC20, ERC721 or ERC1155 based contracts. |
| [`useSetClaimConditions`](https://portal.thirdweb.com/react/react.usesetclaimconditions) | Use this to set claim conditions on your DropContract. |
| [`useResetClaimConditions`](https://portal.thirdweb.com/react/react.useresetclaimconditions) | Use this to reset claim conditions on your DropContract. |
@@ -0,0 +1,342 @@
/**
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
/**
* Optionally specifies another JSON config file that this file extends from. This provides a way for
* standard settings to be shared across multiple projects.
*
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
* resolved using NodeJS require().
*
* SUPPORTED TOKENS: none
* DEFAULT VALUE: ""
*/
// "extends": "./shared/api-extractor-base.json"
// "extends": "my-package/include/api-extractor-base.json"
/**
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
*
* The path is resolved relative to the folder of the config file that contains the setting.
*
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
* will be reported.
*
* SUPPORTED TOKENS: <lookup>
* DEFAULT VALUE: "<lookup>"
*/
// "projectFolder": "..",
/**
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
* analyzes the symbols exported by this module.
*
* The file extension must be ".d.ts" and not ".ts".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
"mainEntryPointFilePath": "<projectFolder>/evm/connectors/gnosis-safe/dist/thirdweb-dev-react-evm-connectors-gnosis-safe.cjs.d.ts",
/**
* A list of NPM package names whose exports should be treated as part of this package.
*
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
* imports library2. To avoid this, we can specify:
*
* "bundledPackages": [ "library2" ],
*
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
* local files for library1.
*/
"bundledPackages": [
"wagmi",
"wagmi-core"
],
/**
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
*/
"compiler": {
/**
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* Note: This setting will be ignored if "overrideTsconfig" is used.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
*/
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
/**
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
* The object must conform to the TypeScript tsconfig schema:
*
* http://json.schemastore.org/tsconfig
*
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
*
* DEFAULT VALUE: no overrideTsconfig section
*/
"overrideTsconfig": {
"skipLibCheck": true
}
/**
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
*
* DEFAULT VALUE: false
*/
// "skipLibCheck": true,
},
/**
* Configures how the API report file (*.api.md) will be generated.
*/
"apiReport": {
/**
* (REQUIRED) Whether to generate an API report.
*/
"enabled": true
/**
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
* a full file path.
*
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
*
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
*/
// "reportFileName": "<unscopedPackageName>.api.md",
/**
* Specifies the folder where the API report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
* e.g. for an API review.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/etc/"
*/
// "reportFolder": "<projectFolder>/etc/",
/**
* Specifies the folder where the temporary report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
* If they are different, a production build will fail.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
// "reportTempFolder": "<projectFolder>/temp/"
},
/**
* Configures how the doc model file (*.api.json) will be generated.
*/
"docModel": {
/**
* (REQUIRED) Whether to generate a doc model file.
*/
"enabled": true
/**
* The output path for the doc model file. The file extension should be ".api.json".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json"
},
/**
* Configures how the .d.ts rollup file will be generated.
*/
"dtsRollup": {
/**
* (REQUIRED) Whether to generate the .d.ts rollup file.
*/
"enabled": false,
/**
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
* This file will include all declarations that are exported by the main entry point.
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/
"untrimmedFilePath": "<projectFolder>/lib/<unscopedPackageName>.d.ts"
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
* This file will include only declarations that are marked as "@public" or "@beta".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
* This file will include only declarations that are marked as "@public".
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
/**
* When a declaration is trimmed, by default it will be replaced by a code comment such as
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
* declaration completely.
*
* DEFAULT VALUE: false
*/
// "omitTrimmingComments": true
},
/**
* Configures how the tsdoc-metadata.json file will be generated.
*/
"tsdocMetadata": {
/**
* Whether to generate the tsdoc-metadata.json file.
*
* DEFAULT VALUE: true
*/
// "enabled": true,
/**
* Specifies where the TSDoc metadata file should be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
* falls back to "tsdoc-metadata.json" in the package folder.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<lookup>"
*/
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
},
/**
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
* To use the OS's default newline kind, specify "os".
*
* DEFAULT VALUE: "crlf"
*/
// "newlineKind": "crlf",
/**
* Configures how API Extractor reports error and warning messages produced during analysis.
*
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
*/
"messages": {
/**
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
* the input .d.ts files.
*
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"compilerMessageReporting": {
/**
* Configures the default routing for messages that don't match an explicit rule in this table.
*/
"default": {
/**
* Specifies whether the message should be written to the the tool's output log. Note that
* the "addToApiReportFile" property may supersede this option.
*
* Possible values: "error", "warning", "none"
*
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
* the "--local" option), the warning is displayed but the build will not fail.
*
* DEFAULT VALUE: "warning"
*/
"logLevel": "warning"
/**
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
* then the message will be written inside that file; otherwise, the message is instead logged according to
* the "logLevel" option.
*
* DEFAULT VALUE: false
*/
// "addToApiReportFile": false
}
// "TS2551": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by API Extractor during its analysis.
*
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
*
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
*/
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
}
// "ae-extra-release-tag": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
*
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
}
// "tsdoc-link-tag-unescaped-text": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
}
}
}
@@ -0,0 +1,342 @@
/**
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
/**
* Optionally specifies another JSON config file that this file extends from. This provides a way for
* standard settings to be shared across multiple projects.
*
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
* resolved using NodeJS require().
*
* SUPPORTED TOKENS: none
* DEFAULT VALUE: ""
*/
// "extends": "./shared/api-extractor-base.json"
// "extends": "my-package/include/api-extractor-base.json"
/**
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
*
* The path is resolved relative to the folder of the config file that contains the setting.
*
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
* will be reported.
*
* SUPPORTED TOKENS: <lookup>
* DEFAULT VALUE: "<lookup>"
*/
// "projectFolder": "..",
/**
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
* analyzes the symbols exported by this module.
*
* The file extension must be ".d.ts" and not ".ts".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
"mainEntryPointFilePath": "<projectFolder>/evm/connectors/magic/dist/thirdweb-dev-react-evm-connectors-magic.cjs.d.ts",
/**
* A list of NPM package names whose exports should be treated as part of this package.
*
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
* imports library2. To avoid this, we can specify:
*
* "bundledPackages": [ "library2" ],
*
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
* local files for library1.
*/
"bundledPackages": [
"wagmi",
"wagmi-core"
],
/**
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
*/
"compiler": {
/**
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* Note: This setting will be ignored if "overrideTsconfig" is used.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
*/
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
/**
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
* The object must conform to the TypeScript tsconfig schema:
*
* http://json.schemastore.org/tsconfig
*
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
*
* DEFAULT VALUE: no overrideTsconfig section
*/
"overrideTsconfig": {
"skipLibCheck": true
}
/**
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
*
* DEFAULT VALUE: false
*/
// "skipLibCheck": true,
},
/**
* Configures how the API report file (*.api.md) will be generated.
*/
"apiReport": {
/**
* (REQUIRED) Whether to generate an API report.
*/
"enabled": true
/**
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
* a full file path.
*
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
*
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
*/
// "reportFileName": "<unscopedPackageName>.api.md",
/**
* Specifies the folder where the API report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
* e.g. for an API review.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/etc/"
*/
// "reportFolder": "<projectFolder>/etc/",
/**
* Specifies the folder where the temporary report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
* If they are different, a production build will fail.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
// "reportTempFolder": "<projectFolder>/temp/"
},
/**
* Configures how the doc model file (*.api.json) will be generated.
*/
"docModel": {
/**
* (REQUIRED) Whether to generate a doc model file.
*/
"enabled": true
/**
* The output path for the doc model file. The file extension should be ".api.json".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json"
},
/**
* Configures how the .d.ts rollup file will be generated.
*/
"dtsRollup": {
/**
* (REQUIRED) Whether to generate the .d.ts rollup file.
*/
"enabled": false,
/**
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
* This file will include all declarations that are exported by the main entry point.
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/
"untrimmedFilePath": "<projectFolder>/lib/<unscopedPackageName>.d.ts"
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
* This file will include only declarations that are marked as "@public" or "@beta".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
* This file will include only declarations that are marked as "@public".
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
/**
* When a declaration is trimmed, by default it will be replaced by a code comment such as
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
* declaration completely.
*
* DEFAULT VALUE: false
*/
// "omitTrimmingComments": true
},
/**
* Configures how the tsdoc-metadata.json file will be generated.
*/
"tsdocMetadata": {
/**
* Whether to generate the tsdoc-metadata.json file.
*
* DEFAULT VALUE: true
*/
// "enabled": true,
/**
* Specifies where the TSDoc metadata file should be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
* falls back to "tsdoc-metadata.json" in the package folder.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<lookup>"
*/
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
},
/**
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
* To use the OS's default newline kind, specify "os".
*
* DEFAULT VALUE: "crlf"
*/
// "newlineKind": "crlf",
/**
* Configures how API Extractor reports error and warning messages produced during analysis.
*
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
*/
"messages": {
/**
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
* the input .d.ts files.
*
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"compilerMessageReporting": {
/**
* Configures the default routing for messages that don't match an explicit rule in this table.
*/
"default": {
/**
* Specifies whether the message should be written to the the tool's output log. Note that
* the "addToApiReportFile" property may supersede this option.
*
* Possible values: "error", "warning", "none"
*
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
* the "--local" option), the warning is displayed but the build will not fail.
*
* DEFAULT VALUE: "warning"
*/
"logLevel": "warning"
/**
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
* then the message will be written inside that file; otherwise, the message is instead logged according to
* the "logLevel" option.
*
* DEFAULT VALUE: false
*/
// "addToApiReportFile": false
}
// "TS2551": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by API Extractor during its analysis.
*
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
*
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
*/
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
}
// "ae-extra-release-tag": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
*
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
}
// "tsdoc-link-tag-unescaped-text": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
}
}
}
+12 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.7.0",
"version": "3.8.2",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -54,7 +54,10 @@
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/",
"generate-docs": "yarn generate-docs:evm && yarn generate-docs:solana && yarn generate-snippets",
"generate-docs:evm": "api-extractor run --local --config ./config/api-extractor-evm.json && api-documenter markdown -i ./temp -o ./docs/evm && rm -rf ./temp-evm && mv ./temp ./temp-evm",
"generate-docs:evm": "yarn generate-docs:evm:base && yarn generate-docs:evm:magic && yarn generate-docs:evm:gnosis-safe",
"generate-docs:evm:base": "api-extractor run --local --config ./config/api-extractor-evm.json && api-documenter markdown -i ./temp -o ./docs/evm && rm -rf ./temp-evm && mv ./temp ./temp-evm",
"generate-docs:evm:magic": "api-extractor run --local --config ./config/api-extractor-magic.json && api-documenter markdown -i ./temp -o ./docs/evm/magic && rm -rf ./temp-evm-magic && mv ./temp ./temp-evm-magic && mv ./docs/evm/magic/react.usemagic.md ./docs/evm/react.usemagic.md && rm -rf ./docs/evm/magic",
"generate-docs:evm:gnosis-safe": "api-extractor run --local --config ./config/api-extractor-gnosis.json && api-documenter markdown -i ./temp -o ./docs/evm/gnosis-safe && rm -rf ./temp-evm-gnosis-safe && mv ./temp ./temp-evm-gnosis-safe && mv ./docs/evm/gnosis-safe/react.usegnosis.md ./docs/evm/react.usegnosis.md && rm -rf ./docs/evm/gnosis-safe",
"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",
@@ -72,9 +75,9 @@
"@microsoft/tsdoc": "^0.14.1",
"@preconstruct/cli": "^2.2.1",
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/auth": "*",
"@thirdweb-dev/sdk": "*",
@@ -97,16 +100,17 @@
"typescript": "^4.7.4"
},
"dependencies": {
"@thirdweb-dev/react-core": "*",
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@google/model-viewer": "^2.1.1",
"@react-icons/all-files": "^4.1.0",
"@safe-global/safe-core-sdk": "^3.2.0",
"@safe-global/safe-ethers-adapters": "0.1.0-alpha.13",
"@safe-global/safe-ethers-lib": "^1.7.0",
"@tanstack/react-query": "^4.0.10",
"@zag-js/menu": "^0.3.0",
"@zag-js/react": "^0.3.1",
"@thirdweb-dev/react-core": "*",
"@zag-js/menu": "0.3.4",
"@zag-js/react": "0.3.4",
"buffer": "^6.0.3",
"color": "^4.2.3",
"copy-to-clipboard": "^3.3.2",
@@ -119,9 +123,9 @@
},
"peerDependencies": {
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/sdk": "*",
"ethers": ">=5.5.1",
@@ -149,4 +153,4 @@
"optional": true
}
}
}
}
@@ -9,9 +9,19 @@ import {
CarbonPlayFilledAlt,
} from "./Icons";
import { useQuery } from "@tanstack/react-query";
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, {
ReactNode,
useEffect,
useMemo,
useRef,
useState,
lazy,
Suspense,
} from "react";
import useDimensions from "react-cool-dimensions";
const ModelViewer = lazy(() => import("./ModelViewer"));
export interface SharedMediaProps {
className?: string;
style?: React.CSSProperties;
@@ -25,6 +35,8 @@ export interface SharedMediaProps {
* Show the media controls (where applicable) (default false)
*/
controls?: HTMLVideoElement["controls"];
children?: ReactNode;
}
/**
@@ -45,6 +57,8 @@ export interface MediaRendererProps extends SharedMediaProps {
* The media poster image uri. (if applicable)
*/
poster?: string | null;
width?: string;
height?: string;
}
interface PlayButtonProps {
@@ -399,6 +413,7 @@ LinkPlayer.displayName = "LinkPlayer";
* This component can be used to render any media type, including image, audio, video, and html files.
* Its convenient for rendering NFT media files, as these can be a variety of different types.
* The component falls back to a external link if the media type is not supported.
* The default size is 300px by 300px, but this can be changed using the `width` and `height` props.
*
* Props: {@link MediaRendererProps}
*
@@ -420,10 +435,10 @@ export const MediaRenderer = React.forwardRef<
MediaRendererProps
>(
(
{ src, poster, alt, requireInteraction = false, style, ...restProps },
{ src, poster, alt, requireInteraction = false, width = "300px", height = "300px", style, ...restProps },
ref,
) => {
const mergedStyle: React.CSSProperties = { objectFit: "contain", ...style };
const mergedStyle: React.CSSProperties = { objectFit: "contain", width, height, ...style };
const videoOrImageSrc = useResolvedMediaType(src ?? undefined);
const possiblePosterSrc = useResolvedMediaType(poster ?? undefined);
if (!videoOrImageSrc.mimeType) {
@@ -445,6 +460,30 @@ export const MediaRenderer = React.forwardRef<
{...restProps}
/>
);
} else if (videoOrImageSrc.mimeType.startsWith("model")) {
return (
<Suspense
fallback={
poster ? (
<img
style={mergedStyle}
src={poster}
alt={alt}
ref={ref as unknown as React.LegacyRef<HTMLImageElement>}
{...restProps}
/>
) : null
}
>
<ModelViewer
style={mergedStyle}
src={videoOrImageSrc.url || ""}
poster={poster}
alt={alt}
{...restProps}
/>
</Suspense>
);
} else if (shouldRenderVideoTag(videoOrImageSrc.mimeType)) {
return (
<VideoPlayer
@@ -0,0 +1,34 @@
import type { MediaRendererProps } from "./MediaRenderer";
import "@google/model-viewer";
import type { ModelViewerElement } from "@google/model-viewer";
import React from "react";
declare global {
namespace JSX {
interface IntrinsicElements {
"model-viewer": Partial<ModelViewerElement>;
}
}
}
export const ModelViewer = React.forwardRef<HTMLDivElement, MediaRendererProps>(
({ src, alt, poster, style }, ref) => {
return (
<div style={{ ...style }} ref={ref}>
{src ? (
<model-viewer
src={src}
alt={alt || "3D Model"}
camera-controls
poster={poster ? poster : null}
style={{ width: '100%', height: '100%'} as CSSStyleDeclaration}
/>
) : null}
</div>
);
},
);
ModelViewer.displayName = "ModelViewer";
export default ModelViewer;
@@ -10,10 +10,15 @@ export interface ThirdwebNftMediaProps extends SharedMediaProps {
* The NFT metadata of the NFT returned by the thirdweb sdk.
*/
metadata: NFTMetadata;
width?: string;
height?: string;
}
/**
* This component can be used to render NFTs from the thirdweb SDK.
* It will render the animation_url if it exists, otherwise it will render the image.
* The default size is 300px by 300px, but this can be changed using the `width` and `height` props.
*
* Props: {@link ThirdwebNftMediaProps}
*
* @example
@@ -38,13 +43,16 @@ export interface ThirdwebNftMediaProps extends SharedMediaProps {
export const ThirdwebNftMedia = React.forwardRef<
HTMLMediaElement,
ThirdwebNftMediaProps
>(({ metadata, ...props }, ref) => {
>(({ metadata, width = "300px", height = "300px", style, ...props }, ref) => {
return (
<MediaRenderer
src={metadata.animation_url || metadata.image}
poster={metadata.image}
alt={metadata.name?.toString() || ""}
ref={ref}
width={width}
height={height}
style={{ ...style }}
{...props}
/>
);
File diff suppressed because one or more lines are too long
+64
View File
@@ -1,5 +1,69 @@
# @thirdweb-dev/sdk
## 3.8.2
### Patch Changes
- [#555](https://github.com/thirdweb-dev/js/pull/555) [`55b00ef`](https://github.com/thirdweb-dev/js/commit/55b00eff2668eb751b9f907e035b3a0c122d2183) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add gas estimators to each marketplace plugin
- Updated dependencies [[`76d219e`](https://github.com/thirdweb-dev/js/commit/76d219e6bea1496b45623b3081152854ce1eaa6e)]:
- @thirdweb-dev/contracts-js@1.3.1
- @thirdweb-dev/storage@1.0.8
## 3.8.1
### Patch Changes
- [#550](https://github.com/thirdweb-dev/js/pull/550) [`448b357`](https://github.com/thirdweb-dev/js/commit/448b357ed4b864e05e531837ca3bc97fbcab3291) Thanks [@adam-maj](https://github.com/adam-maj)! - Add solidity stack trace to error messages
## 3.8.0
### Minor Changes
- [#474](https://github.com/thirdweb-dev/js/pull/474) [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Introducing marketplace-v3 class
### Patch Changes
- [#547](https://github.com/thirdweb-dev/js/pull/547) [`0b38dac`](https://github.com/thirdweb-dev/js/commit/0b38dac852e54d0bccd263631f4342aee39d7c29) Thanks [@adam-maj](https://github.com/adam-maj)! - Add proper error hanling for no funds error
- [#545](https://github.com/thirdweb-dev/js/pull/545) [`29d0c62`](https://github.com/thirdweb-dev/js/commit/29d0c62610cdb88f75d19574bddcb028b5c40393) Thanks [@adam-maj](https://github.com/adam-maj)! - Add error info fields to transaction error
- [#474](https://github.com/thirdweb-dev/js/pull/474) [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Support for Plugin Pattern based contracts
- [#539](https://github.com/thirdweb-dev/js/pull/539) [`f295ec6`](https://github.com/thirdweb-dev/js/commit/f295ec67b4cc422c51beda94cbc4d54a31ce9566) Thanks [@adam-maj](https://github.com/adam-maj)! - Upgrade error messages on failed transactions
- [#544](https://github.com/thirdweb-dev/js/pull/544) [`f1aecc3`](https://github.com/thirdweb-dev/js/commit/f1aecc30499e39fb23a205bade939bd939f0d0e4) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Contracts v3.4.0
- Updated dependencies [[`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`912738b`](https://github.com/thirdweb-dev/js/commit/912738bd0afdf81e118c720811911ba3d1979ac0), [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`f1aecc3`](https://github.com/thirdweb-dev/js/commit/f1aecc30499e39fb23a205bade939bd939f0d0e4)]:
- @thirdweb-dev/contracts-js@1.3.0
- @thirdweb-dev/storage@1.0.7
## 3.7.4
### Patch Changes
- [#542](https://github.com/thirdweb-dev/js/pull/542) [`ec1f232`](https://github.com/thirdweb-dev/js/commit/ec1f2324c7a0e58c2d2a209cfe0d194807ba5b70) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Solana - Expose getTransaction pagination options + transactions for given wallet
- [#540](https://github.com/thirdweb-dev/js/pull/540) [`d29f7e7`](https://github.com/thirdweb-dev/js/commit/d29f7e78f09063630e812dd41164d8a41d4f97dd) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - expose `getMintTransaction()` for fine grain control on mint calls
## 3.7.3
### Patch Changes
- [#410](https://github.com/thirdweb-dev/js/pull/410) [`b45d761`](https://github.com/thirdweb-dev/js/commit/b45d761f8f2695b48312ec6eff15e0549e6a8a41) Thanks [@adam-maj](https://github.com/adam-maj)! - Add creator and royalty settings to drop
- [#535](https://github.com/thirdweb-dev/js/pull/535) [`5f8ec45`](https://github.com/thirdweb-dev/js/commit/5f8ec45af7c81d075a84846098f38cad90d03fb9) Thanks [@jnsdls](https://github.com/jnsdls)! - [Solana] - add `burnBatch()` and `getTransactions()` for NFTCollection and NFTDrop
- [#471](https://github.com/thirdweb-dev/js/pull/471) [`04de5b2`](https://github.com/thirdweb-dev/js/commit/04de5b278362573991140c3a496de7451e5c770d) Thanks [@adam-maj](https://github.com/adam-maj)! - Expose getSnapshotEntryForAddress
## 3.7.2
### Patch Changes
- [#525](https://github.com/thirdweb-dev/js/pull/525) [`19b8e18`](https://github.com/thirdweb-dev/js/commit/19b8e18b6bf68e374c817fefbf2fd29ac5573052) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix `getClaimTransaction().estimateGasCostInEther()` for claims costing native currency
## 3.7.1
## 3.7.0
### Minor Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.7.0",
"version": "3.8.2",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
@@ -54,7 +54,7 @@
"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.mjs && node ./scripts/generate-feature-snippets-evm.mjs",
"build": "tsc && preconstruct build",
"test:evm:all": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --config './test/evm/.mocharc.json' --timeout 30000 --parallel './test/evm/**/*.test.ts'",
"test:evm:all": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --config './test/evm/.mocharc.json' --timeout 90000 --parallel './test/evm/**/*.test.ts'",
"test:evm": "make test-evm",
"test:evm:single": "SWC_NODE_PROJECT=./tsconfig.test.json mocha --config './test/evm/.mocharc.json' --timeout 90000",
"node:solana:start": "DEBUG='amman:(info|error|debug)' amman start --forceClone",
+118 -148
View File
@@ -1,5 +1,6 @@
import { Feature } from "../constants/contract-features";
import { NATIVE_TOKENS, SUPPORTED_CHAIN_ID } from "../constants/index";
import { ContractSource } from "../schema";
import { BigNumber, BigNumberish, ethers, providers } from "ethers";
/**
@@ -264,176 +265,137 @@ export type FunctionInfo = {
value: BigNumber;
};
export type TransactionErrorInfo = {
reason: string;
from: string;
to: string;
network: providers.Network;
method?: string;
data?: string;
rpcUrl?: string;
value?: BigNumber;
hash?: string;
contractName?: string;
sources?: ContractSource[];
};
/**
* @public
*/
export class TransactionError extends Error {
public reason: string;
public from: string;
public to: string;
public data: string;
public chain: providers.Network;
public rpcUrl: string;
public functionInfo: FunctionInfo | undefined;
#reason: string;
#info: TransactionErrorInfo;
constructor(
reason: string,
from: string,
to: string,
data: string,
network: providers.Network,
rpcUrl: string,
raw: string,
functionInfo: FunctionInfo | undefined,
) {
let builtErrorMsg = "\n| Contract transaction failed |\n\n";
builtErrorMsg += `Message: ${reason}`;
builtErrorMsg += "\n\n| Transaction info |\n";
builtErrorMsg += withSpaces("from", from);
builtErrorMsg += withSpaces("to", to);
builtErrorMsg += withSpaces(
constructor(info: TransactionErrorInfo) {
let errorMessage = `\n\n\n╔═══════════════════╗\n║ TRANSACTION ERROR ║\n╚═══════════════════╝\n\n`;
errorMessage += `Reason: ${info.reason}`;
errorMessage += `\n\n\n╔═════════════════════════╗\n║ TRANSACTION INFORMATION ║\n╚═════════════════════════╝\n`;
errorMessage += withSpaces("from", info.from);
errorMessage += withSpaces(
"to",
info.contractName ? `${info.to} (${info.contractName})` : info.to,
);
errorMessage += withSpaces(
`chain`,
`${network.name} (${network.chainId})`,
`${info.network.name} (${info.network.chainId})`,
);
if (functionInfo) {
builtErrorMsg += "\n\n| Failed contract call info |\n";
builtErrorMsg += withSpaces("function", functionInfo.signature);
builtErrorMsg += withSpaces(
`arguments`,
JSON.stringify(functionInfo.inputs, null, 2),
if (info.rpcUrl) {
try {
const url = new URL(info.rpcUrl);
errorMessage += withSpaces(`rpc`, url.hostname);
} catch (e2) {
// ignore if can't parse URL
}
}
if (info.hash) {
errorMessage += withSpaces(`tx hash`, info.hash);
}
if (info.value && info.value.gt(0)) {
errorMessage += withSpaces(
"value",
`${ethers.utils.formatEther(info.value)} ${
NATIVE_TOKENS[info.network.chainId as SUPPORTED_CHAIN_ID]?.symbol ||
""
}`,
);
if (functionInfo.value.gt(0)) {
builtErrorMsg += withSpaces(
"value",
`${ethers.utils.formatEther(functionInfo.value)} ${
NATIVE_TOKENS[network.chainId as SUPPORTED_CHAIN_ID]?.symbol
}`,
}
errorMessage += withSpaces(`data`, `${info.data}`);
if (info.method) {
errorMessage += withSpaces("method", info.method);
}
if (info.sources) {
const revertFile = info.sources.find((file) =>
file.source.includes(info.reason),
);
if (revertFile) {
const lines = revertFile.source
.split("\n")
.map((line, index) => `${index + 1} ${line}`);
const revertLine = lines.findIndex((line) =>
line.includes(info.reason),
);
lines[revertLine] += " <-- REVERT";
const errorLines = lines.slice(revertLine - 8, revertLine + 4);
errorMessage += `\n\n\n╔══════════════════════╗\n║ SOLIDITY STACK TRACE ║\n╚══════════════════════╝\n\n`;
errorMessage += `File: ${revertFile.filename.replace(
"node_modules/",
"",
)}\n\n`;
errorMessage += errorLines.join("\n");
}
}
try {
const url = new URL(rpcUrl);
builtErrorMsg += withSpaces(`RPC`, url.hostname);
} catch (e2) {
// ignore if can't parse URL
}
builtErrorMsg += "\n\n";
builtErrorMsg += "| Raw error |";
builtErrorMsg += "\n\n";
builtErrorMsg += raw;
super(builtErrorMsg);
this.reason = reason;
this.from = from;
this.to = to;
this.data = data;
this.chain = network;
this.rpcUrl = rpcUrl;
this.functionInfo = functionInfo;
errorMessage += `\n\n\n╔═════════════════════╗\n║ DEBUGGING RESOURCES ║\n╚═════════════════════╝\n\n`;
errorMessage += `Need helping debugging? Join our Discord: https://discord.gg/thirdweb`;
errorMessage += `\n\n`;
super(errorMessage);
this.#reason = info.reason;
this.#info = info;
}
// Keep reason here for backwards compatibility
get reason(): string {
return this.#reason;
}
get info(): TransactionErrorInfo {
return this.#info;
}
}
/**
* @internal
* @param data
* @param contractInterface
*/
function parseFunctionInfo(
data: string,
contractInterface: ethers.utils.Interface,
): FunctionInfo | undefined {
try {
const fnFragment = contractInterface.parseTransaction({
data,
});
const results: Record<string, any> = {};
const args = fnFragment.args;
fnFragment.functionFragment.inputs.forEach((param, index) => {
if (Array.isArray(args[index])) {
const obj: Record<string, unknown> = {};
const components = param.components;
if (components) {
const arr = args[index];
for (let i = 0; i < components.length; i++) {
const name = components[i].name;
obj[name] = arr[i];
}
results[param.name] = obj;
}
} else {
results[param.name] = args[index];
}
});
return {
signature: fnFragment.signature,
inputs: results,
value: fnFragment.value,
};
} catch (e) {
return undefined;
export function parseRevertReason(error: any): string {
if (error.reason) {
return error.reason as string;
}
}
/**
* @internal
* @param error
* @param network
* @param signerAddress
* @param contractAddress
* @param contractInterface
*/
export async function convertToTWError(
error: any,
network: ethers.providers.Network,
signerAddress: string,
contractAddress: string,
contractInterface: ethers.utils.Interface,
): Promise<TransactionError> {
let raw: string;
// I think this code path should never be hit, but just in case
let errorString: string = error;
if (typeof error === "object") {
// metamask errors comes as objects, apply parsing on data object
raw = JSON.stringify(error);
} else {
// not sure what this is, just throw it back
raw = error.toString();
// MetaMask errors come as objects so parse them first
errorString = JSON.stringify(error);
} else if (typeof error !== "string") {
errorString = error.toString();
}
let reason =
parseMessageParts(/.*?"message[^a-zA-Z0-9]*([^"\\]*).*?/, raw) ||
parseMessageParts(/.*?"reason[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
if (reason && reason.toLowerCase().includes("cannot estimate gas")) {
// the error might be in the next reason block
const nextReason = parseMessageParts(
/.*?"reason[^a-zA-Z0-9]*([^"\\]*).*?/,
raw.slice(raw.indexOf(reason) + reason.length),
);
if (nextReason) {
reason = nextReason;
}
}
const data = parseMessageParts(/.*?"data[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
const rpcUrl = parseMessageParts(/.*?"url[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
let from = parseMessageParts(/.*?"from[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
let to = parseMessageParts(/.*?"to[^a-zA-Z0-9]*([^"\\]*).*?/, raw);
if (to === "") {
// fallback to contractAddress
to = contractAddress;
}
if (from === "") {
// fallback to signerAddress
from = signerAddress;
}
const functionInfo =
data.length > 0 ? parseFunctionInfo(data, contractInterface) : undefined;
return new TransactionError(
reason,
from,
to,
data,
network,
rpcUrl,
raw,
functionInfo,
return (
parseMessageParts(/.*?"message":"([^"\\]*).*?/, errorString) ||
parseMessageParts(/.*?"reason":"([^"\\]*).*?/, errorString) ||
error.message ||
""
);
}
@@ -441,10 +403,18 @@ function withSpaces(label: string, content: string) {
if (content === "") {
return content;
}
const spaces = Array(10 - label.length)
.fill(" ")
.join("");
return `\n${label}:${spaces}${content}`;
if (content.includes("\n")) {
content = "\n\n " + content.split("\n").join(`\n `);
} else {
content = `${spaces}${content}`;
}
return `\n${label}:${content}`;
}
function parseMessageParts(regex: RegExp, raw: string): string {
+4 -1
View File
@@ -10,4 +10,7 @@ export {
fetchCurrencyMetadata,
normalizePriceValue,
} from "./currency";
export { convertToReadableQuantity } from "./claim-conditions";
export {
convertToReadableQuantity,
fetchSnapshotEntryForAddress,
} from "./claim-conditions";
+23 -1
View File
@@ -1,3 +1,4 @@
import { DEFAULT_QUERY_ALL_COUNT } from "../../core/schema/QueryParams";
import { MAX_BPS } from "../../core/schema/shared";
import {
InterfaceId_IERC1155,
@@ -15,7 +16,13 @@ import type { IERC1155, IERC165, IERC721 } from "@thirdweb-dev/contracts-js";
import ERC165Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC165.json";
import ERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC721.json";
import ERC1155Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC1155.json";
import { BigNumber, BigNumberish, Contract, providers } from "ethers";
import {
BigNumber,
BigNumberish,
Contract,
ContractFunction,
providers,
} from "ethers";
import invariant from "tiny-invariant";
/**
@@ -235,3 +242,18 @@ export function isWinningBid(
const buffer = newBidPrice.sub(winningPrice).mul(MAX_BPS).div(winningPrice);
return buffer.gte(bidBuffer);
}
export async function getAllInBatches(
start: number,
end: number,
fn: ContractFunction,
): Promise<any[]> {
let batches: any[] = [];
while (end - start > DEFAULT_QUERY_ALL_COUNT) {
batches.push(fn(start, start + DEFAULT_QUERY_ALL_COUNT - 1));
start += DEFAULT_QUERY_ALL_COUNT;
}
batches.push(fn(start, end - 1));
return await Promise.all(batches);
}
+3 -2
View File
@@ -26,7 +26,6 @@ export async function getCompositePluginABI(
AbiSchema.parse(abi),
"PluginRouter",
);
if (isPluginRouter) {
const contract = new ContractWrapper<IRouter>(
provider,
@@ -85,5 +84,7 @@ export function joinABIs(abis: Abi[]): Abi {
);
});
return AbiSchema.parse(filteredABIs);
const finalABIs = filteredABIs.filter((item) => item.type !== "constructor");
return AbiSchema.parse(finalABIs);
}
@@ -55,6 +55,9 @@ import {
FEATURE_GASLESS,
FEATURE_PACK_VRF,
FEATURE_PLUGIN_ROUTER,
FEATURE_DIRECT_LISTINGS,
FEATURE_ENGLISH_AUCTIONS,
FEATURE_OFFERS,
} from "./thirdweb-features";
/**
@@ -110,7 +113,10 @@ export type Feature =
| typeof FEATURE_OWNER
| typeof FEATURE_GASLESS
| typeof FEATURE_PACK_VRF
| typeof FEATURE_PLUGIN_ROUTER;
| typeof FEATURE_PLUGIN_ROUTER
| typeof FEATURE_DIRECT_LISTINGS
| typeof FEATURE_ENGLISH_AUCTIONS
| typeof FEATURE_OFFERS;
/**
* @internal
@@ -141,4 +147,7 @@ export const SUPPORTED_FEATURES: Record<string, Feature> = {
[FEATURE_GASLESS.name]: FEATURE_GASLESS,
[FEATURE_PACK_VRF.name]: FEATURE_PACK_VRF,
[FEATURE_PLUGIN_ROUTER.name]: FEATURE_PLUGIN_ROUTER,
[FEATURE_DIRECT_LISTINGS.name]: FEATURE_DIRECT_LISTINGS,
[FEATURE_ENGLISH_AUCTIONS.name]: FEATURE_ENGLISH_AUCTIONS,
[FEATURE_OFFERS.name]: FEATURE_OFFERS,
};
@@ -1,6 +1,9 @@
import IERC2771ContextAbi from "@thirdweb-dev/contracts-js/dist/abis/ERC2771Context.json";
import IAppURI from "@thirdweb-dev/contracts-js/dist/abis/IAppURI.json";
import IContractMetadataAbi from "@thirdweb-dev/contracts-js/dist/abis/IContractMetadata.json";
import IDirectListingsAbi from "@thirdweb-dev/contracts-js/dist/abis/IDirectListings.json";
import IEnglishAuctionsAbi from "@thirdweb-dev/contracts-js/dist/abis/IEnglishAuctions.json";
import IOffersAbi from "@thirdweb-dev/contracts-js/dist/abis/IOffers.json";
import IPackVRFAbi from "@thirdweb-dev/contracts-js/dist/abis/IPackVRFDirect.json";
import IPermissionsAbi from "@thirdweb-dev/contracts-js/dist/abis/IPermissions.json";
import IPermissionsEnumerableAbi from "@thirdweb-dev/contracts-js/dist/abis/IPermissionsEnumerable.json";
@@ -133,3 +136,39 @@ export const FEATURE_PLUGIN_ROUTER = {
abis: [IRouterAbi],
features: {},
} as const;
export const FEATURE_DIRECT_LISTINGS = {
name: "DirectListings",
namespace: "direct.listings",
docLinks: {
// TODO
sdk: "",
contracts: "",
},
abis: [IDirectListingsAbi],
features: {},
} as const;
export const FEATURE_ENGLISH_AUCTIONS = {
name: "EnglishAuctions",
namespace: "english.auctions",
docLinks: {
// TODO
sdk: "",
contracts: "",
},
abis: [IEnglishAuctionsAbi],
features: {},
} as const;
export const FEATURE_OFFERS = {
name: "Offers",
namespace: "offers",
docLinks: {
// TODO
sdk: "",
contracts: "",
},
abis: [IOffersAbi],
features: {},
} as const;
+58 -1
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { getPrebuiltInfo } from "../common/legacy";
import { fetchAbiFromAddress } from "../common/metadata-resolver";
import { getCompositePluginABI } from "../common/plugin";
import { ALL_ROLES } from "../common/role";
import { getSignerAndProvider } from "../core/classes/rpc-connection-handler";
import type {
@@ -20,7 +21,11 @@ import {
TokenErc721ContractSchema,
VoteContractSchema,
} from "../schema";
import { Abi, CustomContractSchema } from "../schema/contracts/custom";
import {
Abi,
AbiSchema,
CustomContractSchema,
} from "../schema/contracts/custom";
import { DropErc20ContractSchema } from "../schema/contracts/drop-erc20";
import { MultiwrapContractSchema } from "../schema/contracts/multiwrap";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
@@ -152,6 +157,7 @@ export const MarketplaceInitializer = {
if (abi) {
return abi;
}
// Deprecated - only needed for backwards compatibility with non-released contracts - should remove in v4
return (
await import("@thirdweb-dev/contracts-js/dist/abis/Marketplace.json")
@@ -159,6 +165,56 @@ export const MarketplaceInitializer = {
},
};
export const MarketplaceV3Initializer = {
name: "MarketplaceRouter" as const,
contractType: "marketplace-v3" as const,
schema: MarketplaceContractSchema,
roles: ["admin", "lister", "asset"] as const,
initialize: async (
...[network, address, storage, options]: InitalizeParams
) => {
const [, provider] = getSignerAndProvider(network, options);
const [abi, contract, _network] = await Promise.all([
MarketplaceV3Initializer.getAbi(address, provider, storage),
import("./prebuilt-implementations/marketplacev3"),
provider.getNetwork(),
]);
const parsedABI = typeof abi === "string" ? JSON.parse(abi) : abi;
const compositeABI = await getCompositePluginABI(
address,
AbiSchema.parse(parsedABI),
provider,
options,
storage,
);
return new contract.MarketplaceV3(
network,
address,
storage,
options,
compositeABI,
_network.chainId,
);
},
getAbi: async (
address: string,
provider: ethers.providers.Provider,
storage: ThirdwebStorage,
) => {
const abi = await fetchAbiFromAddress(address, provider, storage);
if (abi) {
return abi;
}
// Deprecated - only needed for backwards compatibility with non-released contracts - should remove in v4
return (
await import("@thirdweb-dev/contracts-js/dist/abis/MarketplaceV3.json")
).default;
},
};
export const MultiwrapInitializer = {
name: "Multiwrap" as const,
contractType: "multiwrap" as const,
@@ -558,6 +614,7 @@ export const PREBUILT_CONTRACTS_MAP = {
[EditionDropInitializer.contractType]: EditionDropInitializer,
[EditionInitializer.contractType]: EditionInitializer,
[MarketplaceInitializer.contractType]: MarketplaceInitializer,
[MarketplaceV3Initializer.contractType]: MarketplaceV3Initializer,
[MultiwrapInitializer.contractType]: MultiwrapInitializer,
[NFTCollectionInitializer.contractType]: NFTCollectionInitializer,
[NFTDropInitializer.contractType]: NFTDropInitializer,
@@ -258,6 +258,19 @@ export class Edition extends StandardErc1155<TokenERC1155> {
return this.erc1155.mintTo(to, metadataWithSupply);
}
/**
* Construct a mint transaction without executing it.
* This is useful for estimating the gas cost of a mint transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param receiver - Address you want to send the token to
* @param metadataWithSupply - The metadata of the NFT you want to mint
*/
public async getMintTransaction(
receiver: string,
metadataWithSupply: EditionMetadataOrUri,
) {
return this.erc1155.getMintTransaction(receiver, metadataWithSupply);
}
/**
* Increase the supply of an existing NFT and mint it to the connected wallet
*
@@ -0,0 +1,294 @@
import { assertEnabled, detectContractFeature } from "../../common";
import {
FEATURE_DIRECT_LISTINGS,
FEATURE_ENGLISH_AUCTIONS,
FEATURE_OFFERS,
} from "../../constants/thirdweb-features";
import { ContractEncoder } from "../../core/classes/contract-encoder";
import { ContractEvents } from "../../core/classes/contract-events";
import { ContractInterceptor } from "../../core/classes/contract-interceptor";
import { ContractMetadata } from "../../core/classes/contract-metadata";
import { ContractPlatformFee } from "../../core/classes/contract-platform-fee";
import { ContractRoles } from "../../core/classes/contract-roles";
import { ContractWrapper } from "../../core/classes/contract-wrapper";
import { GasCostEstimator } from "../../core/classes/gas-cost-estimator";
import { MarketplaceV3DirectListings } from "../../core/classes/marketplacev3-direct-listings";
import { MarketplaceV3EnglishAuctions } from "../../core/classes/marketplacev3-english-auction";
import { MarketplaceV3Offers } from "../../core/classes/marketplacev3-offers";
import { UpdateableNetwork } from "../../core/interfaces/contract";
import { NetworkOrSignerOrProvider } from "../../core/types";
import { Abi } from "../../schema/contracts/custom";
import { MarketplaceContractSchema } from "../../schema/contracts/marketplace";
import { SDKOptions } from "../../schema/sdk-options";
import type {
MarketplaceV3 as MarketplaceV3Contract,
DirectListingsLogic,
EnglishAuctionsLogic,
OffersLogic,
} from "@thirdweb-dev/contracts-js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { CallOverrides } from "ethers";
/**
* Create your own whitelabel marketplace that enables users to buy and sell any digital assets.
*
* @example
*
* ```javascript
* import { ThirdwebSDK } from "@thirdweb-dev/sdk";
*
* const sdk = new ThirdwebSDK("{{chainName}}");
* const contract = await sdk.getContract("{{contract_address}}", "marketplace");
* ```
*
* @public
*/
export class MarketplaceV3 implements UpdateableNetwork {
static contractRoles = ["admin", "lister", "asset"] as const;
public abi: Abi;
private contractWrapper: ContractWrapper<MarketplaceV3Contract>;
private storage: ThirdwebStorage;
public encoder: ContractEncoder<MarketplaceV3Contract>;
public events: ContractEvents<MarketplaceV3Contract>;
public estimator: GasCostEstimator<MarketplaceV3Contract>;
public platformFees: ContractPlatformFee<MarketplaceV3Contract>;
public metadata: ContractMetadata<
MarketplaceV3Contract,
typeof MarketplaceContractSchema
>;
public roles: ContractRoles<
MarketplaceV3Contract,
typeof MarketplaceV3.contractRoles[number]
>;
/**
* @internal
*/
public interceptor: ContractInterceptor<MarketplaceV3Contract>;
/**
* Direct listings
* @remarks Create and manage direct listings in your marketplace.
* ```javascript
* // Data of the listing you want to create
* const listing = {
* // address of the contract the asset you want to list is on
* assetContractAddress: "0x...",
* // token ID of the asset you want to list
* tokenId: "0",
* // how many of the asset you want to list
* quantity: 1,
* // address of the currency contract that will be used to pay for the listing
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // The price to pay per unit of NFTs listed.
* pricePerToken: 1.5,
* // when should the listing open up for offers
* startTimestamp: new Date(Date.now()),
* // how long the listing will be open for
* endTimestamp: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
* // Whether the listing is reserved for a specific set of buyers.
* isReservedListing: false
* }
*
* const tx = await contract.directListings.createListing(listing);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created listing
*
* // And on the buyers side:
* // The ID of the listing you want to buy from
* const listingId = 0;
* // Quantity of the asset you want to buy
* const quantityDesired = 1;
*
* await contract.directListings.buyFromListing(listingId, quantityDesired);
* ```
*/
get directListings(): MarketplaceV3DirectListings<DirectListingsLogic> {
return assertEnabled(this.detectDirectListings(), FEATURE_DIRECT_LISTINGS);
}
/**
* Auctions
* @remarks Create and manage auctions in your marketplace.
* @example
* ```javascript
* // Data of the auction you want to create
* const auction = {
* // address of the contract of the asset you want to auction
* assetContractAddress: "0x...",
* // token ID of the asset you want to auction
* tokenId: "0",
* // how many of the asset you want to auction
* quantity: 1,
* // address of the currency contract that will be used to pay for the auctioned tokens
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // the minimum bid that will be accepted for the token
* minimumBidAmount: "1.5",
* // how much people would have to bid to instantly buy the asset
* buyoutBidAmount: "10",
* // If a bid is made less than these many seconds before expiration, the expiration time is increased by this.
* timeBufferInSeconds: "1000",
* // A bid must be at least this much bps greater than the current winning bid
* bidBufferBps: "100", // 100 bps stands for 1%
* // when should the auction open up for bidding
* startTimestamp: new Date(Date.now()),
* // end time of auction
* endTimestamp: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
* }
*
* const tx = await contract.englishAuctions.createAuction(auction);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created auction
*
* // And on the buyers side:
* // The auction ID of the asset you want to bid on
* const auctionId = 0;
* // The total amount you are willing to bid for auctioned tokens
* const bidAmount = 1;
*
* await contract.englishAuctions.makeBid(auctionId, bidAmount);
* ```
*/
get englishAuctions(): MarketplaceV3EnglishAuctions<EnglishAuctionsLogic> {
return assertEnabled(
this.detectEnglishAuctions(),
FEATURE_ENGLISH_AUCTIONS,
);
}
/**
* Offers
* @remarks Make and manage offers.
* @example
* ```javascript
* // Data of the offer you want to make
* const offer = {
* // address of the contract the asset you want to make an offer for
* assetContractAddress: "0x...",
* // token ID of the asset you want to buy
* tokenId: "0",
* // how many of the asset you want to buy
* quantity: 1,
* // address of the currency contract that you offer to pay in
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // Total price you offer to pay for the mentioned token(s)
* totalPrice: "1.5",
* // Offer valid until
* endTimestamp: new Date(),
* }
*
* const tx = await contract.offers.makeOffer(offer);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created offer
*
* // And on the seller's side:
* // The ID of the offer you want to accept
* const offerId = 0;
* await contract.offers.acceptOffer(offerId);
* ```
*/
get offers(): MarketplaceV3Offers<OffersLogic> {
return assertEnabled(this.detectOffers(), FEATURE_OFFERS);
}
private _chainId;
get chainId() {
return this._chainId;
}
constructor(
network: NetworkOrSignerOrProvider,
address: string,
storage: ThirdwebStorage,
options: SDKOptions = {},
abi: Abi,
chainId: number,
contractWrapper = new ContractWrapper<MarketplaceV3Contract>(
network,
address,
abi,
options,
),
) {
this._chainId = chainId;
this.abi = abi;
this.contractWrapper = contractWrapper;
this.storage = storage;
this.metadata = new ContractMetadata(
this.contractWrapper,
MarketplaceContractSchema,
this.storage,
);
this.roles = new ContractRoles(
this.contractWrapper,
MarketplaceV3.contractRoles,
);
this.encoder = new ContractEncoder(this.contractWrapper);
this.estimator = new GasCostEstimator(this.contractWrapper);
this.events = new ContractEvents(this.contractWrapper);
this.platformFees = new ContractPlatformFee(this.contractWrapper);
this.interceptor = new ContractInterceptor(this.contractWrapper);
}
onNetworkUpdated(network: NetworkOrSignerOrProvider) {
this.contractWrapper.updateSignerOrProvider(network);
}
getAddress(): string {
return this.contractWrapper.readContract.address;
}
/**
* @internal
*/
public async call(
functionName: string,
...args: unknown[] | [...unknown[], CallOverrides]
): Promise<any> {
return this.contractWrapper.call(functionName, ...args);
}
/** ********************
* FEATURE DETECTION
* ********************/
private detectDirectListings() {
if (
detectContractFeature<DirectListingsLogic>(
this.contractWrapper,
"DirectListings",
)
) {
return new MarketplaceV3DirectListings(
this.contractWrapper as unknown as ContractWrapper<DirectListingsLogic>,
this.storage,
);
}
return undefined;
}
private detectEnglishAuctions() {
if (
detectContractFeature<EnglishAuctionsLogic>(
this.contractWrapper,
"EnglishAuctions",
)
) {
return new MarketplaceV3EnglishAuctions(
this
.contractWrapper as unknown as ContractWrapper<EnglishAuctionsLogic>,
this.storage,
);
}
return undefined;
}
private detectOffers() {
if (detectContractFeature<OffersLogic>(this.contractWrapper, "Offers")) {
return new MarketplaceV3Offers(
this.contractWrapper as unknown as ContractWrapper<OffersLogic>,
this.storage,
);
}
return undefined;
}
}
@@ -229,6 +229,19 @@ export class NFTCollection extends StandardErc721<TokenERC721> {
return this.erc721.mintTo(walletAddress, metadata);
}
/**
* Construct a mint transaction without executing it.
* This is useful for estimating the gas cost of a mint transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param receiver - Address you want to send the token to
* @param metadata - The metadata of the NFT you want to mint
*/
public async getMintTransaction(
receiver: string,
metadata: NFTMetadataOrUri,
) {
return this.erc721.getMintTransaction(receiver, metadata);
}
/**
* Mint Many unique NFTs
*
@@ -186,6 +186,16 @@ export class Token extends StandardErc20<TokenERC20> {
return this.erc20.mintTo(to, amount);
}
/**
* Construct a mint transaction without executing it.
* This is useful for estimating the gas cost of a mint transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param receiver - Address you want to send the token to
* @param amount - The amount of tokens you want to mint
*/
public async getMintTransaction(to: string, amount: Amount) {
return this.erc20.getMintTransaction(to, amount);
}
/**
* Mint Tokens To Many Wallets
*
@@ -104,7 +104,10 @@ export class TransactionTask {
* @returns the gas limit in gas units
*/
public async estimateGasLimit(): Promise<BigNumber> {
return await this.estimator.gasLimitOf(this.functionName, this.args);
return await this.estimator.gasLimitOf(this.functionName, [
...this.args,
this.overrides || {},
]);
}
/**
@@ -112,7 +115,10 @@ export class TransactionTask {
* @returns the gas cost in ether
*/
public async estimateGasCostInEther(): Promise<string> {
return await this.estimator.gasCostOf(this.functionName, this.args);
return await this.estimator.gasCostOf(this.functionName, [
...this.args,
this.overrides || {},
]);
}
// ////////////// Actions ////////////////
@@ -11,6 +11,7 @@ import {
EditionInitializer,
getContractName,
MarketplaceInitializer,
MarketplaceV3Initializer,
MultiwrapInitializer,
NFTCollectionInitializer,
NFTDropInitializer,
@@ -27,6 +28,7 @@ import { SDKOptions } from "../../schema/sdk-options";
import { DeployEvent, DeployEvents } from "../../types";
import {
MarketplaceContractDeployMetadata,
MarketplaceV3ContractDeployMetadata,
MultiwrapContractDeployMetadata,
NFTContractDeployMetadata,
SplitContractDeployMetadata,
@@ -309,6 +311,30 @@ export class ContractDeployer extends RPCConnectionHandler {
);
}
/**
* Deploys a new Marketplace-V3 contract
*
* @remarks Deploys a Marketplace-V3 contract and returns the address of the deployed contract
*
* @example
* ```javascript
* const contractAddress = await sdk.deployer.deployMarketplaceV3({
* name: "My Marketplace",
* primary_sale_recipient: "your-address",
* });
* ```
* @param metadata - the contract metadata
* @returns the address of the deployed contract
*/
public async deployMarketplaceV3(
metadata: MarketplaceV3ContractDeployMetadata,
): Promise<string> {
return await this.deployBuiltInContract(
MarketplaceV3Initializer.contractType,
metadata,
);
}
/**
* Deploys a new Pack contract
*
@@ -463,7 +489,6 @@ export class ContractDeployer extends RPCConnectionHandler {
parsedMetadata,
contractURI,
);
return this.deployReleasedContract(
// 0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024 === deployer.thirdweb.eth
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
@@ -1,4 +1,10 @@
import { convertToTWError, extractFunctionsFromAbi } from "../../common";
import {
TransactionError,
extractFunctionsFromAbi,
fetchContractMetadataFromAddress,
fetchSourceFilesFromMetadata,
parseRevertReason,
} from "../../common";
import {
BiconomyForwarderAbi,
ChainAwareForwardRequest,
@@ -13,7 +19,7 @@ import { CONTRACT_ADDRESSES, ChainId } from "../../constants";
import { getContractAddressByChainId } from "../../constants/addresses";
import { EventType } from "../../constants/events";
import { CallOverrideSchema } from "../../schema";
import { AbiSchema } from "../../schema/contracts/custom";
import { AbiSchema, ContractSource } from "../../schema/contracts/custom";
import { SDKOptions } from "../../schema/sdk-options";
import {
ForwardRequestMessage,
@@ -23,6 +29,7 @@ import {
} from "../types";
import { RPCConnectionHandler } from "./rpc-connection-handler";
import ForwarderABI from "@thirdweb-dev/contracts-js/dist/abis/Forwarder.json";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import fetch from "cross-fetch";
import {
BaseContract,
@@ -35,6 +42,7 @@ import {
ethers,
providers,
} from "ethers";
import { ConnectionInfo } from "ethers/lib/utils.js";
import invariant from "tiny-invariant";
/**
@@ -43,6 +51,9 @@ import invariant from "tiny-invariant";
export class ContractWrapper<
TContract extends BaseContract,
> extends RPCConnectionHandler {
// TOOO: In another PR, make this storage private, and have extending classes pass
// down storage to be stored in contract wrapper.
#storage: ThirdwebStorage;
private isValidContract = false;
private customOverrides: () => CallOverrides = () => ({});
/**
@@ -70,6 +81,7 @@ export class ContractWrapper<
this.readContract = this.writeContract.connect(
this.getProvider(),
) as TContract;
this.#storage = new ThirdwebStorage();
}
public override updateSignerOrProvider(
@@ -375,7 +387,26 @@ export class ContractWrapper<
callOverrides,
);
this.emitTransactionEvent("submitted", tx.hash);
const receipt = tx.wait();
// tx.wait() can fail so we need to wrap it with a catch
let receipt;
try {
receipt = await tx.wait();
} catch (err) {
try {
// If tx.wait() fails, it just gives us a generic "transaction failed"
// error. So instead, we need to call static to get an informative error message
await this.writeContract.callStatic[fn as string](
...args,
...(callOverrides.value ? [{ value: callOverrides.value }] : []),
);
} catch (staticErr: any) {
throw await this.formatError(staticErr, fn, args, callOverrides);
}
throw await this.formatError(err, fn, args, callOverrides);
}
this.emitTransactionEvent("completed", tx.hash);
return receipt;
}
@@ -395,22 +426,133 @@ export class ContractWrapper<
if (!func) {
throw new Error(`invalid function: "${fn.toString()}"`);
}
// First, if no gasLimit is passed, call estimate gas ourselves
if (!callOverrides.gasLimit) {
try {
callOverrides.gasLimit = await this.writeContract.estimateGas[
fn as string
](...args, callOverrides);
} catch (e) {
// If gas estimation fails, we'll call static to get a better error message
try {
await this.writeContract.callStatic[fn as string](
...args,
...(callOverrides.value ? [{ value: callOverrides.value }] : []),
);
} catch (err: any) {
throw await this.formatError(err, fn, args, callOverrides);
}
}
}
// Now there should be no gas estimate errors
try {
return await func(...args, callOverrides);
} catch (e) {
const network = await this.getProvider().getNetwork();
const signerAddress = await this.getSignerAddress();
const contractAddress = await this.readContract.address;
throw await convertToTWError(
e,
network,
signerAddress,
contractAddress,
this.readContract.interface,
);
} catch (err) {
const from = await (callOverrides.from || this.getSignerAddress());
const value = await (callOverrides.value ? callOverrides.value : 0);
const balance = await this.getProvider().getBalance(from);
if (balance.eq(0) || (value && balance.lt(value))) {
throw await this.formatError(
new Error(
"You have insufficient funds in your account to execute this transaction.",
),
fn,
args,
callOverrides,
);
}
throw await this.formatError(err, fn, args, callOverrides);
}
}
private async formatError(
error: any,
fn: keyof TContract["functions"],
args: any[],
callOverrides: CallOverrides,
) {
const provider = this.getProvider() as ethers.providers.Provider & {
connection?: ConnectionInfo;
};
// Get metadata for transaction to populate into error
const network = await provider.getNetwork();
const from = await (callOverrides.from || this.getSignerAddress());
const to = this.readContract.address;
const data = this.readContract.interface.encodeFunctionData(
fn as string,
args,
);
const value = BigNumber.from(callOverrides.value || 0);
const rpcUrl = provider.connection?.url;
// Render function signature with arguments filled in
const functionSignature = this.readContract.interface.getFunction(
fn as string,
);
const methodArgs = args.map((arg) => {
if (JSON.stringify(arg).length <= 80) {
return JSON.stringify(arg);
}
return JSON.stringify(arg, undefined, 2);
});
const joinedArgs =
methodArgs.join(", ").length <= 80
? methodArgs.join(", ")
: "\n" +
methodArgs
.map((arg) => " " + arg.split("\n").join("\n "))
.join(",\n") +
"\n";
const method = `${functionSignature.name}(${joinedArgs})`;
const hash =
error.transactionHash ||
error.transaction?.hash ||
error.receipt?.transactionHash;
// Parse the revert reason from the error
const reason = parseRevertReason(error);
// Get contract sources for stack trace
let sources: ContractSource[] | undefined = undefined;
let contractName: string | undefined = undefined;
try {
const metadata = await fetchContractMetadataFromAddress(
this.readContract.address,
this.getProvider(),
this.#storage,
);
if (metadata.name) {
contractName = metadata.name;
}
if (metadata.metadata.sources) {
sources = await fetchSourceFilesFromMetadata(metadata, this.#storage);
}
} catch (err) {
// no-op
}
return new TransactionError({
reason,
from,
to,
method,
data,
network,
rpcUrl,
value,
hash,
contractName,
sources,
});
}
/**
* @internal
*/
@@ -5,6 +5,7 @@ import { FEATURE_EDITION_MINTABLE } from "../../constants/erc1155-features";
import { EditionMetadataOrUri } from "../../schema";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResultWithId } from "../types";
import { TransactionTask } from "./TransactionTask";
import { ContractWrapper } from "./contract-wrapper";
import { Erc1155 } from "./erc-1155";
import { Erc1155BatchMintable } from "./erc-1155-batch-mintable";
@@ -77,16 +78,8 @@ export class Erc1155Mintable implements DetectableFeature {
to: string,
metadataWithSupply: EditionMetadataOrUri,
): Promise<TransactionResultWithId<NFT>> {
const uri = await uploadOrExtractURI(
metadataWithSupply.metadata,
this.storage,
);
const receipt = await this.contractWrapper.sendTransaction("mintTo", [
to,
ethers.constants.MaxUint256,
uri,
metadataWithSupply.supply,
]);
const tx = await this.getMintTransaction(to, metadataWithSupply);
const { receipt } = await tx.execute();
const event = this.contractWrapper.parseLogs<TransferSingleEvent>(
"TransferSingle",
receipt?.logs,
@@ -102,6 +95,21 @@ export class Erc1155Mintable implements DetectableFeature {
};
}
public async getMintTransaction(
to: string,
metadataWithSupply: EditionMetadataOrUri,
): Promise<TransactionTask> {
const uri = await uploadOrExtractURI(
metadataWithSupply.metadata,
this.storage,
);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "mintTo",
args: [to, ethers.constants.MaxUint256, uri, metadataWithSupply.supply],
});
}
/**
* Increase the supply of an existing NFT and mint it to a given wallet address
*
@@ -473,6 +473,22 @@ export class Erc1155<
);
}
/**
* Construct a mint transaction without executing it.
* This is useful for estimating the gas cost of a mint transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param receiver - Address you want to send the token to
* @param metadataWithSupply - The metadata of the NFT you want to mint
*/
public async getMintTransaction(
receiver: string,
metadataWithSupply: EditionMetadataOrUri,
) {
return assertEnabled(
this.mintable,
FEATURE_EDITION_MINTABLE,
).getMintTransaction(receiver, metadataWithSupply);
}
/**
* Increase the supply of an existing NFT
* @remarks Increase the supply of an existing NFT and mint it to the connected wallet address
@@ -3,6 +3,7 @@ import { FEATURE_TOKEN_MINTABLE } from "../../constants/erc20-features";
import { Amount } from "../../types";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult } from "../types";
import { TransactionTask } from "./TransactionTask";
import { ContractWrapper } from "./contract-wrapper";
import { Erc20 } from "./erc-20";
import { Erc20BatchMintable } from "./erc-20-batch-mintable";
@@ -47,12 +48,19 @@ export class Erc20Mintable implements DetectableFeature {
* ```
*/
public async to(to: string, amount: Amount): Promise<TransactionResult> {
return {
receipt: await this.contractWrapper.sendTransaction("mintTo", [
to,
await this.erc20.normalizeAmount(amount),
]),
};
const tx = await this.getMintTransaction(to, amount);
return tx.execute();
}
public async getMintTransaction(
to: string,
amount: Amount,
): Promise<TransactionTask> {
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "mintTo",
args: [to, await this.erc20.normalizeAmount(amount)],
});
}
private detectErc20BatchMintable() {
@@ -376,6 +376,19 @@ export class Erc20<
);
}
/**
* Construct a mint transaction without executing it.
* This is useful for estimating the gas cost of a mint transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param receiver - Address you want to send the token to
* @param amount - The amount of tokens you want to mint
*/
public async getMintTransaction(receiver: string, amount: Amount) {
return assertEnabled(
this.mintable,
FEATURE_TOKEN_MINTABLE,
).getMintTransaction(receiver, amount);
}
////// ERC20 BatchMintable Extension //////
/**
@@ -4,6 +4,7 @@ import { uploadOrExtractURI } from "../../common/nft";
import { FEATURE_NFT_MINTABLE } from "../../constants/erc721-features";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResultWithId } from "../types";
import { TransactionTask } from "./TransactionTask";
import { ContractWrapper } from "./contract-wrapper";
import { Erc721 } from "./erc-721";
import { Erc721BatchMintable } from "./erc-721-batch-mintable";
@@ -67,11 +68,8 @@ export class Erc721Mintable implements DetectableFeature {
to: string,
metadata: NFTMetadataOrUri,
): Promise<TransactionResultWithId<NFT>> {
const uri = await uploadOrExtractURI(metadata, this.storage);
const receipt = await this.contractWrapper.sendTransaction("mintTo", [
to,
uri,
]);
const tx = await this.getMintTransaction(to, metadata);
const { receipt } = await tx.execute();
const event = this.contractWrapper.parseLogs<TransferEvent>(
"Transfer",
receipt?.logs,
@@ -87,6 +85,18 @@ export class Erc721Mintable implements DetectableFeature {
};
}
public async getMintTransaction(
to: string,
metadata: NFTMetadataOrUri,
): Promise<TransactionTask> {
const uri = await uploadOrExtractURI(metadata, this.storage);
return TransactionTask.make({
contractWrapper: this.contractWrapper,
functionName: "mintTo",
args: [to, uri],
});
}
private detectErc721BatchMintable(): Erc721BatchMintable | undefined {
if (
detectContractFeature<IMintableERC721 & IMulticall>(
@@ -409,6 +409,22 @@ export class Erc721<
);
}
/**
* Construct a mint transaction without executing it.
* This is useful for estimating the gas cost of a mint transaction, overriding transaction options and having fine grained control over the transaction execution.
* @param receiver - Address you want to send the token to
* @param metadata - The metadata of the NFT you want to mint
*/
public async getMintTransaction(
receiver: string,
metadata: NFTMetadataOrUri,
) {
return assertEnabled(
this.mintable,
FEATURE_NFT_MINTABLE,
).getMintTransaction(receiver, metadata);
}
////// ERC721 Batch Mintable Extension //////
/**
@@ -7,6 +7,7 @@ import {
EditionInitializer,
getContractName,
MarketplaceInitializer,
MarketplaceV3Initializer,
MultiwrapInitializer,
NFTCollectionInitializer,
NFTDropInitializer,
@@ -59,6 +60,7 @@ export class ContractFactory extends ContractWrapper<TWFactory> {
[VoteInitializer.contractType]: 1,
[SplitInitializer.contractType]: 1,
[MarketplaceInitializer.contractType]: 2,
[MarketplaceV3Initializer.contractType]: 1,
[PackInitializer.contractType]: 2,
};
@@ -296,6 +298,16 @@ export class ContractFactory extends ContractWrapper<TWFactory> {
marketplaceMetadata.platform_fee_recipient,
marketplaceMetadata.platform_fee_basis_points,
];
case MarketplaceV3Initializer.contractType:
const marketplaceV3Metadata =
MarketplaceV3Initializer.schema.deploy.parse(metadata);
return [
await this.getSignerAddress(),
contractURI,
trustedForwarders,
marketplaceV3Metadata.platform_fee_recipient,
marketplaceV3Metadata.platform_fee_basis_points,
];
case PackInitializer.contractType:
const packsMetadata = PackInitializer.schema.deploy.parse(metadata);
return [
@@ -0,0 +1,872 @@
import {
cleanCurrencyAddress,
fetchCurrencyValue,
normalizePriceValue,
setErc20Allowance,
} from "../../common/currency";
import {
getAllInBatches,
handleTokenApproval,
isTokenApprovedForTransfer,
} from "../../common/marketplace";
import { fetchTokenMetadataForContract } from "../../common/nft";
import {
InterfaceId_IERC1155,
InterfaceId_IERC721,
} from "../../constants/contract";
import { FEATURE_DIRECT_LISTINGS } from "../../constants/thirdweb-features";
import { Status } from "../../enums";
import {
DirectListingInputParams,
DirectListingInputParamsSchema,
} from "../../schema/marketplacev3/direct-listings";
import { DirectListingV3, MarketplaceFilter } from "../../types";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult, TransactionResultWithId } from "../types";
import { ContractEncoder } from "./contract-encoder";
import { ContractEvents } from "./contract-events";
import { ContractInterceptor } from "./contract-interceptor";
import { ContractWrapper } from "./contract-wrapper";
import { GasCostEstimator } from "./gas-cost-estimator";
import type {
IERC1155,
IERC165,
IERC721,
IDirectListings,
DirectListingsLogic,
} from "@thirdweb-dev/contracts-js";
import ERC165Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC165.json";
import ERC721Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC721.json";
import ERC1155Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC1155.json";
import {
NewListingEvent,
UpdatedListingEvent,
} from "@thirdweb-dev/contracts-js/dist/declarations/src/DirectListingsLogic";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, Contract } from "ethers";
import invariant from "tiny-invariant";
/**
* Handles direct listings
* @public
*/
export class MarketplaceV3DirectListings<TContract extends DirectListingsLogic>
implements DetectableFeature
{
featureName = FEATURE_DIRECT_LISTINGS.name;
private contractWrapper: ContractWrapper<DirectListingsLogic>;
private storage: ThirdwebStorage;
// utilities
public events: ContractEvents<DirectListingsLogic>;
public interceptor: ContractInterceptor<DirectListingsLogic>;
public encoder: ContractEncoder<DirectListingsLogic>;
public estimator: GasCostEstimator<DirectListingsLogic>;
constructor(
contractWrapper: ContractWrapper<TContract>,
storage: ThirdwebStorage,
) {
this.contractWrapper = contractWrapper;
this.storage = storage;
this.events = new ContractEvents(this.contractWrapper);
this.encoder = new ContractEncoder(this.contractWrapper);
this.interceptor = new ContractInterceptor(this.contractWrapper);
this.estimator = new GasCostEstimator(this.contractWrapper);
}
getAddress(): string {
return this.contractWrapper.readContract.address;
}
/** ******************************
* READ FUNCTIONS
*******************************/
/**
* Get the total number of direct listings.
* @returns Returns the total number of direct listings created.
* @public
*
* @example
* ```javascript
* const totalListings = await contract.directListings.getTotalCount();
* ```
*/
public async getTotalCount(): Promise<BigNumber> {
return await this.contractWrapper.readContract.totalListings();
}
/**
* Get all direct listings.
*
* @example
* ```javascript
* const listings = await contract.directListings.getAll();
* const priceOfFirstListing = listings[0].price;
* ```
*
* @param filter - optional filter parameters
* @returns the Direct listing object array
*/
public async getAll(filter?: MarketplaceFilter): Promise<DirectListingV3[]> {
const totalListings = await this.getTotalCount();
let start = BigNumber.from(filter?.start || 0).toNumber();
let end = totalListings.toNumber();
if (end === 0) {
throw new Error(`No listings exist on the contract.`);
}
let rawListings: IDirectListings.ListingStructOutput[] = [];
let batches = await getAllInBatches(
start,
end,
this.contractWrapper.readContract.getAllListings,
);
rawListings = batches.flat();
const filteredListings = this.applyFilter(rawListings, filter);
return await Promise.all(
filteredListings.map((listing) => this.mapListing(listing)),
);
}
/**
* Get all valid direct listings.
*
* A valid listing is where the listing is active,
* and the creator still owns & has approved Marketplace to transfer the listed NFTs.
*
* @example
* ```javascript
* const listings = await contract.directListings.getAllValid();
* const priceOfFirstListing = listings[0].price;
* ```
*
* @param filter - optional filter parameters
* @returns the Direct listing object array
*/
public async getAllValid(
filter?: MarketplaceFilter,
): Promise<DirectListingV3[]> {
const totalListings = await this.getTotalCount();
let start = BigNumber.from(filter?.start || 0).toNumber();
let end = totalListings.toNumber();
if (end === 0) {
throw new Error(`No listings exist on the contract.`);
}
let rawListings: IDirectListings.ListingStructOutput[] = [];
let batches = await getAllInBatches(
start,
end,
this.contractWrapper.readContract.getAllValidListings,
);
rawListings = batches.flat();
const filteredListings = this.applyFilter(rawListings, filter);
return await Promise.all(
filteredListings.map((listing) => this.mapListing(listing)),
);
}
/**
* Get a direct listing by id.
*
* @param listingId - the listing id
* @returns the Direct listing object
*
* @example
* ```javascript
* const listingId = 0;
* const listing = await contract.directListings.getListing(listingId);
* ```
*/
public async getListing(listingId: BigNumberish): Promise<DirectListingV3> {
const listing = await this.contractWrapper.readContract.getListing(
listingId,
);
return await this.mapListing(listing);
}
/**
* Check whether a buyer is approved for a reserved listing.
*
* @param listingId - the listing id
* @param buyer - buyer address
*/
public async isBuyerApprovedForListing(
listingId: BigNumberish,
buyer: string,
): Promise<boolean> {
const listing = await this.validateListing(BigNumber.from(listingId));
if (!listing.isReservedListing) {
throw new Error(`Listing ${listingId} is not a reserved listing.`);
}
return await this.contractWrapper.readContract.isBuyerApprovedForListing(
listingId,
buyer,
);
}
/**
* Check whether a currency is approved for a listing.
*
* @param listingId - the listing id
* @param currency - currency address
*/
public async isCurrencyApprovedForListing(
listingId: BigNumberish,
currency: string,
): Promise<boolean> {
await this.validateListing(BigNumber.from(listingId));
return await this.contractWrapper.readContract.isCurrencyApprovedForListing(
listingId,
currency,
);
}
/**
* Check price per token for an approved currency.
*
* @param listingId - the listing id
* @param currencyContractAddress - currency contract address
*/
public async currencyPriceForListing(
listingId: BigNumberish,
currencyContractAddress: string,
): Promise<BigNumberish> {
const listing = await this.validateListing(BigNumber.from(listingId));
if (currencyContractAddress === listing.currencyContractAddress) {
return listing.pricePerToken;
}
const isApprovedCurrency = await this.isCurrencyApprovedForListing(
listingId,
currencyContractAddress,
);
if (!isApprovedCurrency) {
throw new Error(
`Currency ${currencyContractAddress} is not approved for Listing ${listingId}.`,
);
}
return await this.contractWrapper.readContract.currencyPriceForListing(
listingId,
currencyContractAddress,
);
}
/** ******************************
* WRITE FUNCTIONS
*******************************/
/**
* Create Direct Listing
*
* @remarks Create a new listing on the marketplace where people can buy an asset directly.
*
* @example
* ```javascript
* // Data of the listing you want to create
* const listing = {
* // address of the contract the asset you want to list is on
* assetContractAddress: "0x...",
* // token ID of the asset you want to list
* tokenId: "0",
* // how many of the asset you want to list
* quantity: 1,
* // address of the currency contract that will be used to pay for the listing
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // The price to pay per unit of NFTs listed.
* pricePerToken: 1.5,
* // when should the listing open up for offers
* startTimestamp: new Date(Date.now()),
* // how long the listing will be open for
* endTimestamp: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
* // Whether the listing is reserved for a specific set of buyers.
* isReservedListing: false
* }
*
* const tx = await contract.directListings.createListing(listing);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created listing
* ```
*/
public async createListing(
listing: DirectListingInputParams,
): Promise<TransactionResultWithId> {
const parsedListing = DirectListingInputParamsSchema.parse(listing);
await handleTokenApproval(
this.contractWrapper,
this.getAddress(),
parsedListing.assetContractAddress,
parsedListing.tokenId,
await this.contractWrapper.getSignerAddress(),
);
const normalizedPricePerToken = await normalizePriceValue(
this.contractWrapper.getProvider(),
parsedListing.pricePerToken,
parsedListing.currencyContractAddress,
);
const block = await this.contractWrapper.getProvider().getBlock("latest");
const blockTime = block.timestamp;
if (parsedListing.startTimestamp.lt(blockTime)) {
parsedListing.startTimestamp = BigNumber.from(blockTime);
}
const receipt = await this.contractWrapper.sendTransaction(
"createListing",
[
{
assetContract: parsedListing.assetContractAddress,
tokenId: parsedListing.tokenId,
quantity: parsedListing.quantity,
currency: cleanCurrencyAddress(parsedListing.currencyContractAddress),
pricePerToken: normalizedPricePerToken,
startTimestamp: parsedListing.startTimestamp,
endTimestamp: parsedListing.endTimestamp,
reserved: parsedListing.isReservedListing,
} as IDirectListings.ListingParametersStruct,
],
{
// Higher gas limit for create listing
gasLimit: 500000,
},
);
const event = this.contractWrapper.parseLogs<NewListingEvent>(
"NewListing",
receipt?.logs,
);
return {
id: event[0].args.listingId,
receipt,
};
}
/**
* Update a Direct listing with new metadata.
*
* Note: cannot update a listing with a new quantity of 0. Use `cancelDirectListing` to remove a listing instead.
*
* @param listing - the new listing information
*
* @example
* ```javascript
* // Data of the listing you want to update
*
* const listingId = 0; // ID of the listing you want to update
*
* const listing = {
* // address of the contract the asset you want to list is on
* assetContractAddress: "0x...", // should be same as original listing
* // token ID of the asset you want to list
* tokenId: "0", // should be same as original listing
* // how many of the asset you want to list
* quantity: 1,
* // address of the currency contract that will be used to pay for the listing
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // The price to pay per unit of NFTs listed.
* pricePerToken: 1.5,
* // when should the listing open up for offers
* startTimestamp: new Date(Date.now()), // can't change this if listing already active
* // how long the listing will be open for
* endTimestamp: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
* // Whether the listing is reserved for a specific set of buyers.
* isReservedListing: false
* }
*
* const tx = await contract.directListings.updateListing(listingId, listing);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created listing
* ```
*/
public async updateListing(
listingId: BigNumberish,
listing: DirectListingInputParams,
): Promise<TransactionResultWithId> {
const parsedListing = DirectListingInputParamsSchema.parse(listing);
await handleTokenApproval(
this.contractWrapper,
this.getAddress(),
parsedListing.assetContractAddress,
parsedListing.tokenId,
await this.contractWrapper.getSignerAddress(),
);
const normalizedPricePerToken = await normalizePriceValue(
this.contractWrapper.getProvider(),
parsedListing.pricePerToken,
parsedListing.currencyContractAddress,
);
const receipt = await this.contractWrapper.sendTransaction(
"updateListing",
[
listingId,
{
assetContract: parsedListing.assetContractAddress,
tokenId: parsedListing.tokenId,
quantity: parsedListing.quantity,
currency: cleanCurrencyAddress(parsedListing.currencyContractAddress),
pricePerToken: normalizedPricePerToken,
startTimestamp: parsedListing.startTimestamp,
endTimestamp: parsedListing.endTimestamp,
reserved: parsedListing.isReservedListing,
} as IDirectListings.ListingParametersStruct,
],
{
// Higher gas limit for create listing
gasLimit: 500000,
},
);
const event = this.contractWrapper.parseLogs<UpdatedListingEvent>(
"UpdatedListing",
receipt?.logs,
);
return {
id: event[0].args.listingId,
receipt,
};
}
/**
* Cancel Direct Listing
*
* @remarks Cancel a direct listing on the marketplace
*
* @example
* ```javascript
* // The listing ID of the direct listing you want to cancel
* const listingId = 0;
*
* await contract.directListings.cancelListing(listingId);
* ```
*/
public async cancelListing(
listingId: BigNumberish,
): Promise<TransactionResult> {
return {
receipt: await this.contractWrapper.sendTransaction("cancelListing", [
listingId,
]),
};
}
/**
* Buy from a Listing
*
* @remarks Buy from a specific direct listing from the marketplace.
*
* @example
* ```javascript
* // The ID of the listing you want to buy from
* const listingId = 0;
* // Quantity of the asset you want to buy
* const quantityDesired = 1;
*
* await contract.directListings.buyFromListing(listingId, quantityDesired);
* ```
*
* @param listingId - The listing id to buy
* @param quantityDesired - the quantity to buy
* @param receiver - optional receiver of the bought listing if different from the connected wallet
*/
public async buyFromListing(
listingId: BigNumberish,
quantityDesired: BigNumberish,
receiver?: string,
): Promise<TransactionResult> {
const listing = await this.validateListing(BigNumber.from(listingId));
const { valid, error } = await this.isStillValidListing(
listing,
quantityDesired,
);
if (!valid) {
throw new Error(`Listing ${listingId} is no longer valid. ${error}`);
}
const buyFor = receiver
? receiver
: await this.contractWrapper.getSignerAddress();
const quantity = BigNumber.from(quantityDesired);
const value = BigNumber.from(listing.pricePerToken).mul(quantity);
const overrides = (await this.contractWrapper.getCallOverrides()) || {};
await setErc20Allowance(
this.contractWrapper,
value,
listing.currencyContractAddress,
overrides,
);
return {
receipt: await this.contractWrapper.sendTransaction(
"buyFromListing",
[listingId, buyFor, quantity, listing.currencyContractAddress, value],
overrides,
),
};
}
/**
* Approve buyer for reserved listing.
*
* @remarks Approve a buyer to buy from a reserved listing.
*
* @example
* ```javascript
* // The listing ID of the direct listing you want approve buyer for
* const listingId = "0";
*
* await contract.directListings.approveBuyerForReservedListing(listingId, buyer);
* ```
*
* @param listingId - The listing id to buy
* @param buyer - Address of buyer being approved
*/
public async approveBuyerForReservedListing(
listingId: BigNumberish,
buyer: string,
): Promise<TransactionResult> {
const isApproved = await this.isBuyerApprovedForListing(listingId, buyer);
if (!isApproved) {
return {
receipt: await this.contractWrapper.sendTransaction(
"approveBuyerForListing",
[listingId, buyer, true],
),
};
} else {
throw new Error(
`Buyer ${buyer} already approved for listing ${listingId}.`,
);
}
}
/**
* Revoke approval of a buyer for reserved listing.
*
* @example
* ```javascript
* // The listing ID of the direct listing you want approve buyer for
* const listingId = "0";
*
* await contract.directListings.revokeBuyerApprovalForReservedListing(listingId, buyer);
* ```
*
* @param listingId - The listing id to buy
* @param buyer - Address of buyer being approved
*/
public async revokeBuyerApprovalForReservedListing(
listingId: BigNumberish,
buyer: string,
): Promise<TransactionResult> {
const isApproved = await this.isBuyerApprovedForListing(listingId, buyer);
if (isApproved) {
return {
receipt: await this.contractWrapper.sendTransaction(
"approveBuyerForListing",
[listingId, buyer, false],
),
};
} else {
throw new Error(`Buyer ${buyer} not approved for listing ${listingId}.`);
}
}
/**
* Approve a currency for a listing.
*
*
* @example
* ```javascript
* // The listing ID of the direct listing you want approve currency for
* const listingId = "0";
*
* await contract.directListings.approveCurrencyForListing(listingId, currencyContractAddress, pricePerTokenInCurrency);
* ```
*
* @param listingId - The listing id to buy
* @param currencyContractAddress - Address of currency being approved
* @param pricePerTokenInCurrency - Price per token in the currency
*/
public async approveCurrencyForListing(
listingId: BigNumberish,
currencyContractAddress: string,
pricePerTokenInCurrency: BigNumberish,
): Promise<TransactionResult> {
const listing = await this.validateListing(BigNumber.from(listingId));
if (currencyContractAddress === listing.currencyContractAddress) {
invariant(
pricePerTokenInCurrency === listing.pricePerToken,
"Approving listing currency with a different price.",
);
}
const currencyPrice =
await this.contractWrapper.readContract.currencyPriceForListing(
listingId,
currencyContractAddress,
);
invariant(
pricePerTokenInCurrency === currencyPrice,
"Currency already approved with this price.",
);
return {
receipt: await this.contractWrapper.sendTransaction(
"approveCurrencyForListing",
[listingId, currencyContractAddress, pricePerTokenInCurrency],
),
};
}
/**
* Revoke approval of a currency from a listing.
*
*
* @example
* ```javascript
* // The listing ID of the direct listing you want to revoke currency for
* const listingId = "0";
*
* await contract.directListings.revokeCurrencyApprovalForListing(listingId, currencyContractAddress);
* ```
*
* @param listingId - The listing id to buy
* @param currencyContractAddress - Address of currency
*/
public async revokeCurrencyApprovalForListing(
listingId: BigNumberish,
currencyContractAddress: string,
): Promise<TransactionResult> {
const listing = await this.validateListing(BigNumber.from(listingId));
if (currencyContractAddress === listing.currencyContractAddress) {
throw new Error(`Can't revoke approval for main listing currency.`);
}
const currencyPrice =
await this.contractWrapper.readContract.currencyPriceForListing(
listingId,
currencyContractAddress,
);
invariant(!currencyPrice.isZero(), "Currency not approved.");
return {
receipt: await this.contractWrapper.sendTransaction(
"approveCurrencyForListing",
[listingId, currencyContractAddress, BigNumber.from(0)],
),
};
}
/** ******************************
* PRIVATE FUNCTIONS
*******************************/
/**
* Throws error if listing could not be found
*
* @param listingId - Listing to check for
*/
private async validateListing(
listingId: BigNumber,
): Promise<DirectListingV3> {
try {
return await this.getListing(listingId);
} catch (err) {
console.error(`Error getting the listing with id ${listingId}`);
throw err;
}
}
/**
* Helper method maps the auction listing to the direct listing interface.
*
* @internal
* @param listing - The listing to map, as returned from the contract.
* @returns - The mapped interface.
*/
private async mapListing(
listing: IDirectListings.ListingStruct,
): Promise<DirectListingV3> {
let status: Status = Status.UNSET;
const block = await this.contractWrapper.getProvider().getBlock("latest");
const blockTime = block.timestamp;
switch (listing.status) {
case 1:
status = BigNumber.from(listing.startTimestamp).gt(blockTime)
? Status.Created
: BigNumber.from(listing.endTimestamp).lt(blockTime)
? Status.Expired
: Status.Active;
break;
case 2:
status = Status.Completed;
break;
case 3:
status = Status.Cancelled;
break;
}
return {
assetContractAddress: listing.assetContract,
currencyContractAddress: listing.currency,
pricePerToken: listing.pricePerToken.toString(),
currencyValuePerToken: await fetchCurrencyValue(
this.contractWrapper.getProvider(),
listing.currency,
listing.pricePerToken,
),
id: listing.listingId.toString(),
tokenId: listing.tokenId.toString(),
quantity: listing.quantity.toString(),
startTimeInSeconds: BigNumber.from(listing.startTimestamp).toNumber(),
asset: await fetchTokenMetadataForContract(
listing.assetContract,
this.contractWrapper.getProvider(),
listing.tokenId,
this.storage,
),
endTimeInSeconds: BigNumber.from(listing.endTimestamp).toNumber(),
creatorAddress: listing.listingCreator,
isReservedListing: listing.reserved,
status: status,
};
}
/**
* Use this method to check if a direct listing is still valid.
*
* Ways a direct listing can become invalid:
* 1. The asset holder transferred the asset to another wallet
* 2. The asset holder burned the asset
* 3. The asset holder removed the approval on the marketplace
*
* @internal
* @param listing - The listing to check.
* @returns - True if the listing is valid, false otherwise.
*/
private async isStillValidListing(
listing: DirectListingV3,
quantity?: BigNumberish,
): Promise<{ valid: boolean; error?: string }> {
const approved = await isTokenApprovedForTransfer(
this.contractWrapper.getProvider(),
this.getAddress(),
listing.assetContractAddress,
listing.tokenId,
listing.creatorAddress,
);
if (!approved) {
return {
valid: false,
error: `Token '${listing.tokenId}' from contract '${listing.assetContractAddress}' is not approved for transfer`,
};
}
const provider = this.contractWrapper.getProvider();
const erc165 = new Contract(
listing.assetContractAddress,
ERC165Abi,
provider,
) as IERC165;
const isERC721 = await erc165.supportsInterface(InterfaceId_IERC721);
const isERC1155 = await erc165.supportsInterface(InterfaceId_IERC1155);
if (isERC721) {
const asset = new Contract(
listing.assetContractAddress,
ERC721Abi,
provider,
) as IERC721;
const valid =
(await asset.ownerOf(listing.tokenId)).toLowerCase() ===
listing.creatorAddress.toLowerCase();
return {
valid,
error: valid
? undefined
: `Seller is not the owner of Token '${listing.tokenId}' from contract '${listing.assetContractAddress} anymore'`,
};
} else if (isERC1155) {
const asset = new Contract(
listing.assetContractAddress,
ERC1155Abi,
provider,
) as IERC1155;
const balance = await asset.balanceOf(
listing.creatorAddress,
listing.tokenId,
);
const valid = balance.gte(quantity || listing.quantity);
return {
valid,
error: valid
? undefined
: `Seller does not have enough balance of Token '${listing.tokenId}' from contract '${listing.assetContractAddress} to fulfill the listing`,
};
} else {
return {
valid: false,
error: "Contract does not implement ERC 1155 or ERC 721.",
};
}
}
private applyFilter(
listings: IDirectListings.ListingStructOutput[],
filter?: MarketplaceFilter,
) {
let rawListings = [...listings];
if (filter) {
if (filter.seller) {
rawListings = rawListings.filter(
(seller) =>
seller.listingCreator.toString().toLowerCase() ===
filter?.seller?.toString().toLowerCase(),
);
}
if (filter.tokenContract) {
rawListings = rawListings.filter(
(tokenContract) =>
tokenContract.assetContract.toString().toLowerCase() ===
filter?.tokenContract?.toString().toLowerCase(),
);
}
if (filter.tokenId !== undefined) {
rawListings = rawListings.filter(
(tokenContract) =>
tokenContract.tokenId.toString() === filter?.tokenId?.toString(),
);
}
}
return filter?.count && filter.count < rawListings.length
? rawListings.slice(0, filter.count)
: rawListings;
}
}
@@ -0,0 +1,818 @@
import { AuctionHasNotEndedError } from "../../common";
import {
cleanCurrencyAddress,
fetchCurrencyMetadata,
fetchCurrencyValue,
normalizePriceValue,
setErc20Allowance,
} from "../../common/currency";
import { getAllInBatches, handleTokenApproval } from "../../common/marketplace";
import { fetchTokenMetadataForContract } from "../../common/nft";
import { FEATURE_ENGLISH_AUCTIONS } from "../../constants/thirdweb-features";
import { Status } from "../../enums";
import {
EnglishAuctionInputParams,
EnglishAuctionInputParamsSchema,
} from "../../schema/marketplacev3/english-auctions";
import { MarketplaceFilter } from "../../types";
import { CurrencyValue, Price } from "../../types/currency";
import { EnglishAuction, Bid } from "../../types/marketplacev3";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult, TransactionResultWithId } from "../types";
import { ContractEncoder } from "./contract-encoder";
import { ContractEvents } from "./contract-events";
import { ContractInterceptor } from "./contract-interceptor";
import { ContractWrapper } from "./contract-wrapper";
import { GasCostEstimator } from "./gas-cost-estimator";
import type {
IEnglishAuctions,
EnglishAuctionsLogic,
} from "@thirdweb-dev/contracts-js";
import { NewAuctionEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/EnglishAuctionsLogic";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish, ethers, constants } from "ethers";
import invariant from "tiny-invariant";
/**
* Handles auctions
* @public
*/
export class MarketplaceV3EnglishAuctions<
TContract extends EnglishAuctionsLogic,
> implements DetectableFeature
{
featureName = FEATURE_ENGLISH_AUCTIONS.name;
private contractWrapper: ContractWrapper<EnglishAuctionsLogic>;
private storage: ThirdwebStorage;
// utilities
public events: ContractEvents<EnglishAuctionsLogic>;
public interceptor: ContractInterceptor<EnglishAuctionsLogic>;
public encoder: ContractEncoder<EnglishAuctionsLogic>;
public estimator: GasCostEstimator<EnglishAuctionsLogic>;
constructor(
contractWrapper: ContractWrapper<TContract>,
storage: ThirdwebStorage,
) {
this.contractWrapper = contractWrapper;
this.storage = storage;
this.events = new ContractEvents(this.contractWrapper);
this.encoder = new ContractEncoder(this.contractWrapper);
this.interceptor = new ContractInterceptor(this.contractWrapper);
this.estimator = new GasCostEstimator(this.contractWrapper);
}
getAddress(): string {
return this.contractWrapper.readContract.address;
}
/** ******************************
* READ FUNCTIONS
*******************************/
/**
* Get the total number of english auctions.
* @returns Returns the total number of auctions created.
* @public
*
* @example
* ```javascript
* const totalAuctions = await contract.englishAuctions.getTotalCount();
* ```
*/
public async getTotalCount(): Promise<BigNumber> {
return await this.contractWrapper.readContract.totalAuctions();
}
/**
* Get all auctions.
*
* @example
* ```javascript
* const auctions = await contract.englishAuctions.getAll();
* ```
*
* @param filter - optional filter parameters
* @returns the Auction object array
*/
public async getAll(filter?: MarketplaceFilter): Promise<EnglishAuction[]> {
const totalAuctions = await this.getTotalCount();
let start = BigNumber.from(filter?.start || 0).toNumber();
let end = totalAuctions.toNumber();
if (end === 0) {
throw new Error(`No auctions exist on the contract.`);
}
let rawAuctions: IEnglishAuctions.AuctionStructOutput[] = [];
let batches = await getAllInBatches(
start,
end,
this.contractWrapper.readContract.getAllAuctions,
);
rawAuctions = batches.flat();
const filteredAuctions = this.applyFilter(rawAuctions, filter);
return await Promise.all(
filteredAuctions.map((auction) => this.mapAuction(auction)),
);
}
/**
* Get all valid auctions.
*
* @example
* ```javascript
* const auctions = await contract.englishAuctions.getAllValid();
* ```
*
* @param filter - optional filter parameters
* @returns the Auction object array
*/
public async getAllValid(
filter?: MarketplaceFilter,
): Promise<EnglishAuction[]> {
const totalAuctions = await this.getTotalCount();
let start = BigNumber.from(filter?.start || 0).toNumber();
let end = totalAuctions.toNumber();
if (end === 0) {
throw new Error(`No auctions exist on the contract.`);
}
let rawAuctions: IEnglishAuctions.AuctionStructOutput[] = [];
let batches = await getAllInBatches(
start,
end,
this.contractWrapper.readContract.getAllValidAuctions,
);
rawAuctions = batches.flat();
const filteredAuctions = this.applyFilter(rawAuctions, filter);
return await Promise.all(
filteredAuctions.map((auction) => this.mapAuction(auction)),
);
}
/**
* Get an Auction by id.
*
* @example
* ```javascript
* const auctionId = 0;
* const auction = await contract.englishAuctions.getAuction(auctionId);
* ```
*
* @param auctionId - the auction Id
* @returns the Auction object
*/
public async getAuction(auctionId: BigNumberish): Promise<EnglishAuction> {
const auction = await this.contractWrapper.readContract.getAuction(
auctionId,
);
return await this.mapAuction(auction);
}
/**
* Get Highest Bid
*
* @remarks Get the current highest bid of an active auction.
*
* @example
* ```javascript
* // The ID of the auction
* const auctionId = 0;
*
* contract.englishAuctions.
* .getWinningBid(auctionId)
* .then((bid) => console.log(bid))
* .catch((err) => console.error(err));
* ```
*/
public async getWinningBid(
auctionId: BigNumberish,
): Promise<Bid | undefined> {
await this.validateAuction(BigNumber.from(auctionId));
const bid = await this.contractWrapper.readContract.getWinningBid(
auctionId,
);
if (bid._bidder === constants.AddressZero) {
return undefined;
}
return await this.mapBid(
auctionId.toString(),
bid._bidder,
bid._currency,
bid._bidAmount.toString(),
);
}
/**
* Check if a bid-amount is/will be a winning bid.
*
* @param auctionId - Auction Id
* @param bidAmount - Amount to bid
* @returns the Auction object
*/
public async isWinningBid(
auctionId: BigNumberish,
bidAmount: BigNumberish,
): Promise<boolean> {
return await this.contractWrapper.readContract.isNewWinningBid(
auctionId,
bidAmount,
);
}
/**
* Get Auction Winner
*
* @remarks Get the winner of the auction after an auction ends.
*
* @example
* ```javascript
* // The auction ID of the auction that closed
* const auctionId = 0;
*
* contract.englishAuctions.
* .getWinner(auctionId)
* .then((auctionWinner) => console.log(auctionWinner))
* .catch((err) => console.error(err));
* ```
*/
public async getWinner(auctionId: BigNumberish): Promise<string> {
const auction = await this.validateAuction(BigNumber.from(auctionId));
const bid = await this.contractWrapper.readContract.getWinningBid(
auctionId,
);
const now = BigNumber.from(Math.floor(Date.now() / 1000));
const endTime = BigNumber.from(auction.endTimeInSeconds);
// if we have a winner in the map and the current time is past the endtime of the auction return the address of the winner
if (now.gt(endTime) && bid._bidder !== constants.AddressZero) {
return bid._bidder;
}
// otherwise fall back to query filter things
// TODO this should be via indexer or direct contract call
const closedAuctions = await this.contractWrapper.readContract.queryFilter(
this.contractWrapper.readContract.filters.AuctionClosed(),
);
const closed = closedAuctions.find((a) =>
a.args.auctionId.eq(BigNumber.from(auctionId)),
);
if (!closed) {
throw new Error(
`Could not find auction with ID ${auctionId} in closed auctions`,
);
}
return closed.args.winningBidder;
}
/** ******************************
* WRITE FUNCTIONS
*******************************/
/**
* Create Auction
*
* @remarks Create a new auction where people can bid on an asset.
*
* @example
* ```javascript
* // Data of the auction you want to create
* const auction = {
* // address of the contract of the asset you want to auction
* assetContractAddress: "0x...",
* // token ID of the asset you want to auction
* tokenId: "0",
* // how many of the asset you want to auction
* quantity: 1,
* // address of the currency contract that will be used to pay for the auctioned tokens
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // the minimum bid that will be accepted for the token
* minimumBidAmount: "1.5",
* // how much people would have to bid to instantly buy the asset
* buyoutBidAmount: "10",
* // If a bid is made less than these many seconds before expiration, the expiration time is increased by this.
* timeBufferInSeconds: "900", // 15 minutes by default
* // A bid must be at least this much bps greater than the current winning bid
* bidBufferBps: "500", // 5% by default
* // when should the auction open up for bidding
* startTimestamp: new Date(Date.now()),
* // end time of auction
* endTimestamp: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
* }
*
* const tx = await contract.englishAuctions.createAuction(auction);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created auction
* ```
*/
public async createAuction(
auction: EnglishAuctionInputParams,
): Promise<TransactionResultWithId> {
const parsedAuction = EnglishAuctionInputParamsSchema.parse(auction);
await handleTokenApproval(
this.contractWrapper,
this.getAddress(),
parsedAuction.assetContractAddress,
parsedAuction.tokenId,
await this.contractWrapper.getSignerAddress(),
);
const normalizedBuyoutAmount = await normalizePriceValue(
this.contractWrapper.getProvider(),
parsedAuction.buyoutBidAmount,
parsedAuction.currencyContractAddress,
);
const normalizedMinBidAmount = await normalizePriceValue(
this.contractWrapper.getProvider(),
parsedAuction.minimumBidAmount,
parsedAuction.currencyContractAddress,
);
const block = await this.contractWrapper.getProvider().getBlock("latest");
const blockTime = block.timestamp;
if (parsedAuction.startTimestamp.lt(blockTime)) {
parsedAuction.startTimestamp = BigNumber.from(blockTime);
}
const receipt = await this.contractWrapper.sendTransaction(
"createAuction",
[
{
assetContract: parsedAuction.assetContractAddress,
tokenId: parsedAuction.tokenId,
quantity: parsedAuction.quantity,
currency: cleanCurrencyAddress(parsedAuction.currencyContractAddress),
minimumBidAmount: normalizedMinBidAmount,
buyoutBidAmount: normalizedBuyoutAmount,
timeBufferInSeconds: parsedAuction.timeBufferInSeconds,
bidBufferBps: parsedAuction.bidBufferBps,
startTimestamp: parsedAuction.startTimestamp,
endTimestamp: parsedAuction.endTimestamp,
} as IEnglishAuctions.AuctionParametersStruct,
],
{
// Higher gas limit for create auction
gasLimit: 500000,
},
);
const event = this.contractWrapper.parseLogs<NewAuctionEvent>(
"NewAuction",
receipt?.logs,
);
return {
id: event[0].args.auctionId,
receipt,
};
}
/**
* Buyout Auction
*
* @remarks Buy a specific auction from the marketplace.
*
* @example
* ```javascript
* // The auction ID of the asset you want to buy
* const auctionId = 0;
*
* await contract.englishAuctions.buyoutAuction(auctionId);
* ```
*/
public async buyoutAuction(
auctionId: BigNumberish,
): Promise<TransactionResult> {
const auction = await this.validateAuction(BigNumber.from(auctionId));
const currencyMetadata = await fetchCurrencyMetadata(
this.contractWrapper.getProvider(),
auction.currencyContractAddress,
);
return this.makeBid(
auctionId,
ethers.utils.formatUnits(
auction.buyoutBidAmount,
currencyMetadata.decimals,
),
);
}
/**
* Bid On Auction
*
* @remarks Make a bid on an auction
*
* @example
* ```javascript
* // The auction ID of the asset you want to bid on
* const auctionId = 0;
* // The total amount you are willing to bid for auctioned tokens
* const bidAmount = 1;
*
* await contract.englishAuctions.makeBid(auctionId, bidAmount);
* ```
*/
public async makeBid(
auctionId: BigNumberish,
bidAmount: Price,
): Promise<TransactionResult> {
const auction = await this.validateAuction(BigNumber.from(auctionId));
const normalizedBidAmount = await normalizePriceValue(
this.contractWrapper.getProvider(),
bidAmount,
auction.currencyContractAddress,
);
if (normalizedBidAmount.eq(BigNumber.from(0))) {
throw new Error("Cannot make a bid with 0 value");
}
if (
BigNumber.from(auction.buyoutBidAmount).gt(0) &&
normalizedBidAmount.gt(auction.buyoutBidAmount)
) {
throw new Error(
"Bid amount must be less than or equal to buyoutBidAmount",
);
}
const winningBid = await this.getWinningBid(auctionId);
if (winningBid) {
const isWinnner = await this.isWinningBid(auctionId, normalizedBidAmount);
invariant(
isWinnner,
"Bid price is too low based on the current winning bid and the bid buffer",
);
} else {
const tokenPrice = normalizedBidAmount;
const minimumBidAmount = BigNumber.from(auction.minimumBidAmount);
invariant(
tokenPrice.gte(minimumBidAmount),
"Bid price is too low based on minimum bid amount",
);
}
const overrides = (await this.contractWrapper.getCallOverrides()) || {};
await setErc20Allowance(
this.contractWrapper,
normalizedBidAmount,
auction.currencyContractAddress,
overrides,
);
return {
receipt: await this.contractWrapper.sendTransaction(
"bidInAuction",
[auctionId, normalizedBidAmount],
overrides,
),
};
}
/**
* Cancel Auction
*
* @remarks Cancel an auction on the marketplace
*
* @example
* ```javascript
* // The ID of the auction you want to cancel
* const auctionId = "0";
*
* await contract.englishAuctions.cancelAuction(auctionId);
* ```
*/
public async cancelAuction(
auctionId: BigNumberish,
): Promise<TransactionResult> {
const winningBid = await this.getWinningBid(auctionId);
if (winningBid) {
throw new Error(`Bids already made.`);
}
return {
receipt: await this.contractWrapper.sendTransaction("cancelAuction", [
auctionId,
]),
};
}
/**
* Close the Auction for the buyer
*
* @remarks Closes the Auction and executes the sale for the buyer.
*
* @example
* ```javascript
* // The ID of the auction you want to close
* const auction = "0";
* await contract.englishAuctions.closeAuctionForBidder(auctionId);
* ```
*
* @param auctionId - the auction id to close
* @param closeFor - optionally pass the winning bid offeror address to close the auction on their behalf
*/
public async closeAuctionForBidder(
auctionId: BigNumberish,
closeFor?: string,
): Promise<TransactionResult> {
if (!closeFor) {
closeFor = await this.contractWrapper.getSignerAddress();
}
const auction = await this.validateAuction(BigNumber.from(auctionId));
try {
return {
receipt: await this.contractWrapper.sendTransaction(
"collectAuctionTokens",
[BigNumber.from(auctionId)],
),
};
} catch (err: any) {
if (err.message.includes("Marketplace: auction still active.")) {
throw new AuctionHasNotEndedError(
auctionId.toString(),
auction.endTimeInSeconds.toString(),
);
} else {
throw err;
}
}
}
/**
* Close the Auction for the seller, i.e. the auction creator
*
* @remarks Closes the Auction and executes the sale for the seller.
*
* @example
* ```javascript
* // The ID of the auction you want to close
* const auctionId = "0";
* await contract.englishAuctions.closeAuctionForSeller(auctionId);
* ```
*
* @param auctionId - the auction id to close
*/
public async closeAuctionForSeller(
auctionId: BigNumberish,
): Promise<TransactionResult> {
const auction = await this.validateAuction(BigNumber.from(auctionId));
try {
return {
receipt: await this.contractWrapper.sendTransaction(
"collectAuctionPayout",
[BigNumber.from(auctionId)],
),
};
} catch (err: any) {
if (err.message.includes("Marketplace: auction still active.")) {
throw new AuctionHasNotEndedError(
auctionId.toString(),
auction.endTimeInSeconds.toString(),
);
} else {
throw err;
}
}
}
/**
* Execute the Auction Sale
*
* @remarks Closes the Auction and executes the sale for both parties.
*
* @example
* ```javascript
* // The ID of the auction you want to close
* const auction = "0";
* await contract.englishAuctions.executeSale(auctionId);
* ```
*
* @param auctionId - the auction to close
*/
public async executeSale(auctionId: BigNumberish) {
const auction = await this.validateAuction(BigNumber.from(auctionId));
try {
const winningBid = await this.getWinningBid(auctionId);
invariant(winningBid, "No winning bid found");
const closeForSeller = this.encoder.encode("collectAuctionPayout", [
auctionId,
]);
const closeForBuyer = this.encoder.encode("collectAuctionTokens", [
auctionId,
]);
return await this.contractWrapper.multiCall([
closeForSeller,
closeForBuyer,
]);
} catch (err: any) {
if (err.message.includes("Marketplace: auction still active.")) {
throw new AuctionHasNotEndedError(
auctionId.toString(),
auction.endTimeInSeconds.toString(),
);
} else {
throw err;
}
}
}
/**
* Get the buffer in basis points between bids for an auction.
*
* @param auctionId - id of the auction
*/
public async getBidBufferBps(auctionId: BigNumberish): Promise<number> {
return (await this.getAuction(auctionId)).bidBufferBps;
}
/**
* returns the minimum bid a user can place to outbid the previous highest bid
* @param auctionId - id of the auction
*/
public async getMinimumNextBid(
auctionId: BigNumberish,
): Promise<CurrencyValue> {
// we can fetch all of these at the same time using promise.all
const [currentBidBufferBps, winningBid, auction] = await Promise.all([
this.getBidBufferBps(auctionId),
this.getWinningBid(auctionId),
await this.validateAuction(BigNumber.from(auctionId)),
]);
const currentBidOrReservePrice = winningBid
? // if there is a winning bid use the value of it
BigNumber.from(winningBid.bidAmount)
: // if there is no winning bid use the reserve price
BigNumber.from(auction.minimumBidAmount);
const minimumNextBid = currentBidOrReservePrice.add(
// the addition of the current bid and the buffer
// (have to divide by 10000 to get the fraction of the buffer (since it's in basis points))
currentBidOrReservePrice.mul(currentBidBufferBps).div(10000),
);
// it's more useful to return a currency value here
return fetchCurrencyValue(
this.contractWrapper.getProvider(),
auction.currencyContractAddress,
minimumNextBid,
);
}
/** ******************************
* PRIVATE FUNCTIONS
*******************************/
/**
* Throws error if auction could not be found
*
* @param auctionId - Auction to check for
*/
private async validateAuction(auctionId: BigNumber): Promise<EnglishAuction> {
try {
return await this.getAuction(auctionId);
} catch (err) {
console.error(`Error getting the auction with id ${auctionId}`);
throw err;
}
}
/**
* Helper method maps the auction to the auction interface.
*
* @internal
* @param auction - The auction to map, as returned from the contract.
* @returns - The mapped interface.
*/
private async mapAuction(
auction: IEnglishAuctions.AuctionStruct,
): Promise<EnglishAuction> {
let status: Status = Status.UNSET;
const block = await this.contractWrapper.getProvider().getBlock("latest");
const blockTime = block.timestamp;
switch (auction.status) {
case 1:
status = BigNumber.from(auction.startTimestamp).gt(blockTime)
? Status.Created
: BigNumber.from(auction.endTimestamp).lt(blockTime)
? Status.Expired
: Status.Active;
break;
case 2:
status = Status.Completed;
break;
case 3:
status = Status.Cancelled;
break;
}
return {
id: auction.auctionId.toString(),
creatorAddress: auction.auctionCreator,
assetContractAddress: auction.assetContract,
tokenId: auction.tokenId.toString(),
quantity: auction.quantity.toString(),
currencyContractAddress: auction.currency,
minimumBidAmount: auction.minimumBidAmount.toString(),
minimumBidCurrencyValue: await fetchCurrencyValue(
this.contractWrapper.getProvider(),
auction.currency,
auction.minimumBidAmount,
),
buyoutBidAmount: auction.buyoutBidAmount.toString(),
buyoutCurrencyValue: await fetchCurrencyValue(
this.contractWrapper.getProvider(),
auction.currency,
auction.buyoutBidAmount,
),
timeBufferInSeconds: BigNumber.from(
auction.timeBufferInSeconds,
).toNumber(),
bidBufferBps: BigNumber.from(auction.bidBufferBps).toNumber(),
startTimeInSeconds: BigNumber.from(auction.startTimestamp).toNumber(),
endTimeInSeconds: BigNumber.from(auction.endTimestamp).toNumber(),
asset: await fetchTokenMetadataForContract(
auction.assetContract,
this.contractWrapper.getProvider(),
auction.tokenId,
this.storage,
),
status: status,
};
}
/**
* Maps an auction-bid to the strict interface
*
* @internal
* @param bid
* @returns - A `Bid` object
*/
private async mapBid(
auctionId: string,
bidderAddress: string,
currencyContractAddress: string,
bidAmount: string,
): Promise<Bid> {
return {
auctionId,
bidderAddress,
currencyContractAddress,
bidAmount,
bidAmountCurrencyValue: await fetchCurrencyValue(
this.contractWrapper.getProvider(),
currencyContractAddress,
bidAmount,
),
} as Bid;
}
private applyFilter(
auctions: IEnglishAuctions.AuctionStructOutput[],
filter?: MarketplaceFilter,
) {
let rawAuctions = [...auctions];
if (filter) {
if (filter.seller) {
rawAuctions = rawAuctions.filter(
(seller) =>
seller.auctionCreator.toString().toLowerCase() ===
filter?.seller?.toString().toLowerCase(),
);
}
if (filter.tokenContract) {
rawAuctions = rawAuctions.filter(
(tokenContract) =>
tokenContract.assetContract.toString().toLowerCase() ===
filter?.tokenContract?.toString().toLowerCase(),
);
}
if (filter.tokenId !== undefined) {
rawAuctions = rawAuctions.filter(
(tokenContract) =>
tokenContract.tokenId.toString() === filter?.tokenId?.toString(),
);
}
}
return filter?.count && filter.count < rawAuctions.length
? rawAuctions.slice(0, filter.count)
: rawAuctions;
}
}
@@ -0,0 +1,474 @@
import {
fetchCurrencyValue,
isNativeToken,
normalizePriceValue,
setErc20Allowance,
} from "../../common/currency";
import { getAllInBatches, handleTokenApproval } from "../../common/marketplace";
import { fetchTokenMetadataForContract } from "../../common/nft";
import { NATIVE_TOKENS, SUPPORTED_CHAIN_ID } from "../../constants";
import { FEATURE_OFFERS } from "../../constants/thirdweb-features";
import { Status } from "../../enums";
import {
OfferInputParams,
OfferInputParamsSchema,
} from "../../schema/marketplacev3/offer";
import { MarketplaceFilter } from "../../types";
import { OfferV3 } from "../../types/marketplacev3";
import { DetectableFeature } from "../interfaces/DetectableFeature";
import { TransactionResult, TransactionResultWithId } from "../types";
import { ContractEncoder } from "./contract-encoder";
import { ContractEvents } from "./contract-events";
import { ContractInterceptor } from "./contract-interceptor";
import { ContractWrapper } from "./contract-wrapper";
import { GasCostEstimator } from "./gas-cost-estimator";
import type { IERC20, IOffers, OffersLogic } from "@thirdweb-dev/contracts-js";
import ERC20Abi from "@thirdweb-dev/contracts-js/dist/abis/IERC20.json";
import { NewOfferEvent } from "@thirdweb-dev/contracts-js/dist/declarations/src/OffersLogic";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { BigNumber, BigNumberish } from "ethers";
/**
* Handles marketplace offers
* @public
*/
export class MarketplaceV3Offers<TContract extends OffersLogic>
implements DetectableFeature
{
featureName = FEATURE_OFFERS.name;
private contractWrapper: ContractWrapper<OffersLogic>;
private storage: ThirdwebStorage;
// utilities
public events: ContractEvents<OffersLogic>;
public interceptor: ContractInterceptor<OffersLogic>;
public encoder: ContractEncoder<OffersLogic>;
public estimator: GasCostEstimator<OffersLogic>;
constructor(
contractWrapper: ContractWrapper<TContract>,
storage: ThirdwebStorage,
) {
this.contractWrapper = contractWrapper;
this.storage = storage;
this.events = new ContractEvents(this.contractWrapper);
this.encoder = new ContractEncoder(this.contractWrapper);
this.interceptor = new ContractInterceptor(this.contractWrapper);
this.estimator = new GasCostEstimator(this.contractWrapper);
}
getAddress(): string {
return this.contractWrapper.readContract.address;
}
/** ******************************
* READ FUNCTIONS
*******************************/
/**
* Get the total number of offers
* @returns Returns the total number of offers created.
* @public
*
* @example
* ```javascript
* const totalOffers = await contract.offers.getTotalCount();
* ```
*/
public async getTotalCount(): Promise<BigNumber> {
return await this.contractWrapper.readContract.totalOffers();
}
/**
* Get all offers.
*
* @example
* ```javascript
* const offers = await contract.offers.getAll();
* ```
*
* @param filter - optional filter parameters
* @returns the Offer object array
*/
public async getAll(filter?: MarketplaceFilter): Promise<OfferV3[]> {
const totalOffers = await this.getTotalCount();
let start = BigNumber.from(filter?.start || 0).toNumber();
let end = totalOffers.toNumber();
if (end === 0) {
throw new Error(`No offers exist on the contract.`);
}
let rawOffers: IOffers.OfferStructOutput[] = [];
let batches = await getAllInBatches(
start,
end,
this.contractWrapper.readContract.getAllOffers,
);
rawOffers = batches.flat();
const filteredOffers = this.applyFilter(rawOffers, filter);
return await Promise.all(
filteredOffers.map((offer) => this.mapOffer(offer)),
);
}
/**
* Get all valid offers.
*
* @example
* ```javascript
* const offers = await contract.offers.getAllValid();
* ```
*
* @param filter - optional filter parameters
* @returns the Offer object array
*/
public async getAllValid(filter?: MarketplaceFilter): Promise<OfferV3[]> {
const totalOffers = await this.getTotalCount();
let start = BigNumber.from(filter?.start || 0).toNumber();
let end = totalOffers.toNumber();
if (end === 0) {
throw new Error(`No offers exist on the contract.`);
}
let rawOffers: IOffers.OfferStructOutput[] = [];
let batches = await getAllInBatches(
start,
end,
this.contractWrapper.readContract.getAllValidOffers,
);
rawOffers = batches.flat();
const filteredOffers = this.applyFilter(rawOffers, filter);
return await Promise.all(
filteredOffers.map((offer) => this.mapOffer(offer)),
);
}
/**
* Get a offer by id
*
* @example
* ```javascript
* const offerId = 0;
* const offer = await contract.offers.getOffer(offerId);
* ```
*
* @param offerId - the listing id
* @returns the Direct listing object
*/
public async getOffer(offerId: BigNumberish): Promise<OfferV3> {
const offer = await this.contractWrapper.readContract.getOffer(offerId);
return await this.mapOffer(offer);
}
/** ******************************
* WRITE FUNCTIONS
*******************************/
/**
* Make Offer
*
* @remarks Make an offer on the marketplace for an asset.
*
* @example
* ```javascript
* // Data of the offer you want to make
* const offer = {
* // address of the contract the asset you want to make an offer for
* assetContractAddress: "0x...",
* // token ID of the asset you want to buy
* tokenId: "0",
* // how many of the asset you want to buy
* quantity: 1,
* // address of the currency contract that you offer to pay in
* currencyContractAddress: NATIVE_TOKEN_ADDRESS,
* // Total price you offer to pay for the mentioned token(s)
* totalPrice: "1.5",
* // Offer valid until
* endTimestamp: new Date(),
* }
*
* const tx = await contract.offers.makeOffer(offer);
* const receipt = tx.receipt; // the transaction receipt
* const id = tx.id; // the id of the newly created offer
* ```
*/
public async makeOffer(
offer: OfferInputParams,
): Promise<TransactionResultWithId> {
const parsedOffer = OfferInputParamsSchema.parse(offer);
const chainId = await this.contractWrapper.getChainID();
const currency = isNativeToken(parsedOffer.currencyContractAddress)
? NATIVE_TOKENS[chainId as SUPPORTED_CHAIN_ID].wrapped.address
: parsedOffer.currencyContractAddress;
const normalizedTotalPrice = await normalizePriceValue(
this.contractWrapper.getProvider(),
parsedOffer.totalPrice,
currency,
);
const overrides = await this.contractWrapper.getCallOverrides();
await setErc20Allowance(
this.contractWrapper,
normalizedTotalPrice,
currency,
overrides,
);
const receipt = await this.contractWrapper.sendTransaction(
"makeOffer",
[
{
assetContract: parsedOffer.assetContractAddress,
tokenId: parsedOffer.tokenId,
quantity: parsedOffer.quantity,
currency: currency,
totalPrice: normalizedTotalPrice,
expirationTimestamp: parsedOffer.endTimestamp,
} as IOffers.OfferParamsStruct,
],
{
// Higher gas limit for create listing
gasLimit: 500000,
},
);
const event = this.contractWrapper.parseLogs<NewOfferEvent>(
"NewOffer",
receipt?.logs,
);
return {
id: event[0].args.offerId,
receipt,
};
}
/**
* Cancel Offer
*
* @remarks Cancel an offer on the marketplace
*
* @example
* ```javascript
* // The ID of the offer you want to cancel
* const offerId = "0";
*
* await contract.offers.cancelOffer(offerId);
* ```
*/
public async cancelOffer(offerId: BigNumberish): Promise<TransactionResult> {
return {
receipt: await this.contractWrapper.sendTransaction("cancelOffer", [
offerId,
]),
};
}
/**
* Accept an offer
*
* @example
* ```javascript
* // The ID of the offer you want to accept
* const offerId = 0;
*
* await contract.offers.acceptOffer(offerId);
* ```
*
* @param offerId - The offer id
*/
public async acceptOffer(offerId: BigNumberish): Promise<TransactionResult> {
const offer = await this.validateOffer(BigNumber.from(offerId));
const { valid, error } = await this.isStillValidOffer(offer);
if (!valid) {
throw new Error(`Offer ${offerId} is no longer valid. ${error}`);
}
const overrides = (await this.contractWrapper.getCallOverrides()) || {};
await handleTokenApproval(
this.contractWrapper,
this.getAddress(),
offer.assetContractAddress,
offer.tokenId,
await this.contractWrapper.getSignerAddress(),
);
return {
receipt: await this.contractWrapper.sendTransaction(
"acceptOffer",
[offerId],
overrides,
),
};
}
/** ******************************
* PRIVATE FUNCTIONS
*******************************/
/**
* Throws error if offer could not be found
*
* @param offerId - offer to check for
*/
private async validateOffer(offerId: BigNumber): Promise<OfferV3> {
try {
return await this.getOffer(offerId);
} catch (err) {
console.error(`Error getting the offer with id ${offerId}`);
throw err;
}
}
/**
* Helper method maps the offer to the offer interface.
*
* @internal
* @param offer - The offer to map, as returned from the contract.
* @returns - The mapped interface.
*/
private async mapOffer(offer: IOffers.OfferStruct): Promise<OfferV3> {
let status: Status = Status.UNSET;
const block = await this.contractWrapper.getProvider().getBlock("latest");
const blockTime = block.timestamp;
switch (offer.status) {
case 1:
status = BigNumber.from(offer.expirationTimestamp).lt(blockTime)
? Status.Expired
: Status.Active;
break;
case 2:
status = Status.Completed;
break;
case 3:
status = Status.Cancelled;
break;
}
return {
id: offer.offerId.toString(),
offerorAddress: offer.offeror,
assetContractAddress: offer.assetContract,
currencyContractAddress: offer.currency,
tokenId: offer.tokenId.toString(),
quantity: offer.quantity.toString(),
totalPrice: offer.totalPrice.toString(),
currencyValue: await fetchCurrencyValue(
this.contractWrapper.getProvider(),
offer.currency,
offer.totalPrice,
),
asset: await fetchTokenMetadataForContract(
offer.assetContract,
this.contractWrapper.getProvider(),
offer.tokenId,
this.storage,
),
endTimeInSeconds: BigNumber.from(offer.expirationTimestamp).toNumber(),
status: status,
};
}
/**
* Use this method to check if an offer is still valid.
*
* Ways an offer can become invalid:
* 1. The offer has expired
* 2. The offeror doesn't have enough balance of currency tokens
* 3. The offeror removed the approval of currency tokens on the marketplace
*
* @internal
* @param offer - The offer to check.
* @returns - True if the offer is valid, false otherwise.
*/
private async isStillValidOffer(
offer: OfferV3,
): Promise<{ valid: boolean; error?: string }> {
const now = BigNumber.from(Math.floor(Date.now() / 1000));
if (now.gt(offer.endTimeInSeconds)) {
return {
valid: false,
error: `Offer with ID ${offer.id} has expired`,
};
}
const chainId = await this.contractWrapper.getChainID();
const currency = isNativeToken(offer.currencyContractAddress)
? NATIVE_TOKENS[chainId as SUPPORTED_CHAIN_ID].wrapped.address
: offer.currencyContractAddress;
const provider = this.contractWrapper.getProvider();
const erc20 = new ContractWrapper<IERC20>(provider, currency, ERC20Abi, {});
const offerorBalance = await erc20.readContract.balanceOf(
offer.offerorAddress,
);
if (offerorBalance.lt(offer.totalPrice)) {
return {
valid: false,
error: `Offeror ${offer.offerorAddress} doesn't have enough balance of token ${currency}`,
};
}
const offerorAllowance = await erc20.readContract.allowance(
offer.offerorAddress,
this.getAddress(),
);
if (offerorAllowance.lt(offer.totalPrice)) {
return {
valid: false,
error: `Offeror ${offer.offerorAddress} hasn't approved enough amount of token ${currency}`,
};
}
return {
valid: true,
error: "",
};
}
private applyFilter(
offers: IOffers.OfferStructOutput[],
filter?: MarketplaceFilter,
) {
let rawOffers = [...offers];
if (filter) {
if (filter.offeror) {
rawOffers = rawOffers.filter(
(offeror) =>
offeror.offeror.toString().toLowerCase() ===
filter?.offeror?.toString().toLowerCase(),
);
}
if (filter.tokenContract) {
rawOffers = rawOffers.filter(
(tokenContract) =>
tokenContract.assetContract.toString().toLowerCase() ===
filter?.tokenContract?.toString().toLowerCase(),
);
}
if (filter.tokenId !== undefined) {
rawOffers = rawOffers.filter(
(tokenContract) =>
tokenContract.tokenId.toString() === filter?.tokenId?.toString(),
);
}
}
return filter?.count && filter.count < rawOffers.length
? rawOffers.slice(0, filter.count)
: rawOffers;
}
}
+14
View File
@@ -353,6 +353,20 @@ export class ThirdwebSDK extends RPCConnectionHandler {
return await this.getContract(contractAddress, "marketplace");
}
/**
* Get an instance of a Marketplace contract
* @param contractAddress - the address of the deployed contract
* @deprecated
* This method is deprecated and will be removed in a future major version. You should use {@link getContract} instead.
* ```diff
* - const marketplace = await sdk.getMarketplaceV3("0x1234...");
* + const marketplace = await sdk.getContract("0x1234...", "marketplace-v3");
* ```
*/
public async getMarketplaceV3(contractAddress: string) {
return await this.getContract(contractAddress, "marketplace-v3");
}
/**
* Get an instance of a Pack contract
* @param contractAddress - the address of the deployed contract
@@ -0,0 +1,8 @@
export enum Status {
UNSET = 0,
Created = 1,
Completed = 2,
Cancelled = 3,
Active = 4,
Expired = 5,
}
@@ -1 +1,2 @@
export * from "./ListingType";
export * from "./Status";
+1
View File
@@ -33,6 +33,7 @@ export * from "./integrations/thirdweb-checkout";
export type { Edition } from "./contracts/prebuilt-implementations/edition";
export type { EditionDrop } from "./contracts/prebuilt-implementations/edition-drop";
export type { Marketplace } from "./contracts/prebuilt-implementations/marketplace";
export type { MarketplaceV3 } from "./contracts/prebuilt-implementations/marketplacev3";
export type { Multiwrap } from "./contracts/prebuilt-implementations/multiwrap";
export type { NFTCollection } from "./contracts/prebuilt-implementations/nft-collection";
export type { NFTDrop } from "./contracts/prebuilt-implementations/nft-drop";
@@ -0,0 +1,63 @@
import { AmountSchema } from "../../../core/schema/shared";
import { NATIVE_TOKEN_ADDRESS } from "../../constants";
import {
AddressSchema,
BigNumberishSchema,
EndDateSchema,
RawDateSchema,
} from "../shared";
import { z } from "zod";
/**
* @internal
*/
export const DirectListingInputParamsSchema = z.object({
/**
* The address of the asset being listed.
*/
assetContractAddress: AddressSchema,
/**
* The ID of the token to list.
*/
tokenId: BigNumberishSchema,
/**
* The quantity of tokens to include in the listing.
*
* For ERC721s, this value should always be 1 (and will be forced internally regardless of what is passed here).
*/
quantity: BigNumberishSchema.default(1),
/**
* The address of the currency to accept for the listing.
*/
currencyContractAddress: AddressSchema.default(NATIVE_TOKEN_ADDRESS),
/**
* The price to pay per unit of NFTs listed.
*/
pricePerToken: AmountSchema,
/**
* The start time of the listing.
*/
startTimestamp: RawDateSchema.default(new Date()),
/**
* The end time of the listing.
*/
endTimestamp: EndDateSchema,
/**
* Whether the listing is reserved to be bought from a specific set of buyers.
*/
isReservedListing: z.boolean().default(false),
});
/**
* @public
*/
export type DirectListingInputParams = z.input<
typeof DirectListingInputParamsSchema
>;
@@ -0,0 +1,79 @@
import { AmountSchema } from "../../../core/schema/shared";
import { NATIVE_TOKEN_ADDRESS } from "../../constants";
import {
AddressSchema,
BigNumberishSchema,
EndDateSchema,
RawDateSchema,
} from "../shared";
import { z } from "zod";
/**
* @internal
*/
export const EnglishAuctionInputParamsSchema = z.object({
/**
* The address of the asset being auctioned.
*/
assetContractAddress: AddressSchema,
/**
* The ID of the token to auction.
*/
tokenId: BigNumberishSchema,
/**
* The quantity of tokens to include in the listing.
*
* For ERC721s, this value should always be 1 (and will be forced internally regardless of what is passed here).
*/
quantity: BigNumberishSchema.default(1),
/**
* The address of the currency to accept for the listing.
*/
currencyContractAddress: AddressSchema.default(NATIVE_TOKEN_ADDRESS),
/**
* The minimum price that a bid must be in order to be accepted.
*/
minimumBidAmount: AmountSchema,
/**
* The buyout price of the auction.
*/
buyoutBidAmount: AmountSchema,
/**
* This is a buffer e.g. x seconds.
*
* If a new winning bid is made less than x seconds before expirationTimestamp, the
* expirationTimestamp is increased by x seconds.
*/
timeBufferInSeconds: BigNumberishSchema.default(900), // 15 minutes by default
/**
* This is a buffer in basis points e.g. x%.
*
* To be considered as a new winning bid, a bid must be at least x% greater than
* the current winning bid.
*/
bidBufferBps: BigNumberishSchema.default(500), // 5% by default
/**
* The start time of the auction.
*/
startTimestamp: RawDateSchema.default(new Date()),
/**
* The end time of the auction.
*/
endTimestamp: EndDateSchema,
});
/**
* @public
*/
export type EnglishAuctionInputParams = z.input<
typeof EnglishAuctionInputParamsSchema
>;
@@ -0,0 +1,46 @@
import { AmountSchema } from "../../../core/schema/shared";
import { NATIVE_TOKEN_ADDRESS } from "../../constants";
import { AddressSchema, BigNumberishSchema, EndDateSchema } from "../shared";
import { z } from "zod";
/**
* @internal
*/
export const OfferInputParamsSchema = z.object({
/**
* The address of the asset being sought.
*/
assetContractAddress: AddressSchema,
/**
* The ID of the token.
*/
tokenId: BigNumberishSchema,
/**
* The quantity of tokens to buy.
*
* For ERC721s, this value should always be 1 (and will be forced internally regardless of what is passed here).
*/
quantity: BigNumberishSchema.default(1),
/**
* The address of the currency offered for the NFTs.
*/
currencyContractAddress: AddressSchema.default(NATIVE_TOKEN_ADDRESS),
/**
* The total offer amount for the NFTs.
*/
totalPrice: AmountSchema,
/**
* The end time of the offer.
*/
endTimestamp: EndDateSchema,
});
/**
* @public
*/
export type OfferInputParams = z.input<typeof OfferInputParamsSchema>;
@@ -130,6 +130,41 @@ export interface MarketplaceContractDeployMetadata {
platform_fee_basis_points?: number;
}
/**
* Options for deploying a Marketplace-V3 contract
* @public
*/
export interface MarketplaceV3ContractDeployMetadata {
/**
* name of the contract
*/
name: string;
/**
* Optional description of the contract
*/
description?: string;
/**
* Optional image for the contract
*/
image?: FileOrBufferOrString;
/**
* Optional url for the contract
*/
external_link?: string;
/**
* Custom gasless trusted forwarder addresses
*/
trusted_forwarders?: string[];
/**
* The address that will receive the proceeds from platform fees
*/
platform_fee_recipient?: string;
/**
* The percentage (in basis points) of platform fees
*/
platform_fee_basis_points?: number;
}
/**
* Options for deploying a Vote contract
* @public
+1
View File
@@ -1,6 +1,7 @@
export * from "./claim-conditions/claim-conditions";
export * from "./airdrop/airdrop";
export * from "./marketplace";
export * from "./marketplacev3";
export * from "./currency";
export * from "./delayed-reveal";
export * from "./vote";
@@ -8,4 +8,5 @@ export interface MarketplaceFilter extends QueryAllParams {
seller?: string;
tokenContract?: string;
tokenId?: BigNumberish;
offeror?: string;
}
@@ -0,0 +1,28 @@
import { CurrencyValue } from "../currency";
export interface Bid {
/**
* The id of the auction.
*/
auctionId: string;
/**
* The address of the buyer who made the offer.
*/
bidderAddress: string;
/**
* The currency contract address of the offer token.
*/
currencyContractAddress: string;
/**
* The amount of coins offered per token.
*/
bidAmount: string;
/**
* The `CurrencyValue` of the listing. Useful for displaying the price information.
*/
bidAmountCurrencyValue: CurrencyValue;
}
@@ -0,0 +1,75 @@
import { NFTMetadata } from "../../../core/schema/nft";
import { Status } from "../../enums";
import { CurrencyValue } from "../currency";
/**
* Represents a marketplace direct listing.
*/
export interface DirectListingV3 {
/**
* The id of the listing.
*/
id: string;
/**
* The address of the creator of listing.
*/
creatorAddress: string;
/**
* The address of the asset being listed.
*/
assetContractAddress: string;
/**
* The ID of the token to list.
*/
tokenId: string;
/**
* The quantity of tokens to include in the listing.
*
* For ERC721s, this value should always be 1 (and will be forced internally regardless of what is passed here).
*/
quantity: string;
/**
* The address of the currency to accept for the listing.
*/
currencyContractAddress: string;
/**
* The `CurrencyValue` of the listing. Useful for displaying the price information.
*/
currencyValuePerToken: CurrencyValue;
/**
* The price to pay per unit of NFTs listed.
*/
pricePerToken: string;
/**
* The asset being listed.
*/
asset: NFTMetadata;
/**
* The start time of the listing.
*/
startTimeInSeconds: number;
/**
* The end time of the listing.
*/
endTimeInSeconds: number;
/**
* Whether the listing is reserved to be bought from a specific set of buyers.
*/
isReservedListing: boolean;
/**
* Whether the listing is CREATED, COMPLETED, or CANCELLED.
*/
status: Status;
}
@@ -0,0 +1,98 @@
import { NFTMetadata } from "../../../core/schema/nft";
import { Status } from "../../enums";
import { CurrencyValue } from "../currency";
/**
* Represents a new marketplace english-auction.
*/
export interface EnglishAuction {
/**
* The id of the auction
*/
id: string;
/**
* The address of the creator of auction.
*/
creatorAddress: string;
/**
* The address of the asset being auctioned.
*/
assetContractAddress: string;
/**
* The ID of the token to auction.
*/
tokenId: string;
/**
* The quantity of tokens to include in the auction.
*
* For ERC721s, this value should always be 1 (and will be forced internally regardless of what is passed here).
*/
quantity: string;
/**
* The address of the currency to accept for the auction.
*/
currencyContractAddress: string;
/**
* The minimum price that a bid must be in order to be accepted.
*/
minimumBidAmount: string;
/**
* The `CurrencyValue` of the minimum bid amount.
* Useful for displaying the price information.
*/
minimumBidCurrencyValue: CurrencyValue;
/**
* The buyout price of the auction.
*/
buyoutBidAmount: string;
/**
* The `CurrencyValue` of the buyout price.
* Useful for displaying the price information.
*/
buyoutCurrencyValue: CurrencyValue;
/**
* This is a buffer e.g. x seconds.
*
* If a new winning bid is made less than x seconds before expirationTimestamp, the
* expirationTimestamp is increased by x seconds.
*/
timeBufferInSeconds: number;
/**
* This is a buffer in basis points e.g. x%.
*
* To be considered as a new winning bid, a bid must be at least x% greater than
* the current winning bid.
*/
bidBufferBps: number;
/**
* The start time of the auction.
*/
startTimeInSeconds: number;
/**
* The end time of the auction.
*/
endTimeInSeconds: number;
/**
* The asset being auctioned.
*/
asset: NFTMetadata;
/**
* Whether the listing is CREATED, COMPLETED, or CANCELLED.
*/
status: Status;
}
@@ -0,0 +1,65 @@
import { NFTMetadata } from "../../../core/schema/nft";
import { Status } from "../../enums";
import { CurrencyValue } from "../currency";
/**
* Represents a marketplace offer.
*/
export interface OfferV3 {
/**
* The id of the offer.
*/
id: string;
/**
* The address of the creator of offer.
*/
offerorAddress: string;
/**
* The address of the asset being sought.
*/
assetContractAddress: string;
/**
* The ID of the token.
*/
tokenId: string;
/**
* The quantity of tokens to buy.
*
* For ERC721s, this value should always be 1 (and will be forced internally regardless of what is passed here).
*/
quantity: string;
/**
* The address of the currency offered for the NFTs.
*/
currencyContractAddress: string;
/**
* The `CurrencyValue` of the listing. Useful for displaying the price information.
*/
currencyValue: CurrencyValue;
/**
* The total offer amount for the NFTs.
*/
totalPrice: string;
/**
* The asset to buy.
*/
asset: NFTMetadata;
/**
* The end time of the offer.
*/
endTimeInSeconds: number;
/**
* Whether the listing is CREATED, COMPLETED, or CANCELLED.
*/
status: Status;
}
@@ -0,0 +1,4 @@
export * from "./DirectListingV3";
export * from "./EnglishAuction";
export * from "./Bid";
export * from "./OfferV3";
@@ -33,6 +33,8 @@ import {
Connection,
ParsedAccountData,
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
/**
@@ -174,30 +176,42 @@ export class NFTHelper {
return nfts;
}
private async getAllMetadataAddresses(collectionAddress: string) {
async getTransactions(
collectionAddress: string,
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
const collectionKey = new PublicKey(collectionAddress);
// TODO cache signatures <> transactions mapping in memory so pagination doesn't re-request this everytime
const allSignatures: ConfirmedSignatureInfo[] = [];
// This returns the first 1000, so we need to loop through until we run out of signatures to get.
let signatures = await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
);
allSignatures.push(...signatures);
do {
const options = {
before: signatures[signatures.length - 1]?.signature,
};
signatures = await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
options,
if (options) {
// only fetch the specified options
allSignatures.push(
...(await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
options,
)),
);
} else {
// fetch everything
// This returns the first 1000, so we need to loop through until we run out of signatures to get.
let signatures = await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
);
allSignatures.push(...signatures);
} while (signatures.length > 0);
const metadataAddresses: PublicKey[] = [];
do {
signatures = await this.metaplex.connection.getSignaturesForAddress(
collectionKey,
{
before: signatures[signatures.length - 1]?.signature,
},
);
allSignatures.push(...signatures);
} while (signatures.length > 0);
}
let txns: TransactionResponse[] = [];
// TODO RPC's will throttle this, need to do some optimizations here
const batchSize = 1000; // alchemy RPC batch limit
for (let i = 0; i < allSignatures.length; i += batchSize) {
@@ -206,13 +220,25 @@ export class NFTHelper {
Math.min(allSignatures.length, i + batchSize),
);
const transactions = (
await this.metaplex.connection.getTransactions(
batch.map((s) => s.signature),
)
).reverse();
txns = [
...txns,
...((
await this.metaplex.connection.getTransactions(
batch.map((s) => s.signature),
)
).filter((tx) => !!tx) as TransactionResponse[]),
];
}
return txns;
}
for (const tx of transactions) {
async getAllMetadataAddresses(
collectionAddress: string,
): Promise<PublicKey[]> {
const txns = (await this.getTransactions(collectionAddress)).reverse();
return txns
.map((tx) => {
if (tx) {
const programIds = tx.transaction.message
.programIds()
@@ -230,9 +256,8 @@ export class NFTHelper {
accountKeys[ix.programIdIndex] === METAPLEX_PROGRAM_ID
) {
const metadataAddressIndex = ix.accounts[0];
const metadata_address =
tx.transaction.message.accountKeys[metadataAddressIndex];
metadataAddresses.push(metadata_address);
return tx.transaction.message.accountKeys[metadataAddressIndex];
}
}
} else if (programIds.includes(CANDYMACHINE_PROGRAM_ID)) {
@@ -243,17 +268,14 @@ export class NFTHelper {
ix.data === "JEuNFGs7wrU"
) {
const metadataAddressIndex = ix.accounts[1];
const metadata_address =
tx.transaction.message.accountKeys[metadataAddressIndex];
metadataAddresses.push(metadata_address);
return tx.transaction.message.accountKeys[metadataAddressIndex];
}
}
}
}
}
}
return metadataAddresses;
})
.filter((a) => !!a) as PublicKey[];
}
async toNFTMetadata(
+21 -1
View File
@@ -1,6 +1,7 @@
import { CurrencyValue } from "../../core/schema/token";
import { WalletSigner } from "../types/common";
import { toCurrencyValue } from "../utils/token";
import { NFTHelper } from "./helpers/nft-helper";
import {
amount,
guestIdentity,
@@ -12,7 +13,12 @@ import {
SOL,
walletAdapterIdentity,
} from "@metaplex-foundation/js";
import { Keypair, PublicKey } from "@solana/web3.js";
import {
Keypair,
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import bs58 from "bs58";
import EventEmitter from "eventemitter3";
import invariant from "tiny-invariant";
@@ -172,6 +178,20 @@ export class UserWallet {
return toCurrencyValue(amount(value, SOL));
}
/**
* Get the all transactions for this program
* @beta
*/
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
const helper = new NFTHelper(this.metaplex);
return helper.getTransactions(
this.metaplex.identity().publicKey.toBase58(),
options,
);
}
private connectToMetaplex(signer: WalletSigner) {
invariant(signer, "Wallet is not connected");
const plugin = isKeypairSigner(signer)
@@ -18,7 +18,11 @@ import {
toOriginalEditionAccount,
TransactionBuilder,
} from "@metaplex-foundation/js";
import { PublicKey } from "@solana/web3.js";
import {
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
@@ -140,6 +144,16 @@ export class NFTCollection {
return this.nft.getAll(this.publicKey.toBase58(), queryParams);
}
/**
* Get the all transactions for this program
* @beta
*/
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58(), options);
}
/**
* Get the total number of nfts minted on this program
* @returns the total number of NFTs minted on this program
@@ -420,14 +434,39 @@ export class NFTCollection {
* ```
*/
async burn(nftAddress: string): Promise<TransactionResult> {
const tx = await this.metaplex.nfts().delete({
mintAddress: new PublicKey(nftAddress),
collection: this.publicKey,
});
return this.burnBatch([nftAddress]).then((txs) => txs[0]);
}
return {
signature: tx.response.signature,
};
/**
* Burn multiple NFTs
* @param nftAddresses - the mint addresses of the NFT to burn
* @returns the transaction signature
*
* @example
* ```jsx
* // Specify the address of the NFT to burn
* const nftAddress1 = "..."
* const nftAddress2 = "..."
* // And send the actual burn transaction
* const tx = await program.burnBatch([nftAddress1, nftAddress2]);
* ```
*/
async burnBatch(nftAddresses: string[]): Promise<TransactionResult[]> {
const txs: TransactionBuilder[] = [];
for (let nftAddress of nftAddresses) {
txs.push(
this.metaplex
.nfts()
.builders()
.delete({
mintAddress: new PublicKey(nftAddress),
collection: this.publicKey,
}),
);
}
return await sendMultipartTransaction(txs, this.metaplex);
}
/**
@@ -472,7 +511,6 @@ export class NFTCollection {
.nfts()
.builders()
.update({
collection: this.publicKey,
nftOrSft: metaplexNft,
creators: enforceCreator(
creators,
@@ -530,7 +568,6 @@ export class NFTCollection {
txs.push(
this.metaplex.nfts().builders().update({
collection: this.publicKey,
nftOrSft: metaplexNft,
sellerFeeBasisPoints,
}),
+169 -24
View File
@@ -21,10 +21,16 @@ import { sendMultipartTransaction } from "../utils/transactions";
import { getNework } from "../utils/urls";
import {
CandyMachineV2Item,
findCandyMachineV2CreatorPda,
Metaplex,
toBigNumber,
TransactionBuilder,
} from "@metaplex-foundation/js";
import { PublicKey } from "@solana/web3.js";
import {
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import { ThirdwebStorage, UploadProgressEvent } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
@@ -218,6 +224,16 @@ export class NFTDrop {
return [...claimedNfts, ...unclaimedNfts];
}
/**
* Get the all transactions for this program
* @beta
*/
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58(), options);
}
/**
* Get the metadata for all the claimed NFTs on this drop
* @returns metadata for all claimed NFTs
@@ -493,47 +509,176 @@ export class NFTDrop {
* ```
*/
async burn(nftAddress: string): Promise<TransactionResult> {
return this.burnBatch([nftAddress]).then((txs) => txs[0]);
}
/**
* Burn multiple NFTs
* @param nftAddresses - the mint addresses of the NFT to burn
* @returns the transaction signature
*
* @example
* ```jsx
* // Specify the address of the NFT to burn
* const nftAddress1 = "..."
* const nftAddress2 = "..."
* // And send the actual burn transaction
* const tx = await program.burnBatch([nftAddress1, nftAddress2]);
* ```
*/
async burnBatch(nftAddresses: string[]): Promise<TransactionResult[]> {
const candyMachine = await this.getCandyMachine();
const collection = candyMachine.collectionMintAddress
? candyMachine.collectionMintAddress
: undefined;
const tx = await this.metaplex.nfts().delete({
mintAddress: new PublicKey(nftAddress),
collection,
});
return {
signature: tx.response.signature,
};
const txs: TransactionBuilder[] = [];
for (let nftAddress of nftAddresses) {
txs.push(
this.metaplex
.nfts()
.builders()
.delete({
mintAddress: new PublicKey(nftAddress),
collection,
}),
);
}
return await sendMultipartTransaction(txs, this.metaplex);
}
/**
* Update the creators of the collection
* @param creators - the creators to update
* @param updateAll - whether or not to retroactively update the creators of all past NFTs
*/
async updateCreators(creators: CreatorInput[]) {
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
creators: enforceCreator(creators, this.metaplex.identity().publicKey),
});
async updateCreators(
creators: CreatorInput[],
updateAll: boolean = false,
): Promise<TransactionResult[]> {
if (updateAll) {
const txs: TransactionBuilder[] = [];
return {
signature: tx.response.signature,
};
const candyMachine = await this.getCandyMachine();
const collectionAddress = candyMachine.collectionMintAddress;
invariant(collectionAddress, "Collection mint address not found");
// We need to find the candy machine creator PDA which is auto populated onto NFTs
// minted with the candy machine, and add it to the list of creators for each NFT
const candyMachineCreatorPda = findCandyMachineV2CreatorPda(
candyMachine.address,
);
const nftCreators = [
{
address: candyMachineCreatorPda.toBase58(),
share: 0,
},
...creators,
];
const allNfts = await this.nft.getAll(collectionAddress.toBase58());
await Promise.all(
allNfts.map(async (nft) => {
const metaplexNft = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(nft.metadata.id) });
txs.push(
this.metaplex
.nfts()
.builders()
.update({
nftOrSft: metaplexNft,
creators: enforceCreator(
nftCreators,
this.metaplex.identity().publicKey,
),
}),
);
}),
);
const results = await sendMultipartTransaction(txs, this.metaplex);
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
creators: enforceCreator(creators, this.metaplex.identity().publicKey),
});
return [
{
signature: tx.response.signature,
},
...results,
];
} else {
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
creators: enforceCreator(creators, this.metaplex.identity().publicKey),
});
return [
{
signature: tx.response.signature,
},
];
}
}
/**
* Update the royalty basis points of the collection
* @param sellerFeeBasisPoints - the royalty basis points of the collection
* @param updateAll - whether or not to retroactively update the royalty basis points of all past NFTs
*/
async updateRoyalty(sellerFeeBasisPoints: number) {
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
sellerFeeBasisPoints,
});
async updateRoyalty(
sellerFeeBasisPoints: number,
updateAll: boolean = false,
) {
if (updateAll) {
const txs: TransactionBuilder[] = [];
return {
signature: tx.response.signature,
};
const candyMachine = await this.getCandyMachine();
const collectionAddress = candyMachine.collectionMintAddress;
invariant(collectionAddress, "Collection mint address not found");
const allNfts = await this.nft.getAll(collectionAddress.toBase58());
await Promise.all(
allNfts.map(async (nft) => {
const metaplexNft = await this.metaplex
.nfts()
.findByMint({ mintAddress: new PublicKey(nft.metadata.id) });
txs.push(
this.metaplex.nfts().builders().update({
nftOrSft: metaplexNft,
sellerFeeBasisPoints,
}),
);
}),
);
const results = await sendMultipartTransaction(txs, this.metaplex);
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
sellerFeeBasisPoints,
});
return [
{
signature: tx.response.signature,
},
...results,
];
} else {
const tx = await this.metaplex.candyMachinesV2().update({
candyMachine: await this.getCandyMachine(),
sellerFeeBasisPoints,
});
return {
signature: tx.response.signature,
};
}
}
private async getCandyMachine() {
+95 -4
View File
@@ -21,6 +21,8 @@ import {
} from "../../src/evm";
import { Plugin } from "../../src/evm/types/plugins";
import { MockStorage } from "./mock/MockStorage";
import weth from "./mock/WETH9.json";
import { deployContractAndUploadMetadata } from "./utils";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
ContractPublisher,
@@ -53,10 +55,13 @@ import {
PluginMap,
PluginMap__factory,
VoteERC20__factory,
MarketplaceV3__factory,
DirectListingsLogic__factory,
EnglishAuctionsLogic__factory,
OffersLogic__factory,
} from "@thirdweb-dev/contracts-js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import { ethers } from "ethers";
import { FormatTypes, FunctionFragment, Interface } from "ethers/lib/utils";
import hardhat from "hardhat";
// it's there, trust me bro
@@ -74,6 +79,7 @@ let signer: SignerWithAddress;
let signers: SignerWithAddress[];
let storage: ThirdwebStorage;
let implementations: { [key in ContractType]?: string };
let mock_weth_address: string;
const fastForwardTime = async (timeInSeconds: number): Promise<void> => {
const now = Math.floor(Date.now() / 1000);
@@ -101,6 +107,15 @@ export const mochaHooks = {
"0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81";
await jsonProvider.send("hardhat_reset", []);
const mock_weth_deployer = new ethers.ContractFactory(
weth.abi,
weth.bytecode,
)
.connect(signer)
.deploy();
const mock_weth = await (await mock_weth_deployer).deployed();
mock_weth_address = mock_weth.address;
const registry = (await new ethers.ContractFactory(
TWRegistry__factory.abi,
TWRegistry__factory.bytecode,
@@ -164,7 +179,7 @@ export const mochaHooks = {
}
for (const contractType in CONTRACTS_MAP) {
if (contractType === "custom") {
if (contractType === "custom" || contractType === "marketplace-v3") {
continue;
}
let factories: any[] = [];
@@ -230,6 +245,13 @@ export const mochaHooks = {
}
}
// setup marketplace-v3 and add implementation to factory
const marketplaceEntrypointAddress = await setupMarketplaceV3();
const tx = await thirdwebFactoryDeployer.addImplementation(
marketplaceEntrypointAddress,
);
await tx.wait();
// eslint-disable-next-line turbo/no-undeclared-env-vars
process.env.registryAddress = thirdwebRegistryAddress;
// eslint-disable-next-line turbo/no-undeclared-env-vars
@@ -254,6 +276,18 @@ export const mochaHooks = {
},
};
const getFunctionSignature = (fnInputs: any): string => {
return (
"(" +
fnInputs
.map((i) => {
return i.type === "tuple" ? getFunctionSignature(i.components) : i.type;
})
.join(",") +
")"
);
};
const generatePluginFunctions = (
pluginAddress: string,
pluginAbi: Abi,
@@ -263,10 +297,12 @@ const generatePluginFunctions = (
// TODO - filter out common functions like _msgSender(), contractType(), etc.
for (const fnFragment of Object.values(pluginInterface.functions)) {
const fn = pluginInterface.getFunction(fnFragment.name);
if (fn.name.includes("_")) {
continue;
}
pluginFunctions.push({
functionSelector: pluginInterface.getSighash(fn),
functionSignature:
fn.name + "(" + fn.inputs.map((i) => i.type).join(",") + ")",
functionSignature: fn.name + getFunctionSignature(fn.inputs),
pluginAddress,
});
}
@@ -312,6 +348,61 @@ async function setupMultichainRegistry(
return multichainRegistryRouter.address;
}
// Setup marketplace-v3 for tests
async function setupMarketplaceV3(): Promise<string> {
// Direct Listings
const directListingsPluginAddress = await deployContractAndUploadMetadata(
DirectListingsLogic__factory.abi,
DirectListingsLogic__factory.bytecode,
signer,
[mock_weth_address],
);
const pluginsDirectListings: Plugin[] = generatePluginFunctions(
directListingsPluginAddress,
DirectListingsLogic__factory.abi,
);
// English Auctions
const englishAuctionPluginAddress = await deployContractAndUploadMetadata(
EnglishAuctionsLogic__factory.abi,
EnglishAuctionsLogic__factory.bytecode,
signer,
[mock_weth_address],
);
const pluginsEnglishAuctions: Plugin[] = generatePluginFunctions(
englishAuctionPluginAddress,
EnglishAuctionsLogic__factory.abi,
);
// Offers
const offersLogicPluginAddress = await deployContractAndUploadMetadata(
OffersLogic__factory.abi,
OffersLogic__factory.bytecode,
signer,
);
const pluginsOffers: Plugin[] = generatePluginFunctions(
offersLogicPluginAddress,
OffersLogic__factory.abi,
);
// Map
const pluginMapAddress = await deployContractAndUploadMetadata(
PluginMap__factory.abi,
PluginMap__factory.bytecode,
signer,
[[...pluginsDirectListings, ...pluginsEnglishAuctions, ...pluginsOffers]],
);
// Router
const marketplaceV3Address = await deployContractAndUploadMetadata(
MarketplaceV3__factory.abi,
MarketplaceV3__factory.bytecode,
signer,
[pluginMapAddress],
);
return marketplaceV3Address;
}
export {
ipfsGatewayUrl,
sdk,
+18 -2
View File
@@ -1,3 +1,4 @@
import { NFTMetadata, NFTMetadataInput } from "../../src/core/schema/nft";
import {
ClaimEligibility,
EditionDrop,
@@ -49,7 +50,7 @@ describe("Edition Drop Contract (V2)", async () => {
platform_fee_basis_points: 10,
platform_fee_recipient: adminWallet.address,
},
2,
"2",
);
bdContract = await sdk.getEditionDrop(address);
});
@@ -60,10 +61,25 @@ describe("Edition Drop Contract (V2)", async () => {
"mock://12398172398172389/0",
]);
expect(parseFloat(cost)).gt(0);
await bdContract.createBatch([{ name: `test$`, description: `desc` }]);
await bdContract.claimConditions.set(0, [
{
price: 0.1,
},
]);
const claimTx = await bdContract.getClaimTransaction(
adminWallet.address,
0,
1,
);
const cost2 = await claimTx.estimateGasCostInEther();
console.log("cost2", cost2);
expect(parseFloat(cost2)).gt(0);
});
it("comprehensive test", async () => {
const metadata = [];
const metadata: NFTMetadataInput[] = [];
for (let i = 0; i < 3; i++) {
metadata.push({ name: `test${i}`, description: `desc${i}` });
}
+104
View File
@@ -0,0 +1,104 @@
import { ThirdwebSDK } from "../../src/evm";
import { expectError, sdk } from "./before-setup";
import { expect } from "chai";
import { ethers } from "ethers";
describe("Error Handling", async () => {
it("should throw proper error on account with no balance", async () => {
const newSdk = ThirdwebSDK.fromSigner(
ethers.Wallet.createRandom().connect(sdk.getProvider()),
);
try {
await newSdk.deployer.deployNFTCollection({
name: "Should Fail Collection",
primary_sale_recipient: "0x0000000000000000000000000000000000000000",
});
expect.fail();
} catch (err) {
expectError(
err,
"You have insufficient funds in your account to execute this transaction.",
);
}
});
it("should throw !BALNFT revert message without gas limit", async () => {
const collectionAddress = await sdk.deployer.deployNFTCollection({
name: "Test Collection",
primary_sale_recipient: "0x0000000000000000000000000000000000000000",
});
const marketplaceAddress = await sdk.deployer.deployMarketplace({
name: "Test Marketplace",
});
const collection = await sdk.getContract(collectionAddress);
await collection.erc721.mintTo(marketplaceAddress, { name: "Test NFT" });
const marketplace = await sdk.getContract(marketplaceAddress);
try {
await marketplace.call("createListing", {
assetContract: collectionAddress,
tokenId: 0,
startTime: 1000000000000,
secondsUntilEndTime: 10000000000000,
quantityToList: 1,
currencyToAccept: "0x0000000000000000000000000000000000000000",
reservePricePerToken: 0,
buyoutPricePerToken: 0,
listingType: 0,
});
expect.fail();
} catch (err) {
// In this case, call static should go through before sendTransaction
// so there should be no tx hash on the error
expectError(err, "!BALNFT");
// eslint-disable-next-line no-unused-expressions
expect(err.info.hash).to.be.undefined;
}
});
// Here we hit a different code path by passing gas limit, send transaction should go through
it("should throw !BALNFT revert message without gas limit", async () => {
const collectionAddress = await sdk.deployer.deployNFTCollection({
name: "Test Collection",
primary_sale_recipient: "0x0000000000000000000000000000000000000000",
});
const marketplaceAddress = await sdk.deployer.deployMarketplace({
name: "Test Marketplace",
});
const collection = await sdk.getContract(collectionAddress);
await collection.erc721.mintTo(marketplaceAddress, { name: "Test NFT" });
const marketplace = await sdk.getContract(marketplaceAddress);
try {
await marketplace.call(
"createListing",
{
assetContract: collectionAddress,
tokenId: 0,
startTime: 1000000000000,
secondsUntilEndTime: 10000000000000,
quantityToList: 1,
currencyToAccept: "0x0000000000000000000000000000000000000000",
reservePricePerToken: 0,
buyoutPricePerToken: 0,
listingType: 0,
},
{
gasLimit: 800000,
},
);
expect.fail();
} catch (err) {
// In this case, call static should go through after sendTransaction
// so we should have a hash on the error
expectError(err, "!BALNFT");
expect(err.info.hash).to.be.a("string");
expect(err.info.hash).to.satisfy(
(hash: string) => hash.startsWith("0x") && hash.length === 66,
);
}
});
});
File diff suppressed because it is too large Load Diff
@@ -62,8 +62,10 @@ describe("Marketplace Contract", async () => {
{
name: "Test Marketplace",
},
"2",
),
);
dummyNftContract = await sdk.getNFTCollection(
await sdk.deployer.deployBuiltInContract(
NFTCollectionInitializer.contractType,
+12 -2
View File
@@ -2,13 +2,23 @@ import {
MockUploader,
MockDownloader,
ThirdwebStorage,
IpfsUploadBatchOptions,
FileOrBufferOrString,
} from "@thirdweb-dev/storage";
const storage = {};
export function MockStorage(): ThirdwebStorage {
// Store mapping of URIs to files/objects
const storage = {};
const uploader = new MockUploader(storage);
const downloader = new MockDownloader(storage);
return new ThirdwebStorage({ uploader, downloader });
}
export async function mockUploadWithCID(
cid: any,
file: FileOrBufferOrString,
options?: IpfsUploadBatchOptions | undefined,
) {
storage[cid] = file;
}
File diff suppressed because one or more lines are too long
+46
View File
@@ -0,0 +1,46 @@
import { resolveContractUriFromAddress } from "../../src/evm";
import { defaultProvider } from "./before-setup";
import { mockUploadWithCID } from "./mock/MockStorage";
import { ContractInterface, ethers } from "ethers";
export const deployContractAndUploadMetadata = async (
abi: ContractInterface,
bytecode: string,
signer: ethers.Signer,
args: any[] = [],
) => {
const deployer = await new ethers.ContractFactory(abi, bytecode)
.connect(signer)
.deploy(...args);
const deployed = await deployer.deployed();
await mockUploadContractMetadata(deployed.address, abi);
return deployed.address;
};
const mockUploadContractMetadata = async (address: string, abi: any) => {
const ipfsHash = (await resolveContractUriFromAddress(
address,
defaultProvider,
)) as string;
const metadata = {
compiler: {},
output: {
abi,
devdoc: {},
userdoc: {},
},
settings: {
compilationTarget: {},
evmVersion: {},
metadata: {},
optimizer: {},
remappings: [],
},
sources: {},
version: 1,
};
const cid = ipfsHash.replace("ipfs://", "");
await mockUploadWithCID(cid, JSON.stringify(metadata));
};
+37 -27
View File
@@ -40,6 +40,11 @@ describe("NFTCollection", async () => {
expect(all[0].metadata.name).to.eq("Test NFT");
});
it("should fetch transactions", async () => {
const txns = await collection.getTransactions();
expect(txns.length).to.eq(2);
});
it("should fetch balance of NFTs", async () => {
const all = await collection.getAll();
const balance = await collection.balanceOf(
@@ -148,14 +153,19 @@ describe("NFTCollection", async () => {
expect(creators[0].address).to.equal(sdk.wallet.getAddress());
expect(creators[0].share).to.equal(100);
const numNfts = (await collection.getAll()).length;
const newCreator = Keypair.generate().publicKey.toBase58();
await collection.updateCreators([
{
address: sdk.wallet.getAddress() as string,
share: 75,
},
{ address: newCreator, share: 25 },
]);
await collection.updateCreators(
[
{
address: sdk.wallet.getAddress() as string,
share: 75,
},
{ address: newCreator, share: 25 },
],
true,
);
creators = await collection.getCreators();
expect(creators.length).to.equal(2);
@@ -164,36 +174,36 @@ describe("NFTCollection", async () => {
expect(creators[1].address).to.equal(newCreator);
expect(creators[1].share).to.equal(25);
// TODO (adam): fix this test
// const all = await collection.getAll();
// for (const nft of all) {
// // @ts-ignore
// const creatorsOfNft = await collection.nft.creatorsOf(nft.metadata.id);
// expect(creatorsOfNft.length).to.equal(2);
// expect(creatorsOfNft[0].address).to.equal(sdk.wallet.getAddress());
// expect(creatorsOfNft[0].share).to.equal(75);
// expect(creatorsOfNft[1].address).to.equal(newCreator);
// expect(creatorsOfNft[1].share).to.equal(25);
// }
const all = await collection.getAll();
expect(all.length).to.equal(numNfts);
for (const nft of all) {
// @ts-ignore
const creatorsOfNft = await collection.nft.creatorsOf(nft.metadata.id);
expect(creatorsOfNft.length).to.equal(2);
expect(creatorsOfNft[0].address).to.equal(sdk.wallet.getAddress());
expect(creatorsOfNft[0].share).to.equal(75);
expect(creatorsOfNft[1].address).to.equal(newCreator);
expect(creatorsOfNft[1].share).to.equal(25);
}
});
it("Should update royalty", async () => {
let royalty = await collection.getRoyalty();
expect(royalty).to.equal(0);
const mintAddress = (await collection.getAll())[0].metadata.id;
// @ts-ignore
let nft = await collection.nft.getRaw(mintAddress);
expect(nft.isMutable).to.equal(true);
expect(nft.sellerFeeBasisPoints).to.equal(0);
const numNfts = (await collection.getAll()).length;
await collection.updateRoyalty(100);
await collection.updateRoyalty(100, true);
royalty = await collection.getRoyalty();
expect(royalty).to.equal(100);
// TODO (adam): fix this test
// nft = await collection.nft.getRaw(mintAddress);
// expect(nft.sellerFeeBasisPoints).to.equal(100);
const all = await collection.getAll();
expect(all.length).to.equal(numNfts);
for (const singleNft of all) {
// @ts-ignore
const royaltyOfNft = await collection.nft.getRaw(singleNft.metadata.id);
expect(royaltyOfNft.sellerFeeBasisPoints).to.equal(100);
}
});
});
+39 -5
View File
@@ -131,11 +131,20 @@ describe("NFTDrop", async () => {
expect(creators[0].address).to.equal(sdk.wallet.getAddress());
expect(creators[0].share).to.equal(100);
const numNfts = (await drop.getAllClaimed()).length;
const newCreator = Keypair.generate().publicKey.toBase58();
await drop.updateCreators([
{ address: sdk.wallet.getAddress() as string, share: 75 },
{ address: newCreator, share: 25 },
]);
await drop.updateCreators(
[
{
address: sdk.wallet.getAddress() as string,
share: 75,
verified: true,
},
{ address: newCreator, share: 25, verified: true },
],
true,
);
creators = await drop.getCreators();
expect(creators.length).to.equal(2);
@@ -143,14 +152,39 @@ describe("NFTDrop", async () => {
expect(creators[0].share).to.equal(75);
expect(creators[1].address).to.equal(newCreator);
expect(creators[1].share).to.equal(25);
const all = await drop.getAllClaimed();
expect(all.length).to.equal(numNfts);
for (const nft of all) {
// @ts-ignore
const creatorsOfNft = await drop.nft.creatorsOf(nft.metadata.id);
expect(creatorsOfNft.length).to.equal(3);
// Skip checking the first creator which is the candy machine creator pda
expect(creatorsOfNft[1].address).to.equal(sdk.wallet.getAddress());
expect(creatorsOfNft[1].share).to.equal(75);
expect(creatorsOfNft[2].address).to.equal(newCreator);
expect(creatorsOfNft[2].share).to.equal(25);
}
});
it("Should update royalty", async () => {
let royalty = await drop.getRoyalty();
expect(royalty).to.equal(0);
await drop.updateRoyalty(100);
const mintAddress = (await drop.getAll())[0].metadata.id;
// @ts-ignore
let nft = await drop.nft.getRaw(mintAddress);
expect(nft.isMutable).to.equal(true);
expect(nft.sellerFeeBasisPoints).to.equal(0);
await drop.updateRoyalty(100, true);
royalty = await drop.getRoyalty();
expect(royalty).to.equal(100);
// @ts-ignore
nft = await drop.nft.getRaw(mintAddress);
expect(nft.sellerFeeBasisPoints).to.equal(100);
});
it("Should get all correctly", async () => {
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/storage
## 1.0.8
### Patch Changes
- [#553](https://github.com/thirdweb-dev/js/pull/553) [`76d219e`](https://github.com/thirdweb-dev/js/commit/76d219e6bea1496b45623b3081152854ce1eaa6e) Thanks [@jnsdls](https://github.com/jnsdls)! - force release
## 1.0.7
### Patch Changes
- [#474](https://github.com/thirdweb-dev/js/pull/474) [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Edit mock uploader, downloader
## 1.0.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/storage",
"version": "1.0.6",
"version": "1.0.8",
"main": "dist/thirdweb-dev-storage.cjs.js",
"module": "dist/thirdweb-dev-storage.esm.js",
"exports": {
@@ -17,8 +17,11 @@ export class MockDownloader implements IStorageDownloader {
}
async download(url: string): Promise<Response> {
const [cid, name] = url.replace("mock://", "").split("/");
const data = this.storage[cid][name];
const [cid, name] = url.includes("mock://")
? url.replace("mock://", "").split("/")
: url.replace("ipfs://", "").split("/");
const data = name ? this.storage[cid][name] : this.storage[cid];
return {
async json() {
return Promise.resolve(JSON.parse(data));
+51
View File
@@ -1,5 +1,56 @@
# @thirdweb-dev/unity-js-bridge
## 0.2.7
### Patch Changes
- Updated dependencies [[`55b00ef`](https://github.com/thirdweb-dev/js/commit/55b00eff2668eb751b9f907e035b3a0c122d2183), [`76d219e`](https://github.com/thirdweb-dev/js/commit/76d219e6bea1496b45623b3081152854ce1eaa6e)]:
- @thirdweb-dev/sdk@3.8.2
- @thirdweb-dev/storage@1.0.8
## 0.2.6
### Patch Changes
- Updated dependencies [[`448b357`](https://github.com/thirdweb-dev/js/commit/448b357ed4b864e05e531837ca3bc97fbcab3291)]:
- @thirdweb-dev/sdk@3.8.1
## 0.2.5
### Patch Changes
- Updated dependencies [[`0b38dac`](https://github.com/thirdweb-dev/js/commit/0b38dac852e54d0bccd263631f4342aee39d7c29), [`29d0c62`](https://github.com/thirdweb-dev/js/commit/29d0c62610cdb88f75d19574bddcb028b5c40393), [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`f295ec6`](https://github.com/thirdweb-dev/js/commit/f295ec67b4cc422c51beda94cbc4d54a31ce9566), [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`1686fb4`](https://github.com/thirdweb-dev/js/commit/1686fb4b2c0d93004623bc02fcb0e32233fe582c), [`f1aecc3`](https://github.com/thirdweb-dev/js/commit/f1aecc30499e39fb23a205bade939bd939f0d0e4)]:
- @thirdweb-dev/sdk@3.8.0
- @thirdweb-dev/storage@1.0.7
## 0.2.4
### Patch Changes
- Updated dependencies [[`ec1f232`](https://github.com/thirdweb-dev/js/commit/ec1f2324c7a0e58c2d2a209cfe0d194807ba5b70), [`d29f7e7`](https://github.com/thirdweb-dev/js/commit/d29f7e78f09063630e812dd41164d8a41d4f97dd)]:
- @thirdweb-dev/sdk@3.7.4
## 0.2.3
### Patch Changes
- Updated dependencies [[`b45d761`](https://github.com/thirdweb-dev/js/commit/b45d761f8f2695b48312ec6eff15e0549e6a8a41), [`5f8ec45`](https://github.com/thirdweb-dev/js/commit/5f8ec45af7c81d075a84846098f38cad90d03fb9), [`04de5b2`](https://github.com/thirdweb-dev/js/commit/04de5b278362573991140c3a496de7451e5c770d)]:
- @thirdweb-dev/sdk@3.7.3
## 0.2.2
### Patch Changes
- Updated dependencies [[`19b8e18`](https://github.com/thirdweb-dev/js/commit/19b8e18b6bf68e374c817fefbf2fd29ac5573052)]:
- @thirdweb-dev/sdk@3.7.2
## 0.2.1
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/sdk@3.7.1
## 0.2.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.2.0",
"version": "0.2.7",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
+36 -121
View File
@@ -2,51 +2,24 @@
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"outputs": [
"dist/**"
],
"dependsOn": [
"^build"
]
"outputs": ["dist/**"],
"dependsOn": ["^build"]
},
"@thirdweb-dev/wallets#build": {
"outputs": [
"dist/**",
"evm/**",
"solana/**"
],
"dependsOn": [
"^build"
]
"outputs": ["dist/**", "evm/**", "solana/**"],
"dependsOn": ["^build"]
},
"@thirdweb-dev/pay#build": {
"outputs": [
"dist/**",
"integrations/**"
],
"dependsOn": [
"^build"
]
"outputs": ["dist/**", "integrations/**"],
"dependsOn": ["^build"]
},
"@thirdweb-dev/sdk#build": {
"outputs": [
"dist/**",
"evm/**",
"solana/**"
],
"dependsOn": [
"^build"
]
"outputs": ["dist/**", "evm/**", "solana/**"],
"dependsOn": ["^build"]
},
"@thirdweb-dev/react#build": {
"outputs": [
"dist/**",
"evm/**",
"solana/**"
],
"dependsOn": [
"^build"
]
"outputs": ["dist/**", "evm/**", "solana/**"],
"dependsOn": ["^build"]
},
"@thirdweb-dev/react-core#build": {
"outputs": ["dist/**", "evm/**", "solana/**"],
@@ -61,106 +34,52 @@
"next/**",
"next-auth/**"
],
"dependsOn": [
"^build"
]
"dependsOn": ["^build"]
},
"@thirdweb-dev/contracts-js#build": {
"outputs": [
"dist/**",
"factories/**",
"src/**"
],
"dependsOn": [
"^build"
]
"outputs": ["dist/**", "factories/**", "src/**"],
"inputs": ["package.json"],
"dependsOn": ["^build"]
},
"test": {
"outputs": [],
"inputs": [
"src/**",
"test/**"
],
"dependsOn": [
"build"
]
"inputs": ["src/**", "test/**"],
"dependsOn": ["build"]
},
"test:solana": {
"outputs": [],
"inputs": [
"src/core/**",
"src/solana/**",
"test/solana/**"
],
"dependsOn": [
"build"
]
"inputs": ["src/core/**", "src/solana/**", "test/solana/**"],
"dependsOn": ["build"]
},
"test:evm": {
"outputs": [],
"inputs": [
"src/core/**",
"src/evm/**",
"test/evm/**"
],
"dependsOn": [
"build"
]
"inputs": ["src/core/**", "src/evm/**", "test/evm/**"],
"dependsOn": ["build"]
},
"e2e": {
"outputs": [],
"inputs": [
"src/**"
],
"dependsOn": [
"build"
]
"inputs": ["src/**"],
"dependsOn": ["build"]
},
"e2e:e2e-cra-5": {
"outputs": [
"build"
],
"inputs": [
"src/**",
"fixtures/**"
],
"dependsOn": [
"build"
]
"outputs": ["build"],
"inputs": ["src/**", "fixtures/**"],
"dependsOn": ["build"]
},
"e2e:e2e-next-12": {
"outputs": [
".next"
],
"inputs": [
"src/**",
"fixtures/**"
],
"dependsOn": [
"build"
]
"outputs": [".next"],
"inputs": ["src/**", "fixtures/**"],
"dependsOn": ["build"]
},
"e2e:e2e-vite-3": {
"outputs": [
"dist"
],
"inputs": [
"src/**",
"fixtures/**"
],
"dependsOn": [
"build"
]
"outputs": ["dist"],
"inputs": ["src/**", "fixtures/**"],
"dependsOn": ["build"]
},
"thirdweb#e2e": {
"outputs": [],
"inputs": [
"src/**",
"e2e/**"
],
"dependsOn": [
"build"
]
"inputs": ["src/**", "e2e/**"],
"dependsOn": ["build"]
},
"lint": {
"outputs": []
@@ -176,12 +95,8 @@
},
"push": {
"cache": false,
"dependsOn": [
"build"
]
"dependsOn": ["build"]
}
},
"globalEnv": [
"CI"
]
}
"globalEnv": ["CI"]
}
+3071 -3028
View File
File diff suppressed because it is too large Load Diff