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
73 changed files with 10954 additions and 3357 deletions
+36
View File
@@ -1,5 +1,41 @@
# 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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.9.14",
"version": "0.9.19",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+6 -2
View File
@@ -82,8 +82,12 @@ const main = async () => {
const versionInfo = await notifier.fetchInfo();
versionSpinner.stop();
// Set cache to prevent checking for updates again for 24 hours
cache.set("last-version-check", new Date().toISOString());
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({
@@ -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
)
{}
}`,
};
+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"
}
}
}
+12
View File
@@ -1,5 +1,17 @@
# @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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "3.7.3",
"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",
@@ -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;
}
@@ -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
+30
View File
@@ -1,5 +1,35 @@
# @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
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.7.3",
"version": "3.8.2",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -102,6 +102,7 @@
"dependencies": {
"@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",
@@ -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
+46
View File
@@ -1,5 +1,51 @@
# @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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.7.3",
"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 {
+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
*
@@ -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,7 @@ import {
Connection,
ParsedAccountData,
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
@@ -177,27 +178,38 @@ export class NFTHelper {
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);
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
@@ -210,13 +222,11 @@ export class NFTHelper {
txns = [
...txns,
...(
(
await this.metaplex.connection.getTransactions(
batch.map((s) => s.signature),
)
).filter((tx) => !!tx) as TransactionResponse[]
).reverse(),
...((
await this.metaplex.connection.getTransactions(
batch.map((s) => s.signature),
)
).filter((tx) => !!tx) as TransactionResponse[]),
];
}
return txns;
@@ -225,7 +235,7 @@ export class NFTHelper {
async getAllMetadataAddresses(
collectionAddress: string,
): Promise<PublicKey[]> {
const txns = await this.getTransactions(collectionAddress);
const txns = (await this.getTransactions(collectionAddress)).reverse();
return txns
.map((tx) => {
+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, TransactionResponse } from "@solana/web3.js";
import {
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
@@ -144,8 +148,10 @@ export class NFTCollection {
* Get the all transactions for this program
* @beta
*/
async getTransactions(): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58());
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58(), options);
}
/**
+9 -3
View File
@@ -26,7 +26,11 @@ import {
toBigNumber,
TransactionBuilder,
} from "@metaplex-foundation/js";
import { PublicKey, TransactionResponse } from "@solana/web3.js";
import {
PublicKey,
SignaturesForAddressOptions,
TransactionResponse,
} from "@solana/web3.js";
import { ThirdwebStorage, UploadProgressEvent } from "@thirdweb-dev/storage";
import invariant from "tiny-invariant";
@@ -224,8 +228,10 @@ export class NFTDrop {
* Get the all transactions for this program
* @beta
*/
async getTransactions(): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58());
async getTransactions(
options?: SignaturesForAddressOptions,
): Promise<TransactionResponse[]> {
return this.nft.getTransactions(this.publicKey.toBase58(), options);
}
/**
+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,
+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));
};
+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));
+30
View File
@@ -1,5 +1,35 @@
# @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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.2.3",
"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"]
}
+2979 -2954
View File
File diff suppressed because it is too large Load Diff