Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
8516fc11f9 Version Packages (#2817)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-19 14:36:14 -07:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b4e7c74ce5 chore(chains): update chains (#2812)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-19 11:11:44 -07:00
Jonas DanielsandGitHub d6646475e2 refactor(viem): explicitly type walletClient return type (#2816) 2024-04-19 17:30:13 +00:00
Jonas DanielsandGitHub 4dbcbd13db feat(react): initial auth for <ConnectButton /> (#2793)
Co-authored-by: Manan Tank <[email protected]>
2024-04-19 10:36:11 -07:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
4eb628363a Version Packages (#2808)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-19 21:00:12 +12:00
Joaquim VergesandGitHub 381b669cd1 fix: react query invalidation (#2815) 2024-04-19 20:42:24 +12:00
Jonas DanielsandGitHub bebcf46c3a feat: Add support for updating metadata for ERC1155 contracts (#2814) 2024-04-18 14:43:22 -07:00
1b2bfc83d7 feat: add ERC20 and ERC1155 signature mint extensions (#2811)
Co-authored-by: Jonas Daniels <[email protected]>
2024-04-18 21:22:03 +00:00
Jonas DanielsandGitHub 576bb579f6 refactor: Refactor test code for ERC721 and ERC1155 direct listings (#2813) 2024-04-18 11:01:19 -07:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
23680d19cb chore(chains): update chains (#2809)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-18 09:10:29 -07:00
Jonas DanielsandGitHub 94e354aad3 feat(marketplace/offers): implement extensions for marketplace offers (#2806)
Signed-off-by: Jonas Daniels <[email protected]>
2024-04-18 01:54:56 -07:00
Jonas DanielsandGitHub e18606e034 chore: update stale issue settings (#2810) 2024-04-17 23:43:45 -07:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ca2576f818 chore(chains): update chains (#2802)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-17 22:55:46 -07:00
FirekeeperandGitHub ec35f32d2b Unity 4.12.1 (#2805) 2024-04-18 05:45:13 +00:00
112 changed files with 4399 additions and 1044 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/auth
## 4.1.64
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/wallets@2.5.6
## 4.1.63
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/wallets@2.5.5
## 4.1.62
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "4.1.62",
"version": "4.1.64",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"browser": {
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/chains
## 0.1.97
### Patch Changes
- [#2812](https://github.com/thirdweb-dev/js/pull/2812) [`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648) Thanks [@github-actions](https://github.com/apps/github-actions)! - Synced Chains Package
## 0.1.96
### Patch Changes
- [#2802](https://github.com/thirdweb-dev/js/pull/2802) [`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028) Thanks [@github-actions](https://github.com/apps/github-actions)! - Synced Chains Package
## 0.1.95
### Patch Changes
+31
View File
@@ -0,0 +1,31 @@
import type { Chain } from "../src/types";
export default {
"chain": "IOTA EVM",
"chainId": 1075,
"explorers": [],
"faucets": [
"https://evm-toolkit.evm.testnet.iotaledger.net"
],
"icon": {
"url": "ipfs://bafkreibky2sy6qhi6arktayvologkrgu5kudpgdxfkx4uosbvmstz7v4di",
"width": 720,
"height": 720,
"format": "png"
},
"infoURL": "https://www.iota.org",
"name": "IOTA EVM Testnet",
"nativeCurrency": {
"name": "IOTA",
"symbol": "IOTA",
"decimals": 18
},
"networkId": 1075,
"rpc": [
"https://1075.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://json-rpc.evm.testnet.iotaledger.net"
],
"shortName": "iotaevm-testnet",
"slug": "iota-evm-testnet",
"testnet": true,
"title": "IOTA EVM Testnet"
} as const satisfies Chain;
@@ -0,0 +1,50 @@
import type { Chain } from "../src/types";
export default {
"chain": "Cyber",
"chainId": 111557560,
"explorers": [
{
"name": "Cyber Testnet Explorer",
"url": "https://testnet.cyberscan.co",
"standard": "EIP3091",
"icon": {
"url": "ipfs://QmYtUimyqHkkFxYdbXXRbUqNg2VLPUg6Uu2C2nmFWowiZM",
"width": 551,
"height": 540,
"format": "png"
}
}
],
"faucets": [],
"icon": {
"url": "ipfs://QmP61yDNPv7fxY9ZzPs4CjQDbZLoKtF8eWWjszVYbwkabd",
"width": 1000,
"height": 1000,
"format": "svg"
},
"infoURL": "https://cyber.co/",
"name": "Cyber Testnet",
"nativeCurrency": {
"name": "Sepolia Ether",
"symbol": "ETH",
"decimals": 18
},
"networkId": 111557560,
"parent": {
"type": "L2",
"chain": "eip155-11155111",
"bridges": [
{
"url": "https://op-bridge.alt.technology/deposit?id=111557560"
}
]
},
"rpc": [
"https://111557560.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://cyber-testnet.alt.technology/",
"wss://cyber-testnet.alt.technology/ws"
],
"shortName": "cysep",
"slug": "cyber-testnet",
"testnet": true
} as const satisfies Chain;
+40
View File
@@ -0,0 +1,40 @@
import type { Chain } from "../src/types";
export default {
"chain": "MOS",
"chainId": 1188,
"explorers": [
{
"name": "mosscan",
"url": "https://www.mosscan.com",
"standard": "none",
"icon": {
"url": "ipfs://QmczQf61mZmg3NmmGnwseDxzH1AqapA252KCTT85GuNG96",
"width": 51,
"height": 50,
"format": "png"
}
}
],
"faucets": [],
"icon": {
"url": "ipfs://QmczQf61mZmg3NmmGnwseDxzH1AqapA252KCTT85GuNG96",
"width": 51,
"height": 50,
"format": "png"
},
"infoURL": "https://www.mosscan.com",
"name": "ClubMos Mainnet",
"nativeCurrency": {
"name": "ClubMos",
"symbol": "MOS",
"decimals": 18
},
"networkId": 1188,
"rpc": [
"https://1188.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://mainnet.mosscan.com"
],
"shortName": "MOS",
"slug": "clubmos",
"testnet": false
} as const satisfies Chain;
+28
View File
@@ -0,0 +1,28 @@
import type { Chain } from "../src/types";
export default {
"chain": "ETH",
"chainId": 1221,
"explorers": [],
"faucets": [],
"icon": {
"url": "ipfs://QmeC4mKGbBpwmLfFxUSsareWkSWJomevUmex8ajjxo1zHx",
"width": 500,
"height": 500,
"format": "png"
},
"infoURL": "https://www.cyclenetwork.io/",
"name": "Cycle Network Testnet",
"nativeCurrency": {
"name": "Ether",
"symbol": "ETH",
"decimals": 18
},
"networkId": 1221,
"rpc": [
"https://1221.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc-testnet.cyclenetwork.io"
],
"shortName": "Cycle",
"slug": "cycle-network-testnet",
"testnet": true
} as const satisfies Chain;
+46
View File
@@ -0,0 +1,46 @@
import type { Chain } from "../src/types";
export default {
"chain": "MagApe",
"chainId": 141319,
"explorers": [
{
"name": "etherscan",
"url": "http://testnet-api.magape.io:81",
"standard": "EIP3091",
"icon": {
"url": "ipfs://QmPsxWMvnxnZ9dZcRJJMaE24o34dQsnsAMCJh3kVDrE1aB",
"width": 1000,
"height": 1000,
"format": "png"
}
}
],
"faucets": [],
"features": [
{
"name": "EIP155"
}
],
"icon": {
"url": "ipfs://QmPsxWMvnxnZ9dZcRJJMaE24o34dQsnsAMCJh3kVDrE1aB",
"width": 1000,
"height": 1000,
"format": "png"
},
"infoURL": "https://magape.io",
"name": "MagApe Testnet",
"nativeCurrency": {
"name": "MagApe",
"symbol": "MAG",
"decimals": 18
},
"networkId": 141319,
"rpc": [
"https://141319.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://testnet-api.magape.io/chain/"
],
"shortName": "mag",
"slug": "magape-testnet",
"testnet": true,
"title": "MagApeChain"
} as const satisfies Chain;
+30
View File
@@ -0,0 +1,30 @@
import type { Chain } from "../src/types";
export default {
"chain": "ETH",
"chainId": 167000,
"explorers": [],
"faucets": [],
"icon": {
"url": "ipfs://QmcHdmVr5VRUJq13jnM6tgah5Ge7hn3Dm14eY6vwivJ5ui",
"width": 288,
"height": 258,
"format": "png"
},
"infoURL": "https://taiko.xyz",
"name": "Taiko Mainnet",
"nativeCurrency": {
"name": "Ether",
"symbol": "ETH",
"decimals": 18
},
"networkId": 167000,
"rpc": [
"https://167000.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc.taiko.xyz",
"wss://ws.taiko.xyz"
],
"shortName": "tko-mainnet",
"slug": "taiko",
"status": "incubating",
"testnet": false
} as const satisfies Chain;
+1 -1
View File
@@ -29,7 +29,7 @@ export default {
"rpc": [
"https://167008.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc.katla.taiko.xyz",
"wss://rpc.katla.taiko.xyz",
"wss://ws.katla.taiko.xyz",
"https://taiko-katla.drpc.org",
"wss://taiko-katla.drpc.org"
],
+36
View File
@@ -0,0 +1,36 @@
import type { Chain } from "../src/types";
export default {
"chain": "Nexis Network",
"chainId": 2370,
"explorers": [
{
"name": "Nexis Testnet Explorer",
"url": "https://evm-testnet.nexscan.io",
"standard": "EIP3091"
}
],
"faucets": [
"https://evm-faucet.nexis.network"
],
"icon": {
"url": "ipfs://QmdJwWuMgbhoZhgFM4zWrZne6qs5ktcL1vBLKX12VGgALM",
"width": 1892,
"height": 1892,
"format": "png"
},
"infoURL": "https://nexis.network/",
"name": "Nexis Network Testnet",
"nativeCurrency": {
"name": "Nexis",
"symbol": "NZT",
"decimals": 18
},
"networkId": 2370,
"rpc": [
"https://2370.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://evm-testnet.nexis.network"
],
"shortName": "nzt",
"slug": "nexis-network-testnet",
"testnet": true
} as const satisfies Chain;
+32 -4
View File
@@ -2,8 +2,33 @@ import type { Chain } from "../src/types";
export default {
"chain": "NEURA",
"chainId": 267,
"explorers": [],
"faucets": [],
"explorers": [
{
"name": "blockscout",
"url": "https://explorer.neura-testnet.ankr.com",
"standard": "EIP3091",
"icon": {
"url": "ipfs://QmYtUimyqHkkFxYdbXXRbUqNg2VLPUg6Uu2C2nmFWowiZM",
"width": 551,
"height": 540,
"format": "png"
}
},
{
"name": "ankrscan-neura",
"url": "https://testnet.explorer.neuraprotocol.io",
"standard": "EIP3091",
"icon": {
"url": "ipfs://QmWdpK5WFKrosaCSpJRCvU7uXeKk2qVeCckTxh6Zw2JrK8",
"width": 600,
"height": 600,
"format": "png"
}
}
],
"faucets": [
"https://testnet.neuraprotocol.io/faucet"
],
"features": [
{
"name": "EIP155"
@@ -26,11 +51,14 @@ export default {
"decimals": 18
},
"networkId": 267,
"rpc": [],
"rpc": [
"https://267.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc.ankr.com/neura_testnet"
],
"shortName": "tneura",
"slip44": 1,
"slug": "neura-testnet",
"status": "incubating",
"status": "active",
"testnet": true,
"title": "Neura Testnet"
} as const satisfies Chain;
+39
View File
@@ -0,0 +1,39 @@
import type { Chain } from "../src/types";
export default {
"chain": "ETH",
"chainId": 2810,
"explorers": [
{
"name": "Morph Holesky Testnet Explorer",
"url": "https://explorer-holesky.morphl2.io",
"standard": "EIP3091"
}
],
"faucets": [],
"infoURL": "https://morphl2.io",
"name": "Morph Holesky",
"nativeCurrency": {
"name": "Ether",
"symbol": "ETH",
"decimals": 18
},
"networkId": 2810,
"parent": {
"type": "L2",
"chain": "eip155-1",
"bridges": [
{
"url": "https://bridge-holesky.morphl2.io"
}
]
},
"rpc": [
"https://2810.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc-holesky.morphl2.io"
],
"shortName": "hmorph",
"slip44": 1,
"slug": "morph-holesky",
"testnet": true,
"title": "Morph Holesky Testnet"
} as const satisfies Chain;
+30
View File
@@ -0,0 +1,30 @@
import type { Chain } from "../src/types";
export default {
"chain": "Avalanche",
"chainId": 58316,
"explorers": [],
"faucets": [],
"features": [],
"icon": {
"url": "https://images.ctfassets.net/9bazykntljf6/62CceHSYsRS4D9fgDSkLRB/877cb8f26954e1743ff535fd7fdaf78f/avacloud-placeholder.svg",
"width": 256,
"height": 256,
"format": "svg"
},
"infoURL": "https://avacloud.io",
"name": "QaUser1943 Testnet",
"nativeCurrency": {
"name": "QaUser1943 Testnet Token",
"symbol": "JAI",
"decimals": 18
},
"networkId": 58316,
"redFlags": [],
"rpc": [
"https://58316.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc"
],
"shortName": "QaUser1943 Testnet",
"slug": "qauser1943-testnet",
"testnet": true
} as const satisfies Chain;
+28
View File
@@ -0,0 +1,28 @@
import type { Chain } from "../src/types";
export default {
"chain": "Planq",
"chainId": 7077,
"explorers": [],
"faucets": [],
"icon": {
"url": "ipfs://QmWEy9xK5BoqxPuVs7T48WM4exJrxzkEFt45iHcxWqUy8D",
"width": 256,
"height": 256,
"format": "png"
},
"infoURL": "https://planq.network",
"name": "Planq Atlas Testnet",
"nativeCurrency": {
"name": "Planq",
"symbol": "tPLQ",
"decimals": 18
},
"networkId": 7077,
"rpc": [
"https://7077.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://evm-rpc-atlas.planq.network"
],
"shortName": "planq-atlas-testnet",
"slug": "planq-atlas-testnet",
"testnet": true
} as const satisfies Chain;
+30
View File
@@ -0,0 +1,30 @@
import type { Chain } from "../src/types";
export default {
"chain": "Cyber",
"chainId": 7560,
"explorers": [],
"faucets": [],
"icon": {
"url": "ipfs://QmP61yDNPv7fxY9ZzPs4CjQDbZLoKtF8eWWjszVYbwkabd",
"width": 1000,
"height": 1000,
"format": "svg"
},
"infoURL": "https://cyber.co/",
"name": "Cyber Mainnet",
"nativeCurrency": {
"name": "Ether",
"symbol": "ETH",
"decimals": 18
},
"networkId": 7560,
"parent": {
"type": "L2",
"chain": "eip155-1"
},
"rpc": [],
"shortName": "cyeth",
"slug": "cyber",
"status": "incubating",
"testnet": false
} as const satisfies Chain;
+30
View File
@@ -0,0 +1,30 @@
import type { Chain } from "../src/types";
export default {
"chain": "Avalanche",
"chainId": 79490,
"explorers": [],
"faucets": [],
"features": [],
"icon": {
"url": "https://images.ctfassets.net/9bazykntljf6/62CceHSYsRS4D9fgDSkLRB/877cb8f26954e1743ff535fd7fdaf78f/avacloud-placeholder.svg",
"width": 256,
"height": 256,
"format": "svg"
},
"infoURL": "https://avacloud.io",
"name": "QaUser2672 Testnet",
"nativeCurrency": {
"name": "QaUser2672 Testnet Token",
"symbol": "ENU",
"decimals": 18
},
"networkId": 79490,
"redFlags": [],
"rpc": [
"https://79490.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc"
],
"shortName": "QaUser2672 Testnet",
"slug": "qauser2672-testnet",
"testnet": true
} as const satisfies Chain;
+12 -12
View File
@@ -11,22 +11,22 @@ export default {
"url": "https://explorerl2new-camp-network-4xje7wy105.t.conduit.xyz",
"standard": "EIP3091",
"icon": {
"url": "ipfs://QmYtUimyqHkkFxYdbXXRbUqNg2VLPUg6Uu2C2nmFWowiZM",
"width": 83,
"height": 82,
"format": "svg"
"url": "ipfs://Qmd5ux27W44fjxHP2opz7eLhJ6CJJm9WR6VcKNhbQBxiSd/QOyzwbO2_400x400.jpg",
"width": 400,
"height": 400,
"format": "jpg"
}
}
],
"faucets": [],
"features": [],
"icon": {
"url": "ipfs://QmYtUimyqHkkFxYdbXXRbUqNg2VLPUg6Uu2C2nmFWowiZM",
"width": 551,
"height": 540,
"format": "png"
"url": "ipfs://Qmd5ux27W44fjxHP2opz7eLhJ6CJJm9WR6VcKNhbQBxiSd/QOyzwbO2_400x400.jpg",
"width": 400,
"height": 400,
"format": "jpg"
},
"name": "Camp Network",
"name": "Camp Network Testnet",
"nativeCurrency": {
"name": "Ether",
"symbol": "ETH",
@@ -38,7 +38,7 @@ export default {
"https://90354.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc-camp-network-4xje7wy105.t.conduit.xyz/"
],
"shortName": "campaign-l2",
"slug": "camp-network",
"testnet": false
"shortName": "campaign-l2-test",
"slug": "camp-network-testnet",
"testnet": true
} as const satisfies Chain;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/chains",
"version": "0.1.95",
"version": "0.1.97",
"main": "dist/thirdweb-dev-chains.cjs.js",
"module": "dist/thirdweb-dev-chains.esm.js",
"exports": {
File diff suppressed because one or more lines are too long
+20
View File
@@ -1,5 +1,25 @@
# thirdweb
## 0.13.72
### Patch Changes
- Updated dependencies [[`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- @thirdweb-dev/chains@0.1.97
- @thirdweb-dev/sdk@4.0.66
- @thirdweb-dev/wallets@2.5.6
- @thirdweb-dev/auth@4.1.64
## 0.13.71
### Patch Changes
- Updated dependencies [[`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- @thirdweb-dev/chains@0.1.96
- @thirdweb-dev/sdk@4.0.65
- @thirdweb-dev/wallets@2.5.5
- @thirdweb-dev/auth@4.1.63
## 0.13.70
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/cli",
"main": "dist/cli/index.js",
"version": "0.13.70",
"version": "0.13.72",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+20
View File
@@ -1,5 +1,25 @@
# @thirdweb-dev/react-core
## 4.6.5
### Patch Changes
- Updated dependencies [[`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- @thirdweb-dev/chains@0.1.97
- @thirdweb-dev/sdk@4.0.66
- @thirdweb-dev/wallets@2.5.6
- @thirdweb-dev/auth@4.1.64
## 4.6.4
### Patch Changes
- Updated dependencies [[`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- @thirdweb-dev/chains@0.1.96
- @thirdweb-dev/sdk@4.0.65
- @thirdweb-dev/wallets@2.5.5
- @thirdweb-dev/auth@4.1.63
## 4.6.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "4.6.3",
"version": "4.6.5",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -1,5 +1,9 @@
# @thirdweb-dev/react-native-compat
## 0.7.37
## 0.7.36
## 0.7.35
### Patch Changes
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/react-native-compat",
"description": "Shims for Thirdweb in React Native Projects",
"version": "0.7.35",
"version": "0.7.37",
"author": "thirdweb eng <[email protected]>",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-native-compat",
"license": "Apache-2.0",
+20
View File
@@ -1,5 +1,25 @@
# @thirdweb-dev/react-native
## 0.7.37
### Patch Changes
- Updated dependencies [[`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- @thirdweb-dev/chains@0.1.97
- @thirdweb-dev/sdk@4.0.66
- @thirdweb-dev/react-core@4.6.5
- @thirdweb-dev/wallets@2.5.6
## 0.7.36
### Patch Changes
- Updated dependencies [[`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- @thirdweb-dev/chains@0.1.96
- @thirdweb-dev/sdk@4.0.65
- @thirdweb-dev/react-core@4.6.4
- @thirdweb-dev/wallets@2.5.5
## 0.7.35
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-native",
"version": "0.7.35",
"version": "0.7.37",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react-native",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+20
View File
@@ -1,5 +1,25 @@
# @thirdweb-dev/react
## 4.6.5
### Patch Changes
- Updated dependencies [[`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- @thirdweb-dev/chains@0.1.97
- @thirdweb-dev/sdk@4.0.66
- @thirdweb-dev/react-core@4.6.5
- @thirdweb-dev/wallets@2.5.6
## 4.6.4
### Patch Changes
- Updated dependencies [[`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- @thirdweb-dev/chains@0.1.96
- @thirdweb-dev/sdk@4.0.65
- @thirdweb-dev/react-core@4.6.4
- @thirdweb-dev/wallets@2.5.5
## 4.6.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "4.6.3",
"version": "4.6.5",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+16
View File
@@ -1,5 +1,21 @@
# @thirdweb-dev/sdk
## 4.0.66
### Patch Changes
- Updated dependencies [[`4dbcbd1`](https://github.com/thirdweb-dev/js/commit/4dbcbd13dbd388454993aee7e67b43bb9d9249aa), [`d664647`](https://github.com/thirdweb-dev/js/commit/d6646475e2715c666316174e1c035a035a075ab1), [`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- thirdweb@5.9.0
- @thirdweb-dev/chains@0.1.97
## 4.0.65
### Patch Changes
- Updated dependencies [[`381b669`](https://github.com/thirdweb-dev/js/commit/381b669cd1b4c621519636211f47a6910ca048f0), [`94e354a`](https://github.com/thirdweb-dev/js/commit/94e354aad30fc94b4d4c379e01810b0f4e2087bf), [`bebcf46`](https://github.com/thirdweb-dev/js/commit/bebcf46c3a900fcbff6d544b18c7f0140c5ca9be), [`1b2bfc8`](https://github.com/thirdweb-dev/js/commit/1b2bfc83d786d1c7b7d1fefa480dbab559e84c54), [`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- thirdweb@5.8.0
- @thirdweb-dev/chains@0.1.96
## 4.0.64
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "4.0.64",
"version": "4.0.66",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/legacy_packages/sdk",
"license": "Apache-2.0",
@@ -1,5 +1,27 @@
# @thirdweb-dev/unity-js-bridge
## 0.6.15
### Patch Changes
- Updated dependencies [[`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- @thirdweb-dev/chains@0.1.97
- @thirdweb-dev/sdk@4.0.66
- @thirdweb-dev/wallets@2.5.6
- @thirdweb-dev/auth@4.1.64
## 0.6.14
### Patch Changes
- [#2805](https://github.com/thirdweb-dev/js/pull/2805) [`ec35f32`](https://github.com/thirdweb-dev/js/commit/ec35f32d2baf5a8ff70531fc07a6b1ad57164db7) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - Unity 4.12.1
- Updated dependencies [[`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- @thirdweb-dev/chains@0.1.96
- @thirdweb-dev/sdk@4.0.65
- @thirdweb-dev/wallets@2.5.5
- @thirdweb-dev/auth@4.1.63
## 0.6.13
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.6.13",
"version": "0.6.15",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
@@ -171,7 +171,7 @@ class ThirdwebBridge implements TWBridge {
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix use of any
(globalThis as any).X_SDK_PLATFORM = "unity";
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix use of any
(globalThis as any).X_SDK_VERSION = "4.12.0";
(globalThis as any).X_SDK_VERSION = "4.12.1";
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix use of any
(globalThis as any).X_SDK_OS = browser?.os ?? "unknown";
}
+16
View File
@@ -1,5 +1,21 @@
# @thirdweb-dev/wallets
## 2.5.6
### Patch Changes
- Updated dependencies [[`b4e7c74`](https://github.com/thirdweb-dev/js/commit/b4e7c74ce5ddc88a1aa3169ea89ff139d185a648)]:
- @thirdweb-dev/chains@0.1.97
- @thirdweb-dev/sdk@4.0.66
## 2.5.5
### Patch Changes
- Updated dependencies [[`ca2576f`](https://github.com/thirdweb-dev/js/commit/ca2576f8180b6b5b9fe47087a53d1f13665c5028)]:
- @thirdweb-dev/chains@0.1.96
- @thirdweb-dev/sdk@4.0.65
## 2.5.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wallets",
"version": "2.5.4",
"version": "2.5.6",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"browser": {
+56
View File
@@ -1,5 +1,61 @@
# thirdweb
## 5.9.0
### Minor Changes
- [#2793](https://github.com/thirdweb-dev/js/pull/2793) [`4dbcbd1`](https://github.com/thirdweb-dev/js/commit/4dbcbd13dbd388454993aee7e67b43bb9d9249aa) Thanks [@jnsdls](https://github.com/jnsdls)! - **thirdweb/react**
Add a new optional `auth` prop to `<ConnectButton />` to allow specifying a SIWE auth flow after users connect their wallets.
```jsx
<ConnectButton
client={client}
auth={{
isLoggedIn: async (address) => {
// check if the user is logged in by calling your server, etc.
// then return a boolean value
return true || false;
},
getLoginPayload: async ({ address, chainId }) => {
// send the address (and optional chainId) to your server to generate the login payload for the user to sign
// you can use the `generatePayload` function from `thirdweb/auth` to generate the payload
// once you have retrieved the payload return it from this function
return; // <the login payload here>
},
doLogin: async (loginParams) => {
// send the login params to your server where you can validate them using the `verifyPayload` function
// from `thirdweb/auth`
// you can then set a cookie or return a token to save in local storage, etc
// `isLoggedIn` will automatically get called again after this function resolves
},
doLogout: async () => {
// do anything you need to do such as clearing cookies, etc when the user should be logged out
// `isLoggedIn` will automatically get called again after this function resolves
},
}}
/>
```
### Patch Changes
- [#2816](https://github.com/thirdweb-dev/js/pull/2816) [`d664647`](https://github.com/thirdweb-dev/js/commit/d6646475e2715c666316174e1c035a035a075ab1) Thanks [@jnsdls](https://github.com/jnsdls)! - adapters(viem): explicitly type walletClient return type
## 5.8.0
### Minor Changes
- [#2806](https://github.com/thirdweb-dev/js/pull/2806) [`94e354a`](https://github.com/thirdweb-dev/js/commit/94e354aad30fc94b4d4c379e01810b0f4e2087bf) Thanks [@jnsdls](https://github.com/jnsdls)! - Implement `Offers` in the Marketplace extensions, available via `thirdweb/extensions/marketplace`.
- [#2814](https://github.com/thirdweb-dev/js/pull/2814) [`bebcf46`](https://github.com/thirdweb-dev/js/commit/bebcf46c3a900fcbff6d544b18c7f0140c5ca9be) Thanks [@jnsdls](https://github.com/jnsdls)! - Add support for updating metadata for ERC1155 contracts
- [#2811](https://github.com/thirdweb-dev/js/pull/2811) [`1b2bfc8`](https://github.com/thirdweb-dev/js/commit/1b2bfc83d786d1c7b7d1fefa480dbab559e84c54) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Added ERC1155 and ERC20 signature mint extensions
### Patch Changes
- [#2815](https://github.com/thirdweb-dev/js/pull/2815) [`381b669`](https://github.com/thirdweb-dev/js/commit/381b669cd1b4c621519636211f47a6910ca048f0) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix query invalidation when using react hooks
## 5.7.0
### Minor Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.7.0",
"version": "5.9.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -226,7 +226,7 @@
"push": "yalc push",
"size": "size-limit",
"test:watch": "vitest -c ./test/vitest.config.ts dev",
"test": "vitest run -c ./test/vitest.config.ts",
"test": "vitest run -c ./test/vitest.config.ts --coverage",
"test:cov": "vitest dev -c ./test/vitest.config.ts --coverage",
"test:dev": "vitest run -c ./test/vitest.config.ts",
"typedoc": "node scripts/typedoc.mjs",
@@ -1,5 +1,5 @@
[
"event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, (address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, uint256 tokenId, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) mintRequest)",
"function mintWithSignature((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, uint256 tokenId, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) req, bytes signature) payable returns (address signer)",
"function mintWithSignature((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, uint256 tokenId, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) payload, bytes signature) payable returns (address signer)",
"function verify((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, uint256 tokenId, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) req, bytes signature) view returns (bool success, address signer)"
]
@@ -1,5 +1,5 @@
[
"event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, (address to, address primarySaleRecipient, uint256 quantity, uint256 price, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) mintRequest)",
"function mintWithSignature((address to, address primarySaleRecipient, uint256 quantity, uint256 price, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) req, bytes signature) payable returns (address signer)",
"function mintWithSignature((address to, address primarySaleRecipient, uint256 quantity, uint256 price, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) payload, bytes signature) payable returns (address signer)",
"function verify((address to, address primarySaleRecipient, uint256 quantity, uint256 price, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) req, bytes signature) view returns (bool success, address signer)"
]
@@ -4,6 +4,7 @@
"event NewOffer(address indexed offeror, uint256 indexed offerId, address indexed assetContract, (uint256 offerId, uint256 tokenId, uint256 quantity, uint256 totalPrice, uint256 expirationTimestamp, address offeror, address assetContract, address currency, uint8 tokenType, uint8 status) offer)",
"function acceptOffer(uint256 _offerId)",
"function cancelOffer(uint256 _offerId)",
"function totalOffers() view returns (uint256)",
"function getAllOffers(uint256 _startId, uint256 _endId) view returns ((uint256 offerId, uint256 tokenId, uint256 quantity, uint256 totalPrice, uint256 expirationTimestamp, address offeror, address assetContract, address currency, uint8 tokenType, uint8 status)[] offers)",
"function getAllValidOffers(uint256 _startId, uint256 _endId) view returns ((uint256 offerId, uint256 tokenId, uint256 quantity, uint256 totalPrice, uint256 expirationTimestamp, address offeror, address assetContract, address currency, uint8 tokenType, uint8 status)[] offers)",
"function getOffer(uint256 _offerId) view returns ((uint256 offerId, uint256 tokenId, uint256 quantity, uint256 totalPrice, uint256 expirationTimestamp, address offeror, address assetContract, address currency, uint8 tokenType, uint8 status) offer)",
+1 -1
View File
@@ -170,7 +170,7 @@ type ToViemWalletClientOptions = {
chain: Chain;
};
function toViemWalletClient(options: ToViemWalletClientOptions) {
function toViemWalletClient(options: ToViemWalletClientOptions): WalletClient {
const { account, chain, client } = options;
if (!account) {
throw new Error("Wallet not connected.");
@@ -68,6 +68,11 @@ export {
mintAdditionalSupplyTo,
type MintAdditionalSupplyToParams,
} from "../../extensions/erc1155/write/mintAdditionalSupplyTo.js";
export {
setTokenURI,
type SetTokenURIParams,
} from "../../extensions/erc1155/__generated__/INFTMetadata/write/setTokenURI.js";
export { freezeMetadata } from "../../extensions/erc1155/__generated__/INFTMetadata/write/freezeMetadata.js";
// EVENTS
@@ -83,6 +88,9 @@ export {
approvalForAllEvent,
type ApprovalForAllEventFilters,
} from "../../extensions/erc1155/__generated__/IERC1155/events/ApprovalForAll.js";
export { metadataUpdateEvent } from "../../extensions/erc1155/__generated__/INFTMetadata/events/MetadataUpdate.js";
export { metadataFrozenEvent } from "../../extensions/erc1155/__generated__/INFTMetadata/events/MetadataFrozen.js";
export { batchMetadataUpdateEvent } from "../../extensions/erc1155/__generated__/INFTMetadata/events/BatchMetadataUpdate.js";
/**
* DROPS extension for ERC1155
@@ -103,6 +111,15 @@ export {
type SetClaimConditionsParams,
} from "../../extensions/erc1155/drops/write/setClaimConditions.js";
/**
* SIGNATURE extension for ERC1155
*/
export {
mintWithSignature,
type GenerateMintSignatureOptions,
generateMintSignature,
} from "../../extensions/erc1155/write/sigMint.js";
// EVENTS
export { tokensLazyMintedEvent } from "../../extensions/erc1155/__generated__/ILazyMint/events/TokensLazyMinted.js";
export { tokensClaimedEvent } from "../../extensions/erc1155/__generated__/IDrop1155/events/TokensClaimed.js";
@@ -77,3 +77,24 @@ export {
setClaimConditions,
type SetClaimConditionsParams,
} from "../../extensions/erc20/drops/write/setClaimConditions.js";
/**
* SIGNATURE extension for ERC20
*/
export {
mintWithSignature,
type GenerateMintSignatureOptions,
generateMintSignature,
} from "../../extensions/erc20/write/sigMint.js";
// ----------------------------
// WETH
// ----------------------------
export {
deposit,
type DepositParams,
} from "../../extensions/erc20/write/deposit.js";
export {
withdraw,
type WithdrawParams,
} from "../../extensions/erc20/__generated__/IWETH/write/withdraw.js";
@@ -1,34 +1,27 @@
// ----------------------------
// COMMON
// ----------------------------
export type {
DirectListing,
ListingStatus,
} from "../../extensions/marketplace/types.js";
// ----------------------------
// DIRECT LISTINGS
// ----------------------------
export type { DirectListing } from "../../extensions/marketplace/direct-listings/types.js";
// READ
export { totalListings } from "../../extensions/marketplace/__generated__/IDirectListings/read/totalListings.js";
export {
getListing,
type GetListingParams,
} from "../../extensions/marketplace/read/direct/getListing.js";
} from "../../extensions/marketplace/direct-listings/read/getListing.js";
export {
getAllListings,
type GetAllListingParams,
} from "../../extensions/marketplace/read/direct/getAllListings.js";
} from "../../extensions/marketplace/direct-listings/read/getAllListings.js";
export {
getAllValidListings,
type GetAllValidListingParams,
} from "../../extensions/marketplace/read/direct/getAllValidListings.js";
} from "../../extensions/marketplace/direct-listings/read/getAllValidListings.js";
export {
isBuyerApprovedForListing,
type IsBuyerApprovedForListingParams,
} from "../../extensions/marketplace/read/direct/isBuyerApprovedForListing.js";
} from "../../extensions/marketplace/direct-listings/read/isBuyerApprovedForListing.js";
export {
isCurrencyApprovedForListing,
type IsCurrencyApprovedForListingParams,
@@ -36,17 +29,17 @@ export {
export {
currencyPriceForListing,
type CurrencyPriceForListingParams,
} from "../../extensions/marketplace/read/direct/currencyPriceForListing.js";
} from "../../extensions/marketplace/direct-listings/read/currencyPriceForListing.js";
// WRITE
export {
createListing,
type CreateListingParams,
} from "../../extensions/marketplace/write/direct/createListing.js";
} from "../../extensions/marketplace/direct-listings/write/createListing.js";
export {
updateListing,
type UpdateListingParams,
} from "../../extensions/marketplace/write/direct/updateListing.js";
} from "../../extensions/marketplace/direct-listings/write/updateListing.js";
export {
cancelListing,
type CancelListingParams,
@@ -54,7 +47,7 @@ export {
export {
buyFromListing,
type BuyFromListingParams,
} from "../../extensions/marketplace/write/direct/buyFromListing.js";
} from "../../extensions/marketplace/direct-listings/write/buyFromListing.js";
export {
approveBuyerForListing,
type ApproveBuyerForListingParams,
@@ -94,24 +87,26 @@ export {
// ENGLISH AUCTIONS
// ----------------------------
export type { EnglishAuction } from "../../extensions/marketplace/english-auctions/types.js";
// READ
export { totalAuctions } from "../../extensions/marketplace/__generated__/IEnglishAuctions/read/totalAuctions.js";
export {
getAuction,
type GetAuctionParams,
} from "../../extensions/marketplace/read/english-auction/getAuction.js";
} from "../../extensions/marketplace/english-auctions/read/getAuction.js";
export {
getAllAuctions,
type GetAllAuctionParams,
} from "../../extensions/marketplace/read/english-auction/getAllAuctions.js";
} from "../../extensions/marketplace/english-auctions/read/getAllAuctions.js";
export {
getAllValidAuctions,
type GetAllValidAuctionParams,
} from "../../extensions/marketplace/read/english-auction/getAllValidAuctions.js";
} from "../../extensions/marketplace/english-auctions/read/getAllValidAuctions.js";
export {
getWinningBid,
type GetWinningBidParams,
} from "../../extensions/marketplace/read/english-auction/getWinningBid.js";
} from "../../extensions/marketplace/english-auctions/read/getWinningBid.js";
export {
isNewWinningBid,
type IsNewWinningBidParams,
@@ -121,11 +116,11 @@ export {
export {
createAuction,
type CreateAuctionParams,
} from "../../extensions/marketplace/write/english-auction/createAuction.js";
} from "../../extensions/marketplace/english-auctions/write/createAuction.js";
export {
bidInAuction,
type BidInAuctionParams,
} from "../../extensions/marketplace/write/english-auction/bidInAuction.js";
} from "../../extensions/marketplace/english-auctions/write/bidInAuction.js";
export {
cancelAuction,
type CancelAuctionParams,
@@ -133,7 +128,7 @@ export {
export {
buyoutAuction,
type BuyoutAuctionParams,
} from "../../extensions/marketplace/write/english-auction/buyoutAuction.js";
} from "../../extensions/marketplace/english-auctions/write/buyoutAuction.js";
export {
collectAuctionPayout,
type CollectAuctionPayoutParams,
@@ -145,7 +140,7 @@ export {
export {
executeSale,
type ExecuteSaleParams,
} from "../../extensions/marketplace/write/english-auction/executeSale.js";
} from "../../extensions/marketplace/english-auctions/write/executeSale.js";
// EVENTS
export {
@@ -164,3 +159,52 @@ export {
newBidEvent,
type NewBidEventFilters,
} from "../../extensions/marketplace/__generated__/IEnglishAuctions/events/NewBid.js";
// ----------------------------
// OFFERS
// ----------------------------
export type { Offer } from "../../extensions/marketplace/offers/types.js";
// READ
export { totalOffers } from "../../extensions/marketplace/__generated__/IOffers/read/totalOffers.js";
export {
getAllOffers,
type GetAllOffersParams,
} from "../../extensions/marketplace/offers/read/getAllOffers.js";
export {
getAllValidOffers,
type GetAllValidOffersParams,
} from "../../extensions/marketplace/offers/read/getAllValidOffers.js";
export {
getOffer,
type GetOfferParams,
} from "../../extensions/marketplace/offers/read/getOffer.js";
// WRITE
export {
makeOffer,
type MakeOfferParams,
} from "../../extensions/marketplace/offers/write/makeOffer.js";
export {
cancelOffer,
type CancelOfferParams,
} from "../../extensions/marketplace/__generated__/IOffers/write/cancelOffer.js";
export {
acceptOffer,
type AcceptOfferParams,
} from "../../extensions/marketplace/offers/write/acceptOffer.js";
// EVENTS
export {
acceptedOfferEvent,
type AcceptedOfferEventFilters,
} from "../../extensions/marketplace/__generated__/IOffers/events/AcceptedOffer.js";
export {
cancelledOfferEvent,
type CancelledOfferEventFilters,
} from "../../extensions/marketplace/__generated__/IOffers/events/CancelledOffer.js";
export {
newOfferEvent,
type NewOfferEventFilters,
} from "../../extensions/marketplace/__generated__/IOffers/events/NewOffer.js";
+3
View File
@@ -96,3 +96,6 @@ export {
AutoConnect,
type AutoConnectProps,
} from "../react/core/hooks/connection/useAutoConnect.js";
// auth
export { type SiweAuthOptions } from "../react/core/hooks/auth/useSiweAuth.js";
@@ -13,9 +13,9 @@ import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
* Represents the parameters for the "mintWithSignature" function.
*/
export type MintWithSignatureParams = WithOverrides<{
req: AbiParameterToPrimitiveType<{
payload: AbiParameterToPrimitiveType<{
type: "tuple";
name: "req";
name: "payload";
components: [
{ type: "address"; name: "to" },
{ type: "address"; name: "royaltyRecipient" },
@@ -38,7 +38,7 @@ export const FN_SELECTOR = "0x98a6e993" as const;
const FN_INPUTS = [
{
type: "tuple",
name: "req",
name: "payload",
components: [
{
type: "address",
@@ -132,7 +132,7 @@ export async function isMintWithSignatureSupported(
* ```ts
* import { encodeMintWithSignatureParams } "thirdweb/extensions/erc1155";
* const result = encodeMintWithSignatureParams({
* req: ...,
* payload: ...,
* signature: ...,
* });
* ```
@@ -140,7 +140,7 @@ export async function isMintWithSignatureSupported(
export function encodeMintWithSignatureParams(
options: MintWithSignatureParams,
) {
return encodeAbiParameters(FN_INPUTS, [options.req, options.signature]);
return encodeAbiParameters(FN_INPUTS, [options.payload, options.signature]);
}
/**
@@ -152,7 +152,7 @@ export function encodeMintWithSignatureParams(
* ```ts
* import { encodeMintWithSignature } "thirdweb/extensions/erc1155";
* const result = encodeMintWithSignature({
* req: ...,
* payload: ...,
* signature: ...,
* });
* ```
@@ -177,7 +177,7 @@ export function encodeMintWithSignature(options: MintWithSignatureParams) {
*
* const transaction = mintWithSignature({
* contract,
* req: ...,
* payload: ...,
* signature: ...,
* });
*
@@ -203,7 +203,7 @@ export function mintWithSignature(
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
params: async () => {
const resolvedOptions = await asyncOptions();
return [resolvedOptions.req, resolvedOptions.signature] as const;
return [resolvedOptions.payload, resolvedOptions.signature] as const;
},
value: async () => (await asyncOptions()).overrides?.value,
});
@@ -7,7 +7,7 @@ import {
TEST_ACCOUNT_B,
} from "../../../test/src/test-wallets.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../exports/transaction.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { getContractMetadata } from "../common/read/getContractMetadata.js";
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
@@ -6,7 +6,7 @@ import {
TEST_ACCOUNT_B,
} from "../../../test/src/test-wallets.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../exports/transaction.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { getContractMetadata } from "../common/read/getContractMetadata.js";
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
@@ -0,0 +1,188 @@
import type { AbiParameterToPrimitiveType, Address } from "abitype";
import { maxUint256 } from "viem";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import { toBigInt } from "../../../utils/bigint.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import type { Hex } from "../../../utils/encoding/hex.js";
import type { NFTInput } from "../../../utils/nft/parseNft.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { mintWithSignature as generatedMintWithSignature } from "../__generated__/ISignatureMintERC1155/write/mintWithSignature.js";
/**
* Mints a new ERC1155 token with the given minter signature
* @param options - The transaction options.
* @example
* ```ts
* import { mintWithSignature, generateMintSignature } from "thirdweb/extensions/erc1155";
*
* const { payload, signature } = await generateMintSignature(...)
*
* const transaction = mintWithSignature({
* contract,
* payload,
* signature,
* });
* await sendTransaction({ transaction, account });
* ```
* @extension ERC1155
* @returns A promise that resolves to the transaction result.
*/
export const mintWithSignature = generatedMintWithSignature;
export type GenerateMintSignatureOptions = {
account: Account;
contract: ThirdwebContract;
mintRequest: GeneratePayloadInput;
};
/**
* Generates the payload and signature for minting an ERC1155 token.
* @param options - The options for the minting process.
* @example
* ```ts
* import { mintWithSignature, generateMintSignature } from "thirdweb/extensions/erc1155";
*
* const { payload, signature } = await generateMintSignature({
* account,
* contract,
* mintRequest: {
* to: "0x...",
* quantity: 10n,
* metadata: {
* name: "My NFT",
* description: "This is my NFT",
* image: "https://example.com/image.png",
* },
* },
* });
*
* const transaction = mintWithSignature({
* contract,
* payload,
* signature,
* });
* await sendTransaction({ transaction, account });
* ```
* @extension ERC1155
* @returns A promise that resolves to the payload and signature.
*/
export async function generateMintSignature(
options: GenerateMintSignatureOptions,
) {
const { mintRequest, account, contract } = options;
const currency = mintRequest.currency || NATIVE_TOKEN_ADDRESS;
const [pricePerToken, uri, uid] = await Promise.all([
// price per token in wei
(async () => {
// if priceInWei is provided, use it
if ("pricePerTokenWei" in mintRequest && mintRequest.pricePerTokenWei) {
return mintRequest.pricePerTokenWei;
}
// if price is provided, convert it to wei
if ("pricePerToken" in mintRequest && mintRequest.pricePerToken) {
const { convertErc20Amount } = await import(
"../../../utils/extensions/convert-erc20-amount.js"
);
return await convertErc20Amount({
amount: mintRequest.pricePerToken,
client: contract.client,
chain: contract.chain,
erc20Address: currency,
});
}
// if neither price nor priceInWei is provided, default to 0
return 0n;
})(),
// uri
(async () => {
if ("metadata" in mintRequest) {
if (typeof mintRequest.metadata === "object") {
// async import the upload function because it is not always required
const { upload } = await import("../../../storage/upload.js");
return await upload({
client: options.contract.client,
files: [mintRequest.metadata],
});
}
return mintRequest.metadata;
}
return "";
})(),
// uid computation
mintRequest.uid || (await randomBytes32()),
]);
const startTime = mintRequest.validityStartTimestamp || new Date(0);
const endTime = mintRequest.validityEndTimestamp || tenYearsFromNow();
const payload: PayloadType = {
uri,
currency,
uid,
pricePerToken,
tokenId:
"tokenId" in mintRequest ? mintRequest.tokenId || maxUint256 : maxUint256,
quantity: mintRequest.quantity,
to: mintRequest.to,
royaltyRecipient: mintRequest.royaltyRecipient || account.address,
royaltyBps: toBigInt(mintRequest.royaltyBps || 0),
primarySaleRecipient: mintRequest.primarySaleRecipient || account.address,
validityStartTimestamp: dateToSeconds(startTime),
validityEndTimestamp: dateToSeconds(endTime),
};
const signature = await account.signTypedData({
domain: {
name: "TokenERC1155",
version: "1",
chainId: contract.chain.id,
verifyingContract: contract.address as Hex,
},
types: { MintRequest: MintRequest1155 },
primaryType: "MintRequest",
message: payload,
});
return { payload, signature };
}
type PayloadType = AbiParameterToPrimitiveType<{
type: "tuple";
name: "payload";
components: typeof MintRequest1155;
}>;
type GeneratePayloadInput = {
to: string;
quantity: bigint;
royaltyRecipient?: Address;
royaltyBps?: number;
primarySaleRecipient?: Address;
pricePerToken?: string;
pricePerTokenWei?: bigint;
currency?: Address;
validityStartTimestamp?: Date;
validityEndTimestamp?: Date;
uid?: Hex;
} & (
| {
metadata: NFTInput | string;
}
| { tokenId: bigint }
);
const MintRequest1155 = [
{ name: "to", type: "address" },
{ name: "royaltyRecipient", type: "address" },
{ name: "royaltyBps", type: "uint256" },
{ name: "primarySaleRecipient", type: "address" },
{ name: "tokenId", type: "uint256" },
{ name: "uri", type: "string" },
{ name: "quantity", type: "uint256" },
{ name: "pricePerToken", type: "uint256" },
{ name: "currency", type: "address" },
{ name: "validityStartTimestamp", type: "uint128" },
{ name: "validityEndTimestamp", type: "uint128" },
{ name: "uid", type: "bytes32" },
] as const;
@@ -0,0 +1,152 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { sendTransaction } from "../../../transaction/actions/send-transaction.js";
import { toHex } from "../../../utils/encoding/hex.js";
import { deployERC20Contract } from "../../prebuilts/deploy-erc20.js";
import { deployERC1155Contract } from "../../prebuilts/deploy-erc1155.js";
import { generateMintSignature, mintWithSignature } from "./sigMint.js";
// skip this test suite if there is no secret key available to test with
// TODO: remove reliance on secret key during unit tests entirely
describe.runIf(process.env.TW_SECRET_KEY)("generateMintSignature1155", () => {
let erc1155Contract: ThirdwebContract;
let erc20TokenContract: ThirdwebContract;
beforeAll(async () => {
erc1155Contract = getContract({
address: await deployERC1155Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "Test",
symbol: "TST",
},
type: "TokenERC1155",
}),
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
erc20TokenContract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestToken",
symbol: "TSTT",
},
type: "TokenERC20",
}),
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
}, 60000);
it("should generate a mint signature and mint an NFT", async () => {
const { payload, signature } = await generateMintSignature({
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: 10n,
metadata: {
name: "My NFT",
description: "This is my NFT",
image: "https://example.com/image.png",
},
},
account: TEST_ACCOUNT_A,
contract: erc1155Contract,
});
const transaction = mintWithSignature({
contract: erc1155Contract,
payload,
signature,
});
const { transactionHash } = await sendTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
expect(transactionHash.length).toBe(66);
});
it("should generate a mint signature with default values", async () => {
const { payload, signature } = await generateMintSignature({
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: 10n,
metadata: "https://example.com/token",
},
account: TEST_ACCOUNT_A,
contract: erc1155Contract,
});
expect(payload.to).toBe(TEST_ACCOUNT_B.address);
expect(payload.royaltyRecipient).toBe(TEST_ACCOUNT_A.address);
expect(payload.royaltyBps).toBe(0n);
expect(payload.primarySaleRecipient).toBe(TEST_ACCOUNT_A.address);
expect(payload.uri).toBe("https://example.com/token");
expect(payload.pricePerToken).toBe(0n);
expect(payload.quantity).toBe(10n);
expect(payload.currency).toBe(NATIVE_TOKEN_ADDRESS);
expect(payload.validityStartTimestamp).toBe(0n);
expect(payload.validityEndTimestamp).toBeGreaterThan(0n);
expect(payload.uid).toBeDefined();
expect(signature.length).toBe(132);
const transaction = mintWithSignature({
contract: erc1155Contract,
payload,
signature,
});
const { transactionHash } = await sendTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
expect(transactionHash.length).toBe(66);
});
it("should generate a mint signature with custom values", async () => {
const uid = toHex("abcdef1234567890", { size: 32 });
const { payload, signature } = await generateMintSignature({
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: 10n,
royaltyRecipient: TEST_ACCOUNT_B.address,
royaltyBps: 500,
primarySaleRecipient: TEST_ACCOUNT_A.address,
metadata: "https://example.com/token",
pricePerToken: "0.2",
currency: erc20TokenContract.address,
validityStartTimestamp: new Date(1635724800),
validityEndTimestamp: new Date(1867260800),
uid,
},
account: TEST_ACCOUNT_A,
contract: erc1155Contract,
});
expect(payload.to).toBe(TEST_ACCOUNT_B.address);
expect(payload.royaltyRecipient).toBe(TEST_ACCOUNT_B.address);
expect(payload.royaltyBps).toBe(500n);
expect(payload.primarySaleRecipient).toBe(TEST_ACCOUNT_A.address);
expect(payload.uri).toBe("https://example.com/token");
expect(payload.pricePerToken).toBe(200000000000000000n);
expect(payload.quantity).toBe(10n);
expect(payload.currency).toBe(erc20TokenContract.address);
expect(payload.validityStartTimestamp).toBe(1635724n);
expect(payload.validityEndTimestamp).toBe(1867260n);
expect(payload.uid).toBe(uid);
expect(signature.length).toBe(132);
});
});
@@ -13,9 +13,9 @@ import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
* Represents the parameters for the "mintWithSignature" function.
*/
export type MintWithSignatureParams = WithOverrides<{
req: AbiParameterToPrimitiveType<{
payload: AbiParameterToPrimitiveType<{
type: "tuple";
name: "req";
name: "payload";
components: [
{ type: "address"; name: "to" },
{ type: "address"; name: "primarySaleRecipient" },
@@ -34,7 +34,7 @@ export const FN_SELECTOR = "0x8f0fefbb" as const;
const FN_INPUTS = [
{
type: "tuple",
name: "req",
name: "payload",
components: [
{
type: "address",
@@ -112,7 +112,7 @@ export async function isMintWithSignatureSupported(
* ```ts
* import { encodeMintWithSignatureParams } "thirdweb/extensions/erc20";
* const result = encodeMintWithSignatureParams({
* req: ...,
* payload: ...,
* signature: ...,
* });
* ```
@@ -120,7 +120,7 @@ export async function isMintWithSignatureSupported(
export function encodeMintWithSignatureParams(
options: MintWithSignatureParams,
) {
return encodeAbiParameters(FN_INPUTS, [options.req, options.signature]);
return encodeAbiParameters(FN_INPUTS, [options.payload, options.signature]);
}
/**
@@ -132,7 +132,7 @@ export function encodeMintWithSignatureParams(
* ```ts
* import { encodeMintWithSignature } "thirdweb/extensions/erc20";
* const result = encodeMintWithSignature({
* req: ...,
* payload: ...,
* signature: ...,
* });
* ```
@@ -157,7 +157,7 @@ export function encodeMintWithSignature(options: MintWithSignatureParams) {
*
* const transaction = mintWithSignature({
* contract,
* req: ...,
* payload: ...,
* signature: ...,
* });
*
@@ -183,7 +183,7 @@ export function mintWithSignature(
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
params: async () => {
const resolvedOptions = await asyncOptions();
return [resolvedOptions.req, resolvedOptions.signature] as const;
return [resolvedOptions.payload, resolvedOptions.signature] as const;
},
value: async () => (await asyncOptions()).overrides?.value,
});
@@ -7,7 +7,7 @@ import {
TEST_ACCOUNT_B,
} from "../../../test/src/test-wallets.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../exports/transaction.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { getContractMetadata } from "../common/read/getContractMetadata.js";
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
import { claimTo } from "./drops/write/claimTo.js";
@@ -0,0 +1,34 @@
import { prepareContractCall } from "../../../transaction/prepare-contract-call.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { toWei } from "../../../utils/units.js";
import { FN_SELECTOR } from "../__generated__/IWETH/write/deposit.js";
export type DepositParams =
| {
amount: string;
}
| { amountWei: bigint };
/**
* Calls the "deposit" function on the contract.
* @param options - The options for the "deposit" function.
* @returns A prepared transaction object.
* @extension ERC20
* @example
* ```ts
* import { deposit } from "thirdweb/extensions/erc20";
*
* const transaction = deposit();
*
* // Send the transaction
* ...
*
* ```
*/
export function deposit(options: BaseTransactionOptions<DepositParams>) {
return prepareContractCall({
contract: options.contract,
method: [FN_SELECTOR, [], []] as const,
value: "amountWei" in options ? options.amountWei : toWei(options.amount),
});
}
@@ -0,0 +1,171 @@
import type { AbiParameterToPrimitiveType, Address } from "abitype";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import type { Hex } from "../../../utils/encoding/hex.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { name } from "../../common/read/name.js";
import { mintWithSignature as generatedMintWithSignature } from "../__generated__/ISignatureMintERC20/write/mintWithSignature.js";
/**
* Mints a new ERC20 token with the given minter signature
* @param options - The transaction options.
* @example
* ```ts
* import { mintWithSignature, generateMintSignature } from "thirdweb/extensions/erc20";
*
* const { payload, signature } = await generateMintSignature(...)
*
* const transaction = mintWithSignature({
* contract,
* payload,
* signature,
* });
* await sendTransaction({ transaction, account });
* ```
* @extension ERC20
* @returns A promise that resolves to the transaction result.
*/
export const mintWithSignature = generatedMintWithSignature;
export type GenerateMintSignatureOptions = {
account: Account;
contract: ThirdwebContract;
mintRequest: GeneratePayloadInput;
};
/**
* Generates the payload and signature for minting an ERC20 token.
* @param options - The options for the minting process.
* @example
* ```ts
* import { mintWithSignature, generateMintSignature } from "thirdweb/extensions/erc20";
*
* const { payload, signature } = await generateMintSignature({
* account,
* contract,
* mintRequest: {
* to: "0x...",
* quantity: "10",
* },
* });
*
* const transaction = mintWithSignature({
* contract,
* payload,
* signature,
* });
* await sendTransaction({ transaction, account });
* ```
* @extension ERC20
* @returns A promise that resolves to the payload and signature.
*/
export async function generateMintSignature(
options: GenerateMintSignatureOptions,
) {
const { mintRequest, account, contract } = options;
const currency = mintRequest.currency || NATIVE_TOKEN_ADDRESS;
const [price, quantity, uid, tokenName] = await Promise.all([
// price per token in wei
(async () => {
// if priceInWei is provided, use it
if ("priceInWei" in mintRequest && mintRequest.priceInWei) {
return mintRequest.priceInWei;
}
// if price is provided, convert it to wei
if ("price" in mintRequest && mintRequest.price) {
const { convertErc20Amount } = await import(
"../../../utils/extensions/convert-erc20-amount.js"
);
return await convertErc20Amount({
amount: mintRequest.price,
client: contract.client,
chain: contract.chain,
erc20Address: currency,
});
}
// if neither price nor priceInWei is provided, default to 0
return 0n;
})(),
// quantity in wei
(async () => {
// if the quantity is already passed in wei, use it
if ("quantityWei" in mintRequest) {
return mintRequest.quantityWei;
}
// otherwise convert the quantity to wei using the contract's OWN decimals
const { convertErc20Amount } = await import(
"../../../utils/extensions/convert-erc20-amount.js"
);
return await convertErc20Amount({
amount: mintRequest.quantity,
client: contract.client,
chain: contract.chain,
erc20Address: contract.address,
});
})(),
// uid computation
mintRequest.uid || (await randomBytes32()),
// ERC20Permit (EIP-712) spec differs from signature mint 721, 1155.
// it uses the token name in the domain separator
name({
contract,
}),
]);
const startTime = mintRequest.validityStartTimestamp || new Date(0);
const endTime = mintRequest.validityEndTimestamp || tenYearsFromNow();
const payload: PayloadType = {
price,
quantity,
uid,
currency,
to: mintRequest.to,
primarySaleRecipient: mintRequest.primarySaleRecipient || account.address,
validityStartTimestamp: dateToSeconds(startTime),
validityEndTimestamp: dateToSeconds(endTime),
};
const signature = await account.signTypedData({
domain: {
name: tokenName,
version: "1",
chainId: contract.chain.id,
verifyingContract: contract.address as Hex,
},
types: { MintRequest: MintRequest20 },
primaryType: "MintRequest",
message: payload,
});
return { payload, signature };
}
type PayloadType = AbiParameterToPrimitiveType<{
type: "tuple";
name: "payload";
components: typeof MintRequest20;
}>;
type GeneratePayloadInput = {
to: string;
primarySaleRecipient?: Address;
price?: string;
priceInWei?: bigint;
currency?: Address;
validityStartTimestamp?: Date;
validityEndTimestamp?: Date;
uid?: Hex;
} & ({ quantity: string } | { quantityWei: bigint });
export const MintRequest20 = [
{ name: "to", type: "address" },
{ name: "primarySaleRecipient", type: "address" },
{ name: "quantity", type: "uint256" },
{ name: "price", type: "uint256" },
{ name: "currency", type: "address" },
{ name: "validityStartTimestamp", type: "uint128" },
{ name: "validityEndTimestamp", type: "uint128" },
{ name: "uid", type: "bytes32" },
] as const;
@@ -0,0 +1,140 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { sendTransaction } from "../../../transaction/actions/send-transaction.js";
import { toHex } from "../../../utils/encoding/hex.js";
import { deployERC20Contract } from "../../prebuilts/deploy-erc20.js";
import { generateMintSignature, mintWithSignature } from "./sigMint.js";
// skip this test suite if there is no secret key available to test with
// TODO: remove reliance on secret key during unit tests entirely
describe.runIf(process.env.TW_SECRET_KEY)("generateMintSignature20", () => {
let erc20Contract: ThirdwebContract;
let erc20TokenContract: ThirdwebContract;
beforeAll(async () => {
erc20Contract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "Test",
symbol: "TST",
},
type: "TokenERC20",
}),
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
erc20TokenContract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestToken",
symbol: "TSTT",
},
type: "TokenERC20",
}),
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
}, 60000);
it("should generate a mint signature and mint an NFT", async () => {
const { payload, signature } = await generateMintSignature({
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: "0.1",
},
account: TEST_ACCOUNT_A,
contract: erc20Contract,
});
const transaction = mintWithSignature({
contract: erc20Contract,
payload,
signature,
});
const { transactionHash } = await sendTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
expect(transactionHash.length).toBe(66);
});
it("should generate a mint signature with default values", async () => {
const { payload, signature } = await generateMintSignature({
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: "0.1",
},
account: TEST_ACCOUNT_A,
contract: erc20Contract,
});
expect(payload.to).toBe("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
expect(payload.primarySaleRecipient).toBe(
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
);
expect(payload.quantity).toBe(100000000000000000n);
expect(payload.price).toBe(0n);
expect(payload.currency).toBe(NATIVE_TOKEN_ADDRESS);
expect(payload.validityStartTimestamp).toBe(0n);
expect(payload.validityEndTimestamp).toBeGreaterThan(0n);
expect(payload.uid).toBeDefined();
expect(signature.length).toBe(132);
const transaction = mintWithSignature({
contract: erc20Contract,
payload,
signature,
});
const { transactionHash } = await sendTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
expect(transactionHash.length).toBe(66);
});
it("should generate a mint signature with custom values", async () => {
const { payload, signature } = await generateMintSignature({
mintRequest: {
to: TEST_ACCOUNT_B.address,
quantity: "0.005",
primarySaleRecipient: TEST_ACCOUNT_A.address,
price: "0.2",
currency: erc20TokenContract.address,
validityStartTimestamp: new Date(1635724800),
validityEndTimestamp: new Date(1867260800),
uid: toHex("abcdef1234567890", { size: 32 }),
},
account: TEST_ACCOUNT_A,
contract: erc20Contract,
});
expect(payload.to).toBe("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
expect(payload.primarySaleRecipient).toBe(
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
);
expect(payload.quantity).toBe(5000000000000000n);
expect(payload.price).toBe(200000000000000000n);
expect(payload.currency).toBe(erc20TokenContract.address);
expect(payload.validityStartTimestamp).toBe(1635724n);
expect(payload.validityEndTimestamp).toBe(1867260n);
expect(payload.uid).toBe(
"0x6162636465663132333435363738393000000000000000000000000000000000",
);
expect(signature.length).toBe(132);
});
});
@@ -7,7 +7,7 @@ import {
TEST_ACCOUNT_B,
} from "../../../test/src/test-wallets.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../exports/transaction.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { getContractMetadata } from "../common/read/getContractMetadata.js";
import { deployERC721Contract } from "../prebuilts/deploy-erc721.js";
import { balanceOf } from "./__generated__/IERC721A/read/balanceOf.js";
@@ -1,14 +1,12 @@
import type { AbiParameterToPrimitiveType } from "abitype";
import type { AbiParameterToPrimitiveType, Address } from "abitype";
import type { Hex } from "viem";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import { upload } from "../../../storage/upload.js";
import { toBigInt } from "../../../utils/bigint.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import type { NFTInput } from "../../../utils/nft/parseNft.js";
import { toUnits } from "../../../utils/units.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { decimals } from "../../erc20/read/decimals.js";
import { mintWithSignature as generatedMintWithSignature } from "../__generated__/ISignatureMintERC721/write/mintWithSignature.js";
/**
@@ -72,39 +70,64 @@ export async function generateMintSignature(
options: GenerateMintSignatureOptions,
) {
const { mintRequest, account, contract } = options;
let priceInWei = 0n;
if (mintRequest.price) {
const d = await decimals(options).catch(() => 18);
priceInWei = toUnits(mintRequest.price.toString(), d);
}
const currency = mintRequest.currency || NATIVE_TOKEN_ADDRESS;
const [price, uri, uid] = await Promise.all([
// price per token in wei
(async () => {
// if priceInWei is provided, use it
if ("priceInWei" in mintRequest && mintRequest.priceInWei) {
return mintRequest.priceInWei;
}
// if price is provided, convert it to wei
if ("price" in mintRequest && mintRequest.price) {
const { convertErc20Amount } = await import(
"../../../utils/extensions/convert-erc20-amount.js"
);
return await convertErc20Amount({
amount: mintRequest.price,
client: contract.client,
chain: contract.chain,
erc20Address: currency,
});
}
// if neither price nor priceInWei is provided, default to 0
return 0n;
})(),
// uri
(async () => {
if ("metadata" in mintRequest) {
if (typeof mintRequest.metadata === "object") {
// async import the upload function because it is not always required
const { upload } = await import("../../../storage/upload.js");
return await upload({
client: options.contract.client,
files: [mintRequest.metadata],
});
}
return mintRequest.metadata;
}
return "";
})(),
// uid computation
mintRequest.uid || (await randomBytes32()),
]);
const startTime = mintRequest.validityStartTimestamp || new Date(0);
const endTime = mintRequest.validityEndTimestamp || tenYearsFromNow();
const uid = mintRequest.uid || (await randomBytes32());
let uri: string;
if (typeof mintRequest.metadata === "object") {
uri = await upload({
client: options.contract.client,
files: [mintRequest.metadata],
});
} else {
uri = mintRequest.metadata;
}
const payload = {
const payload: PayloadType = {
uri,
currency,
uid,
price,
to: mintRequest.to,
royaltyRecipient: mintRequest.royaltyRecipient || account.address,
royaltyBps: toBigInt(mintRequest.royaltyBps || 0),
primarySaleRecipient: mintRequest.primarySaleRecipient || account.address,
uri: uri || "",
price: priceInWei,
currency: mintRequest.currency || NATIVE_TOKEN_ADDRESS,
validityStartTimestamp: dateToSeconds(startTime),
validityEndTimestamp: dateToSeconds(endTime),
uid,
} as PayloadType;
};
const signature = await account.signTypedData({
domain: {
@@ -129,14 +152,15 @@ type PayloadType = AbiParameterToPrimitiveType<{
type GeneratePayloadInput = {
to: string;
metadata: NFTInput | string;
royaltyRecipient?: string;
royaltyRecipient?: Address;
royaltyBps?: number;
primarySaleRecipient?: string;
price?: number | string;
currency?: string;
primarySaleRecipient?: Address;
price?: string;
priceInWei?: bigint;
currency?: Address;
validityStartTimestamp?: Date;
validityEndTimestamp?: Date;
uid?: string;
uid?: Hex;
};
const MintRequest721 = [
@@ -1,7 +1,6 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import { USDT_CONTRACT_ADDRESS } from "../../../../test/src/test-contracts.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
@@ -13,32 +12,46 @@ import {
} from "../../../contract/contract.js";
import { sendTransaction } from "../../../transaction/actions/send-transaction.js";
import { toHex } from "../../../utils/encoding/hex.js";
import { deployERC20Contract } from "../../prebuilts/deploy-erc20.js";
import { deployERC721Contract } from "../../prebuilts/deploy-erc721.js";
import { generateMintSignature, mintWithSignature } from "./sigMint.js";
// skip this test suite if there is no secret key available to test with
// TODO: remove reliance on secret key during unit tests entirely
describe.runIf(process.env.TW_SECRET_KEY)(
"generateMintSignature",
{
timeout: 120000,
},
"generateMintSignature721",
() => {
let erc721Contract: ThirdwebContract;
let erc20TokenContract: ThirdwebContract;
beforeAll(async () => {
const contractAddress = await deployERC721Contract({
account: TEST_ACCOUNT_A,
erc721Contract = getContract({
address: await deployERC721Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "Test",
symbol: "TST",
},
type: "TokenERC721",
}),
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "Test",
symbol: "TST",
},
type: "TokenERC721",
});
erc721Contract = getContract({
address: contractAddress,
erc20TokenContract = getContract({
address: await deployERC20Contract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestToken",
symbol: "TSTT",
},
type: "TokenERC20",
}),
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
@@ -94,6 +107,17 @@ describe.runIf(process.env.TW_SECRET_KEY)(
expect(payload.validityEndTimestamp).toBeGreaterThan(0n);
expect(payload.uid).toBeDefined();
expect(signature.length).toBe(132);
const transaction = mintWithSignature({
contract: erc721Contract,
payload,
signature,
});
const { transactionHash } = await sendTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
expect(transactionHash.length).toBe(66);
});
it("should generate a mint signature with custom values", async () => {
@@ -104,10 +128,10 @@ describe.runIf(process.env.TW_SECRET_KEY)(
royaltyBps: 500,
primarySaleRecipient: TEST_ACCOUNT_A.address,
metadata: "https://example.com/token",
price: 0.2,
currency: USDT_CONTRACT_ADDRESS,
validityStartTimestamp: new Date(1635724800), // October 31, 2021 00:00:00 UTC
validityEndTimestamp: new Date(1667260800), // October 31, 2022 00:00:00 UTC
price: "0.2",
currency: erc20TokenContract.address,
validityStartTimestamp: new Date(1635724800),
validityEndTimestamp: new Date(1867260800),
uid: toHex("abcdef1234567890", { size: 32 }),
},
account: TEST_ACCOUNT_A,
@@ -124,9 +148,9 @@ describe.runIf(process.env.TW_SECRET_KEY)(
);
expect(payload.uri).toBe("https://example.com/token");
expect(payload.price).toBe(200000000000000000n);
expect(payload.currency).toBe(USDT_CONTRACT_ADDRESS);
expect(payload.currency).toBe(erc20TokenContract.address);
expect(payload.validityStartTimestamp).toBe(1635724n);
expect(payload.validityEndTimestamp).toBe(1667260n);
expect(payload.validityEndTimestamp).toBe(1867260n);
expect(payload.uid).toBe(
"0x6162636465663132333435363738393000000000000000000000000000000000",
);
@@ -0,0 +1,70 @@
import { readContract } from "../../../../../transaction/read-contract.js";
import type { BaseTransactionOptions } from "../../../../../transaction/types.js";
import { decodeAbiParameters } from "viem";
import type { Hex } from "../../../../../utils/encoding/hex.js";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
export const FN_SELECTOR = "0xa9fd8ed1" as const;
const FN_INPUTS = [] as const;
const FN_OUTPUTS = [
{
type: "uint256",
},
] as const;
/**
* Checks if the `totalOffers` method is supported by the given contract.
* @param contract The ThirdwebContract.
* @returns A promise that resolves to a boolean indicating if the `totalOffers` method is supported.
* @extension MARKETPLACE
* @example
* ```ts
* import { isTotalOffersSupported } from "thirdweb/extensions/marketplace";
*
* const supported = await isTotalOffersSupported(contract);
* ```
*/
export async function isTotalOffersSupported(contract: ThirdwebContract<any>) {
return detectMethod({
contract,
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
});
}
/**
* Decodes the result of the totalOffers function call.
* @param result - The hexadecimal result to decode.
* @returns The decoded result as per the FN_OUTPUTS definition.
* @extension MARKETPLACE
* @example
* ```ts
* import { decodeTotalOffersResult } from "thirdweb/extensions/marketplace";
* const result = decodeTotalOffersResult("...");
* ```
*/
export function decodeTotalOffersResult(result: Hex) {
return decodeAbiParameters(FN_OUTPUTS, result)[0];
}
/**
* Calls the "totalOffers" function on the contract.
* @param options - The options for the totalOffers function.
* @returns The parsed result of the function call.
* @extension MARKETPLACE
* @example
* ```ts
* import { totalOffers } from "thirdweb/extensions/marketplace";
*
* const result = await totalOffers();
*
* ```
*/
export async function totalOffers(options: BaseTransactionOptions) {
return readContract({
contract: options.contract,
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
params: [],
});
}
@@ -0,0 +1,465 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { balanceOf as balanceOfErc721 } from "../../erc721/__generated__/IERC721A/read/balanceOf.js";
import { approve as approveErc721 } from "../../erc721/__generated__/IERC721A/write/approve.js";
import { tokensMintedEvent as tokensMintedEventErc721 } from "../../erc721/__generated__/IMintableERC721/events/TokensMinted.js";
import { mintTo as mintToErc721 } from "../../erc721/write/mintTo.js";
import { balanceOf as balanceOfErc1155 } from "../../erc1155/__generated__/IERC1155/read/balanceOf.js";
import { setApprovalForAll } from "../../erc1155/__generated__/IERC1155/write/setApprovalForAll.js";
import { claimTo } from "../../erc1155/drops/write/claimTo.js";
import { setClaimConditions } from "../../erc1155/drops/write/setClaimConditions.js";
import { lazyMint } from "../../erc1155/write/lazyMint.js";
import { deployERC721Contract } from "../../prebuilts/deploy-erc721.js";
import { deployERC1155Contract } from "../../prebuilts/deploy-erc1155.js";
import { deployMarketplaceContract } from "../../prebuilts/deploy-marketplace.js";
import { newListingEvent } from "../__generated__/IDirectListings/events/NewListing.js";
import { totalListings } from "../__generated__/IDirectListings/read/totalListings.js";
import { getAllListings } from "./read/getAllListings.js";
import { getAllValidListings } from "./read/getAllValidListings.js";
import { getListing } from "./read/getListing.js";
import { isListingValid } from "./utils.js";
import { buyFromListing } from "./write/buyFromListing.js";
import { createListing } from "./write/createListing.js";
describe("Marketplace: Direct Listings", () => {
describe("ERC721", () => {
let nftTokenId: bigint;
let marketplaceContract: ThirdwebContract;
let erc721Contract: ThirdwebContract;
beforeAll(async () => {
marketplaceContract = getContract({
address: await deployMarketplaceContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestMarketPlace",
},
}),
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// also deploy an ERC721 contract
erc721Contract = getContract({
address: await deployERC721Contract({
type: "TokenERC721",
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestERC721",
},
}),
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
const receipt = await sendAndConfirmTransaction({
transaction: mintToErc721({
contract: erc721Contract,
to: TEST_ACCOUNT_A.address,
nft: { name: "Test:ERC721:DirectListing" },
}),
account: TEST_ACCOUNT_A,
});
const mintEvents = parseEventLogs({
events: [tokensMintedEventErc721()],
logs: receipt.logs,
});
expect(mintEvents.length).toBe(1);
expect(mintEvents[0]?.args.tokenIdMinted).toBeDefined();
nftTokenId = mintEvents[0]?.args.tokenIdMinted as bigint;
// does a lot of stuff, this may take a while
}, 120_000);
it("should work for basic listings (Native Currency)", async () => {
// listings should be 0 length to start
const listings = await getAllListings({
contract: marketplaceContract,
});
expect(listings.length).toBe(0);
// oh and so should totalListings
expect(await totalListings({ contract: marketplaceContract })).toBe(0n);
// approve first
const approveTx = approveErc721({
contract: erc721Contract,
to: marketplaceContract.address,
tokenId: nftTokenId,
});
await sendAndConfirmTransaction({
transaction: approveTx,
account: TEST_ACCOUNT_A,
});
const transaction = createListing({
contract: marketplaceContract,
assetContractAddress: erc721Contract.address,
tokenId: nftTokenId,
pricePerToken: "1",
});
const receipt = await sendAndConfirmTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
const listingEvents = parseEventLogs({
events: [newListingEvent()],
logs: receipt.logs,
});
expect(listingEvents.length).toBe(1);
// biome-ignore lint/style/noNonNullAssertion: OK in tests
const listingEvent = listingEvents[0]!;
expect(listingEvent.args.listingCreator).toBe(TEST_ACCOUNT_A.address);
expect(listingEvent.args.assetContract).toBe(erc721Contract.address);
// at this point listings should be 1
const listingsAfter = await getAllListings({
contract: marketplaceContract,
});
expect(listingsAfter.length).toBe(1);
// valid listings should also be 1!
const validListings = await getAllValidListings({
contract: marketplaceContract,
});
expect(validListings.length).toBe(1);
// and totalListings should be 1
expect(await totalListings({ contract: marketplaceContract })).toBe(1n);
// explicitly retrieve the listing!
const listing = await getListing({
contract: marketplaceContract,
listingId: listingEvent.args.listingId,
});
expect(listing).toBeDefined();
expect(listing.status).toBe("ACTIVE");
expect(listing.creatorAddress).toBe(TEST_ACCOUNT_A.address);
expect(listing.assetContractAddress).toBe(erc721Contract.address);
expect(listing.tokenId).toBe(nftTokenId);
expect(listing.currencyValuePerToken).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "1",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 1000000000000000000n,
}
`);
expect(listing.asset).toMatchInlineSnapshot(`
{
"id": 0n,
"metadata": {
"name": "Test:ERC721:DirectListing",
},
"owner": null,
"tokenURI": "ipfs://QmewoGwuooC1Vno1ZTnmKmEUaimDygraR1fEEHdTeraF34/0",
"type": "ERC721",
}
`);
// check the listing is valid
const listingValidity = await isListingValid({
listing,
contract: marketplaceContract,
quantity: 1n,
});
expect(listingValidity).toMatchInlineSnapshot(`
{
"valid": true,
}
`);
// expect the buyer to have an initial balance of 0
await expect(
balanceOfErc721({
contract: erc721Contract,
owner: TEST_ACCOUNT_B.address,
}),
).resolves.toBe(0n);
const buyTx = buyFromListing({
contract: marketplaceContract,
listingId: listingEvent.args.listingId,
recipient: TEST_ACCOUNT_B.address,
quantity: 1n,
});
await sendAndConfirmTransaction({
transaction: buyTx,
account: TEST_ACCOUNT_B,
});
// expect the buyer to have a new balance of 1
await expect(
balanceOfErc721({
contract: erc721Contract,
owner: TEST_ACCOUNT_B.address,
}),
).resolves.toBe(1n);
// expect the seller to no longer have the token
await expect(
balanceOfErc721({
contract: erc721Contract,
owner: TEST_ACCOUNT_A.address,
}),
).resolves.toBe(0n);
});
});
describe("ERC1155 Drop", () => {
let nftTokenId: bigint;
let marketplaceContract: ThirdwebContract;
let erc1155Contract: ThirdwebContract;
beforeAll(async () => {
marketplaceContract = getContract({
address: await deployMarketplaceContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestMarketPlace",
},
}),
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// also deploy an ERC721 contract
erc1155Contract = getContract({
address: await deployERC1155Contract({
type: "DropERC1155",
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestERC1155",
},
}),
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// lazy mint 10 tokens
await expect(
sendAndConfirmTransaction({
transaction: lazyMint({
contract: erc1155Contract,
nfts: [{ name: "Test:ERC1155:DirectListing" }],
}),
account: TEST_ACCOUNT_A,
}),
).resolves.toBeDefined();
// set claim condition (just public is fine)
await expect(
sendAndConfirmTransaction({
transaction: setClaimConditions({
contract: erc1155Contract,
tokenId: 0n,
phases: [{}],
}),
account: TEST_ACCOUNT_A,
}),
).resolves.toBeDefined();
// claim 10 tokens
await expect(
sendAndConfirmTransaction({
transaction: claimTo({
contract: erc1155Contract,
tokenId: 0n,
to: TEST_ACCOUNT_A.address,
quantity: 10n,
}),
account: TEST_ACCOUNT_A,
}),
).resolves.toBeDefined();
nftTokenId = 0n;
// does a lot of stuff, this may take a while
}, 120_000);
it("should work for basic listings (Native Currency)", async () => {
// listings should be 0 length to start
const listings = await getAllListings({
contract: marketplaceContract,
});
expect(listings.length).toBe(0);
// oh and so should totalListings
expect(await totalListings({ contract: marketplaceContract })).toBe(0n);
// approve first
await sendAndConfirmTransaction({
transaction: setApprovalForAll({
contract: erc1155Contract,
operator: marketplaceContract.address,
approved: true,
}),
account: TEST_ACCOUNT_A,
});
// this should fail because we're listing more than we have
await expect(
sendAndConfirmTransaction({
transaction: createListing({
contract: marketplaceContract,
assetContractAddress: erc1155Contract.address,
tokenId: nftTokenId,
pricePerToken: "1",
quantity: 20n,
}),
account: TEST_ACCOUNT_A,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
[TransactionError: Error - Marketplace: not owner or approved tokens.
contract: ${marketplaceContract.address}
chainId: 31337]
`);
// this should work because we're listing the correct amount
const receipt = await sendAndConfirmTransaction({
transaction: createListing({
contract: marketplaceContract,
assetContractAddress: erc1155Contract.address,
tokenId: nftTokenId,
pricePerToken: "0.01",
quantity: 1n,
}),
account: TEST_ACCOUNT_A,
});
const listingEvents = parseEventLogs({
events: [newListingEvent()],
logs: receipt.logs,
});
expect(listingEvents.length).toBe(1);
// biome-ignore lint/style/noNonNullAssertion: OK in tests
const listingEvent = listingEvents[0]!;
expect(listingEvent.args.listingCreator).toBe(TEST_ACCOUNT_A.address);
expect(listingEvent.args.assetContract).toBe(erc1155Contract.address);
// at this point listings should be 1
const listingsAfter = await getAllListings({
contract: marketplaceContract,
});
expect(listingsAfter.length).toBe(1);
// valid listings should also be 1!
const validListings = await getAllValidListings({
contract: marketplaceContract,
});
expect(validListings.length).toBe(1);
// and totalListings should be 1
expect(await totalListings({ contract: marketplaceContract })).toBe(1n);
// explicitly retrieve the listing!
const listing = await getListing({
contract: marketplaceContract,
listingId: listingEvent.args.listingId,
});
expect(listing).toBeDefined();
expect(listing.status).toBe("ACTIVE");
expect(listing.creatorAddress).toBe(TEST_ACCOUNT_A.address);
expect(listing.assetContractAddress).toBe(erc1155Contract.address);
expect(listing.tokenId).toBe(nftTokenId);
expect(listing.currencyValuePerToken).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "0.01",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 10000000000000000n,
}
`);
expect(listing.asset).toMatchInlineSnapshot(`
{
"id": 0n,
"metadata": {
"name": "Test:ERC1155:DirectListing",
},
"owner": null,
"supply": 10n,
"tokenURI": "ipfs://QmdwzcoSGSzoRSMBk2efkHjGVRsyUaR2Lm79YTuL324NtM/0",
"type": "ERC1155",
}
`);
// check the listing is valid
const listingValidity = await isListingValid({
listing,
contract: marketplaceContract,
quantity: 1n,
});
expect(listingValidity).toMatchInlineSnapshot(`
{
"valid": true,
}
`);
// expect the buyer to have an initial balance of 0
await expect(
balanceOfErc1155({
contract: erc1155Contract,
owner: TEST_ACCOUNT_B.address,
tokenId: nftTokenId,
}),
).resolves.toBe(0n);
const buyTx = buyFromListing({
contract: marketplaceContract,
listingId: listingEvent.args.listingId,
recipient: TEST_ACCOUNT_B.address,
quantity: 1n,
});
await sendAndConfirmTransaction({
transaction: buyTx,
account: TEST_ACCOUNT_B,
});
// expect the buyer to have a new balance of 1
await expect(
balanceOfErc1155({
contract: erc1155Contract,
owner: TEST_ACCOUNT_B.address,
tokenId: nftTokenId,
}),
).resolves.toBe(1n);
// expect the seller to only have 9 of the tokens
await expect(
balanceOfErc1155({
contract: erc1155Contract,
owner: TEST_ACCOUNT_A.address,
tokenId: nftTokenId,
}),
).resolves.toBe(9n);
});
});
});
@@ -4,8 +4,9 @@ import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { min } from "../../../../utils/bigint.js";
import { getAllListings as getAllListingGenerated } from "../../__generated__/IDirectListings/read/getAllListings.js";
import { totalListings } from "../../__generated__/IDirectListings/read/totalListings.js";
import type { DirectListing } from "../../types.js";
import { getAllInBatches, mapDirectListing } from "../../utils.js";
import { getAllInBatches } from "../../utils.js";
import type { DirectListing } from "../types.js";
import { mapDirectListing } from "../utils.js";
const DEFAULT_QUERY_ALL_COUNT = 100n;
@@ -4,8 +4,9 @@ import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { min } from "../../../../utils/bigint.js";
import { getAllValidListings as getAllValidListingGenerated } from "../../__generated__/IDirectListings/read/getAllValidListings.js";
import { totalListings } from "../../__generated__/IDirectListings/read/totalListings.js";
import type { DirectListing } from "../../types.js";
import { getAllInBatches, mapDirectListing } from "../../utils.js";
import { getAllInBatches } from "../../utils.js";
import type { DirectListing } from "../types.js";
import { mapDirectListing } from "../utils.js";
const DEFAULT_QUERY_ALL_COUNT = 100n;
@@ -6,8 +6,8 @@ import {
type GetListingParams as GetListingParamsGenerated,
getListing as getListingGenerated,
} from "../../__generated__/IDirectListings/read/getListing.js";
import type { DirectListing } from "../../types.js";
import { mapDirectListing } from "../../utils.js";
import type { DirectListing } from "../types.js";
import { mapDirectListing } from "../utils.js";
export type GetListingParams = GetListingParamsGenerated;
@@ -0,0 +1,20 @@
import type { Address } from "abitype";
import type { NFT } from "../../../utils/nft/parseNft.js";
import type { GetBalanceResult } from "../../erc20/read/getBalance.js";
import type { ListingStatus } from "../types.js";
export type DirectListing = {
id: bigint;
creatorAddress: Address;
assetContractAddress: Address;
tokenId: bigint;
quantity: bigint;
currencyContractAddress: Address;
currencyValuePerToken: GetBalanceResult;
pricePerToken: bigint;
asset: NFT;
startTimeInSeconds: bigint;
endTimeInSeconds: bigint;
isReservedListing: boolean;
status: ListingStatus;
};
@@ -0,0 +1,178 @@
import { getContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { toTokens } from "../../../utils/units.js";
import { getCurrencyMetadata } from "../../erc20/read/getCurrencyMetadata.js";
import { isERC721 } from "../../erc721/read/isERC721.js";
import { isERC1155 } from "../../erc1155/read/isERC1155.js";
import type { getListing } from "../__generated__/IDirectListings/read/getListing.js";
import { computeStatus, getNFTAsset } from "../utils.js";
import type { DirectListing } from "./types.js";
/**
* @internal
*/
export async function mapDirectListing(
options: BaseTransactionOptions<{
latestBlock: { timestamp: bigint };
rawListing: Awaited<ReturnType<typeof getListing>>;
}>,
): Promise<DirectListing> {
const { latestBlock, rawListing } = options;
// process the listing
const status = computeStatus({
listingStatus: rawListing.status,
blockTimeStamp: latestBlock.timestamp,
startTimestamp: rawListing.startTimestamp,
endTimestamp: rawListing.endTimestamp,
});
const [currencyValuePerToken, nftAsset] = await Promise.all([
getCurrencyMetadata({
contract: getContract({
...options.contract,
address: rawListing.currency,
}),
}),
getNFTAsset({
...options,
contract: getContract({
...options.contract,
address: rawListing.assetContract,
}),
tokenId: rawListing.tokenId,
}),
]);
return {
id: rawListing.listingId,
creatorAddress: rawListing.listingCreator,
assetContractAddress: rawListing.assetContract,
tokenId: rawListing.tokenId,
quantity: rawListing.quantity,
currencyContractAddress: rawListing.currency,
currencyValuePerToken: {
...currencyValuePerToken,
value: rawListing.pricePerToken,
displayValue: toTokens(
rawListing.pricePerToken,
currencyValuePerToken.decimals,
),
},
pricePerToken: rawListing.pricePerToken,
asset: nftAsset,
startTimeInSeconds: rawListing.startTimestamp,
endTimeInSeconds: rawListing.endTimestamp,
isReservedListing: rawListing.reserved,
status,
};
}
export type IsListingValidParams = {
listing: DirectListing;
quantity?: bigint;
};
export type ValidReturn = { valid: true } | { valid: false; reason: string };
export async function isListingValid(
options: BaseTransactionOptions<IsListingValidParams>,
): Promise<ValidReturn> {
const assetContract = getContract({
...options.contract,
address: options.listing.assetContractAddress,
});
const [erc721, erc1155] = await Promise.all([
isERC721({ contract: assetContract }),
isERC1155({ contract: assetContract }),
]);
// if the asset is an erc721 token
if (erc721) {
const [{ isApprovedForAll }, { getApproved }, { ownerOf }] =
await Promise.all([
import("../../erc721/__generated__/IERC721A/read/isApprovedForAll.js"),
import("../../erc721/__generated__/IERC721A/read/getApproved.js"),
import("../../erc721/__generated__/IERC721A/read/ownerOf.js"),
]);
// check for token approval
const [approvedForAll, approvedOperator, tokenOwner] = await Promise.all([
isApprovedForAll({
contract: assetContract,
owner: options.listing.creatorAddress,
// the marketplace contract address has to be approved to transfer the token
operator: options.contract.address,
}),
getApproved({
contract: assetContract,
tokenId: options.listing.tokenId,
}).catch(() => ""),
ownerOf({
contract: assetContract,
tokenId: options.listing.tokenId,
}),
]);
// if the marketplace is not approved for all and the marketplace contract is not the approved operator for the token
// -> the listing is not valid
if (!approvedForAll && approvedOperator !== options.contract.address) {
return { valid: false, reason: "Asset not approved for marketplace." };
}
// if the token owner is not the creator of the listing
// -> the listing is not valid
if (tokenOwner !== options.listing.creatorAddress) {
return {
valid: false,
reason: "Listing creator no longer owns this token.",
};
}
// otherwise the listing is valid
return {
valid: true,
};
}
// if the asset is an erc1155 token
if (erc1155) {
const [{ isApprovedForAll }, { balanceOf }] = await Promise.all([
import("../../erc1155/__generated__/IERC1155/read/isApprovedForAll.js"),
import("../../erc1155/__generated__/IERC1155/read/balanceOf.js"),
]);
const [approvedForAll, balance] = await Promise.all([
isApprovedForAll({
contract: assetContract,
owner: options.listing.creatorAddress,
// the marketplace contract address has to be approved to transfer the token
operator: options.contract.address,
}),
balanceOf({
contract: assetContract,
owner: options.listing.creatorAddress,
tokenId: options.listing.tokenId,
}),
]);
// if the marketplace is not approved for all
// -> the listing is not valid
if (!approvedForAll) {
return { valid: false, reason: "Asset not approved for marketplace." };
}
// if the balance is less than the quantity the user is trying to purchase or the listing quantity
// -> the listing is not valid
const quantityWanted = options.quantity || options.listing.quantity;
if (balance < quantityWanted) {
return {
valid: false,
reason:
"Seller does not have enough balance of token to fulfill order.",
};
}
return {
valid: true,
};
}
// if the asset is neither ERC721 nor ERC1155
return {
valid: false,
reason: "AssetContract must implement ERC 1155 or ERC 721.",
};
}
@@ -2,8 +2,8 @@ import type { Address } from "abitype";
import { isNativeTokenAddress } from "../../../../constants/addresses.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { buyFromListing as generatedBuyFromListing } from "../../__generated__/IDirectListings/write/buyFromListing.js";
import { getListing } from "../../read/direct/getListing.js";
import { isListingValid } from "../../utils.js";
import { getListing } from "../read/getListing.js";
import { isListingValid } from "../utils.js";
export type BuyFromListingParams = {
listingId: bigint;
@@ -11,7 +11,7 @@ import { toUnits } from "../../../../utils/units.js";
import { isERC721 } from "../../../erc721/read/isERC721.js";
import { isERC1155 } from "../../../erc1155/read/isERC1155.js";
import { updateListing as generatedUpdateListing } from "../../__generated__/IDirectListings/write/updateListing.js";
import { getListing } from "../../read/direct/getListing.js";
import { getListing } from "../read/getListing.js";
export type UpdateListingParams = {
/**
@@ -0,0 +1,360 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { approve } from "../../erc721/__generated__/IERC721A/write/approve.js";
import { tokensMintedEvent } from "../../erc721/__generated__/IMintableERC721/events/TokensMinted.js";
import { mintTo } from "../../erc721/write/mintTo.js";
import { deployERC721Contract } from "../../prebuilts/deploy-erc721.js";
import { deployMarketplaceContract } from "../../prebuilts/deploy-marketplace.js";
import { newAuctionEvent } from "../__generated__/IEnglishAuctions/events/NewAuction.js";
import { totalAuctions } from "../__generated__/IEnglishAuctions/read/totalAuctions.js";
import { getAllAuctions } from "./read/getAllAuctions.js";
import { getAllValidAuctions } from "./read/getAllValidAuctions.js";
import { getAuction } from "./read/getAuction.js";
import { getWinningBid } from "./read/getWinningBid.js";
import { bidInAuction } from "./write/bidInAuction.js";
import { buyoutAuction } from "./write/buyoutAuction.js";
import { createAuction } from "./write/createAuction.js";
describe("Marketplace: English Auctions", () => {
let nftTokenId: bigint;
let marketplaceContract: ThirdwebContract;
let erc721Contract: ThirdwebContract;
beforeAll(async () => {
const marketplaceAddress = await deployMarketplaceContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestMarketPlace",
},
});
marketplaceContract = getContract({
address: marketplaceAddress,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// also deploy an ERC721 contract
const erc721Address = await deployERC721Contract({
type: "TokenERC721",
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestERC721",
},
});
erc721Contract = getContract({
address: erc721Address,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
const mintTransaction = mintTo({
contract: erc721Contract,
to: TEST_ACCOUNT_A.address,
nft: { name: "Test:ERC721:EnglishAuction" },
});
const receipt = await sendAndConfirmTransaction({
transaction: mintTransaction,
account: TEST_ACCOUNT_A,
});
const mintEvents = parseEventLogs({
events: [tokensMintedEvent()],
logs: receipt.logs,
});
expect(mintEvents.length).toBe(1);
expect(mintEvents[0]?.args.tokenIdMinted).toBeDefined();
nftTokenId = mintEvents[0]?.args.tokenIdMinted as bigint;
// does a lot of stuff, this may take a while
}, 120_000);
it("should work for basic auctions (Native Currency)", async () => {
// auctions should be 0 length to start
const auctions = await getAllAuctions({
contract: marketplaceContract,
});
expect(auctions.length).toBe(0);
// oh and so should totalAuctions
expect(await totalAuctions({ contract: marketplaceContract })).toBe(0n);
// approve first
const approveTx = approve({
contract: erc721Contract,
to: marketplaceContract.address,
tokenId: nftTokenId,
});
await sendAndConfirmTransaction({
transaction: approveTx,
account: TEST_ACCOUNT_A,
});
const transaction = createAuction({
contract: marketplaceContract,
assetContractAddress: erc721Contract.address,
tokenId: nftTokenId,
minimumBidAmount: "1",
buyoutBidAmount: "10",
});
const receipt = await sendAndConfirmTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
const listingEvents = parseEventLogs({
events: [newAuctionEvent()],
logs: receipt.logs,
});
expect(listingEvents.length).toBe(1);
// biome-ignore lint/style/noNonNullAssertion: OK in tests
const listingEvent = listingEvents[0]!;
expect(listingEvent.args.auctionCreator).toBe(TEST_ACCOUNT_A.address);
expect(listingEvent.args.assetContract).toBe(erc721Contract.address);
// at this point auctions should be 1
const auctionsAfter = await getAllAuctions({
contract: marketplaceContract,
});
expect(auctionsAfter.length).toBe(1);
// valid auctions should also be 1!
const validAuctions = await getAllValidAuctions({
contract: marketplaceContract,
});
expect(validAuctions.length).toBe(1);
// and totalauctions should be 1
expect(await totalAuctions({ contract: marketplaceContract })).toBe(1n);
// explicitly retrieve the listing!
const listing = await getAuction({
contract: marketplaceContract,
auctionId: listingEvent.args.auctionId,
});
expect(listing).toBeDefined();
expect(listing.status).toBe("ACTIVE");
expect(listing.creatorAddress).toBe(TEST_ACCOUNT_A.address);
expect(listing.assetContractAddress).toBe(erc721Contract.address);
expect(listing.tokenId).toBe(nftTokenId);
expect(listing.minimumBidCurrencyValue).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "1",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 1000000000000000000n,
}
`);
expect(listing.buyoutCurrencyValue).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "10",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 10000000000000000000n,
}
`);
expect(listing.asset).toMatchInlineSnapshot(`
{
"id": 0n,
"metadata": {
"name": "Test:ERC721:EnglishAuction",
},
"owner": null,
"tokenURI": "ipfs://QmQSqsA3fJu9dLhBb7FxMf8LxsFtKL3dXzPdQbyEVhGjTF/0",
"type": "ERC721",
}
`);
// check for a winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toBeUndefined();
// invalid bid amount 1: 0 bid (0 is not allowed)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "0",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot("[Error: Bid amount is zero]");
// invalid bid amount 2: 11 bid (over buyout)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "11",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is above the buyout amount]",
);
// invalid bid amount 3: below minimum bid (but not 0)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "0.5",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is below the minimum bid amount]",
);
// valid bid amount: "2"
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "2",
}),
}),
).resolves.toBeDefined();
// check for a new winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toMatchInlineSnapshot(`
{
"bidAmountWei": 2000000000000000000n,
"bidderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"currencyAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"currencyValue": {
"decimals": 18,
"displayValue": "2",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 2000000000000000000n,
},
}
`);
// invalid bid amount, above minimum but below existing winning bid
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "1.5",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is too low to outbid the existing winning bid]",
);
// invalid bid amount, above winning bit but below bid + bidBuffer (default 500bps)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
// 2 * 1.05 = 2.1, so 2.05 is invalid
bidAmount: "2.05",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is too low to outbid the existing winning bid]",
);
// actually outbid the winning bid
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "3",
}),
}),
).resolves.toBeDefined();
// check for a new winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toMatchInlineSnapshot(`
{
"bidAmountWei": 3000000000000000000n,
"bidderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"currencyAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"currencyValue": {
"decimals": 18,
"displayValue": "3",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 3000000000000000000n,
},
}
`);
// buyout auction
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: buyoutAuction({
contract: marketplaceContract,
auctionId: listing.id,
}),
}),
).resolves.toBeDefined();
// check for a new winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toMatchInlineSnapshot(`
{
"bidAmountWei": 10000000000000000000n,
"bidderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"currencyAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"currencyValue": {
"decimals": 18,
"displayValue": "10",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 10000000000000000000n,
},
}
`);
});
});
@@ -4,8 +4,9 @@ import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { min } from "../../../../utils/bigint.js";
import { getAllAuctions as getAllAuctionsGenerated } from "../../__generated__/IEnglishAuctions/read/getAllAuctions.js";
import { totalAuctions } from "../../__generated__/IEnglishAuctions/read/totalAuctions.js";
import type { EnglishAuction } from "../../types.js";
import { getAllInBatches, mapEnglishAuction } from "../../utils.js";
import { getAllInBatches } from "../../utils.js";
import type { EnglishAuction } from "../types.js";
import { mapEnglishAuction } from "../utils.js";
const DEFAULT_QUERY_ALL_COUNT = 100n;
@@ -4,8 +4,9 @@ import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { min } from "../../../../utils/bigint.js";
import { getAllValidAuctions as getAllValidAuctionsGenerated } from "../../__generated__/IEnglishAuctions/read/getAllValidAuctions.js";
import { totalAuctions } from "../../__generated__/IEnglishAuctions/read/totalAuctions.js";
import type { EnglishAuction } from "../../types.js";
import { getAllInBatches, mapEnglishAuction } from "../../utils.js";
import { getAllInBatches } from "../../utils.js";
import type { EnglishAuction } from "../types.js";
import { mapEnglishAuction } from "../utils.js";
const DEFAULT_QUERY_ALL_COUNT = 100n;
@@ -6,8 +6,8 @@ import {
type GetAuctionParams as GetAuctionParamsGenerated,
getAuction as getAuctionGenerated,
} from "../../__generated__/IEnglishAuctions/read/getAuction.js";
import type { EnglishAuction } from "../../types.js";
import { mapEnglishAuction } from "../../utils.js";
import type { EnglishAuction } from "../types.js";
import { mapEnglishAuction } from "../utils.js";
export type GetAuctionParams = GetAuctionParamsGenerated;
@@ -0,0 +1,23 @@
import type { Address } from "abitype";
import type { NFT } from "../../../utils/nft/parseNft.js";
import type { GetBalanceResult } from "../../erc20/read/getBalance.js";
import type { ListingStatus } from "../types.js";
export type EnglishAuction = {
id: bigint;
creatorAddress: Address;
assetContractAddress: Address;
tokenId: bigint;
quantity: bigint;
currencyContractAddress: Address;
minimumBidAmount: bigint;
minimumBidCurrencyValue: GetBalanceResult;
buyoutBidAmount: bigint;
buyoutCurrencyValue: GetBalanceResult;
timeBufferInSeconds: bigint;
bidBufferBps: bigint;
startTimeInSeconds: bigint;
endTimeInSeconds: bigint;
asset: NFT;
status: ListingStatus;
};
@@ -0,0 +1,76 @@
import { getContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { toTokens } from "../../../utils/units.js";
import { getCurrencyMetadata } from "../../erc20/read/getCurrencyMetadata.js";
import type { getAuction } from "../__generated__/IEnglishAuctions/read/getAuction.js";
import { computeStatus, getNFTAsset } from "../utils.js";
import type { EnglishAuction } from "./types.js";
/**
* @internal
*/
export async function mapEnglishAuction(
options: BaseTransactionOptions<{
latestBlock: { timestamp: bigint };
rawAuction: Awaited<ReturnType<typeof getAuction>>;
}>,
): Promise<EnglishAuction> {
const { latestBlock, rawAuction } = options;
// process the listing
const status = computeStatus({
listingStatus: rawAuction.status,
blockTimeStamp: latestBlock.timestamp,
startTimestamp: rawAuction.startTimestamp,
endTimestamp: rawAuction.endTimestamp,
});
const [auctionCurrencyMetadata, nftAsset] = await Promise.all([
getCurrencyMetadata({
contract: getContract({
...options.contract,
address: rawAuction.currency,
}),
}),
getNFTAsset({
...options,
contract: getContract({
...options.contract,
address: rawAuction.assetContract,
}),
tokenId: rawAuction.tokenId,
}),
]);
return {
id: rawAuction.auctionId,
creatorAddress: rawAuction.auctionCreator,
assetContractAddress: rawAuction.assetContract,
tokenId: rawAuction.tokenId,
quantity: rawAuction.quantity,
currencyContractAddress: rawAuction.currency,
asset: nftAsset,
startTimeInSeconds: rawAuction.startTimestamp,
endTimeInSeconds: rawAuction.endTimestamp,
status,
minimumBidAmount: rawAuction.minimumBidAmount,
minimumBidCurrencyValue: {
...auctionCurrencyMetadata,
value: rawAuction.minimumBidAmount,
displayValue: toTokens(
rawAuction.minimumBidAmount,
auctionCurrencyMetadata.decimals,
),
},
buyoutBidAmount: rawAuction.buyoutBidAmount,
buyoutCurrencyValue: {
...auctionCurrencyMetadata,
value: rawAuction.buyoutBidAmount,
displayValue: toTokens(
rawAuction.buyoutBidAmount,
auctionCurrencyMetadata.decimals,
),
},
timeBufferInSeconds: rawAuction.timeBufferInSeconds,
bidBufferBps: rawAuction.bidBufferBps,
};
}
@@ -2,8 +2,8 @@ import { isNativeTokenAddress } from "../../../../constants/addresses.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { isNewWinningBid } from "../../__generated__/IEnglishAuctions/read/isNewWinningBid.js";
import { bidInAuction as generatedBidInAuction } from "../../__generated__/IEnglishAuctions/write/bidInAuction.js";
import { getAuction } from "../../read/english-auction/getAuction.js";
import { getWinningBid } from "../../read/english-auction/getWinningBid.js";
import { getAuction } from "../read/getAuction.js";
import { getWinningBid } from "../read/getWinningBid.js";
export type BidInAuctionParams = {
auctionId: bigint;
@@ -1,7 +1,7 @@
import { isNativeTokenAddress } from "../../../../constants/addresses.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { bidInAuction as generatedBidInAuction } from "../../__generated__/IEnglishAuctions/write/bidInAuction.js";
import { getAuction } from "../../read/english-auction/getAuction.js";
import { getAuction } from "../read/getAuction.js";
export type BuyoutAuctionParams = {
auctionId: bigint;
@@ -1,6 +1,6 @@
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { cancelAuction as generatedCancelAuction } from "../../__generated__/IEnglishAuctions/write/cancelAuction.js";
import { getWinningBid } from "../../read/english-auction/getWinningBid.js";
import { getWinningBid } from "../read/getWinningBid.js";
export type CancelAuctionParams = {
auctionId: bigint;
@@ -2,7 +2,7 @@ import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { multicall } from "../../../common/__generated__/IMulticall/write/multicall.js";
import { encodeCollectAuctionPayout } from "../../__generated__/IEnglishAuctions/write/collectAuctionPayout.js";
import { encodeCollectAuctionTokens } from "../../__generated__/IEnglishAuctions/write/collectAuctionTokens.js";
import { getWinningBid } from "../../read/english-auction/getWinningBid.js";
import { getWinningBid } from "../read/getWinningBid.js";
export type ExecuteSaleParams = {
auctionId: bigint;
@@ -35,7 +35,6 @@ export function executeSale(
contract: options.contract,
auctionId: options.auctionId,
});
console.log("*** winning bid", winningBid);
if (!winningBid) {
throw new Error("Auction is still active");
}
@@ -1,569 +0,0 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../test/src/test-wallets.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { parseEventLogs } from "../../event/actions/parse-logs.js";
import { balanceOf } from "../../exports/extensions/erc721.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
import { getContractMetadata } from "../common/read/getContractMetadata.js";
import { approve } from "../erc721/__generated__/IERC721A/write/approve.js";
import { tokensMintedEvent } from "../erc721/__generated__/IMintableERC721/events/TokensMinted.js";
import { mintTo } from "../erc721/write/mintTo.js";
import { deployERC721Contract } from "../prebuilts/deploy-erc721.js";
import { deployMarketplaceContract } from "../prebuilts/deploy-marketplace.js";
import { newListingEvent } from "./__generated__/IDirectListings/events/NewListing.js";
import { totalListings } from "./__generated__/IDirectListings/read/totalListings.js";
import { newAuctionEvent } from "./__generated__/IEnglishAuctions/events/NewAuction.js";
import { totalAuctions } from "./__generated__/IEnglishAuctions/read/totalAuctions.js";
import { getAllListings } from "./read/direct/getAllListings.js";
import { getAllValidListings } from "./read/direct/getAllValidListings.js";
import { getListing } from "./read/direct/getListing.js";
import { getAllAuctions } from "./read/english-auction/getAllAuctions.js";
import { getAllValidAuctions } from "./read/english-auction/getAllValidAuctions.js";
import { getAuction } from "./read/english-auction/getAuction.js";
import { getWinningBid } from "./read/english-auction/getWinningBid.js";
import { isListingValid } from "./utils.js";
import { buyFromListing } from "./write/direct/buyFromListing.js";
import { createListing } from "./write/direct/createListing.js";
import { bidInAuction } from "./write/english-auction/bidInAuction.js";
import { buyoutAuction } from "./write/english-auction/buyoutAuction.js";
import { createAuction } from "./write/english-auction/createAuction.js";
// import { executeSale } from "./write/english-auction/executeSale.js";
describe.runIf(process.env.TW_SECRET_KEY)(
"Marketplace",
{
timeout: 120000,
},
() => {
let marketplaceContract: ThirdwebContract;
let erc721Contract: ThirdwebContract;
beforeAll(async () => {
const marketplaceAddress = await deployMarketplaceContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestMarketPlace",
},
});
marketplaceContract = getContract({
address: marketplaceAddress,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// also deploy an ERC721 contract
const erc721Address = await deployERC721Contract({
type: "TokenERC721",
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestERC721",
},
});
erc721Contract = getContract({
address: erc721Address,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// does a lot of stuff, this may take a while
}, 60_000);
describe("Deployment", () => {
it("should deploy marketplace contract", () => {
expect(marketplaceContract).toBeDefined();
});
it("should have the correct name", async () => {
const metadata = await getContractMetadata({
contract: marketplaceContract,
});
expect(metadata.name).toBe("TestMarketPlace");
});
});
describe("Direct Listings", () => {
let nftTokenId: bigint;
beforeAll(async () => {
const mintTransaction = mintTo({
contract: erc721Contract,
to: TEST_ACCOUNT_A.address,
nft: { name: "Test:ERC721:DirectListing" },
});
const receipt = await sendAndConfirmTransaction({
transaction: mintTransaction,
account: TEST_ACCOUNT_A,
});
const mintEvents = parseEventLogs({
events: [tokensMintedEvent()],
logs: receipt.logs,
});
expect(mintEvents.length).toBe(1);
expect(mintEvents[0]?.args.tokenIdMinted).toBeDefined();
nftTokenId = mintEvents[0]?.args.tokenIdMinted as bigint;
}, 30_000);
it("should work for basic listings (Native Currency)", async () => {
// listings should be 0 length to start
const listings = await getAllListings({
contract: marketplaceContract,
});
expect(listings.length).toBe(0);
// oh and so should totalListings
expect(await totalListings({ contract: marketplaceContract })).toBe(0n);
// approve first
const approveTx = approve({
contract: erc721Contract,
to: marketplaceContract.address,
tokenId: nftTokenId,
});
await sendAndConfirmTransaction({
transaction: approveTx,
account: TEST_ACCOUNT_A,
});
const transaction = createListing({
contract: marketplaceContract,
assetContractAddress: erc721Contract.address,
tokenId: nftTokenId,
pricePerToken: "1",
});
const receipt = await sendAndConfirmTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
const listingEvents = parseEventLogs({
events: [newListingEvent()],
logs: receipt.logs,
});
expect(listingEvents.length).toBe(1);
// biome-ignore lint/style/noNonNullAssertion: OK in tests
const listingEvent = listingEvents[0]!;
expect(listingEvent.args.listingCreator).toBe(TEST_ACCOUNT_A.address);
expect(listingEvent.args.assetContract).toBe(erc721Contract.address);
// at this point listings should be 1
const listingsAfter = await getAllListings({
contract: marketplaceContract,
});
expect(listingsAfter.length).toBe(1);
// valid listings should also be 1!
const validListings = await getAllValidListings({
contract: marketplaceContract,
});
expect(validListings.length).toBe(1);
// and totalListings should be 1
expect(await totalListings({ contract: marketplaceContract })).toBe(1n);
// explicitly retrieve the listing!
const listing = await getListing({
contract: marketplaceContract,
listingId: listingEvent.args.listingId,
});
expect(listing).toBeDefined();
expect(listing.status).toBe("ACTIVE");
expect(listing.creatorAddress).toBe(TEST_ACCOUNT_A.address);
expect(listing.assetContractAddress).toBe(erc721Contract.address);
expect(listing.tokenId).toBe(nftTokenId);
expect(listing.currencyValuePerToken).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "1",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 1000000000000000000n,
}
`);
expect(listing.asset).toMatchInlineSnapshot(`
{
"id": 0n,
"metadata": {
"name": "Test:ERC721:DirectListing",
},
"owner": null,
"tokenURI": "ipfs://QmewoGwuooC1Vno1ZTnmKmEUaimDygraR1fEEHdTeraF34/0",
"type": "ERC721",
}
`);
// check the listing is valid
const listingValidity = await isListingValid({
listing,
contract: marketplaceContract,
quantity: 1n,
});
expect(listingValidity).toMatchInlineSnapshot(`
{
"valid": true,
}
`);
// expect the buyer to have an initial balance of 0
await expect(
balanceOf({
contract: erc721Contract,
owner: TEST_ACCOUNT_B.address,
}),
).resolves.toBe(0n);
const buyTx = buyFromListing({
contract: marketplaceContract,
listingId: listingEvent.args.listingId,
recipient: TEST_ACCOUNT_B.address,
quantity: 1n,
});
await sendAndConfirmTransaction({
transaction: buyTx,
account: TEST_ACCOUNT_B,
});
// expect the buyer to have a new balance of 1
await expect(
balanceOf({
contract: erc721Contract,
owner: TEST_ACCOUNT_B.address,
}),
).resolves.toBe(1n);
// expect the seller to no longer have the token
await expect(
balanceOf({
contract: erc721Contract,
owner: TEST_ACCOUNT_A.address,
}),
).resolves.toBe(0n);
});
});
describe("English Auctions", () => {
let nftTokenId: bigint;
beforeAll(async () => {
const mintTransaction = mintTo({
contract: erc721Contract,
to: TEST_ACCOUNT_A.address,
nft: { name: "Test:ERC721:EnglishAuction" },
});
const receipt = await sendAndConfirmTransaction({
transaction: mintTransaction,
account: TEST_ACCOUNT_A,
});
const mintEvents = parseEventLogs({
events: [tokensMintedEvent()],
logs: receipt.logs,
});
expect(mintEvents.length).toBe(1);
expect(mintEvents[0]?.args.tokenIdMinted).toBeDefined();
nftTokenId = mintEvents[0]?.args.tokenIdMinted as bigint;
}, 30_000);
it("should work for basic auctions (Native Currency)", async () => {
// auctions should be 0 length to start
const auctions = await getAllAuctions({
contract: marketplaceContract,
});
expect(auctions.length).toBe(0);
// oh and so should totalAuctions
expect(await totalAuctions({ contract: marketplaceContract })).toBe(0n);
// approve first
const approveTx = approve({
contract: erc721Contract,
to: marketplaceContract.address,
tokenId: nftTokenId,
});
await sendAndConfirmTransaction({
transaction: approveTx,
account: TEST_ACCOUNT_A,
});
const transaction = createAuction({
contract: marketplaceContract,
assetContractAddress: erc721Contract.address,
tokenId: nftTokenId,
minimumBidAmount: "1",
buyoutBidAmount: "10",
});
const receipt = await sendAndConfirmTransaction({
transaction,
account: TEST_ACCOUNT_A,
});
const listingEvents = parseEventLogs({
events: [newAuctionEvent()],
logs: receipt.logs,
});
expect(listingEvents.length).toBe(1);
// biome-ignore lint/style/noNonNullAssertion: OK in tests
const listingEvent = listingEvents[0]!;
expect(listingEvent.args.auctionCreator).toBe(TEST_ACCOUNT_A.address);
expect(listingEvent.args.assetContract).toBe(erc721Contract.address);
// at this point auctions should be 1
const auctionsAfter = await getAllAuctions({
contract: marketplaceContract,
});
expect(auctionsAfter.length).toBe(1);
// valid auctions should also be 1!
const validAuctions = await getAllValidAuctions({
contract: marketplaceContract,
});
expect(validAuctions.length).toBe(1);
// and totalauctions should be 1
expect(await totalAuctions({ contract: marketplaceContract })).toBe(1n);
// explicitly retrieve the listing!
const listing = await getAuction({
contract: marketplaceContract,
auctionId: listingEvent.args.auctionId,
});
expect(listing).toBeDefined();
expect(listing.status).toBe("ACTIVE");
expect(listing.creatorAddress).toBe(TEST_ACCOUNT_A.address);
expect(listing.assetContractAddress).toBe(erc721Contract.address);
expect(listing.tokenId).toBe(nftTokenId);
expect(listing.minimumBidCurrencyValue).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "1",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 1000000000000000000n,
}
`);
expect(listing.buyoutCurrencyValue).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "10",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 10000000000000000000n,
}
`);
expect(listing.asset).toMatchInlineSnapshot(`
{
"id": 1n,
"metadata": {
"name": "Test:ERC721:EnglishAuction",
},
"owner": null,
"tokenURI": "ipfs://QmQSqsA3fJu9dLhBb7FxMf8LxsFtKL3dXzPdQbyEVhGjTF/0",
"type": "ERC721",
}
`);
// check for a winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toBeUndefined();
// invalid bid amount 1: 0 bid (0 is not allowed)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "0",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is zero]",
);
// invalid bid amount 2: 11 bid (over buyout)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "11",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is above the buyout amount]",
);
// invalid bid amount 3: below minimum bid (but not 0)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "0.5",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is below the minimum bid amount]",
);
// valid bid amount: "2"
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "2",
}),
}),
).resolves.toBeDefined();
// check for a new winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toMatchInlineSnapshot(`
{
"bidAmountWei": 2000000000000000000n,
"bidderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"currencyAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"currencyValue": {
"decimals": 18,
"displayValue": "2",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 2000000000000000000n,
},
}
`);
// invalid bid amount, above minimum but below existing winning bid
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "1.5",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is too low to outbid the existing winning bid]",
);
// invalid bid amount, above winning bit but below bid + bidBuffer (default 500bps)
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
// 2 * 1.05 = 2.1, so 2.05 is invalid
bidAmount: "2.05",
}),
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Bid amount is too low to outbid the existing winning bid]",
);
// actually outbid the winning bid
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: bidInAuction({
auctionId: listing.id,
contract: marketplaceContract,
bidAmount: "3",
}),
}),
).resolves.toBeDefined();
// check for a new winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toMatchInlineSnapshot(`
{
"bidAmountWei": 3000000000000000000n,
"bidderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"currencyAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"currencyValue": {
"decimals": 18,
"displayValue": "3",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 3000000000000000000n,
},
}
`);
// buyout auction
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: buyoutAuction({
contract: marketplaceContract,
auctionId: listing.id,
}),
}),
).resolves.toBeDefined();
// check for a new winning bid
await expect(
getWinningBid({
contract: marketplaceContract,
auctionId: listing.id,
}),
).resolves.toMatchInlineSnapshot(`
{
"bidAmountWei": 10000000000000000000n,
"bidderAddress": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"currencyAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"currencyValue": {
"decimals": 18,
"displayValue": "10",
"name": "Anvil Ether",
"symbol": "ETH",
"value": 10000000000000000000n,
},
}
`);
// // execute the sale
// await expect(
// sendAndConfirmTransaction({
// account: TEST_ACCOUNT_A,
// transaction: executeSale({
// contract: marketplaceContract,
// auctionId: listing.id,
// }),
// }),
// ).resolves.toBeDefined();
});
});
},
);
@@ -0,0 +1,346 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
} from "../../../../test/src/test-wallets.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { getDeployedInfraContract } from "../../../contract/deployment/utils/infra.js";
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { getBalance } from "../../erc20/read/getBalance.js";
import { approve as approveErc20 } from "../../erc20/write/approve.js";
import { deposit } from "../../erc20/write/deposit.js";
import { ownerOf } from "../../erc721/__generated__/IERC721A/read/ownerOf.js";
import { approve as approveErc721 } from "../../erc721/__generated__/IERC721A/write/approve.js";
import { tokensMintedEvent } from "../../erc721/__generated__/IMintableERC721/events/TokensMinted.js";
import { mintTo } from "../../erc721/write/mintTo.js";
import { deployERC721Contract } from "../../prebuilts/deploy-erc721.js";
import { deployMarketplaceContract } from "../../prebuilts/deploy-marketplace.js";
import { newOfferEvent } from "../__generated__/IOffers/events/NewOffer.js";
import { totalOffers } from "../__generated__/IOffers/read/totalOffers.js";
import { cancelOffer } from "../__generated__/IOffers/write/cancelOffer.js";
import { getAllOffers } from "./read/getAllOffers.js";
import { getAllValidOffers } from "./read/getAllValidOffers.js";
import { getOffer } from "./read/getOffer.js";
import { acceptOffer } from "./write/acceptOffer.js";
import { makeOffer } from "./write/makeOffer.js";
describe("Marketplace: Offers", () => {
let nftTokenId: bigint;
let marketplaceContract: ThirdwebContract;
let erc721Contract: ThirdwebContract;
let WETH9: ThirdwebContract;
beforeAll(async () => {
const marketplaceAddress = await deployMarketplaceContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestMarketPlace",
},
});
marketplaceContract = getContract({
address: marketplaceAddress,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
// also deploy an ERC721 contract
const erc721Address = await deployERC721Contract({
type: "TokenERC721",
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
params: {
name: "TestERC721",
},
});
erc721Contract = getContract({
address: erc721Address,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
});
const mintTransaction = mintTo({
contract: erc721Contract,
to: TEST_ACCOUNT_A.address,
nft: { name: "Test:ERC721:Offers" },
});
const receipt = await sendAndConfirmTransaction({
transaction: mintTransaction,
account: TEST_ACCOUNT_A,
});
const mintEvents = parseEventLogs({
events: [tokensMintedEvent()],
logs: receipt.logs,
});
expect(mintEvents.length).toBe(1);
expect(mintEvents[0]?.args.tokenIdMinted).toBeDefined();
nftTokenId = mintEvents[0]?.args.tokenIdMinted as bigint;
// biome-ignore lint/style/noNonNullAssertion: test only and we know it will exist here because it gets deployed in the beforeAll
WETH9 = (await getDeployedInfraContract({
chain: ANVIL_CHAIN,
contractId: "WETH9",
constructorParams: [],
client: TEST_CLIENT,
}))!;
expect(WETH9.address).toMatchInlineSnapshot(
`"0x81e609b897393731a3d23c1d311330340cebb9e9"`,
);
// does a lot of stuff, this may take a while
}, 120_000);
// NOTE: this test suite relies on the order of operations in order to not re-deploy contracts over and over
// this is generally not ideal, but we're doing it here to save time
it("should start out with 0 offers", async () => {
await expect(totalOffers({ contract: marketplaceContract })).resolves.toBe(
0n,
);
await expect(
getAllOffers({ contract: marketplaceContract }),
).resolves.toMatchInlineSnapshot("[]");
await expect(
getAllValidOffers({ contract: marketplaceContract }),
).resolves.toMatchInlineSnapshot("[]");
});
it("should allow a user to create an offer", async () => {
// first deposit some tokens into the WETH9 contract
// do do that, determine the address of the WETH9 contract
await expect(
sendAndConfirmTransaction({
transaction: deposit({ contract: WETH9, amount: "10" }),
account: TEST_ACCOUNT_B,
}),
).resolves.toBeDefined();
// check to make sure the weth9 balance is correct
await expect(
getBalance({ contract: WETH9, address: TEST_ACCOUNT_B.address }),
).resolves.toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "10",
"name": "Wrapped Ether",
"symbol": "WETH",
"value": 10000000000000000000n,
}
`);
// now approve the marketplace contract to spend the WETH9 tokens
await expect(
sendAndConfirmTransaction({
transaction: approveErc20({
contract: WETH9,
spender: marketplaceContract.address,
amount: "10",
}),
account: TEST_ACCOUNT_B,
}),
).resolves.toBeDefined();
// now create an offer
const receipt = await sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: makeOffer({
assetContractAddress: erc721Contract.address,
tokenId: nftTokenId,
contract: marketplaceContract,
currencyContractAddress: NATIVE_TOKEN_ADDRESS,
offerExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
totalOffer: "1",
}),
});
expect(receipt).toBeDefined();
const [eventLog] = parseEventLogs({
events: [newOfferEvent()],
logs: receipt.logs,
});
expect(eventLog).toBeDefined();
expect(eventLog?.args.offer.assetContract).toBe(erc721Contract.address);
expect(eventLog?.args.offer.tokenId).toBe(nftTokenId);
expect(eventLog?.args.offer.offeror).toBe(TEST_ACCOUNT_B.address);
});
it("should have 1 offer", async () => {
await expect(totalOffers({ contract: marketplaceContract })).resolves.toBe(
1n,
);
await expect(
getAllOffers({ contract: marketplaceContract }),
).resolves.toHaveLength(1);
await expect(
getAllValidOffers({ contract: marketplaceContract }),
).resolves.resolves.toHaveLength(1);
const offer = await getOffer({
contract: marketplaceContract,
offerId: 0n,
});
expect(offer.status).toMatchInlineSnapshot(`"ACTIVE"`);
expect(offer.assetContractAddress).toBe(erc721Contract.address);
expect(offer.tokenId).toBe(nftTokenId);
expect(offer.offerorAddress).toBe(TEST_ACCOUNT_B.address);
expect(offer.asset).toMatchInlineSnapshot(`
{
"id": 0n,
"metadata": {
"name": "Test:ERC721:Offers",
},
"owner": null,
"tokenURI": "ipfs://QmRk3sj4XxUx61SxBxt24uPJXDCf1G9G6iHAJgM6tbAuwD/0",
"type": "ERC721",
}
`);
expect(offer.currencyValue).toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "1",
"name": "Wrapped Ether",
"symbol": "WETH",
"value": 1000000000000000000n,
}
`);
});
it("should allow a user to cancel an offer", async () => {
await expect(
sendAndConfirmTransaction({
transaction: cancelOffer({
contract: marketplaceContract,
offerId: 0n,
}),
account: TEST_ACCOUNT_B,
}),
).resolves.toBeDefined();
await expect(totalOffers({ contract: marketplaceContract })).resolves.toBe(
1n,
);
await expect(
getAllOffers({ contract: marketplaceContract }),
).resolves.toHaveLength(1);
// valid offers should be 0
await expect(
getAllValidOffers({ contract: marketplaceContract }),
).resolves.resolves.toHaveLength(0);
const offer = await getOffer({
contract: marketplaceContract,
offerId: 0n,
});
expect(offer.status).toMatchInlineSnapshot(`"CANCELLED"`);
// should no longer be able to accept the offer
await expect(
sendAndConfirmTransaction({
transaction: acceptOffer({
contract: marketplaceContract,
offerId: 0n,
}),
account: TEST_ACCOUNT_B,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: Offer is not active]",
);
});
it("should allow an offer to be accepted", async () => {
// first gotta create a new offer
// thankfully we already have the WETH9 tokens approved and ready to go
await expect(
sendAndConfirmTransaction({
account: TEST_ACCOUNT_B,
transaction: makeOffer({
assetContractAddress: erc721Contract.address,
tokenId: nftTokenId,
contract: marketplaceContract,
currencyContractAddress: NATIVE_TOKEN_ADDRESS,
offerExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
// 2 WETH this time!
totalOffer: "2",
}),
}),
).resolves.toBeDefined();
// check for the new offer
await expect(totalOffers({ contract: marketplaceContract })).resolves.toBe(
2n,
);
await expect(
getAllOffers({ contract: marketplaceContract }),
).resolves.toHaveLength(2);
await expect(
getAllValidOffers({ contract: marketplaceContract }),
// should only have 1 valid offer!
).resolves.resolves.toHaveLength(1);
const offer = await getOffer({
contract: marketplaceContract,
offerId: 1n,
});
expect(offer.status).toMatchInlineSnapshot(`"ACTIVE"`);
// have to approve the marketplace contract to control the NFT!
await expect(
sendAndConfirmTransaction({
transaction: approveErc721({
contract: erc721Contract,
to: marketplaceContract.address,
tokenId: nftTokenId,
}),
account: TEST_ACCOUNT_A,
}),
).resolves.toBeDefined();
// now we accept the offer!
await expect(
sendAndConfirmTransaction({
transaction: acceptOffer({
contract: marketplaceContract,
offerId: 1n,
}),
account: TEST_ACCOUNT_A,
}),
).resolves.toBeDefined();
// check the balances
await expect(
getBalance({ contract: WETH9, address: TEST_ACCOUNT_A.address }),
).resolves.toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "2",
"name": "Wrapped Ether",
"symbol": "WETH",
"value": 2000000000000000000n,
}
`);
await expect(
getBalance({ contract: WETH9, address: TEST_ACCOUNT_B.address }),
).resolves.toMatchInlineSnapshot(`
{
"decimals": 18,
"displayValue": "8",
"name": "Wrapped Ether",
"symbol": "WETH",
"value": 8000000000000000000n,
}
`);
// check the owner of the NFT
await expect(
ownerOf({ contract: erc721Contract, tokenId: nftTokenId }),
).resolves.toBe(TEST_ACCOUNT_B.address);
});
});
@@ -0,0 +1,79 @@
import { eth_getBlockByNumber } from "../../../../rpc/actions/eth_getBlockByNumber.js";
import { getRpcClient } from "../../../../rpc/rpc.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { min } from "../../../../utils/bigint.js";
import { getAllOffers as getAllOffersGenerated } from "../../__generated__/IOffers/read/getAllOffers.js";
import { totalOffers } from "../../__generated__/IOffers/read/totalOffers.js";
import { getAllInBatches } from "../../utils.js";
import type { Offer } from "../types.js";
import { mapOffer } from "../utils.js";
const DEFAULT_QUERY_ALL_COUNT = 100n;
export type GetAllOffersParams = {
/**
* The start index of the offers to retrieve.
* @default 0
*/
start?: number;
/**
* The number of offers to retrieve.
* @default 100
*/
count?: bigint;
};
/**
* Retrieves all offers based on the provided options.
* @param options - The options for retrieving the offers.
* @returns A promise that resolves to the offers array.
* @extension MARKETPLACE
* @example
*
* ```ts
* import { getAllOffers } from "thirdweb/extensions/marketplace";
*
* const listings = await getAllOffers({ contract, start: 0, count: 10 });
* ```
*/
export async function getAllOffers(
options: BaseTransactionOptions<GetAllOffersParams>,
): Promise<Offer[]> {
const totalCount = await totalOffers(options);
// if the totalListingCount is 0, return an empty array and skip all other work
if (totalCount === 0n) {
return [];
}
const start = BigInt(options.start || 0);
const count = BigInt(options.count || DEFAULT_QUERY_ALL_COUNT);
const end = min(totalCount, start + count);
const rpcClient = getRpcClient(options.contract);
const [rawOffers, latestBlock] = await Promise.all([
getAllInBatches(
(startId, endId) =>
getAllOffersGenerated({ contract: options.contract, startId, endId }),
{
start,
end,
maxSize: DEFAULT_QUERY_ALL_COUNT,
},
// flatten the array of arrays
).then((listings) => listings.flat()),
// get the latest block number once
eth_getBlockByNumber(rpcClient, {
blockTag: "latest",
}),
]);
return await Promise.all(
rawOffers.map((rawOffer) =>
mapOffer({
contract: options.contract,
latestBlock,
rawOffer,
}),
),
);
}
@@ -0,0 +1,83 @@
import { eth_getBlockByNumber } from "../../../../rpc/actions/eth_getBlockByNumber.js";
import { getRpcClient } from "../../../../rpc/rpc.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { min } from "../../../../utils/bigint.js";
import { getAllValidOffers as getAllValidOffersGenerated } from "../../__generated__/IOffers/read/getAllValidOffers.js";
import { totalOffers } from "../../__generated__/IOffers/read/totalOffers.js";
import { getAllInBatches } from "../../utils.js";
import type { Offer } from "../types.js";
import { mapOffer } from "../utils.js";
const DEFAULT_QUERY_ALL_COUNT = 100n;
export type GetAllValidOffersParams = {
/**
* The start index of the offers to retrieve.
* @default 0
*/
start?: number;
/**
* The number of offers to retrieve.
* @default 100
*/
count?: bigint;
};
/**
* Retrieves all valid offers based on the provided options.
* @param options - The options for retrieving the valid offers.
* @returns A promise that resolves to the offers array.
* @extension MARKETPLACE
* @example
*
* ```ts
* import { getAllValidOffers } from "thirdweb/extensions/marketplace";
*
* const validOffers = await getAllValidOffers({ contract, start: 0, count: 10 });
* ```
*/
export async function getAllValidOffers(
options: BaseTransactionOptions<GetAllValidOffersParams>,
): Promise<Offer[]> {
const totalCount = await totalOffers(options);
// if the totalOffers count is 0, return an empty array and skip all other work
if (totalCount === 0n) {
return [];
}
const start = BigInt(options.start || 0);
const count = BigInt(options.count || DEFAULT_QUERY_ALL_COUNT);
const end = min(totalCount, start + count);
const rpcClient = getRpcClient(options.contract);
const [rawOffers, latestBlock] = await Promise.all([
getAllInBatches(
(startId, endId) =>
getAllValidOffersGenerated({
contract: options.contract,
startId,
endId,
}),
{
start,
end,
maxSize: DEFAULT_QUERY_ALL_COUNT,
},
// flatten the array of arrays
).then((listings) => listings.flat()),
// get the latest block number once
eth_getBlockByNumber(rpcClient, {
blockTag: "latest",
}),
]);
return await Promise.all(
rawOffers.map((rawOffer) =>
mapOffer({
contract: options.contract,
latestBlock,
rawOffer,
}),
),
);
}
@@ -0,0 +1,42 @@
import { eth_getBlockByNumber } from "../../../../rpc/actions/eth_getBlockByNumber.js";
import { getRpcClient } from "../../../../rpc/rpc.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import {
type GetOfferParams as GetOfferParamsGenerated,
getOffer as getOfferGenerated,
} from "../../__generated__/IOffers/read/getOffer.js";
import type { Offer } from "../types.js";
import { mapOffer } from "../utils.js";
export type GetOfferParams = GetOfferParamsGenerated;
/**
* Retrieves an offer based on the provided options.
* @param options - The options for retrieving the offer.
* @returns A promise that resolves to the offer.
* @extension MARKETPLACE
* @example
*
* ```ts
* import { getOffer } from "thirdweb/extensions/marketplace";
*
* const listing = await getOffer({ contract, listingId: 1n });
* ```
*/
export async function getOffer(
options: BaseTransactionOptions<GetOfferParams>,
): Promise<Offer> {
const rpcClient = getRpcClient(options.contract);
const [rawOffer, latestBlock] = await Promise.all([
getOfferGenerated(options),
eth_getBlockByNumber(rpcClient, {
blockTag: "latest",
}),
]);
return mapOffer({
contract: options.contract,
latestBlock,
rawOffer,
});
}
@@ -0,0 +1,18 @@
import type { Address } from "abitype";
import type { NFT } from "../../../utils/nft/parseNft.js";
import type { GetBalanceResult } from "../../erc20/read/getBalance.js";
import type { ListingStatus } from "../types.js";
export type Offer = {
id: bigint;
offerorAddress: Address;
assetContractAddress: Address;
tokenId: bigint;
quantity: bigint;
currencyContractAddress: Address;
currencyValue: GetBalanceResult;
totalPrice: bigint;
asset: NFT;
endTimeInSeconds: bigint;
status: ListingStatus;
};
@@ -0,0 +1,65 @@
import { getContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { toTokens } from "../../../utils/units.js";
import { getCurrencyMetadata } from "../../erc20/read/getCurrencyMetadata.js";
import type { getOffer } from "../__generated__/IOffers/read/getOffer.js";
import { computeStatus, getNFTAsset } from "../utils.js";
import type { Offer } from "./types.js";
/**
* @internal
*/
export async function mapOffer(
options: BaseTransactionOptions<{
latestBlock: { timestamp: bigint };
rawOffer: Awaited<ReturnType<typeof getOffer>>;
}>,
): Promise<Offer> {
const { latestBlock, rawOffer } = options;
// process the listing
const status = computeStatus({
listingStatus: rawOffer.status,
blockTimeStamp: latestBlock.timestamp,
// startTimestamp is always 0 for offers (they only have an expiration time not a start time)
startTimestamp: 0n,
endTimestamp: rawOffer.expirationTimestamp,
});
const [currencyValuePerToken, nftAsset] = await Promise.all([
getCurrencyMetadata({
contract: getContract({
...options.contract,
address: rawOffer.currency,
}),
}),
getNFTAsset({
...options,
contract: getContract({
...options.contract,
address: rawOffer.assetContract,
}),
tokenId: rawOffer.tokenId,
}),
]);
return {
id: rawOffer.offerId,
offerorAddress: rawOffer.offeror,
assetContractAddress: rawOffer.assetContract,
tokenId: rawOffer.tokenId,
quantity: rawOffer.quantity,
currencyContractAddress: rawOffer.currency,
currencyValue: {
...currencyValuePerToken,
value: rawOffer.totalPrice,
displayValue: toTokens(
rawOffer.totalPrice,
currencyValuePerToken.decimals,
),
},
totalPrice: rawOffer.totalPrice,
asset: nftAsset,
endTimeInSeconds: rawOffer.expirationTimestamp,
status,
};
}
@@ -0,0 +1,74 @@
import { getContract } from "../../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { allowance } from "../../../erc20/__generated__/IERC20/read/allowance.js";
import { balanceOf } from "../../../erc20/__generated__/IERC20/read/balanceOf.js";
import {
type AcceptOfferParams as GeneratedAcceptOfferParams,
acceptOffer as generatedAcceptOffer,
} from "../../__generated__/IOffers/write/acceptOffer.js";
import { getOffer } from "../read/getOffer.js";
export type AcceptOfferParams = GeneratedAcceptOfferParams;
/**
* Accepts an offer after performing necessary checks and validations.
* Throws an error if the offer is not active, the offeror's balance is insufficient,
* or the offeror's allowance is insufficient.
*
* @param options - The options for accepting the offer.
* @returns A transaction object that can be sent to accept the offer.
* @throws Error when sending the transaction if the offer is not active, the offeror's balance is insufficient,
* or the offeror's allowance is insufficient.
* @extension MARKETPLACE
* @example
* ```ts
* import { acceptOffer } from "thirdweb/extensions/marketplace";
*
* const acceptOfferTx = acceptOffer({
* contract,
* offerId: 1n,
* });
* ```
*/
export function acceptOffer(
options: BaseTransactionOptions<AcceptOfferParams>,
) {
return generatedAcceptOffer({
contract: options.contract,
asyncParams: async () => {
const offer = await getOffer({
contract: options.contract,
offerId: options.offerId,
});
if (offer.status !== "ACTIVE") {
throw new Error("Offer is not active");
}
// check that the offeror has sufficient balance and has given approval to the contract
const currencyContract = getContract({
address: offer.currencyContractAddress,
chain: options.contract.chain,
client: options.contract.client,
});
const [offerorBalance, offerorAllowance] = await Promise.all([
balanceOf({
contract: currencyContract,
address: offer.offerorAddress,
}),
allowance({
contract: currencyContract,
owner: offer.offerorAddress,
// spender is the marketplace contract
spender: options.contract.address,
}),
]);
if (offerorBalance < offer.totalPrice) {
throw new Error("Offeror balance is insufficient");
}
if (offerorAllowance < offer.totalPrice) {
throw new Error("Offeror allowance is insufficient");
}
// if we get here we can accept!
return { offerId: options.offerId };
},
});
}
@@ -0,0 +1,130 @@
import type { Address } from "abitype";
import { isNativeTokenAddress } from "../../../../constants/addresses.js";
import type { BaseTransactionOptions } from "../../../../transaction/types.js";
import { makeOffer as makeOfferGenerated } from "../../__generated__/IOffers/write/makeOffer.js";
export type MakeOfferParams = {
/**
* The address of the asset contract to offer on.
*/
assetContractAddress: Address;
/**
* The ID of the token to make an offer for.
*/
tokenId: bigint;
/**
* The address of the currency contract to make an offer with. (ERC20 or native token address)
*/
currencyContractAddress: Address;
/**
* The datetime when the offer expires.
*/
offerExpiresAt: Date;
/**
* The quantity of the asset to make an offer for.
* @default 1
*/
quantity?: bigint;
} & (
| {
/**
* The total offer amount for the NFT(s) in wei.
*/
totalOfferWei: bigint;
}
| {
/**
* The total offer amount for the NFT(s) in the currency's "Ether" unit
*/
totalOffer: string;
}
);
/**
* Makes an offer for any asset (ERC721 or ERC1155).
*
* @param options - The options for making the offer.
* @returns A transaction object that can be sent to make the offer.
* @extension MARKETPLACE
* @example
* ```ts
* import { makeOffer } from "thirdweb/extensions/marketplace";
*
* const offerTx = makeOffer({
* contract,
* assetContractAddress: "0x1234567890123456789012345678901234567890",
* tokenId: 1n,
* currencyContractAddress: "0x1234567890123456789012345678901234567890",
* offerExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
* totalOffer: "1.0",
* });
* ```
*
*/
export function makeOffer(options: BaseTransactionOptions<MakeOfferParams>) {
return makeOfferGenerated({
contract: options.contract,
asyncParams: async () => {
const [normalizedPrice, currency] = await Promise.all([
(async () => {
// if we already have the bid amount in wei, use that
if ("totalOfferWei" in options) {
return options.totalOfferWei;
}
// otherwise load the utility function and convert the amount
const { convertErc20Amount } = await import(
"../../../../utils/extensions/convert-erc20-amount.js"
);
return await convertErc20Amount({
amount: options.totalOffer,
chain: options.contract.chain,
erc20Address: options.currencyContractAddress,
client: options.contract.client,
});
})(),
(async () => {
// if it's not a native token address offer -> simply return the currency address
if (!isNativeTokenAddress(options.currencyContractAddress)) {
return options.currencyContractAddress;
}
// otherwise determine the wrapped native token address for the chain
// TODO: add known wrapped native token addresses for each chain on the chain config
const { getDeployedInfraContract } = await import(
"../../../../contract/deployment/utils/infra.js"
);
// predict the address for the wrapped token
const WETH9 = await getDeployedInfraContract({
chain: options.contract.chain,
client: options.contract.client,
constructorParams: [],
contractId: "WETH9",
});
if (!WETH9?.address) {
throw new Error(
"WETH9 contract not found on the chain, how did the marketplace get deployed in the first place?",
);
}
return WETH9.address;
})(),
]);
const expirationTimestamp = BigInt(
Math.floor(options.offerExpiresAt.getTime() / 1000),
);
return {
params: {
assetContract: options.assetContractAddress,
currency,
expirationTimestamp,
quantity: options.quantity ?? 1n,
tokenId: options.tokenId,
totalPrice: normalizedPrice,
},
};
},
});
}
@@ -1,7 +1,3 @@
import type { Address } from "abitype";
import type { NFT } from "../../utils/nft/parseNft.js";
import type { GetBalanceResult } from "../erc20/read/getBalance.js";
export type ListingStatus =
| "UNSET"
| "CREATED"
@@ -9,38 +5,3 @@ export type ListingStatus =
| "CANCELLED"
| "ACTIVE"
| "EXPIRED";
export type DirectListing = {
id: bigint;
creatorAddress: Address;
assetContractAddress: Address;
tokenId: bigint;
quantity: bigint;
currencyContractAddress: Address;
currencyValuePerToken: GetBalanceResult;
pricePerToken: bigint;
asset: NFT;
startTimeInSeconds: bigint;
endTimeInSeconds: bigint;
isReservedListing: boolean;
status: ListingStatus;
};
export type EnglishAuction = {
id: bigint;
creatorAddress: Address;
assetContractAddress: Address;
tokenId: bigint;
quantity: bigint;
currencyContractAddress: Address;
minimumBidAmount: bigint;
minimumBidCurrencyValue: GetBalanceResult;
buyoutBidAmount: bigint;
buyoutCurrencyValue: GetBalanceResult;
timeBufferInSeconds: bigint;
bidBufferBps: bigint;
startTimeInSeconds: bigint;
endTimeInSeconds: bigint;
asset: NFT;
status: ListingStatus;
};
@@ -1,13 +1,8 @@
import { getContract } from "../../contract/contract.js";
import type { BaseTransactionOptions } from "../../transaction/types.js";
import type { NFT } from "../../utils/nft/parseNft.js";
import { toTokens } from "../../utils/units.js";
import { getCurrencyMetadata } from "../erc20/read/getCurrencyMetadata.js";
import { isERC721 } from "../erc721/read/isERC721.js";
import { isERC1155 } from "../erc1155/read/isERC1155.js";
import type { getListing } from "./__generated__/IDirectListings/read/getListing.js";
import type { getAuction } from "./__generated__/IEnglishAuctions/read/getAuction.js";
import type { DirectListing, EnglishAuction, ListingStatus } from "./types.js";
import type { ListingStatus } from "./types.js";
type GetAssetParams = {
tokenId: bigint;
@@ -24,7 +19,7 @@ type GetAssetParams = {
* const nft = await getNFTAsset({ contract, tokenId: 1n });
* ```
*/
async function getNFTAsset(
export async function getNFTAsset(
options: BaseTransactionOptions<GetAssetParams>,
): Promise<NFT> {
const [erc721, erc1155] = await Promise.all([
@@ -46,7 +41,7 @@ async function getNFTAsset(
}
}
function computeStatus(options: {
export function computeStatus(options: {
listingStatus: number;
blockTimeStamp: bigint;
startTimestamp: bigint;
@@ -74,134 +69,6 @@ function computeStatus(options: {
}
}
/**
* @internal
*/
export async function mapDirectListing(
options: BaseTransactionOptions<{
latestBlock: { timestamp: bigint };
rawListing: Awaited<ReturnType<typeof getListing>>;
}>,
): Promise<DirectListing> {
const { latestBlock, rawListing } = options;
// process the listing
const status = computeStatus({
listingStatus: rawListing.status,
blockTimeStamp: latestBlock.timestamp,
startTimestamp: rawListing.startTimestamp,
endTimestamp: rawListing.endTimestamp,
});
const [currencyValuePerToken, nftAsset] = await Promise.all([
getCurrencyMetadata({
contract: getContract({
...options.contract,
address: rawListing.currency,
}),
}),
getNFTAsset({
...options,
contract: getContract({
...options.contract,
address: rawListing.assetContract,
}),
tokenId: rawListing.tokenId,
}),
]);
return {
id: rawListing.listingId,
creatorAddress: rawListing.listingCreator,
assetContractAddress: rawListing.assetContract,
tokenId: rawListing.tokenId,
quantity: rawListing.quantity,
currencyContractAddress: rawListing.currency,
currencyValuePerToken: {
...currencyValuePerToken,
value: rawListing.pricePerToken,
displayValue: toTokens(
rawListing.pricePerToken,
currencyValuePerToken.decimals,
),
},
pricePerToken: rawListing.pricePerToken,
asset: nftAsset,
startTimeInSeconds: rawListing.startTimestamp,
endTimeInSeconds: rawListing.endTimestamp,
isReservedListing: rawListing.reserved,
status,
};
}
/**
* @internal
*/
export async function mapEnglishAuction(
options: BaseTransactionOptions<{
latestBlock: { timestamp: bigint };
rawAuction: Awaited<ReturnType<typeof getAuction>>;
}>,
): Promise<EnglishAuction> {
const { latestBlock, rawAuction } = options;
// process the listing
const status = computeStatus({
listingStatus: rawAuction.status,
blockTimeStamp: latestBlock.timestamp,
startTimestamp: rawAuction.startTimestamp,
endTimestamp: rawAuction.endTimestamp,
});
const [auctionCurrencyMetadata, nftAsset] = await Promise.all([
getCurrencyMetadata({
contract: getContract({
...options.contract,
address: rawAuction.currency,
}),
}),
getNFTAsset({
...options,
contract: getContract({
...options.contract,
address: rawAuction.assetContract,
}),
tokenId: rawAuction.tokenId,
}),
]);
return {
id: rawAuction.auctionId,
creatorAddress: rawAuction.auctionCreator,
assetContractAddress: rawAuction.assetContract,
tokenId: rawAuction.tokenId,
quantity: rawAuction.quantity,
currencyContractAddress: rawAuction.currency,
asset: nftAsset,
startTimeInSeconds: rawAuction.startTimestamp,
endTimeInSeconds: rawAuction.endTimestamp,
status,
minimumBidAmount: rawAuction.minimumBidAmount,
minimumBidCurrencyValue: {
...auctionCurrencyMetadata,
value: rawAuction.minimumBidAmount,
displayValue: toTokens(
rawAuction.minimumBidAmount,
auctionCurrencyMetadata.decimals,
),
},
buyoutBidAmount: rawAuction.buyoutBidAmount,
buyoutCurrencyValue: {
...auctionCurrencyMetadata,
value: rawAuction.buyoutBidAmount,
displayValue: toTokens(
rawAuction.buyoutBidAmount,
auctionCurrencyMetadata.decimals,
),
},
timeBufferInSeconds: rawAuction.timeBufferInSeconds,
bidBufferBps: rawAuction.bidBufferBps,
};
}
/**
* @internal
*/
@@ -223,113 +90,3 @@ export async function getAllInBatches<const T>(
return await Promise.all(batches);
}
export type IsListingValidParams = {
listing: DirectListing;
quantity?: bigint;
};
export type ValidReturn = { valid: true } | { valid: false; reason: string };
export async function isListingValid(
options: BaseTransactionOptions<IsListingValidParams>,
): Promise<ValidReturn> {
const assetContract = getContract({
...options.contract,
address: options.listing.assetContractAddress,
});
const [erc721, erc1155] = await Promise.all([
isERC721({ contract: assetContract }),
isERC1155({ contract: assetContract }),
]);
// if the asset is an erc721 token
if (erc721) {
const [{ isApprovedForAll }, { getApproved }, { ownerOf }] =
await Promise.all([
import("../erc721/__generated__/IERC721A/read/isApprovedForAll.js"),
import("../erc721/__generated__/IERC721A/read/getApproved.js"),
import("../erc721/__generated__/IERC721A/read/ownerOf.js"),
]);
// check for token approval
const [approvedForAll, approvedOperator, tokenOwner] = await Promise.all([
isApprovedForAll({
contract: assetContract,
owner: options.listing.creatorAddress,
// the marketplace contract address has to be approved to transfer the token
operator: options.contract.address,
}),
getApproved({
contract: assetContract,
tokenId: options.listing.tokenId,
}).catch(() => ""),
ownerOf({
contract: assetContract,
tokenId: options.listing.tokenId,
}),
]);
// if the marketplace is not approved for all and the marketplace contract is not the approved operator for the token
// -> the listing is not valid
if (!approvedForAll && approvedOperator !== options.contract.address) {
return { valid: false, reason: "Asset not approved for marketplace." };
}
// if the token owner is not the creator of the listing
// -> the listing is not valid
if (tokenOwner !== options.listing.creatorAddress) {
return {
valid: false,
reason: "Listing creator no longer owns this token.",
};
}
// otherwise the listing is valid
return {
valid: true,
};
}
// if the asset is an erc1155 token
if (erc1155) {
const [{ isApprovedForAll }, { balanceOf }] = await Promise.all([
import("../erc1155/__generated__/IERC1155/read/isApprovedForAll.js"),
import("../erc1155/__generated__/IERC1155/read/balanceOf.js"),
]);
const [approvedForAll, balance] = await Promise.all([
isApprovedForAll({
contract: assetContract,
owner: options.listing.creatorAddress,
// the marketplace contract address has to be approved to transfer the token
operator: options.contract.address,
}),
balanceOf({
contract: assetContract,
owner: options.listing.creatorAddress,
tokenId: options.listing.tokenId,
}),
]);
// if the marketplace is not approved for all
// -> the listing is not valid
if (!approvedForAll) {
return { valid: false, reason: "Asset not approved for marketplace." };
}
// if the balance is less than the quantity the user is trying to purchase or the listing quantity
// -> the listing is not valid
const quantityWanted = options.quantity || options.listing.quantity;
if (balance < quantityWanted) {
return {
valid: false,
reason:
"Seller does not have enough balance of token to fulfill order.",
};
}
return {
valid: true,
};
}
// if the asset is neither ERC721 nor ERC1155
return {
valid: false,
reason: "AssetContract must implement ERC 1155 or ERC 721.",
};
}
@@ -1,6 +1,5 @@
// ADAPTED FROM https://github.com/merkletreejs/merkletreejs
import { uint8ArrayToHex } from "../exports/thirdweb.js";
import type { Hex } from "../utils/encoding/hex.js";
import { type Hex, uint8ArrayToHex } from "../utils/encoding/hex.js";
import { hexToBytes } from "../utils/encoding/to-bytes.js";
import { keccak256 } from "../utils/hashing/keccak256.js";
import {
@@ -0,0 +1,139 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { LoginPayload } from "../../../../auth/core/types.js";
import type { VerifyLoginPayloadParams } from "../../../../auth/core/verify-login-payload.js";
import { useActiveWallet } from "../../../core/hooks/wallets/wallet-hooks.js";
/**
* Options for Setting up SIWE (Sign in with Ethereum) Authentication
* @auth
*/
export type SiweAuthOptions = {
// we pass address and chainId and retrieve a login payload (we do not care how)
/**
* Method to get the login payload for given address and chainId
* @param params - The parameters to get the login payload for.
*/
getLoginPayload: (params: {
address: string;
chainId: number;
}) => Promise<LoginPayload>;
// we pass the login payload and signature and the developer passes this to the auth server however they want
/**
* Method to login with the signed login payload
* @param params
*/
doLogin: (params: VerifyLoginPayloadParams) => Promise<void>;
// we call this internally when a user explicitly disconnects their wallet
/**
* Method to logout the user
*/
doLogout: () => Promise<void>;
// the developer specifies how to check if the user is logged in, this is called internally by the component
/**
* Method to check if the user is logged in or not
* @param address
*/
isLoggedIn: (address: string) => Promise<boolean>;
};
/**
* @internal
*/
export function useSiweAuth(authOptions?: SiweAuthOptions) {
const activeWallet = useActiveWallet();
const activeAccount = activeWallet?.getAccount();
const requiresAuth = !!authOptions;
const queryClient = useQueryClient();
const isLoggedInQuery = useQuery({
queryKey: ["siwe_auth", "isLoggedIn", activeAccount?.address],
enabled: requiresAuth && !!activeAccount?.address,
queryFn: () => {
// these cases should never be hit but just in case...
if (!authOptions || !activeAccount?.address) {
return false;
}
return authOptions.isLoggedIn(activeAccount.address);
},
});
const loginMutation = useMutation({
mutationFn: async () => {
if (!authOptions) {
throw new Error("No auth options provided");
}
if (!activeWallet) {
throw new Error("No active wallet");
}
const chain = activeWallet.getChain();
if (!chain) {
throw new Error("No active chain");
}
if (!activeAccount) {
throw new Error("No active account");
}
const [payload, { signLoginPayload }] = await Promise.all([
authOptions.getLoginPayload({
address: activeAccount.address,
chainId: chain.id,
}),
// we lazy-load this because it's only needed when logging in
import("../../../../auth/core/sign-login-payload.js"),
]);
const signedPayload = await signLoginPayload({
payload,
account: activeAccount,
});
return await authOptions.doLogin(signedPayload);
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["siwe_auth", "isLoggedIn"],
});
},
});
const logoutMutation = useMutation({
mutationFn: async () => {
if (!authOptions) {
throw new Error("No auth options provided");
}
return await authOptions.doLogout();
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["siwe_auth", "isLoggedIn"],
});
},
});
return {
// is auth even enabled
requiresAuth,
// login
doLogin: loginMutation.mutateAsync,
isLoggingIn: loginMutation.isPending,
// logout
doLogout: logoutMutation.mutateAsync,
isLoggingOut: logoutMutation.isPending,
// checking if logged in
isLoggedIn: isLoggedInQuery.data,
isLoading: isLoggedInQuery.isLoading,
};
}
@@ -268,7 +268,7 @@ export function AutoConnect(props: AutoConnectProps) {
(async () => {
isAutoConnecting.setValue(true);
startAutoConnect();
await startAutoConnect();
isAutoConnecting.setValue(false);
})();
});
@@ -1,5 +1,5 @@
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { sendBatchTransaction } from "../../../../exports/transaction.js";
import { sendBatchTransaction } from "../../../../transaction/actions/send-batch-transaction.js";
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
import { useActiveAccount } from "../wallets/wallet-hooks.js";

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