Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9919026f38 | ||
|
|
c349f44d58 | ||
|
|
fc7c6b6211 | ||
|
|
55b00eff26 | ||
|
|
76d219e6be | ||
|
|
f13408a33d | ||
|
|
74b19012c2 | ||
|
|
8f85868324 | ||
|
|
448b357ed4 | ||
|
|
912738bd0a | ||
|
|
f1aecc3049 | ||
|
|
0b38dac852 | ||
|
|
440a4ade95 | ||
|
|
29d0c62610 | ||
|
|
f295ec67b4 | ||
|
|
1686fb4b2c | ||
|
|
85dd18e8f0 | ||
|
|
ec1f2324c7 | ||
|
|
b59a31c365 | ||
|
|
d29f7e78f0 | ||
|
|
3e2b392b6b | ||
|
|
cf96333fa2 | ||
|
|
4213cfee77 |
@@ -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,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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
{}
|
||||
}`,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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,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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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));
|
||||
};
|
||||
@@ -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,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));
|
||||
|
||||
@@ -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,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
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user