Compare commits

...

1071 Commits

Author SHA1 Message Date
7490e24a1d Update scripts/request-app-deployment.sh
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m26s
2024-06-25 16:06:14 +00:00
da62b2fbf3 Update scripts/request-app-deployment.sh
Some checks failed
Publish ApplicationRecord to Registry / cns_publish (push) Has been cancelled
2024-06-25 16:05:59 +00:00
8a08acf250 Update scripts/request-app-deployment.sh
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m16s
2024-06-25 16:00:19 +00:00
6c6ee08024 increase CERC_MAX_GENERATE_TIME
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m19s
2024-06-25 15:35:11 +00:00
e6f56c0509 Add .env
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m20s
2024-06-25 15:33:18 +00:00
50489c00cd ffs
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m20s
2024-06-25 15:10:05 +00:00
ac5a7f2cb6 Update package.json
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m24s
2024-06-25 15:07:12 +00:00
04397853d9 set name
All checks were successful
Publish ApplicationRecord to Registry / cns_publish (push) Successful in 1m23s
2024-06-25 15:01:36 +00:00
zramsay
2ab4cb9349 test
Some checks failed
Publish ApplicationRecord to Registry / cns_publish (push) Failing after 1m3s
2024-06-25 10:36:00 -04:00
Serkan Reis
ea7dea6a2a
Merge pull request #391 from public-awesome/collection-creation-summary
Update parser for createVendingMinter transaction result
2024-06-17 22:18:16 +03:00
Serkan Reis
ed9105684c Update parser for createVendingMinter transaction result 2024-06-17 22:17:04 +03:00
Serkan Reis
3511a57eb9 Use hardcoded fees for editing badges 2024-06-11 20:51:55 +03:00
Serkan Reis
28741049ee Remove testnet checks for contract upload 2024-06-09 16:05:30 +03:00
Serkan Reis
1003fcf4ae Contract Upload UI update 2024-06-09 16:02:53 +03:00
Serkan Reis
a96c80462d
Merge pull request #386 from public-awesome/contract-upload-authorization
Enable contract uploads with authorization
2024-06-09 15:52:19 +03:00
Serkan Reis
d889e7e04b Enable contract uploads with authorization 2024-06-09 15:45:28 +03:00
Serkan Reis
be93200f53
Merge pull request #384 from public-awesome/handle-non-stars-creation-fees
Handle non-STARS collection creation fees
2024-05-28 13:39:26 +03:00
Serkan Reis
3bbed658a7 Handle non-STARS creation fees for open/limited edition minter 2024-05-22 21:02:12 +03:00
Serkan Reis
fa1475f109 Handle non-STARS creation fees for vending & base minter 2024-05-22 20:25:41 +03:00
Serkan Reis
d1b5041312
Merge pull request #382 from public-awesome/oe-wl-on-collection-list
Add OE/LE whitelist info to the collection list
2024-05-09 16:34:53 +03:00
Serkan Reis
0958b0db94 Add WL info for OE collections on the collection list 2024-05-09 16:28:10 +03:00
Serkan Reis
0354131ad1
Merge pull request #380 from public-awesome/cosmos-kit-version-update
Update cosmos-kit package versions
2024-05-08 20:04:46 +03:00
Serkan Reis
f7880540ad Update cosmjs package versions 2024-05-08 19:57:36 +03:00
Serkan Reis
123e07362d Update versions for cosmos-kit packages 2024-05-08 19:22:23 +03:00
Serkan Reis
aaf7b82b43
Merge pull request #378 from public-awesome/cancel-auction
Add cancel auction UI
2024-05-06 17:06:25 +03:00
Serkan Reis
467c7a4cfb Add cancel auction UI 2024-05-06 17:05:40 +03:00
Serkan Reis
5e963cf615
Merge pull request #375 from public-awesome/token-factory-update
Address token factory issues
2024-05-01 17:46:18 +03:00
Serkan Reis
41d71a6765 Address token factory issues 2024-05-01 17:38:34 +03:00
Serkan Reis
22d58dbe45
Merge pull request #374 from public-awesome/main
Sync main > dev
2024-04-30 13:37:16 +03:00
Serkan Reis
471ff43fcf
Merge pull request #373 from public-awesome/enable-oe-wl-compatibility-on-mainnet
Enable WL use for Open/Limited Edition collections on mainnet
2024-04-30 13:36:02 +03:00
Serkan Reis
027c4c6821 Enable WL use for Open/Limited Edition collections on mainnet 2024-04-26 13:27:48 +03:00
Serkan Reis
f0b94422b1
Merge pull request #372 from public-awesome/skip-whitelist-checks
Skip whitelist checks on mainnet for OE collections
2024-04-16 19:24:16 +03:00
Serkan Reis
bb8b3e1791 Skip wl checks on mainnet for OE collections 2024-04-16 19:23:02 +03:00
Serkan Reis
42707adc0b
Merge pull request #371 from public-awesome/disable-oe-whitelists
Disable whitelist compatibility for Open Edition collections on mainnet
2024-04-16 19:03:41 +03:00
Serkan Reis
35bed9d6aa Disable whitelist compatibility for OEs on mainnet 2024-04-16 18:51:24 +03:00
Serkan Reis
93f98bcec6
Merge pull request #370 from public-awesome/main
Sync main > dev
2024-04-16 18:36:44 +03:00
Serkan Reis
83835dfba2
Merge branch 'develop' into main 2024-04-16 18:34:30 +03:00
Serkan Reis
d85e19b770
Merge pull request #369 from public-awesome/update-mint-price-denom-list
Update mint price denom list
2024-04-16 18:29:41 +03:00
Serkan Reis
1abb2f82df Update default sg721 code ID for base minter creation 2024-04-16 18:22:53 +03:00
Serkan Reis
b44e4512a5 Remove HUAHUA from mint price denom list 2024-04-16 18:21:26 +03:00
Serkan Reis
a226d8b341
Merge pull request #368 from public-awesome/oe-whitelist-compatibility
Whitelist compatibility for OE collections
2024-04-15 13:55:05 +03:00
Serkan Reis
8a02a7f80d Enable setting both time & token count limit for OE collections 2024-04-15 13:48:44 +03:00
Serkan Reis
122c61055f Select OE factory wrt existing whitelist type 2024-04-15 09:39:19 +03:00
Serkan Reis
81880aecdd Display WL address upon OE collection creation 2024-04-14 21:08:31 +03:00
Serkan Reis
b38562d9fd Improve factory selection logic for OE collections 2024-04-14 19:53:44 +03:00
Serkan Reis
0eb94e4ee8 Add OE whitelist compatibility 2024-04-14 17:08:49 +03:00
Serkan Reis
3a150a50f3
Merge pull request #367 from public-awesome/develop
Sync dev > main
2024-04-10 22:24:14 +03:00
Serkan Reis
03c008d0fa Add discount price update warning 2024-04-10 22:22:30 +03:00
Serkan Reis
6487646f2c
Merge pull request #365 from public-awesome/develop
Sync dev > main
2024-04-10 19:00:42 +03:00
Serkan Reis
b1094f5231
Merge pull request #364 from public-awesome/wl-merkletree-dashboard-update
Add whitelist merkle tree support to whitelist contract dashboard
2024-04-10 18:59:47 +03:00
Serkan Reis
97bb60b3ff Add option to instantiate whitelist merkletree 2024-04-10 18:48:56 +03:00
Serkan Reis
9654ec845e Update execute action list for whitelist merkle tree 2024-04-10 16:06:56 +03:00
Serkan Reis
41e8a3961a Fetch proof hashes for hasMember query 2024-04-10 14:39:00 +03:00
Serkan Reis
43fd0d7848 Update queries for whitelist merkle tree 2024-04-10 13:25:29 +03:00
Serkan Reis
cc16f7ceb1
Merge pull request #363 from public-awesome/develop
Sync dev > main
2024-04-08 09:29:00 +03:00
Serkan Reis
cc58de90a4 Update interface for CollectionInfo 2024-04-08 09:18:22 +03:00
Serkan Reis
27f8510335
Merge pull request #362 from public-awesome/develop
Sync dev > main
2024-04-08 09:01:25 +03:00
Serkan Reis
0fc60f6c05
Merge pull request #361 from public-awesome/transfer-ownership
Allow designating a new creator address when updating collection info
2024-04-08 09:00:39 +03:00
Serkan Reis
c59531f87e Allow designating a new creator address when updating collection info 2024-04-08 08:59:38 +03:00
Serkan Reis
6ddd780338
Merge pull request #360 from public-awesome/develop
Sync dev > main
2024-04-04 13:14:07 +03:00
Serkan Reis
57e36b6dbd
Merge pull request #359 from public-awesome/featured-vending-minter-wl-merkletree
Add featured vending-minter-wl-merkletree option
2024-04-04 13:13:36 +03:00
Serkan Reis
1a866db888 Add featured vending-minter-wl-merkletree option 2024-04-04 13:12:19 +03:00
Serkan Reis
7e97f5d393
Merge pull request #358 from public-awesome/develop
Sync dev > main
2024-04-04 12:10:55 +03:00
Serkan Reis
e26068085d
Merge pull request #357 from public-awesome/wl-merkletree-updates
Display spinner during merkle root hash acquisiton
2024-04-04 12:10:24 +03:00
Serkan Reis
1ccc55a3b2 Temporarily disable importing members for whitelist-merkletree 2024-04-04 12:07:59 +03:00
Serkan Reis
99f7b25f10 Add merkle root fetch spinner 2024-04-04 11:56:09 +03:00
Serkan Reis
3308cfdcf2
Merge pull request #356 from public-awesome/develop
Sync dev > main
2024-04-03 22:43:22 +03:00
Serkan Reis
5e9fdc1cf1
Merge pull request #355 from public-awesome/creation-wl-merkletree-update
Include creation fee for whitelist-merkletree
2024-04-03 22:42:51 +03:00
Serkan Reis
39847d1ab9 Include creation fee for whitelist-merkletree 2024-04-03 22:41:28 +03:00
Serkan Reis
29cd89f6a5
Merge pull request #354 from public-awesome/develop
Sync dev > main
2024-04-02 23:24:14 +03:00
Serkan Reis
6466945e28
Merge pull request #353 from public-awesome/merkle-wl-update
Add whitelist-merkletree option during collection creation
2024-04-02 23:22:58 +03:00
Serkan Reis
db9ffb0899 Revert initial OE whitelist compatibility changes 2024-04-02 23:15:17 +03:00
Serkan Reis
a295dd5d4a Update collection creation logic 2024-04-02 22:58:51 +03:00
Serkan Reis
85ac7a4f71 Add env variables for whitelist-merkletree 2024-04-02 17:02:07 +03:00
Serkan Reis
ee46fa64d3 Include whitelist-merkletree option in whitelist details 2024-04-02 14:18:37 +03:00
Serkan Reis
f5f14fb330 Add whitelist-merkletree contract helpers 2024-04-02 13:04:25 +03:00
Serkan Reis
fd65316d1f Add merkleTree generation helpers 2024-04-02 12:59:43 +03:00
Serkan Reis
004b102540 Test init WL compatible open-edition collection 2024-04-01 13:52:10 +03:00
Serkan Reis
9095c5c06c
Merge pull request #352 from public-awesome/main
Sync main > dev
2024-03-21 16:05:41 +02:00
Serkan Reis
55e46cc830
Merge pull request #351 from public-awesome/update-creation-summary
Update collection creation summary logic
2024-03-21 16:04:43 +02:00
Serkan Reis
d07ad7db04 Update collection creation summary logic 2024-03-21 16:58:31 +03:00
Serkan Reis
8f0a0e84aa
Merge pull request #350 from public-awesome/develop
Sync dev > main
2024-03-19 21:40:51 +02:00
Serkan Reis
4e6db44105
Merge pull request #349 from public-awesome/update-mint-price-denoms
Include TIA among mint price denom options
2024-03-19 21:40:09 +02:00
Serkan Reis
7b0f1f6176 Update denom list to match the right collection code id during creation 2024-03-19 11:52:39 +03:00
Serkan Reis
fe8bfd14bc Update minter list 2024-03-19 07:50:56 +03:00
Serkan Reis
1c7c0a682d Update env variables 2024-03-19 07:36:00 +03:00
Serkan Reis
b58549b1a2 Add TIA to token list 2024-03-19 06:12:12 +03:00
Serkan Reis
6cdd2a24ac
Merge pull request #348 from public-awesome/develop
Sync dev> main
2024-03-17 16:45:12 +02:00
Serkan Reis
437ac5b6f5
Merge pull request #347 from public-awesome/include-infinity-pools-for-snapshots
Add option to include tokens in Infinity Pools for snapshots
2024-03-17 16:44:24 +02:00
Serkan Reis
afef36375b Add option to include tokens in Infinity Pools for snapshots 2024-03-17 17:42:11 +03:00
Serkan Reis
0231692eb1
Merge pull request #346 from public-awesome/develop
Sync dev > main
2024-03-01 10:25:27 +02:00
Serkan Reis
e0d6aaee4d
Merge pull request #345 from public-awesome/remove-collection-offer
Add remove collection offer page
2024-03-01 10:24:37 +02:00
Serkan Reis
c4027859c0 Update testnet marketplace contract address 2024-03-01 11:23:25 +03:00
Serkan Reis
c12fd51525 Add remove collection offer page 2024-03-01 11:18:38 +03:00
Serkan Reis
f1aeeb2167
Merge pull request #344 from public-awesome/develop
Sync dev > main
2024-02-20 22:06:59 +02:00
Serkan Reis
103921d946
Merge pull request #343 from public-awesome/update-collection-info-update
Add update collection info disclaimer
2024-02-20 22:06:04 +02:00
Serkan Reis
b4fcc63de6 Add update collection info disclaimer 2024-02-20 23:05:06 +03:00
Serkan Reis
b3dea5e757
Merge pull request #342 from public-awesome/limited-edition-update
Limited edition collection update
2024-02-14 22:37:02 +02:00
Serkan Reis
e43ec78acc Revert creation summary changes for mainnet 2024-02-14 23:27:36 +03:00
Serkan Reis
4c312d0ad8
Merge pull request #339 from public-awesome/main
Sync main > dev
2024-02-12 22:16:23 +02:00
Serkan Reis
89836ad84e
Merge pull request #338 from public-awesome/sample-wl-flex-airdrop-file
Include sample files for whitelist creation
2024-02-12 22:15:54 +02:00
Serkan Reis
51f38f12d7 Include sample files for whitelist creation 2024-02-12 23:10:18 +03:00
Serkan Reis
609745ce11
Merge pull request #337 from public-awesome/main
Sync main > dev
2024-02-11 18:57:37 +02:00
Serkan Reis
3b9778270b
Merge pull request #332 from public-awesome/add-default-nft-storage-api-key-option
Add default NFT.Storage API key option
2024-02-11 18:57:01 +02:00
Serkan Reis
c66b21792c Add default API key selection option 2024-02-11 19:41:05 +03:00
Serkan Reis
35a9c0eba8 Set up env variable for default API key 2024-02-11 19:35:27 +03:00
Serkan Reis
07152745e0
Merge pull request #336 from public-awesome/main
Sync main > dev
2024-02-08 18:36:06 +02:00
Serkan Reis
7a455b60bc
Merge pull request #335 from public-awesome/remove-wl-flex-allocation-check
Disable whitelist-flex total allocation check
2024-02-08 18:35:35 +02:00
Serkan Reis
67694d4c5d Disable whitelist-flex total allocation check 2024-02-08 19:34:48 +03:00
Serkan Reis
4ec704f588
Merge pull request #334 from public-awesome/main
Sync main > dev
2024-02-07 10:28:13 +02:00
Serkan Reis
3f44e342e8
Merge pull request #333 from public-awesome/fix-factory-validation
Fix factory validation on init issue
2024-02-07 10:27:41 +02:00
Serkan Reis
a3466c869b Fix factory validation on init issue 2024-02-07 11:15:03 +03:00
Serkan Reis
245fd2253b Add API Key input method 2024-02-06 23:48:13 +03:00
Serkan Reis
ed1844fdf0
Merge pull request #331 from public-awesome/main
Sync main > dev
2024-02-05 21:29:45 +02:00
Serkan Reis
1d1a901312
Merge pull request #330 from public-awesome/sample-airdrop-files
Provide sample .csv files for airdrops
2024-02-05 21:17:34 +02:00
Serkan Reis
85d38d7882 Provide sample .csv files for airdrops 2024-02-05 22:10:42 +03:00
Serkan Reis
6316a68408
Merge pull request #329 from public-awesome/main
Sync main > dev
2024-02-03 03:48:44 +02:00
Serkan Reis
1bce40a6f8
Merge pull request #328 from public-awesome/switch-ipfs-gateway
Switch default IPFS gateway
2024-02-03 03:47:49 +02:00
Serkan Reis
d651a10dc0 Switch IPFS gateway used for metadata validation 2024-02-03 04:45:54 +03:00
Serkan Reis
abac647879 Prevalidate default vending factory 2024-02-03 04:44:54 +03:00
Serkan Reis
d6d9e3c666
Merge pull request #327 from public-awesome/time-input-updates
Time input updates (mainnet)
2024-02-02 15:49:21 +02:00
Serkan Reis
a949a1e103
Merge pull request #326 from public-awesome/time-input-updates
Time input updates
2024-02-02 15:45:10 +02:00
Serkan Reis
39385ee82e Address lint issues 2024-02-02 16:34:17 +03:00
Serkan Reis
440ee78122
Merge branch 'develop' into time-input-updates 2024-02-02 15:27:50 +02:00
Serkan Reis
ae2b565af6 Dictate minting start time wrt whitelist start time 2024-02-02 16:23:09 +03:00
Serkan Reis
d28af744a5 Fix date cancellation issues 2024-02-02 13:49:47 +03:00
Serkan Reis
5eff907b9f
Merge pull request #325 from public-awesome/factory-validation
Factory validation (mainnet)
2024-02-02 09:03:51 +02:00
Serkan Reis
1f31cdbe29
Merge pull request #324 from public-awesome/factory-validation
Check if a valid factory exists for the selected parameters prior to collection creation
2024-02-02 09:02:45 +02:00
Serkan Reis
954f73e99e Check if a valid factory exists for the selected parameters prior to collection creation 2024-02-02 09:55:47 +03:00
Serkan Reis
3cb943c6d0
Merge pull request #323 from public-awesome/featured-check
Add option to create featured collections (mainnet)
2024-01-31 20:01:48 +02:00
Serkan Reis
b66e6d0920
Merge pull request #322 from public-awesome/featured-check
Add option to create featured collections
2024-01-31 20:00:17 +02:00
Serkan Reis
0293b12821 Include featured minter on recent sg721 list 2024-01-31 20:25:48 +03:00
Serkan Reis
fce1fa6bf4 Update .env.example 2024-01-31 20:12:44 +03:00
Serkan Reis
c56659e05a Add option to create featured collections 2024-01-31 20:07:47 +03:00
Serkan Reis
ab4d4fa31d
Merge pull request #321 from public-awesome/creation-summary-update
Update collection creation summary logic
2024-01-31 09:24:14 +02:00
Serkan Reis
92ce752b15 Update collection creation summary logic 2024-01-31 10:17:53 +03:00
Serkan Reis
01936d8f5e
Merge pull request #320 from public-awesome/main
Sync main > dev
2024-01-25 11:09:08 +02:00
Serkan Reis
8ceacb6725
Merge pull request #319 from public-awesome/batch-remove-wl-members
Batch remove members from whitelists
2024-01-25 11:08:40 +02:00
Serkan Reis
5a34f3f7e4 Add option to batch remove members from whitelists 2024-01-25 11:59:20 +03:00
Serkan Reis
02df116e09
Merge pull request #318 from public-awesome/main
Sync main > dev
2024-01-24 11:46:53 +02:00
Serkan Reis
0e5d66f79a
Merge pull request #317 from public-awesome/snapshot-active-users
Snapshot active users
2024-01-24 11:46:18 +02:00
Serkan Reis
e219566cd2 Add empty collection address warning - 2 2024-01-24 12:39:14 +03:00
Serkan Reis
2a293f1a62 Add empty collection address warning 2024-01-24 12:35:28 +03:00
Serkan Reis
19e5fd0abb Recommend active user list exports for free mints 2024-01-24 12:27:02 +03:00
Serkan Reis
f35d9954c3 Add loading spinner 2024-01-24 12:14:50 +03:00
Serkan Reis
d60c554125 Update link tabs index 2024-01-23 23:54:24 +03:00
Serkan Reis
e51659b15f Update copy 2024-01-23 23:53:28 +03:00
Serkan Reis
8d3b337afa Add snapshot option for active Stargaze users 2024-01-23 23:50:17 +03:00
Serkan Reis
ab724afb9c
Merge pull request #316 from public-awesome/main
Sync main > dev
2024-01-22 22:37:52 +02:00
Serkan Reis
aba83c21a4
Merge pull request #315 from public-awesome/base-factory-update
Update sg721 code id for 1/1 collection instantiation
2024-01-22 22:37:15 +02:00
Serkan Reis
724fcf1120 Update sg721 code id for 1/1 collection instantiation 2024-01-22 23:29:04 +03:00
Serkan Reis
d7235264f9 Include export options for collection holder snapshots 2024-01-22 23:22:42 +03:00
Serkan Reis
3c3b60ebe8
Merge pull request #314 from public-awesome/main
Sync main > dev
2024-01-18 16:02:27 +02:00
Serkan Reis
6f403fe70a
Merge pull request #313 from public-awesome/launchpad-link-update
Update View on Launchpad URL
2024-01-18 16:00:01 +02:00
Serkan Reis
1ad2de146f Update View on Launchpad URL 2024-01-18 16:50:00 +03:00
Serkan Reis
de5de84873
Merge pull request #312 from public-awesome/open-edition-updates
Token Count limited OE collections
2024-01-18 12:29:56 +02:00
Serkan Reis
8470008360
Merge pull request #311 from public-awesome/develop
Sync dev > main
2024-01-17 12:28:25 +02:00
Serkan Reis
a4b114e01d
Merge pull request #310 from public-awesome/authz-update
Authz update
2024-01-17 12:27:15 +02:00
Serkan Reis
e4564511d1 Address build issues 2024-01-17 13:21:07 +03:00
Serkan Reis
520df5669a Bump Studio version 2024-01-17 13:16:25 +03:00
Serkan Reis
c87af7dbec Remove non-generic typeUrls from revoke options 2024-01-17 13:04:44 +03:00
Serkan Reis
eb1a448896 Add ContractMigrationAuthorization 2024-01-17 13:03:56 +03:00
Serkan Reis
9e63aba259 Fix undefined expiration issue 2024-01-17 12:09:56 +03:00
Serkan Reis
57fabf90da Include MsgStoreCode among authz grant & revoke options 2024-01-16 18:30:13 +03:00
Serkan Reis
58ae7c05af Cover the rest of generic messages for authz grant & revoke 2024-01-16 18:05:31 +03:00
Serkan Reis
1015590bed Update revoke message types 2024-01-16 16:26:12 +03:00
Serkan Reis
490bd3e84d Include ContractExecutionAuthorization among available auth types 2024-01-16 13:30:44 +03:00
Serkan Reis
ab801768b9 Include Send among available auth types 2024-01-15 18:02:45 +03:00
Serkan Reis
7526cc91d1 Bump Studio version 2024-01-15 15:06:48 +03:00
Serkan Reis
f6253dd6c7 Update sidebar 2024-01-15 15:05:16 +03:00
Serkan Reis
680b2ac258 Initial authz logic 2024-01-15 14:55:16 +03:00
Serkan Reis
1f3e3f56da
Merge pull request #309 from public-awesome/develop
Sync dev > main
2024-01-05 11:32:32 +02:00
Serkan Reis
d8afd9f637
Merge pull request #308 from public-awesome/tokenfactory-update
Add tokenfactory related features
2024-01-05 11:31:33 +02:00
Serkan Reis
203e0614b0 Bump Studio version 2024-01-05 12:18:11 +03:00
Serkan Reis
da79f4f6e5 Update sidebar 2024-01-05 12:16:55 +03:00
Serkan Reis
cd6a69970b Add logic for other message types 2024-01-05 11:57:10 +03:00
Serkan Reis
323837e67f Init okenfactory actions 2024-01-04 23:12:07 +03:00
Serkan Reis
f4490fb237
Merge pull request #307 from public-awesome/develop
Sync dev > main
2024-01-01 14:50:38 +02:00
Serkan Reis
5787c90811
Merge pull request #306 from public-awesome/collection-snapshots
Collection holder snapshots
2024-01-01 14:42:58 +02:00
Serkan Reis
df571b6c58 Toastify snapshot related errors 2024-01-01 15:33:18 +03:00
Serkan Reis
6caab5ce69 Fix typo 2023-12-31 22:10:02 +03:00
Serkan Reis
b4290ba9b9 Enable taking collection snapshots 2023-12-31 21:47:30 +03:00
Serkan Reis
57d73d9ed9
Merge pull request #305 from public-awesome/develop
Sync dev > main
2023-12-31 20:04:30 +02:00
Serkan Reis
adfdfd22e8 Update yarn.lock 2023-12-31 20:57:51 +03:00
Serkan Reis
6e51a57e46 Revert snapshot related changes 2023-12-31 20:48:41 +03:00
Serkan Reis
8f56ac0390
Merge pull request #304 from public-awesome/develop
Sync dev > main
2023-12-31 14:54:00 +02:00
Serkan Reis
d88596559a Address build issues 2023-12-31 15:33:38 +03:00
Serkan Reis
b1ce309cb2 Address build issues 2023-12-31 15:25:11 +03:00
Serkan Reis
064d966855
Merge pull request #302 from public-awesome/develop
Sync dev > main
2023-12-31 14:11:01 +02:00
Serkan Reis
59f15801ab
Merge pull request #301 from public-awesome/collection-snapshots
Collection Snapshots
2023-12-31 14:10:28 +02:00
Serkan Reis
dcb2a9c072 Update title for snapshots 2023-12-31 15:09:05 +03:00
Serkan Reis
3065090803 Bump Studio version 2023-12-31 15:04:10 +03:00
Serkan Reis
412a467391 Enable taking holder snapshots for collections 2023-12-31 15:01:48 +03:00
Serkan Reis
529e727a7d
Merge pull request #300 from public-awesome/develop
Sync dev > main
2023-12-27 22:15:04 +02:00
Serkan Reis
ee0953e18a Update mint price update copy 2023-12-27 23:14:01 +03:00
Serkan Reis
71685b8688
Merge pull request #299 from public-awesome/develop
Sync dev > main
2023-12-27 14:56:26 +02:00
Serkan Reis
a44ab5cc75 Improve sidebar responsivity 2023-12-27 15:55:05 +03:00
Serkan Reis
794558dea3
Merge pull request #298 from public-awesome/develop
Sync dev > main
2023-12-21 13:30:24 +02:00
Serkan Reis
4f40da141d
Merge pull request #297 from public-awesome/update-mint-denoms
Include CRBRUS among mint price denom options
2023-12-21 13:29:48 +02:00
Serkan Reis
35f1ba0ca6 Include CRBRUS among mint price denom options 2023-12-21 14:28:45 +03:00
Serkan Reis
7951669ed2 Update checks for OE minting details 2023-12-20 23:36:32 +03:00
Serkan Reis
20944cf5de Update config import logic for OE 2023-12-20 23:35:41 +03:00
Serkan Reis
0f1c4a027b Add limit type selection to OE MintingDetails 2023-12-19 13:53:41 +03:00
Serkan Reis
039f1d02fb
Merge pull request #295 from public-awesome/develop
Sync dev > main
2023-12-13 12:03:30 +02:00
Serkan Reis
9a7e2fea5e
Merge pull request #294 from public-awesome/wallet-loader-token-display-name-fix
Display token names on wallet loader
2023-12-13 12:02:48 +02:00
Serkan Reis
9760ba5bc2 Display token names on wallet loader 2023-12-13 13:01:39 +03:00
Serkan Reis
da3182f5a6
Merge pull request #293 from public-awesome/develop
Sync dev > main
2023-12-12 19:46:43 +02:00
Serkan Reis
1cdc9d8662
Merge pull request #292 from public-awesome/update-mint-denoms
Include BRNCH among OE mint price denom options
2023-12-12 19:44:55 +02:00
Serkan Reis
294fda9136 Include BRNCH among OE mint price denoms 2023-12-12 20:40:54 +03:00
Serkan Reis
c62d7c10ba
Merge pull request #291 from public-awesome/develop
Sync dev > main
2023-12-11 10:24:43 +02:00
Serkan Reis
ac3f65f866
Merge pull request #290 from public-awesome/update-mint-price-denom-list
Include HUAHUA among mint price denoms
2023-12-11 10:23:19 +02:00
Serkan Reis
3d24e5a8f7 Include HUAHUA among mint price denoms 2023-12-11 11:22:12 +03:00
Serkan Reis
cbf401638a
Merge pull request #289 from public-awesome/develop
Sync dev > main
2023-12-08 10:45:51 +02:00
Serkan Reis
2f2b628782 Update wallet connection options 2023-12-08 11:44:35 +03:00
Serkan Reis
005646fd18
Merge pull request #288 from public-awesome/develop
Sync dev > main
2023-12-08 08:48:26 +02:00
Serkan Reis
16e6d45c18 Update wallet connection options 2023-12-08 09:41:13 +03:00
Serkan Reis
f0ba060a14
Merge pull request #287 from public-awesome/develop
Sync dev > main
2023-12-08 07:43:07 +02:00
Serkan Reis
b40eacf5f9
Merge pull request #286 from public-awesome/update-wallet-options
Update wallet connection options
2023-12-08 07:39:25 +02:00
Serkan Reis
ef7bed1479 Update wallet connection options 2023-12-08 08:37:21 +03:00
Serkan Reis
b969e0cd22
Merge pull request #285 from public-awesome/develop
Sync dev > main
2023-12-07 08:04:38 +03:00
Serkan Reis
fde773e2f2
Merge pull request #284 from public-awesome/upload-contract-support
Upload contract support
2023-12-07 08:04:05 +03:00
Serkan Reis
108e6e034d Upload contract support 2023-12-07 08:03:27 +03:00
Serkan Reis
8ef767ef02
Merge pull request #283 from public-awesome/develop
Sync dev > main
2023-11-28 13:31:48 +03:00
Serkan Reis
1d5ba3aa78
Merge pull request #282 from public-awesome/mint-price-denom-kuji-update
Include KUJI among mint price denom options
2023-11-28 13:31:07 +03:00
Serkan Reis
87c2f43540 Include KUJI among mint price denom options 2023-11-28 13:27:12 +03:00
Serkan Reis
939552255b
Merge pull request #281 from public-awesome/develop
Enable OE collection creation for STRDST
2023-11-26 20:06:38 +03:00
Serkan Reis
9e1a558148 Update OE sg721 code id for STRDST 2023-11-26 20:05:13 +03:00
Serkan Reis
70e17fd36e Bump Studio version 2023-11-26 20:01:07 +03:00
Serkan Reis
f4b1760e3c Enable OE collection creation with STRDST 2023-11-26 20:00:21 +03:00
Serkan Reis
cd9861ff54
Merge pull request #280 from public-awesome/develop
Sync dev > main
2023-11-24 07:24:10 +03:00
Serkan Reis
ea0765e1a7 Revert sg721 code ID change for STRDST collections 2023-11-24 07:23:01 +03:00
Serkan Reis
0543b8459d
Merge pull request #279 from public-awesome/develop
Sync dev > main
2023-11-24 06:24:17 +03:00
Serkan Reis
816a834f75 Update sg721 code ID for STRDST collections 2023-11-24 06:22:53 +03:00
Serkan Reis
9111b0b8a7
Merge pull request #278 from public-awesome/develop
Sync dev > main
2023-11-22 23:29:07 +03:00
Serkan Reis
fb78db22cc
Merge pull request #277 from public-awesome/pdf-support
PDF support for collection creation
2023-11-22 23:28:32 +03:00
Serkan Reis
2d5f5ed511 PDF support for collection creation 2023-11-22 23:23:51 +03:00
Serkan Reis
90ec372f96
Merge pull request #276 from public-awesome/develop
Fix configuration import issue for mainnet
2023-11-22 10:46:10 +03:00
Serkan Reis
50a8ea53b1 Fix import config issue for mainnet 2023-11-22 10:44:46 +03:00
Serkan Reis
9ae9948252
Merge pull request #275 from public-awesome/develop
Sync dev > main
2023-11-20 23:19:17 +03:00
Serkan Reis
6ab7d4017b
Merge pull request #274 from public-awesome/wl-easy-access
Include easy access to whitelists on My Collections
2023-11-20 23:18:34 +03:00
Serkan Reis
4d53d498d7 Retain contract address while switching between link tabs 2023-11-20 23:16:16 +03:00
Serkan Reis
3e052e6e1e List whitelists on My Collections 2023-11-20 22:32:59 +03:00
Serkan Reis
8a27afe29a
Merge pull request #273 from public-awesome/develop
Sync dev > main
2023-11-19 15:37:06 +03:00
Serkan Reis
626993ed83
Merge pull request #272 from public-awesome/whitelist-improvements
Add query pagination & member list export for whitelists
2023-11-19 15:36:12 +03:00
Serkan Reis
2ef9e3ccb9 Bump Studio version 2023-11-19 15:34:56 +03:00
Serkan Reis
a13e0610e4 Add members query pagination & list export for whitelists 2023-11-19 15:34:05 +03:00
Serkan Reis
322b8c681c
Merge pull request #271 from public-awesome/develop
Sync dev > main
2023-11-16 18:04:01 +03:00
Serkan Reis
017096ceb0
Merge pull request #270 from public-awesome/blacklist-for-royalty-payment-address
Blacklist minter & sg721 contract addresses for royalty payments
2023-11-16 18:03:27 +03:00
Serkan Reis
f19ced2d32 Blacklist minter & sg721 contract addresses for royalty payments 2023-11-16 17:40:56 +03:00
Serkan Reis
e6de5297da
Merge pull request #269 from public-awesome/develop
Enable contract queries without a wallet connection
2023-11-14 19:07:52 +03:00
Serkan Reis
0ca2d63161 Enable contract queries without a wallet connection 2023-11-14 19:06:49 +03:00
Serkan Reis
a204face96
Merge pull request #268 from public-awesome/develop
Sync dev > main
2023-11-12 21:22:43 +03:00
Serkan Reis
79bb24d33a Address the issue with #s in uploaded file names 2023-11-12 21:18:04 +03:00
Serkan Reis
f475cec576
Merge pull request #267 from public-awesome/develop
Sync dev > main
2023-11-10 09:40:51 +03:00
Serkan Reis
a0bbc0ebeb Disable updatable option for standard collection creation 2023-11-10 09:32:01 +03:00
Serkan Reis
a2b2a072e6 Disable updatable option for collection creation 2023-11-10 09:16:18 +03:00
Serkan Reis
32fe46081e
Merge pull request #266 from public-awesome/develop
Include USK & USDC among OE mint price denom options
2023-11-08 17:29:02 +03:00
Serkan Reis
e8f3e0e4b5 Include USK & USDC among OE mint price denom options 2023-11-08 17:27:26 +03:00
Serkan Reis
a611016953
Merge pull request #265 from public-awesome/develop
Sync dev > main
2023-10-27 10:14:50 +03:00
Serkan Reis
bfcb84c8de
Merge pull request #264 from public-awesome/nbtc-update
Include nBTC as an option for mint price denom
2023-10-27 10:14:12 +03:00
Serkan Reis
77b80fc989 Include nBTC as an option for mint price denom 2023-10-27 10:11:22 +03:00
Serkan Reis
f77ab91245
Merge pull request #263 from public-awesome/develop
Sync dev > main
2023-10-25 11:13:44 +03:00
Serkan Reis
488d330623
Merge pull request #262 from public-awesome/fetch-factory-params-no-wallet
Allow factory parameters to be fetched without a wallet connection
2023-10-25 11:12:50 +03:00
Serkan Reis
187784261f Update mint price denom selection UI 2023-10-25 11:11:46 +03:00
Serkan Reis
758899b031 Allow factory parameters to be fetched with no wallet connection 2023-10-25 11:09:25 +03:00
Serkan Reis
03ae3e522f
Merge pull request #261 from public-awesome/develop
Sync dev > main
2023-10-20 09:30:46 +03:00
Serkan Reis
e65a79bf8b Update airdrop instructions for OE - 2 2023-10-20 09:29:45 +03:00
Serkan Reis
5e64456fe2
Merge pull request #260 from public-awesome/develop
Update airdrop instructions for OE
2023-10-20 09:22:35 +03:00
Serkan Reis
e9d3efefed Update airdrop instructions for OE 2023-10-20 09:21:40 +03:00
Serkan Reis
32b3c5ffd6
Merge pull request #259 from public-awesome/develop
Sync dev > main
2023-10-19 23:34:45 +03:00
Serkan Reis
99e0caabfd
Merge pull request #258 from NoahSaso/noah/update-cosmos-kit
Update Cosmos Kit packages
2023-10-19 23:33:57 +03:00
Noah Saso
aa53f38554 Added recursive iframe architecture 2023-10-19 00:18:56 -07:00
Noah Saso
0968423e05 Update Cosmos Kit packages. 2023-10-18 23:18:47 -07:00
Serkan Reis
4b8f9be8b6
Merge pull request #257 from public-awesome/develop
Include USDC as a mint price denom option
2023-10-19 00:09:15 +03:00
Serkan Reis
aea28679bb Use the latest sg721 code id with the USDC factory 2023-10-18 23:42:15 +03:00
Serkan Reis
e036f22eb1
Merge pull request #256 from public-awesome/develop
Sync dev > main
2023-10-18 23:17:02 +03:00
Serkan Reis
7201210e93 Add USK to token list 2023-10-18 23:15:56 +03:00
Serkan Reis
1901bc7c49
Merge pull request #255 from public-awesome/develop
Sync dev > main
2023-10-18 23:02:39 +03:00
Serkan Reis
2a616fe794
Merge pull request #254 from public-awesome/usk-factory-update
Include USK as a mint price denom option
2023-10-18 23:01:12 +03:00
Serkan Reis
a9efc0cbfa Use the latest sg721 code id with the USK factory 2023-10-18 22:50:44 +03:00
Serkan Reis
9c31d30a0c Update token & minter list 2023-10-18 22:47:40 +03:00
Serkan Reis
a6f30994df Update env variables 2023-10-18 22:34:24 +03:00
Serkan Reis
9b01e7205f
Merge pull request #253 from public-awesome/develop
Sync dev > main
2023-10-17 09:29:25 +03:00
Serkan Reis
b41b927632
Merge pull request #252 from public-awesome/collection-actions-no-wallet-issue
Fix: Collection actions no wallet issue
2023-10-17 09:28:44 +03:00
Serkan Reis
7e084b01d7 Add content check for JSON preview 2023-10-17 09:20:30 +03:00
Serkan Reis
e99832c283 Add no wallet warning on execute 2023-10-17 08:49:44 +03:00
Serkan Reis
f8b7e4aea6
Merge pull request #251 from public-awesome/develop
Sync dev > main
2023-10-16 13:18:29 +03:00
Serkan Reis
3638b04d0d
Merge pull request #250 from public-awesome/leap-wallet-integration
Leap Wallet integration
2023-10-16 13:17:33 +03:00
Serkan Reis
cfa160d5eb Bump Studio version 2023-10-16 13:12:34 +03:00
Serkan Reis
17751c5455 Add Leap Wallet support to Wallet Provider 2023-10-16 13:12:04 +03:00
Serkan Reis
8615176670 Update .env.example 2023-10-16 13:09:58 +03:00
Serkan Reis
fc9ba16ffb
Merge pull request #249 from public-awesome/develop
Sync dev > main
2023-10-15 21:00:22 +03:00
Serkan Reis
3d43af4680
Merge pull request #234 from NoahSaso/noah/cosmos-kit
Use Cosmos Kit with Keplr Extension support
2023-10-15 20:53:04 +03:00
Serkan Reis
d6cc8a700f
Merge branch 'develop' into noah/cosmos-kit 2023-10-15 20:46:20 +03:00
Serkan Reis
cce90213d8
Merge pull request #248 from public-awesome/develop
Sync dev > main
2023-10-13 19:21:54 +03:00
Serkan Reis
120efa9028
Merge pull request #247 from public-awesome/strdst-flexible
Add flexible minter option for STRDST
2023-10-13 19:21:21 +03:00
Serkan Reis
dc4822aa70 Add flexible minter option for STRDST 2023-10-13 19:17:09 +03:00
Serkan Reis
53220109d1
Merge pull request #246 from public-awesome/develop
Sync dev > main
2023-10-13 18:39:27 +03:00
Serkan Reis
b8d416b99f
Merge pull request #245 from public-awesome/disable-oe-on-chain-metadata
Temporarily disable on-chain metadata option for OE collection creation
2023-10-13 18:38:52 +03:00
Serkan Reis
a48fb8c5cc Disable on-chain metadata option for OE collection creation 2023-10-13 18:37:59 +03:00
Serkan Reis
570f6990b0
Merge pull request #244 from public-awesome/develop
Sync dev > main
2023-10-13 18:23:00 +03:00
Serkan Reis
c11e99ecc9
Merge pull request #243 from public-awesome/whitelist-mint-price-denom
Add option to select whitelist unit price denom
2023-10-13 18:22:15 +03:00
Serkan Reis
abdfd74663 Add option to select whitelist unit price denom 2023-10-13 18:21:20 +03:00
Serkan Reis
d94ab200c2
Merge pull request #242 from public-awesome/develop
Sync dev > main
2023-10-13 13:04:36 +03:00
Serkan Reis
1bf56eebf3
Merge pull request #241 from public-awesome/base-minter-upload-logic-update
Update file upload logic for 1/1 collections
2023-10-13 13:04:01 +03:00
Serkan Reis
e8be3c54eb Update base minter upload logic 2023-10-13 12:57:50 +03:00
Serkan Reis
5f675d9177
Merge pull request #240 from public-awesome/develop
Sync dev > main
2023-10-12 16:47:47 +03:00
Serkan Reis
d62873a2db
Merge pull request #239 from public-awesome/inifinity-swap-update
Update Collection Actions > Infinity Swap UI
2023-10-12 16:46:19 +03:00
Serkan Reis
fb51f1519f Update Collection Actions > Infinity Swap UI 2023-10-12 16:45:33 +03:00
Serkan Reis
eaf60d5594
Merge pull request #238 from public-awesome/develop
Sync dev > main
2023-10-12 11:35:37 +03:00
shane.stars
bf6ae87a69
Merge pull request #237 from public-awesome/fix-naming
Added a space in the name
2023-10-12 11:30:46 +03:00
Shane Vitarana
5c098ed313 Added a space in the name 2023-10-12 11:25:18 +03:00
Serkan Reis
fd335f766b
Merge pull request #236 from public-awesome/develop
Sync dev > main
2023-10-12 10:09:55 +03:00
Serkan Reis
3a4e595ae5
Merge pull request #235 from public-awesome/strdst-sg721-update
Use latest sg721 code id with STRDST Vending Factory
2023-10-12 10:09:13 +03:00
Serkan Reis
af3e0f2186 Update VM instantiation logic 2023-10-12 10:02:48 +03:00
Serkan Reis
fddf9e28cf Update env variables 2023-10-12 09:54:43 +03:00
Noah Saso
dfe0a27f77 Use Cosmos Kit with Keplr Extension support. 2023-10-11 16:48:20 -07:00
Serkan Reis
27864e20f1
Merge pull request #233 from public-awesome/develop
Sync dev > main
2023-10-11 08:44:30 +03:00
Serkan Reis
8b1c9e669d
Merge pull request #232 from public-awesome/stardust-update
Include STRDST as a mint price denom option
2023-10-11 08:43:49 +03:00
Serkan Reis
60a03a3069 Include STRDST as a mint price denom option 2023-10-11 08:37:08 +03:00
Serkan Reis
fbeeb4212a
Merge pull request #231 from public-awesome/develop
Sync dev > main
2023-10-10 14:22:18 +03:00
Serkan Reis
8fc3f71413
Merge pull request #230 from public-awesome/royalty-registry-update
Royalty registry update
2023-10-10 14:21:40 +03:00
Serkan Reis
840483a830 Bump Studio version 2023-10-10 14:20:33 +03:00
Serkan Reis
b2e61d5529 Add Infinity Swap related queries to Collection Actions > Queries 2023-10-10 14:19:52 +03:00
Serkan Reis
0365aa5a7b Add Infinity Swap related actions to Collection Actions > Actions 2023-10-10 13:43:14 +03:00
Serkan Reis
2dcbda6f25 Update execute list subtitles for royalty registry 2023-10-10 12:37:30 +03:00
Serkan Reis
5631362990 Update .env.example 2023-10-10 12:16:46 +03:00
Serkan Reis
e6f0a5b91f Update Royalty Registry > Query 2023-10-10 11:41:52 +03:00
Serkan Reis
2e55923ac3 Update Royalty Registry > Execute 2023-10-10 11:01:40 +03:00
Serkan Reis
ca5ffa0a00 Update env veriables 2023-10-09 21:42:54 +03:00
Serkan Reis
1e1acf5e07
Merge pull request #229 from public-awesome/develop
Sync dev > main
2023-10-07 13:03:46 +03:00
Serkan Reis
3a43fb5420
Merge pull request #228 from public-awesome/royalty-registry
Royalty registry support
2023-10-07 13:01:25 +03:00
Serkan Reis
5a80ad1587 Bump Studio version 2023-10-07 12:51:37 +03:00
Serkan Reis
3325a93edb Match inputs with query types 2023-10-07 12:50:45 +03:00
Serkan Reis
51acae6a78 Match inputs and message types for execute 2023-10-07 12:30:54 +03:00
Serkan Reis
b398650794 Update sidebar 2023-10-07 11:58:22 +03:00
Serkan Reis
f092d7d926 Init Royalty Registry dashboard > Query 2023-10-07 11:53:28 +03:00
Serkan Reis
0595c4de25 Init Royalty Registry dashboard > Execute 2023-10-07 11:53:10 +03:00
Serkan Reis
584a33c388 Add link tabs for royalty registry 2023-10-07 11:04:08 +03:00
Serkan Reis
2b893e6e60 Add combobox for royalty registry / execute 2023-10-07 11:01:56 +03:00
Serkan Reis
b697b1a857 Update contracts context 2023-10-07 10:46:37 +03:00
Serkan Reis
ca8f5cca58 Match list items with contract helpers 2023-10-07 10:41:10 +03:00
Serkan Reis
9fcd5b82f4 Init Royalty Registry contract hooks 2023-10-07 09:05:54 +03:00
Serkan Reis
2710be6959 Update env variables 2023-10-07 08:59:28 +03:00
Serkan Reis
d541fe7294 Init Royalty Registry contract helpers 2023-10-07 08:53:18 +03:00
Jorge Hernandez
3a3c4589e6
Update package.json 2023-09-30 11:01:57 -06:00
Serkan Reis
bd18197e88
Merge pull request #227 from public-awesome/develop
Sync dev > main
2023-09-29 23:53:24 +03:00
Serkan Reis
883fc98cad
Merge pull request #226 from public-awesome/update-frnz-denom
Update IBC denoms
2023-09-29 23:52:36 +03:00
Serkan Reis
2160533a60 Update uusdc denom 2023-09-29 23:50:59 +03:00
Serkan Reis
6dd0e5ef8b Update FRNZ denom for mainnet 2023-09-29 23:33:27 +03:00
Serkan Reis
38a33273dc
Merge pull request #224 from public-awesome/update-ibc-tokens-for-testnet
Update IBC denoms for testnet
2023-09-28 19:27:39 +03:00
Serkan Reis
53322784fc Update IBC denoms for testnet 2023-09-28 19:26:55 +03:00
Serkan Reis
03094427bc
Merge pull request #223 from public-awesome/develop
Sync dev > main
2023-09-26 08:47:30 +03:00
Serkan Reis
0eaa66f2e5
Merge pull request #222 from public-awesome/token-description-line-breaks
Enable line breaks with manual metadata input for 1/1 collections
2023-09-26 08:46:51 +03:00
Serkan Reis
defc55abf6 Enable line breaks with manual metadata input for 1/1 collections 2023-09-26 08:46:01 +03:00
Serkan Reis
fbc457f9cd
Merge pull request #221 from public-awesome/develop
Sync dev > main
2023-09-21 14:35:37 +03:00
Serkan Reis
6db97d615c
Merge pull request #220 from public-awesome/display-updatable-price
Display updatable metadata price on collection details
2023-09-21 14:34:44 +03:00
Serkan Reis
2f2a7c6a76 Display updatable metadata price on collection details 2023-09-21 14:29:21 +03:00
Serkan Reis
a857e7fef1
Merge pull request #219 from public-awesome/develop
Sync dev > main
2023-09-21 09:05:07 +03:00
Serkan Reis
23e6278f30
Merge pull request #218 from public-awesome/import-config-update
Update import config logic
2023-09-21 09:01:54 +03:00
Serkan Reis
40fb1933e4 Update import logic to default to non-updatable on mainnet 2023-09-21 08:57:43 +03:00
Serkan Reis
572968cf24
Merge pull request #216 from public-awesome/develop
Sync dev > main
2023-09-21 08:14:16 +03:00
Serkan Reis
9d02f73347
Merge pull request #217 from public-awesome/batch-transfer-specific-addresses
Add multi-address batch transfers to collection actions
2023-09-21 07:51:24 +03:00
Serkan Reis
88177fd446 Update file selection CTO copy 2023-09-13 10:56:12 +03:00
Serkan Reis
dd33b6129c Bump Studio version 2023-09-13 10:39:07 +03:00
Serkan Reis
c455eafb7b Add multi-address batch transfers to collection actions 2023-09-13 10:38:26 +03:00
Jorge Hernandez
e0f41fd692
Merge branch 'main' into develop 2023-09-11 15:08:08 -06:00
Serkan Reis
5a7386020e
Merge pull request #215 from public-awesome/thumbnail-selection
Thumbnail selection & upload for compatible assets
2023-09-11 22:33:28 +03:00
Serkan Reis
b65fd5d3c9 Bump Studio version 2023-09-10 14:55:17 +03:00
Serkan Reis
c4f486f1f0 Update collection creation logic for OE/off-chain metadata 2023-09-10 13:42:37 +03:00
Serkan Reis
75a2d4c089 Update collection creation logic for OE/on-chain metadata 2023-09-10 12:46:52 +03:00
Serkan Reis
26c39e8985 Enable thumbnail selection for OE/on-chain metadata 2023-09-10 12:20:23 +03:00
Serkan Reis
387aa5c703 Update upload & metadata upload logic for standard & 1/1 collections 2023-09-09 22:01:25 +03:00
Serkan Reis
be2d644ec9 Surface thumbnail compatible asset file names 2023-09-09 20:39:05 +03:00
Serkan Reis
df0c7a5f1f File selection logic for thumbnails 2023-09-09 15:30:02 +03:00
Jorge Hernandez
5db159dc96
Merge pull request #214 from public-awesome/hotfix/batch-update-open-edition
Hotfix/batch update open edition
2023-09-05 19:12:51 -06:00
jhernandezb
70dad6b7c6 bump package version 2023-09-05 19:12:20 -06:00
jhernandezb
cefbd37fcf revert temp change 2023-09-05 19:11:57 -06:00
Jorge Hernandez
7aa5256827
Merge pull request #213 from public-awesome/hotfix/batch-update-open-edition
batch update open edition
2023-09-05 19:04:07 -06:00
jhernandezb
1d46945df4 update package.json 2023-09-05 19:03:08 -06:00
jhernandezb
9e7afee3fc update contract execution 2023-09-05 19:02:48 -06:00
Jorge Hernandez
a5b5e89d71
Update package.json 2023-09-05 18:41:36 -06:00
Jorge Hernandez
563a096483
Merge pull request #212 from public-awesome/hotfix/batch-update-open-edition
use same URI
2023-09-05 18:30:14 -06:00
jhernandezb
898c0d2eab use same URI 2023-09-05 18:23:36 -06:00
Jorge Hernandez
c7a682b407
Merge pull request #210 from public-awesome/develop
Sync dev > main
2023-09-05 18:10:48 -06:00
Jorge Hernandez
93a4c6e5b3
Merge pull request #211 from public-awesome/update-disclaimer
Update collection creation disclaimer
2023-09-05 18:10:31 -06:00
Serkan Reis
6d8056fada Disable on-chain metadata for Open Edition collections 2023-09-05 17:25:25 +03:00
Serkan Reis
eab00f140e Update collection creation disclaimer 2023-09-05 10:01:23 +03:00
Serkan Reis
5fc43159e3
Merge pull request #209 from public-awesome/utc-option-for-time-input
Global time input switch (local vs. UTC)
2023-09-04 10:40:30 +03:00
Serkan Reis
50ac9b9545 Re-adjust settings modal size 2023-09-01 18:24:16 +03:00
Serkan Reis
4566c1bdc7 Update settings modal placement 2023-09-01 17:35:51 +03:00
Serkan Reis
6475f55e7e Update settings modal 2023-09-01 17:13:49 +03:00
Serkan Reis
ed568d4a25 Bump Studio version 2023-09-01 15:15:50 +03:00
Serkan Reis
c6534d30f5 Update date & time inputs to reflect timezone settings 2023-09-01 15:14:38 +03:00
Serkan Reis
795d54e4c4 Update default time input as UTC 2023-09-01 15:13:50 +03:00
Serkan Reis
7e7fc41b85 Settings modal init 2023-09-01 12:59:01 +03:00
Serkan Reis
4324b225cc Load previous global settings on load 2023-09-01 12:58:36 +03:00
Serkan Reis
3772dec6e1 Init globalSettings 2023-08-31 23:37:29 +03:00
Serkan Reis
3118d14087
Merge pull request #205 from public-awesome/develop
Sync dev > main
2023-08-31 14:19:37 +03:00
Jorge Hernandez
1a9d7ac9c8
Merge pull request #194 from public-awesome/export-import-collection-config
Export/Import collection creation configuration
2023-08-30 07:52:48 -06:00
Jorge Hernandez
e88d3529f9
Merge pull request #207 from public-awesome/update-enable-updatable-fee
Update enable updatable fee
2023-08-30 07:40:44 -06:00
Jorge Hernandez
1501c6790d
Merge pull request #208 from public-awesome/factory-switching
Fix factory switching related issues
2023-08-30 07:39:57 -06:00
Serkan Reis
bf697745d5 Fix factory switching related issues 2023-08-29 14:13:57 +03:00
Serkan Reis
1c689cbb19 Update enable updatable fee 2023-08-26 18:28:05 +03:00
Jorge Hernandez
1c06ae3eab
Merge pull request #206 from public-awesome/update-usdc-denom
Update USDC denom for testnet
2023-08-25 10:18:00 -06:00
Serkan Reis
f62348df0c Update USDC denom for testnet 2023-08-25 18:24:18 +03:00
Jorge Hernandez
a293c95611
Merge pull request #203 from public-awesome/badge-creation-video-support
Badge creation video asset support
2023-08-22 21:26:17 -06:00
Jorge Hernandez
4adc25728c
Merge pull request #204 from public-awesome/fix-upload-with-no-token-description
Address upload issue when token metadata lacks a description
2023-08-22 21:25:40 -06:00
Serkan Reis
26a5423599 Address upload issue when token metadata lacks a description 2023-08-22 21:52:31 +03:00
Serkan Reis
391b712bde Prevent duplicate protocol in the base token uri 2023-08-21 19:45:23 +03:00
Serkan Reis
1ca1d08b2a Reset upload details on import for 1/1 collections 2023-08-21 16:43:05 +03:00
Serkan Reis
f25807f355 Unmicro whitelist unit price 2023-08-21 16:29:52 +03:00
Serkan Reis
5578c408a5 Bump Studio version 2023-08-21 15:00:06 +03:00
Serkan Reis
4cc6fdc070 Update import/export component placement 2023-08-21 14:59:12 +03:00
Serkan Reis
ae9aec3bd8 Disable default upload method when importing 2023-08-21 12:40:54 +03:00
Serkan Reis
96dda936ae Address OE collection empty metadata file issue 2023-08-21 12:07:11 +03:00
Serkan Reis
8990175b03 Check end time during open edition creation - 2 2023-08-21 11:37:19 +03:00
Serkan Reis
0ed370aa67 Check end time during open edition creation 2023-08-21 10:59:10 +03:00
Serkan Reis
e26253fec5 Badge creation video asset support 2023-08-18 13:19:05 +03:00
Serkan Reis
bc719e1a0c Merge branch 'develop' into export-import-collection-config 2023-08-17 17:24:27 +03:00
Serkan Reis
958671a030
Merge pull request #195 from public-awesome/ibc-minter-creation
IBC minter support
2023-08-17 16:30:28 +03:00
Serkan Reis
8b902a1078 Address royalty address import issue for open edition 2023-08-17 14:22:05 +03:00
Serkan Reis
58d2a4abd7 Export/Import selected mint token 2023-08-17 14:21:30 +03:00
Serkan Reis
71e539a0b4 Export/Import open edition token metadata 2023-08-17 14:20:37 +03:00
Serkan Reis
3fbebbe03d Auto-add wallet address as whitelist admin 2023-08-16 17:57:07 +03:00
Serkan Reis
27e1727fa8 Update export logic for open edition collection summary 2023-08-16 17:48:33 +03:00
Serkan Reis
ea5caff1aa Update .env.example 2023-08-15 20:09:35 +03:00
Serkan Reis
3c392381b2 Address invalid creation fee problem following minting denom change 2023-08-15 20:07:51 +03:00
Serkan Reis
702e47e9e6 Update FRNZ denom for testnet 2023-08-15 20:06:53 +03:00
Serkan Reis
6fc4022c8d Update export logic for standard collection summary 2023-08-15 18:24:27 +03:00
Serkan Reis
f324cb6f50 Add token image URI to open edition minter details 2023-08-12 22:47:53 +03:00
Serkan Reis
51843cade0 Surface open edition collection summary details 2023-08-10 22:38:03 +03:00
Serkan Reis
65c2dabed6 Merge branch 'ibc-minter-creation' into export-import-collection-config 2023-08-08 14:35:46 +03:00
Serkan Reis
85efecd40c Clean up 2023-08-07 14:01:46 +03:00
Serkan Reis
784446a676 Bump Studio version 2023-08-07 13:55:31 +03:00
Serkan Reis
7207e26520 Update .env.example 2023-08-07 13:53:17 +03:00
Serkan Reis
6d0b21ec59 Update IBC denoms 2023-08-07 12:59:23 +03:00
Serkan Reis
fe1cfe884c Fix updatable code id mix up 2023-08-06 21:57:03 +03:00
Serkan Reis
a985e97c2e Use factoryParameters/mint_price/denom for open edition airdrops 2023-08-06 21:27:58 +03:00
Serkan Reis
c77e583d53 Update whitelist mint price denom wrt selected denom 2023-08-06 21:09:43 +03:00
Serkan Reis
8aaff38238 Instantiate vending minter wrt selected denom 2023-08-06 18:44:22 +03:00
Serkan Reis
ade9410a91 Fetch and display factory denom for minimum mint price 2023-08-06 17:24:51 +03:00
Serkan Reis
15427072e4 Add flexible factories to the minter list 2023-08-06 16:53:02 +03:00
Serkan Reis
4c87ac298b
Merge pull request #200 from public-awesome/develop
Sync dev > main
2023-08-04 12:04:53 +03:00
Adnan Deniz Corlu
fa859b23d9
Merge pull request #199 from public-awesome/avoid-escaping-line-breaks
Fix: Use replaceAll instead of replace to ignore all escaped line breaks
2023-08-04 12:01:49 +03:00
Serkan Reis
18cdef9580 Fix: Use replaceAll instead of replace to ignore all escaped line breaks 2023-08-04 11:54:22 +03:00
Adnan Deniz Corlu
9b8c3e3e7c
Merge pull request #198 from public-awesome/develop
Sync dev > main
2023-08-04 10:46:15 +03:00
Adnan Deniz Corlu
ea582297ce
Merge pull request #197 from public-awesome/line-breaks
Avoid escaping line breaks in token and collection descriptions
2023-08-04 10:41:01 +03:00
Serkan Reis
c5e321e7f4 Cover collection updates 2023-08-04 10:22:11 +03:00
Serkan Reis
3d527c6682 Avoid escaping line breaks in token and collection descriptions 2023-08-04 10:11:34 +03:00
Serkan Reis
6fb1504d1f
Merge pull request #192 from public-awesome/develop
Sync dev > main
2023-08-03 00:38:38 +03:00
Jorge Hernandez
b66c6befd2
Merge pull request #196 from public-awesome/revoke-authorization 2023-08-02 15:36:56 -06:00
Serkan Reis
7003325d5d Add temporary revoke authorization UI 2023-08-03 00:31:55 +03:00
Serkan Reis
180eb914b3 Fetch vending factory parameters wrt selected mint denom 2023-08-02 22:56:41 +03:00
Serkan Reis
3621a7363e Add mint denom selection for standard collections 2023-08-01 11:26:24 +03:00
Serkan Reis
ce477c8b76 Include vending minters in the minter list 2023-08-01 11:14:22 +03:00
Serkan Reis
4f473b6bf8 Display the right denom in minimum mint price not met error 2023-07-31 17:24:04 +03:00
Serkan Reis
1bfd1113bb Open edition IBC minter creation success 2023-07-31 16:49:35 +03:00
Serkan Reis
d2d06dffae Match selected denom and fetch open edition factory parameters 2023-07-31 12:25:57 +03:00
Serkan Reis
717fd88e74 Update minter and token list 2023-07-30 21:11:37 +03:00
Serkan Reis
000a67a2f6 Update env variables 2023-07-30 21:11:10 +03:00
Serkan Reis
701369f246 Surface Open Edition Minter details 2023-07-28 23:38:45 +03:00
Serkan Reis
3ff3d094b4 Init minter list 2023-07-27 17:26:51 +03:00
Serkan Reis
75fe1d3387 Init TokenInfo 2023-07-26 22:26:27 +03:00
Serkan Reis
0481032a1f Surface standard & 1/1 collection configuration 2023-07-25 22:27:42 +03:00
Serkan Reis
2a38e79191 Surface open edition collection configuration 2023-07-25 22:26:29 +03:00
Serkan Reis
8921938c6c Initial export/import logic 2023-07-25 22:24:40 +03:00
Serkan Reis
e074413a9e Update AddressList 2023-07-24 21:57:53 +03:00
Serkan Reis
d5b1acc16e Create OpenEditionMinterDetailsDataProps 2023-07-23 21:54:14 +03:00
Serkan Reis
adeba3e9d3
Merge pull request #191 from public-awesome/update-whitespace-handling-in-urls
Update whitespace handling in existing URLs
2023-07-17 17:14:28 +03:00
Serkan Reis
17935bfb7d Update how whitespaces are handled in existing URLs 2023-07-17 11:21:21 +03:00
Serkan Reis
1e84b456a6
Merge pull request #189 from public-awesome/batch-update-metadata-json-extensions
Option to include .json extensions when batch updating metadata
2023-07-14 21:44:57 +03:00
Serkan Reis
e4de89ad58 Add .json extensions toggle for Collection Actions > Batch Update Metadata 2023-07-13 12:58:03 +03:00
Serkan Reis
64f5cf60c5 Update contract helpers 2023-07-13 12:56:46 +03:00
Serkan Reis
0deb4d3faa
Merge pull request #188 from public-awesome/develop
Sync development > main
2023-07-07 15:29:51 +03:00
Serkan Reis
829ecee0bd
Merge pull request #187 from public-awesome/wl-increase-member-limit-update
Upgrade-fee payment for whitelist > increase member limit
2023-07-07 14:59:31 +03:00
Serkan Reis
6a87b4980c Bump Studio version 2023-07-07 12:35:24 +03:00
Serkan Reis
d95ab021ec Add upgrade fee payment for wl member limit increase 2023-07-07 12:34:40 +03:00
Serkan Reis
fd506e402f
Merge pull request #186 from public-awesome/develop
Sync development > main
2023-07-05 12:58:24 +03:00
shane.stars
7c6ca46400
Merge pull request #185 from public-awesome/wallet-balance-check-update 2023-07-05 08:47:07 +01:00
Serkan Reis
862137f51c Bump Studio version 2023-07-05 09:13:51 +03:00
Serkan Reis
660fef4440 Temporarily disable the updatable option for flexible collections on testnet 2023-07-05 09:09:58 +03:00
Serkan Reis
975161bb70 Update tooltip position for open edition existing token URI 2023-07-05 09:03:57 +03:00
Serkan Reis
609b7fce47 Check wallet balance in real time during collection creation 2023-07-05 09:01:10 +03:00
Serkan Reis
29a27c2c3d
Merge pull request #184 from public-awesome/develop
Sync development > main
2023-07-04 09:08:00 +03:00
Serkan Reis
8a7f093d39
Merge pull request #183 from public-awesome/metadata-uri-validation
(Base) Token URI validation during collection creation
2023-07-04 08:43:38 +03:00
Serkan Reis
82f19267f3 Add tooltips for (Base) Token URI fields 2023-07-04 08:38:59 +03:00
Serkan Reis
23ef3ab3ea Update token URI validation logic to cover standard collections as well 2023-07-04 08:20:28 +03:00
Serkan Reis
f3b36f2d17 Bump Studio version 2023-07-03 17:19:54 +03:00
Serkan Reis
f9559bf7eb Add token URI validation for 1/1 collection creation & token addition 2023-07-03 17:17:16 +03:00
Serkan Reis
d811a1333c
Merge pull request #182 from public-awesome/develop
Sync development > main
2023-07-03 16:40:14 +03:00
Serkan Reis
cc5e37cc3d
Merge pull request #181 from public-awesome/studio-survey-update
Increase Studio Survey visibility + minor UI updates
2023-07-03 16:37:33 +03:00
Serkan Reis
2b5c5f3c32 Add token URI validation for open edition collection creation 2023-07-03 16:06:14 +03:00
Serkan Reis
aba25199ea Add helper to validate token URI 2023-07-03 16:05:06 +03:00
Serkan Reis
53ad470617 Update condition for triggering collection sync 2023-06-29 23:37:11 +03:00
Serkan Reis
08f3a8da3e Trigger collection sync following collection creation 2023-06-29 22:26:09 +03:00
Serkan Reis
b292b4b065 Update environment variables 2023-06-29 22:24:53 +03:00
Serkan Reis
a2a0880149 Update landing 2023-06-28 22:35:23 +03:00
Serkan Reis
f131b2f571 Update landing 2023-06-28 21:30:55 +03:00
Serkan Reis
e4459acf9c Update sidebar logo 2023-06-28 21:29:42 +03:00
Serkan Reis
0e1c851eea Bump Studio version 2023-06-28 21:02:02 +03:00
Serkan Reis
cf9c529f76 Studio Survey pop up following collection creation 2023-06-28 21:01:12 +03:00
Serkan Reis
f953510adf Update logo on landing 2023-06-28 21:00:31 +03:00
Serkan Reis
64246a3ed4 Update sidebar links 2023-06-28 20:59:51 +03:00
Serkan Reis
44871ab685 Automatically open Studio Survey sidetab upon collection creation 2023-06-28 18:24:43 +03:00
Jorge Hernandez
7b18cffe87
Merge pull request #180 from public-awesome/account-for-zero-airdrop-fee 2023-06-22 18:58:50 -06:00
Serkan Reis
0f1bad2edb Account for zero airdrop fees for open edition collections 2023-06-23 02:27:16 +03:00
Jorge Hernandez
ef37c8c9ac
Merge pull request #179 from public-awesome/develop
Sync development > main
2023-06-22 15:35:14 -06:00
Serkan Reis
69aece0a83
Merge pull request #178 from public-awesome/query-fee-before-airdrops
Query open edition factory parameters before airdrops
2023-06-23 00:29:53 +03:00
Serkan Reis
06ce126b64 Update open edition factory addresses 2023-06-23 00:26:06 +03:00
Serkan Reis
b7628d0366 Update Studio version 2023-06-23 00:21:40 +03:00
Serkan Reis
edb93c0bbf Update Collection Actions > Actions for open edition collections 2023-06-23 00:20:51 +03:00
Serkan Reis
2def96407f Update open edition minter contract helpers 2023-06-23 00:20:23 +03:00
Serkan Reis
2945105a87
Merge pull request #176 from public-awesome/develop
Sync development > main
2023-06-22 21:55:36 +03:00
Serkan Reis
97fa8a875b
Merge pull request #177 from public-awesome/separate-sg721-for-open-edition-creation
Use separate sg721 code ids for open edition collection creation
2023-06-22 21:43:17 +03:00
Serkan Reis
40243a1317 Update the condition for updatable switch visibility 2023-06-22 21:41:13 +03:00
Serkan Reis
62d0949076 Bump Studio version 2023-06-22 21:36:26 +03:00
Serkan Reis
0913e6c97e Use separate sg721 code ids for open edition collection creation 2023-06-22 21:35:50 +03:00
Serkan Reis
1b4c82b15a
Merge pull request #175 from public-awesome/open-edition-html-support
.html file support for Open Edition & 1/1 collection metadata
2023-06-21 23:19:02 +03:00
Serkan Reis
9f4f5c5f1f .html support for 1/1 & open edition collections 2023-06-21 22:52:14 +03:00
Serkan Reis
209e5ecd8c v3 update to .env.example 2023-06-21 22:50:51 +03:00
Serkan Reis
54bb180cf4
Merge pull request #174 from public-awesome/open-edition-cover-video-support
Add video support for open edition cover image preview
2023-06-21 14:41:41 +03:00
Serkan Reis
e09409550e Bump Studio version 2023-06-21 13:14:14 +03:00
Serkan Reis
8dc78a1887 Add video support for open edition cover image preview 2023-06-21 13:12:23 +03:00
shane.stars
603c3ee88a
Merge pull request #173 from public-awesome/fix-oe-mint-price-issue
Update mint price calculation for open edition collections
2023-06-20 16:07:13 -04:00
Serkan Reis
dc635ff3a2 Revert and move the previous change 2023-06-20 22:50:16 +03:00
Serkan Reis
1f6fa1e34b Update mint price calculation for open edition collections 2023-06-20 22:32:53 +03:00
Serkan Reis
fabc53de42
Merge pull request #172 from public-awesome/hide-open-edition-on-chain-metadata
Temporarily hide metadata storage options for open edition collection creation
2023-06-20 17:13:35 +03:00
Serkan Reis
cd9a21e1e7 Hide metadata storage method selection for open edition collections 2023-06-20 16:36:07 +03:00
Serkan Reis
3acfce0f88 Update .env.example 2023-06-20 16:28:58 +03:00
Serkan Reis
381c55dab3
Merge pull request #171 from public-awesome/open-edition-minter
Open Edition minter integration
2023-06-18 06:36:32 +03:00
Serkan Reis
1f2fef9655 Clean up 2023-06-17 17:02:02 +03:00
Serkan Reis
13071d6155 Update factory addresses 2023-06-17 17:01:47 +03:00
Serkan Reis
221a86563e Bump Studio version 2023-06-17 16:25:19 +03:00
Serkan Reis
b300a37384 Open Edition collection creation summary 2023-06-17 16:23:55 +03:00
Serkan Reis
9292e6218a Update sidebar to include open edition minter contract dashboard 2023-06-17 12:03:23 +03:00
Serkan Reis
fa30f47be7 Implement open edition minter contract dashboard 2023-06-17 12:00:41 +03:00
Serkan Reis
a9688f0314 Include Open Edition Collections in My Collections 2023-06-17 11:32:58 +03:00
Serkan Reis
5044aedee4 Include open edition minter queries in Collection Actions > Queries 2023-06-17 11:24:39 +03:00
Serkan Reis
6b89a60216 Include open edition minter actions in Collection Actions > Actions 2023-06-17 11:01:32 +03:00
Serkan Reis
52632ff42d Update open edition minter messages 2023-06-16 18:40:59 +03:00
Serkan Reis
b7139df038 Update open edition minter contract helpers 2023-06-16 18:27:36 +03:00
Serkan Reis
663b1d9999 Implement wallet balance checks 2023-06-16 14:57:47 +03:00
Serkan Reis
9f513857c9 Implement user input checks 2023-06-16 13:39:24 +03:00
Serkan Reis
092a0f3f33 Auto-handle animation url for on-chain metadata uploads 2023-06-15 17:53:56 +03:00
Serkan Reis
8f14b92a80 Minor UI updates 2023-06-15 16:13:08 +03:00
Serkan Reis
1fa4d0dcf9 Implement open edition minter creator 2023-06-15 13:51:50 +03:00
Serkan Reis
d0e5bf7f4c Customized minting details for open edition minter 2023-06-15 13:51:08 +03:00
Serkan Reis
5e253f24b7 Reset cover image on metadata storage method change 2023-06-15 13:50:27 +03:00
Serkan Reis
6a80af95c2 Add royalty details 2023-06-15 13:25:47 +03:00
Serkan Reis
8446caf3f5 Metadata input for on-chain storage 2023-06-15 13:20:57 +03:00
Serkan Reis
21893e99ae Off-chain metadata upload logic 2023-06-15 13:20:01 +03:00
Serkan Reis
42d0324ce8 Single asset upload logic for on chain metadata storage 2023-06-15 13:18:46 +03:00
Serkan Reis
19d8c7b683 Customize Collection Details for Open Edition Collections 2023-06-15 13:17:44 +03:00
Serkan Reis
c8802bac88 Update collection creation tabs 2023-06-15 13:16:05 +03:00
Serkan Reis
3692114dff Fetch open edition creation fee and minimum mint price from the factory 2023-06-14 12:41:16 +03:00
Serkan Reis
3c775f7502 Add open edition minter to contracts context 2023-06-14 12:40:02 +03:00
Serkan Reis
4799a32359 Update collection creation tabs 2023-06-13 12:04:08 +03:00
Serkan Reis
3065cc35b3 Init open edition minter message list 2023-06-13 11:40:43 +03:00
Serkan Reis
d9b060a4fb Init open edition minter contract helpers 2023-06-13 11:24:36 +03:00
Serkan Reis
d527f65512 Update open edition factory contract helpers 2023-06-12 12:42:47 +03:00
Serkan Reis
434f2e5926 Init open edition factory contract helpers 2023-06-12 12:34:59 +03:00
Serkan Reis
74e035b286 Include open edition factory & minter among constants 2023-06-12 12:24:32 +03:00
Serkan Reis
bed4ad24e5
Merge pull request #170 from public-awesome/develop
Sync development > main
2023-06-08 12:12:18 +03:00
Adnan Deniz corlu
9b126c38ce
Merge pull request #169 from public-awesome/airdrop-specific-tokens
Feature: Airdrop specific tokens to multiple addresses
2023-06-08 12:10:53 +03:00
Serkan Reis
cb9f2bc1da Update action description for airdropping specific tokens 2023-06-08 10:29:25 +03:00
Serkan Reis
be30701c8f Clean up 2023-06-08 10:19:53 +03:00
Serkan Reis
18f77f014f Bump Studio version 2023-06-08 10:10:42 +03:00
Serkan Reis
c041a53b67 Update Collection Actions > Actions 2023-06-08 10:09:50 +03:00
Serkan Reis
1b6f429843 Update vending minter contract helpers 2023-06-08 10:09:13 +03:00
Serkan Reis
9742ec6d04 Update .csv content validation 2023-06-08 10:08:47 +03:00
Serkan Reis
893b5b89c3 Update .csv to array logic 2023-06-08 10:08:18 +03:00
Serkan Reis
fddedd6679 Token id compatibility for AirdropUpload.tsx 2023-06-08 10:06:29 +03:00
Serkan Reis
26663e4f24 Update action list 2023-06-08 08:49:11 +03:00
Serkan Reis
a2029da8a4
Merge pull request #168 from public-awesome/develop
Sync development > main
2023-06-06 16:11:55 +03:00
Adnan Deniz corlu
55f5a4b0a5
Merge pull request #167 from public-awesome/studio-survey
Typeform survey integration
2023-06-06 16:04:48 +03:00
Serkan Reis
aa5c88268d Bump Studio version 2023-06-06 12:18:16 +03:00
Serkan Reis
9c5a47b42c Add a side tab for Stargaze Studio Survey 2023-06-06 12:16:46 +03:00
Serkan Reis
93b8a541e4
Merge pull request #166 from public-awesome/develop
Sync development > main
2023-06-03 14:10:04 +03:00
Serkan Reis
0d3109ba33
Merge pull request #165 from public-awesome/handle-capitalized-file-extensions
Handle capitalized file extensions when generating asset previews
2023-06-03 14:08:56 +03:00
Serkan Reis
578f32f875 Handle capitalized asset file extensions 2023-06-03 07:01:57 +03:00
Serkan Reis
01bbbb0836
Merge pull request #164 from public-awesome/develop
Sync development > main
2023-05-29 12:22:11 +03:00
Adnan Deniz corlu
43afca29e8
Merge pull request #163 from public-awesome/v2-migration
Migration update for mutable v2 collections
2023-05-29 12:20:19 +03:00
Serkan Reis
103cb3ef85 Bump Studio version 2023-05-29 09:53:11 +03:00
Serkan Reis
f0a35565b8 Update Collection Actions list 2023-05-28 20:51:10 +03:00
Serkan Reis
81cac0ff5b Update contract helpers 2023-05-28 17:46:50 +03:00
Serkan Reis
66bfd262f4 Display flexible collections in the My Collections list 2023-05-26 14:59:15 +03:00
Serkan Reis
9534ad399e Catch factory parameters query errors 2023-05-26 14:50:35 +03:00
Serkan Reis
a999f54d48 Update .env.example 2023-05-26 14:40:01 +03:00
Serkan Reis
2f77b2d365
Merge pull request #162 from public-awesome/develop
Sync development > main
2023-05-04 13:44:47 +03:00
Adnan Deniz corlu
42c79fb38e
Merge pull request #161 from public-awesome/free-mint-disclaimer
Disclaimer for free public mints
2023-05-04 13:39:49 +03:00
Serkan Reis
d5bf407d76 Update disclaimer - 2 2023-05-04 13:37:18 +03:00
Serkan Reis
118a7793d2 Update disclaimer 2023-05-04 13:03:03 +03:00
Serkan Reis
7411c9c908 Add free public mint disclaimer 2023-05-04 12:57:45 +03:00
Serkan Reis
686b7494ae
Merge pull request #160 from public-awesome/develop
Sync development > main
2023-05-03 22:08:38 +03:00
Serkan Reis
69da9f6641
Merge pull request #159 from public-awesome/view-logs
Feature: View Studio Logs
2023-05-03 22:07:30 +03:00
Serkan Reis
ec253bd8f7 Revisit timestamp UTC conversion during log download 2023-05-03 21:50:15 +03:00
Serkan Reis
0f8c9dfe7a Include additional errors 2023-05-03 21:39:06 +03:00
Serkan Reis
090b55f038 Bump Studio version 2023-05-03 21:19:33 +03:00
Serkan Reis
f33fd2780f Log common errors 2023-05-03 21:18:09 +03:00
Serkan Reis
be39437383 Update Sidebar 2023-05-03 21:17:42 +03:00
Serkan Reis
972b92bf6a Implement Log Modal 2023-05-03 21:17:18 +03:00
Serkan Reis
5041d34964 Add log state related functions 2023-05-03 21:16:51 +03:00
Serkan Reis
b37b6e44bf
Merge pull request #158 from public-awesome/develop
Sync development > main
2023-05-03 12:07:57 +03:00
Serkan Reis
d3a8fa5a7a
Merge pull request #157 from public-awesome/free-mint-updates
Free mint related changes
2023-05-03 12:03:41 +03:00
Serkan Reis
4dd2ab7b30 Bump Studio version 2023-05-03 11:53:00 +03:00
Serkan Reis
eeafda455a Free mint related changes 2023-05-03 11:52:26 +03:00
Serkan Reis
cbbc6c5272
Merge pull request #156 from public-awesome/develop
Sync development > main
2023-04-28 15:07:14 +03:00
Serkan Reis
a7afc2ca62
Merge pull request #153 from public-awesome/wl-flex-compatibility
whitelist-flex compatibility
2023-04-28 15:05:58 +03:00
Serkan Reis
4d01605fa9 Bump Studio version 2023-04-28 15:02:27 +03:00
Serkan Reis
6f5c204b08 Handle existing wl-flex address during collection creation 2023-04-28 15:00:39 +03:00
Serkan Reis
ccf1b13f5b Instantiate new WL Flex during collection creation 2023-04-27 20:19:42 +03:00
Serkan Reis
0945bcf927 Update vending factory contract helpers 2023-04-27 20:16:40 +03:00
Serkan Reis
5cdcc66611 Update environment variables 2023-04-27 20:12:05 +03:00
Serkan Reis
ad782961df Update Collection Creation > WhitelistDetails 2023-04-24 13:14:06 +03:00
Serkan Reis
76ba766b28 Update WL dashboard > Execute 2023-04-17 19:20:37 +03:00
Serkan Reis
0304d907a9 Implement FlexMemberAttributes 2023-04-17 19:19:02 +03:00
Serkan Reis
4a2864e46f Update whitelist contract helpers 2023-04-17 19:17:29 +03:00
Serkan Reis
4fc7ce5583 Instantiation in working order 2023-04-17 14:05:21 +03:00
Serkan Reis
92e85f64db Update environment variables 2023-04-17 13:35:01 +03:00
Serkan Reis
4d74a230e3 Implement whitelist-flex .csv selector 2023-04-17 12:12:09 +03:00
Serkan Reis
e7e66380e1 Implement whitelist-flex related utils 2023-04-17 12:10:38 +03:00
Serkan Reis
856ae4e53f Select WL type to instantiate 2023-04-17 12:09:40 +03:00
Serkan Reis
d725f33155
Merge pull request #152 from public-awesome/develop
Sync development > main
2023-04-12 10:52:41 +03:00
Adnan Deniz corlu
871ebd4aa7
Merge pull request #151 from public-awesome/splits-weight-in-percent
Display splits contract weight distribution % during initial configuration
2023-04-12 10:51:22 +03:00
Serkan Reis
600aac38e1 Display splits contract weight distribution percentage during initial configuration 2023-04-12 10:43:55 +03:00
Serkan Reis
51364147c9
Merge pull request #148 from public-awesome/develop
Sync development > main
2023-04-07 12:53:04 +03:00
Serkan Reis
f9db6a301d
Merge pull request #147 from public-awesome/mint-price-related-updates
Dynamic minimum mint price checks during collection creation & mint price updates
2023-04-07 12:45:42 +03:00
Serkan Reis
d4c27b2237 Bump Studio version 2023-04-07 12:32:34 +03:00
Serkan Reis
119e703997 Update checks for minimum mint price for Whitelists 2023-04-07 12:28:19 +03:00
Serkan Reis
3471aeb653 Update checks for minimum mint price on Collection Creation 2023-04-07 12:24:38 +03:00
Serkan Reis
ec28295278 Update checks for minimum mint price on Vending Minter Dashboard > Execute > Update Mint Price 2023-04-07 12:24:02 +03:00
Serkan Reis
5efb637a18 Update checks for minimum mint price on Collection Actions > Update Mint Price 2023-04-07 12:23:21 +03:00
Serkan Reis
c1da34dacb
Merge pull request #146 from public-awesome/develop
Sync development > main
2023-04-05 17:22:08 +03:00
Serkan Reis
1307cfc4ab
Merge pull request #145 from public-awesome/validate-splits-contract-address
Validate splits contract address during collection creation & collection info updates
2023-04-05 17:17:41 +03:00
Serkan Reis
5bf2d83c4c Bump Studio version 2023-04-05 16:47:11 +03:00
Serkan Reis
fe38755797 Validate splits contract address on Collection Actions > Update Collection Info 2023-04-05 16:46:18 +03:00
Serkan Reis
f3db72c677 Validate splits contract address on Collection Creation > Royalty Details 2023-04-05 14:54:34 +03:00
Serkan Reis
a7cc57c386
Merge pull request #144 from public-awesome/develop
Sync development > main
2023-04-04 15:49:18 +03:00
Serkan Reis
498c858c3c
Merge pull request #143 from public-awesome/fix-disappearing-base-minter-list
UI Improvements
2023-04-04 15:45:31 +03:00
Serkan Reis
7bee312668 Bump Studio version 2023-04-04 15:31:47 +03:00
Serkan Reis
ac467fb750 Display summary upon adding new tokens to a 1/1 collection 2023-04-04 15:30:05 +03:00
Serkan Reis
bff03ffc66 Update Base Minter Creation and Upload & Mint error messages 2023-04-04 11:46:52 +03:00
Serkan Reis
ffd4b729e4 Increased resolution responsiveness for My Collections 2023-04-04 10:45:13 +03:00
Serkan Reis
8fe784fdf6 Truncate contract addresses on My Collections 2023-04-04 10:17:41 +03:00
Serkan Reis
9c740d0fb0 Remove Trading Start Time from 1/1 Collections > Collection Details 2023-04-03 22:22:27 +03:00
Serkan Reis
a11d0f3bae Adjust metadata attributes width wrt resolution 2023-04-03 21:46:49 +03:00
Serkan Reis
924b10ece0 Debounce base minter contract list 2023-04-03 19:06:43 +03:00
Adnan Deniz corlu
c91e808fc0
Merge pull request #141 from public-awesome/develop
Sync development >main
2023-04-03 13:06:11 +03:00
Adnan Deniz corlu
8f2df3f5cb
Merge pull request #142 from public-awesome/update-creation-summary
Fix View on Marketplace condition on creation summary
2023-04-03 13:05:04 +03:00
Serkan Reis
248b86d7e3 Fix View on Marketplace condition on creation summary 2023-04-03 13:03:24 +03:00
Serkan Reis
f35817aa21
Merge pull request #140 from public-awesome/my-collections-update
My Collections update for 1/1 Collections
2023-04-03 13:00:58 +03:00
Serkan Reis
99a03d4a7b Remove the horizontal scrollbar from My Collections 2023-04-03 12:58:53 +03:00
Serkan Reis
86f38b028b Bump Studio version 2023-04-03 12:41:15 +03:00
Serkan Reis
e1707c1ab5 Update collection creation summary for 1/1 Collections 2023-04-03 12:40:33 +03:00
Serkan Reis
9972e0617a Update My Collections for 1/1 Collections 2023-04-03 12:39:59 +03:00
Serkan Reis
be7ee5c7ad
Merge pull request #139 from public-awesome/develop
Sync development > main
2023-04-01 19:36:28 +03:00
Serkan Reis
82b04215e2
Merge pull request #138 from public-awesome/conditional-sg721-updatable-toggle
Update the condition for displaying sg721-updatable toggle
2023-04-01 19:22:56 +03:00
Serkan Reis
e688f86049 Update the condition for displaying sg721-updatable toggle 2023-04-01 19:13:21 +03:00
Serkan Reis
5bab23ebce
Merge pull request #137 from public-awesome/develop
sync dev > main
2023-04-01 17:08:07 +03:00
Serkan Reis
6124826d20
Merge pull request #136 from public-awesome/dynamic-creation-fees
Dynamic collection creation fees
2023-04-01 16:54:53 +03:00
Serkan Reis
abc46aad4b Bump Studio version 2023-04-01 16:46:53 +03:00
Serkan Reis
1d1c35135f Fetch minter creation fees prior to collection creation 2023-04-01 16:45:49 +03:00
Serkan Reis
babdb5fb94 Temporary update for hardcoded base minter creation fee 2023-03-31 19:38:26 +03:00
Serkan Reis
3efad8a1fd Replace Append -> Add 2023-03-31 19:25:32 +03:00
Serkan Reis
245117da7a
Merge pull request #135 from public-awesome/temporary-creation-fee-update
Temporary change of hardcoded base minter creation fee
2023-03-31 16:04:59 +03:00
Serkan Reis
04be47c18a Temporarily disable sg721-updatable toggle for collection creation 2023-03-31 15:01:27 +03:00
Serkan Reis
a1db0f0572 Temporary change of hardcoded base minter creation fee 2023-03-31 14:47:26 +03:00
Serkan Reis
62299e02f4
Merge pull request #134 from public-awesome/develop
Sync development > main
2023-03-31 13:40:45 +03:00
Serkan Reis
e96a7e12fe
Merge pull request #133 from public-awesome/base-minter-wallet-balance-check
Incorporate wallet balance checks for 1/1 collection creation
2023-03-31 13:36:26 +03:00
Serkan Reis
11425e70a8 Update base minter creation fee 2023-03-31 13:29:36 +03:00
Serkan Reis
61642c5480 Bump Studio version 2023-03-31 09:50:28 +03:00
Serkan Reis
00cf2fe88a Wallet balance check for 1/1 collection creation 2023-03-31 09:49:33 +03:00
Serkan Reis
fe75598b17
Merge pull request #132 from public-awesome/develop
Sync development > main
2023-03-27 16:41:07 +03:00
Serkan Reis
f51e7709ac
Merge pull request #131 from public-awesome/fix-linting-issues
Fix linting issues
2023-03-27 16:34:48 +03:00
Serkan Reis
8609f59b5f Make linter happy 2023-03-27 15:44:21 +03:00
Serkan Reis
9cb0410ae6
Merge pull request #129 from public-awesome/revert-recent-changes
Revert Splits & WL related changes
2023-03-27 15:07:19 +03:00
Serkan Reis
ae380dea0d
Merge pull request #130 from public-awesome/sg721-updatable-switch
Conditionally disable sg721-updatable functionality
2023-03-27 15:06:43 +03:00
Serkan Reis
28f4c81bde Update paymentAddress checks 2023-03-27 12:18:39 +03:00
Serkan Reis
cd0c3f1d8d Conditionally disable sg721-updatable switch 2023-03-27 12:06:02 +03:00
Serkan Reis
f3418100d3 Revert Splits & WL related changes 2023-03-27 11:07:53 +03:00
Serkan Reis
2e0d8e8ed9
Merge pull request #128 from public-awesome/wl-related-changes
WL related changes
2023-03-27 07:06:31 +03:00
Serkan Reis
95a8b39b6d WL related changes 2023-03-26 04:07:53 +03:00
Serkan Reis
e3ae0fb65c
Merge pull request #127 from public-awesome/splits-related-updates
Splits contract dashboard & optional mint revenue payment address
2023-03-24 11:36:53 +03:00
Serkan Reis
7a21240bed Add splits contract dashboard & mint revenue payment address 2023-03-24 10:43:10 +03:00
Adnan Deniz corlu
80f07f95ca
Merge pull request #126 from public-awesome/splits-dashboard
Splits contract dashboard
2023-03-19 22:26:54 +03:00
Serkan Reis
67ebb8acab
Merge branch 'develop' into splits-dashboard 2023-03-19 21:55:48 +03:00
Serkan Reis
4cee660462 Update splits contract description 2023-03-19 21:54:02 +03:00
Serkan Reis
cdf8064768 Bump Studio version 2023-03-19 21:50:38 +03:00
Serkan Reis
fbfcdf9cd4 Allow splits admin to be undefined when using an existing cw4-group 2023-03-19 21:49:33 +03:00
Serkan Reis
2e080488e3 Init splits contract dashboard > Migrate 2023-03-19 21:40:54 +03:00
Serkan Reis
d27dfbd452 Minor UI updates 2023-03-19 21:28:33 +03:00
Serkan Reis
8501f6bf64 Include splits contract dashboard homecard on the dashboard landing page 2023-03-19 21:23:09 +03:00
Serkan Reis
45ecf7aaba Include splits contract dashboard link on the sidebar 2023-03-19 21:18:43 +03:00
Serkan Reis
1874d5d7d2 Init splits contract dashboard > Instantiate 2023-03-19 21:14:14 +03:00
Serkan Reis
d7f2e6a231 Include code ids for splits & cw4-group in .env.example 2023-03-19 21:13:43 +03:00
Serkan Reis
625411d0c6 Update Query Members 2023-03-19 21:10:11 +03:00
Adnan Deniz corlu
0908027171
Merge pull request #124 from public-awesome/optional-payment-address
Add optional payment address for standard collection minting revenues
2023-03-19 16:05:14 +03:00
Serkan Reis
4bde7c8ed6 Fetch splits & cw4-group code ids from env 2023-03-19 11:58:37 +03:00
Serkan Reis
74ea4c90dd Implement MemberAttributes 2023-03-19 11:57:37 +03:00
Serkan Reis
fddf22d919 Init splits contract dashboard > Query 2023-03-19 11:04:13 +03:00
Serkan Reis
8fed125b4c Init splits contract dashboard > Execute 2023-03-19 09:59:00 +03:00
Serkan Reis
c375e94bf8 Update contracts context to include splits contract 2023-03-19 09:57:42 +03:00
Serkan Reis
2dd56b1ddd Update actions types for splits 2023-03-19 09:56:41 +03:00
Serkan Reis
51af711d9b Update link tabs for splits contract 2023-03-19 09:55:18 +03:00
Serkan Reis
6914a40258 Init splits contract helpers 2023-03-18 20:47:39 +03:00
Serkan Reis
866039ebe3 Bump Studio version 2023-03-18 16:24:18 +03:00
Serkan Reis
d71bf2147c Update insufficient funds error messages 2023-03-18 16:23:24 +03:00
Serkan Reis
6da52258b2 Standard collection creation now includes optional payment address input 2023-03-18 16:11:40 +03:00
Serkan Reis
8255c8dd92
Merge pull request #123 from public-awesome/hardcoded-wl-code-id
sg-whitelist update related changes
2023-03-18 14:22:26 +03:00
Serkan Reis
3abed21beb Update freeze() description 2023-03-18 13:22:34 +03:00
Serkan Reis
006d4d61fd Add freeze() to WL dashboard > Execute > Action Types 2023-03-18 13:18:07 +03:00
Serkan Reis
e48d261943 Add freeze() to WL contract helpers 2023-03-18 13:15:37 +03:00
Serkan Reis
66a22b4094 Bump Studio version 2023-03-18 11:40:10 +03:00
Serkan Reis
ec7799ad74 Update WL checks 2023-03-18 11:38:44 +03:00
Serkan Reis
4d9ba5a76d Add AdminList among WL Query Types 2023-03-18 11:34:23 +03:00
Serkan Reis
2c2fc2efbe Render WL admin list mutable by default 2023-03-18 11:18:34 +03:00
Serkan Reis
68961ce5f3 Fetch WL code id from env variables 2023-03-18 09:48:49 +03:00
Serkan Reis
88f0c7ca93
Merge branch 'develop' into hardcoded-wl-code-id 2023-03-18 09:33:17 +03:00
Serkan Reis
145d56e498
Merge pull request #113 from public-awesome/sg721-updatable-integration
sg721-updatable integration
2023-03-17 18:34:36 +03:00
Serkan Reis
d50f2efe6b Bump Studio version 2023-03-17 18:28:25 +03:00
Serkan Reis
3b378b2232 Include Update Admins action in Whitelist dashboard > Execute 2023-03-09 00:24:15 +03:00
Serkan Reis
deb149809e Update Whitelist dashboard > Instantiate UI 2023-03-09 00:01:34 +03:00
Serkan Reis
f422b9458b Update New Whitelist UI 2023-03-08 23:53:12 +03:00
Serkan Reis
31ed420a77 Update WL instantiateMsg for collection creation 2023-03-08 23:11:04 +03:00
Serkan Reis
6ec1ff40cc Remove mint & purge from collection actions (still available on dashboard) 2023-03-08 07:50:34 +03:00
Serkan Reis
9fdbf173e5 Temporary switch to a hardcoded code id for WL instantiation 2023-03-08 07:14:49 +03:00
Serkan Reis
3dc1843d06
Merge branch 'develop' into sg721-updatable-integration 2023-03-07 19:07:59 +03:00
Adnan Deniz corlu
1a076026e0
Merge pull request #121 from public-awesome/develop
Sync development > main
2023-03-07 18:05:05 +03:00
Serkan Reis
46e7072521
Merge pull request #122 from public-awesome/tooltip-update
Update badge creation tooltips
2023-03-07 17:56:44 +03:00
Serkan Reis
065d7b3e82 Update badge creation tooltips 2023-03-07 17:53:05 +03:00
Serkan Reis
be9849b662
Merge pull request #120 from public-awesome/badge-related-improvements
Badge Creation related improvements
2023-03-07 16:16:04 +03:00
Serkan Reis
c224654c4c Tooltip color update 2023-03-07 16:13:08 +03:00
Serkan Reis
423bcd7c55 Add metadata file selection tooltip 2023-03-07 16:09:09 +03:00
Serkan Reis
227bf0e67e Update parseMetadata() logic 2023-03-07 15:54:14 +03:00
Serkan Reis
8e0f1d3b51 Edit tooltip label for Mint Rule: By Minter 2023-03-07 14:59:54 +03:00
Serkan Reis
cf4378bbad Clean up parseMetadata() 2023-03-07 12:52:20 +03:00
Serkan Reis
cf07cc3cf5 Add fee estimation warning 2023-03-07 12:27:46 +03:00
Serkan Reis
e2c755515b Update tooltips 2023-03-07 12:12:43 +03:00
Serkan Reis
b708f35ec5 Bump Studio version 2023-03-07 11:08:11 +03:00
Serkan Reis
3fb7721fb0 Update placeholder for royalty fee percentage inputs 2023-03-07 11:06:35 +03:00
Serkan Reis
43ae5eb03c Improved responsiveness to changes in screen resolution 2023-03-07 11:02:56 +03:00
Serkan Reis
cf7f00f2ee Add Tooltips for Mint Rule selection 2023-03-07 10:33:59 +03:00
Serkan Reis
278c989703 Update My Badges layout & add placeholders 2023-03-06 22:29:08 +03:00
Serkan Reis
d5d9d414fc Add image preview for existing image URL 2023-03-06 22:14:24 +03:00
Serkan Reis
4f1a37654e Display badge creation fee estimate 2023-03-06 21:49:05 +03:00
Serkan Reis
1f98d146fb Move metadata selection under badge details 2023-03-06 18:08:19 +03:00
Serkan Reis
9c1057a6c2 Enable metadata file selection during badge creation 2023-03-06 12:06:38 +03:00
Serkan Reis
4442c7c4f7 Adjust tooltip activation area for Updatable Metadata toggle 2023-03-05 19:14:48 +03:00
Serkan Reis
34598997e2 Added toasts for Updatable Token Metadata toggle 2023-03-05 18:41:08 +03:00
Serkan Reis
e5c212751b
Merge pull request #119 from public-awesome/develop
Sync development > main
2023-03-04 12:52:29 +03:00
Adnan Deniz corlu
38feff06a7
Merge pull request #118 from public-awesome/wl-per-address-limit-update
Update WL per_address_limit checks on Vending Minter creation
2023-03-04 12:51:09 +03:00
Serkan Reis
1ca3c1fb7b Minor UI update on Confirmation Modal 2023-03-04 12:49:01 +03:00
Serkan Reis
8f75c24553 Update WL per_address_limit checks on Vending Minter creation 2023-03-04 12:43:04 +03:00
Serkan Reis
d4ad8148bb
Merge pull request #117 from public-awesome/develop
Sync development > main
2023-03-04 08:24:01 +03:00
Adnan Deniz corlu
91e4b6893c
Merge pull request #116 from public-awesome/v2.1-updates
Launchpad v2.1 - Vending Minter updates
2023-03-03 18:15:30 +03:00
Serkan Reis
f9504c622a Bump Studio version 2023-03-03 12:53:40 +03:00
Serkan Reis
816eda2324 Update Vending Minter Contract dashboard > Execute UI 2023-03-03 12:50:37 +03:00
Serkan Reis
93534b7c4b Update dispatcher logic for Vending Minter dashboard > Execute 2023-03-03 12:45:41 +03:00
Serkan Reis
ad4aa1b274 Update Collection Actions > Actions UI 2023-03-03 12:41:27 +03:00
Serkan Reis
a09d59887c Update dispatcher logic for Vending Minter actions 2023-03-03 12:40:36 +03:00
Serkan Reis
58a186e2ab Update Vending Minter helpers 2023-03-03 12:17:16 +03:00
Serkan Reis
e1dd8dadc2 Fix typo in Studio description 2023-03-03 11:07:41 +03:00
Serkan Reis
4f76a73fb6 Update per_address_limit checks performed during Vending Minter creation 2023-03-03 11:05:16 +03:00
Serkan Reis
eb86960ca6
Merge pull request #115 from public-awesome/develop
Sync development > main
2023-02-28 18:49:47 +03:00
Adnan Deniz corlu
dd2d1b3670
Merge pull request #114 from public-awesome/additional-badge-features
Additional badge related features
2023-02-28 18:46:15 +03:00
Serkan Reis
9a3282a0c3 Update .env.example 2023-02-28 18:23:55 +03:00
Serkan Reis
12a95097f6 Enable queryKey & queryKeys on Badge Hub dashboard > Query 2023-02-28 17:17:18 +03:00
Serkan Reis
f1b12ad56d Bump Studio version 2023-02-28 17:11:02 +03:00
Serkan Reis
f486e4c39e Name resolution support for designated minter address during badge creation 2023-02-28 17:09:06 +03:00
Serkan Reis
3c2478cd38 Enable Mint Rule: By Keys & By Minter for badge creation using the dashboard 2023-02-28 17:00:44 +03:00
Serkan Reis
4bf6e6ee5f Implement Mint by Minter for Badge Hub Dashboard > Execute 2023-02-28 13:56:04 +03:00
Serkan Reis
f9f0946b41 Implement Mint by Keys for Badge Hub Dashboard > Execute 2023-02-28 12:59:31 +03:00
Serkan Reis
cfbec6baeb Implement Purge Keys & Owners for Badge Hub Dashboard > Execute 2023-02-28 12:42:07 +03:00
Serkan Reis
38dc24e0b8 Implement Add Keys for Badge Hub Dashboard > Execute 2023-02-28 12:38:42 +03:00
Serkan Reis
765cce575e Enable Mint Rule: By Minter & By Keys related actions on Badge Hub Dashboard 2023-02-28 12:17:43 +03:00
Serkan Reis
8323fc908b Implement Add Keys for Badge Actions > Actions 2023-02-28 12:10:39 +03:00
Serkan Reis
3a5dd27dff Enable Purge Keys/Owners on Badge Actions > Actions 2023-02-28 11:33:28 +03:00
Serkan Reis
0696e83bfa Implement Mint by Keys for Badge Actions > Actions 2023-02-28 11:15:33 +03:00
Serkan Reis
1396445f19 Update Mint Rule: By Keys summary UI 2023-02-28 10:05:06 +03:00
Serkan Reis
68c3d44d01 Mint Rule: By Keys summary with downloadable key pairs 2023-02-27 22:55:48 +03:00
Serkan Reis
cc31179516 Enable badge creation using Mint Rule: By Keys 2023-02-27 21:43:57 +03:00
Serkan Reis
68efd8d361 Implement Mint by Minter for Badge Actions 2023-02-27 19:01:38 +03:00
Serkan Reis
d30760e642 Update Mint Rule: By Minter badge creation summary 2023-02-27 18:33:57 +03:00
Serkan Reis
57e16dfa8b Enable badge creation using Mint Rule: By Minter 2023-02-27 13:28:14 +03:00
Serkan Reis
14983047cf Update the list of actions for SG721 Dashboard > Execute 2023-02-26 13:45:18 +03:00
Serkan Reis
d27d22367e Update factory contract helpers 2023-02-26 13:08:03 +03:00
Serkan Reis
5f8dea9cc0 Update wallet balance checks 2023-02-26 12:42:21 +03:00
Serkan Reis
4a8c199c53 Incorporate metadata updatability into the collection creation process 2023-02-26 12:36:34 +03:00
Serkan Reis
fbcd58a4d0 Update input fields on Collection Actions 2023-02-26 10:52:58 +03:00
Serkan Reis
16c859bbd8 Connect helper functions to update metadata related actions 2023-02-26 10:24:46 +03:00
Serkan Reis
4e646b9cf4 Update sg721 helpers to include metadata update related functions 2023-02-26 10:13:26 +03:00
Serkan Reis
a764b96727 Update action list wrt SG721 type on Collection Actions 2023-02-25 21:09:43 +03:00
Serkan Reis
88335ea733 Retrieve SG721 type on Collection Actions 2023-02-25 20:36:48 +03:00
Serkan Reis
69e7525349
Merge pull request #112 from public-awesome/develop
Sync development > main
2023-02-23 18:04:25 +03:00
Adnan Deniz corlu
6858e57a46
Merge pull request #111 from public-awesome/update-dashboard-name
Update dashboard name
2023-02-23 18:02:36 +03:00
Serkan Reis
611e4c5600 Update dashboard name 2023-02-23 17:59:41 +03:00
Adnan Deniz corlu
2b9c6c2d24
Merge pull request #110 from public-awesome/develop
Sync development > main
2023-02-23 16:01:03 +03:00
Serkan Reis
697052b7e2
Merge pull request #109 from public-awesome/update-welcome-screen
Welcome screen update
2023-02-23 15:59:53 +03:00
Serkan Reis
c962353ff4 Update welcome screen 2023-02-23 15:58:02 +03:00
Serkan Reis
b81980c3d7
Merge pull request #108 from public-awesome/develop
Sync development > main
2023-02-23 15:50:11 +03:00
Adnan Deniz corlu
d308c90fb1
Merge pull request #103 from public-awesome/badge-integration
Badge integration
2023-02-23 15:45:37 +03:00
Serkan Reis
907b18e161 Bump Studio version 2023-02-23 15:33:11 +03:00
Serkan Reis
9995fa40c0 Clean up before initial release - 2 2023-02-23 15:26:42 +03:00
Serkan Reis
6a8cf279a2 Clean up before initial release 2023-02-23 15:22:55 +03:00
Serkan Reis
a45384ba07 Enable mint_by_key() on Badge Hub Dashboard > Execute 2023-02-23 13:47:47 +03:00
Serkan Reis
e173d6e7c3 Temporarily disable MintRule: By_Keys & By_Minter related types 2023-02-23 13:18:30 +03:00
Serkan Reis
4f71cad38e Enable createBadge() on Badge Hub Dashboard 2023-02-23 13:07:30 +03:00
Serkan Reis
769be8d369 Simplify Creator Income Dashboard link 2023-02-23 12:25:35 +03:00
Serkan Reis
baf0a33475 Add a Creator Income Dashboard link to the sidebar for mainnet use 2023-02-23 11:33:42 +03:00
Serkan Reis
80f8c77001 Make linter happy after conflict resolution 2023-02-23 09:55:30 +03:00
Serkan Reis
26a8823757
Merge branch 'develop' into badge-integration 2023-02-23 09:47:05 +03:00
Serkan Reis
56afc889f4
Merge pull request #107 from public-awesome/develop
Sync development>main
2023-02-22 23:27:07 +03:00
Adnan Deniz corlu
b6033ef62a
Merge pull request #106 from public-awesome/income-dashboard
Include Creator Income Dashboard link on the sidebar
2023-02-22 23:23:58 +03:00
Serkan Reis
518c0f2dbe Switch redirection on/off wrt agreement-checkbox status 2023-02-22 22:43:52 +03:00
Serkan Reis
d1b8988cc0 Update placeholder href for Creator Income Dashboard 2023-02-22 22:34:31 +03:00
Serkan Reis
eee78a1d94 Update Confirm button width 2023-02-22 21:00:53 +03:00
Serkan Reis
e8175c3da2 Update disclaimer modal ID 2023-02-22 20:57:51 +03:00
Serkan Reis
7a4460e9b9 Include Creator Income Dashboard link on the sidebar 2023-02-22 20:52:00 +03:00
Serkan Reis
0a66d747dd Display private key on badge creation summary UI 2023-02-22 10:17:24 +03:00
Serkan Reis
a6ab7c2044 Update visit profile icon on My Badges 2023-02-22 09:29:14 +03:00
Serkan Reis
920f6507e8 My Badges init 2023-02-21 21:22:44 +03:00
Serkan Reis
acd182dc60 Implement Badge Actions > Airdrop by Key 2023-02-21 20:39:42 +03:00
Serkan Reis
e1adca8ddf Implement initial mint_by_key logic for Badge Actions 2023-02-21 18:26:10 +03:00
Serkan Reis
bdd39d2dc2 Remove create badge option from Badge Hub Dashboard > Execute 2023-02-21 14:58:25 +03:00
Serkan Reis
4a11d08ca9 Implement editBadge for Badge Hub Dashboard > Execute 2023-02-21 10:50:13 +03:00
Serkan Reis
edccae535e Clean up Badge Actions > Actions 2023-02-21 09:45:26 +03:00
Serkan Reis
080c74a110 Calculate editBadge() fee on the go 2023-02-21 00:00:11 +03:00
Serkan Reis
53e43476d6
Merge pull request #105 from public-awesome/develop
Sync Development > Main
2023-02-20 13:01:34 +03:00
Adnan Deniz corlu
e5c5881f42
Merge pull request #104 from public-awesome/update-my-collections
IPFS gateway update for My Collections
2023-02-20 12:57:15 +03:00
Serkan Reis
e1adf87dac Display Mint Rule on Badge Actions 2023-02-20 12:13:36 +03:00
Serkan Reis
b6f6a0fb52 Filter queries by Mint Rule 2023-02-20 12:01:33 +03:00
Serkan Reis
655e5f69d2 Filter actions by Mint Rule 2023-02-20 11:55:41 +03:00
Serkan Reis
189b636ff9 Badge Actions init 2023-02-19 20:01:33 +03:00
Serkan Reis
78303905c5 Init Queries.tsx for Badge Actions > Queries 2023-02-19 20:00:54 +03:00
Serkan Reis
b3db6e2ed8 Init Action.tsx for Badge Actions > Actions 2023-02-19 20:00:24 +03:00
Serkan Reis
965fbeede7 Init query.ts for Badge Actions 2023-02-19 19:58:42 +03:00
Serkan Reis
8655449914 Implement combobox for Badge Actions > Queries 2023-02-19 19:57:40 +03:00
Serkan Reis
71eed12e71 Implement combobox for Badge Actions > Actions 2023-02-19 13:59:37 +03:00
Serkan Reis
16a0f037e0 Init actions.ts for Badge Actions 2023-02-19 13:42:07 +03:00
Serkan Reis
d70903920d Bump Studio version 2023-02-19 10:46:20 +03:00
Serkan Reis
72670c7022 Update IPFS Gateway for My Collections 2023-02-19 10:45:53 +03:00
Serkan Reis
4bb9c61c6f Update landing pages 2023-02-16 10:51:30 +03:00
Serkan Reis
431d9245c4 Sidebar update 2023-02-15 17:17:30 +03:00
Serkan Reis
c16300e84b Update wallet popover z-index 2023-02-14 21:27:33 +03:00
Serkan Reis
d5b2c0066d Update font size for sidebar items 2023-02-14 21:16:28 +03:00
Serkan Reis
262dade7a5 Update Sidebar 2023-02-14 20:45:25 +03:00
Serkan Reis
3ec389fc39 Update Badge Hub dashboard > Query 2023-02-14 11:26:22 +03:00
Serkan Reis
3a080daf25 Update confirmation modal 2023-02-13 19:59:16 +03:00
Serkan Reis
007c0127ad Update upload splash 2023-02-13 19:58:45 +03:00
Serkan Reis
e83df5ee67 Display badge creation info 2023-02-13 17:58:28 +03:00
Serkan Reis
4b1b64b656 Display QR code 2023-02-13 13:33:36 +03:00
Serkan Reis
c84071637c Image upload prior to badge creation 2023-02-13 12:32:14 +03:00
Serkan Reis
8c3556cf9c Badge Details > Badge creation payload 2023-02-12 20:06:16 +03:00
Serkan Reis
a3c5a23095 BadgeDetails init 2023-02-12 17:41:14 +03:00
Serkan Reis
db22331175 Create Badge init 2023-02-11 20:16:36 +03:00
Serkan Reis
b9003f4f75 Update badge related Sidebar links 2023-02-10 16:46:42 +03:00
Serkan Reis
4aefaaf8d2 Update Sidebar 2023-02-10 16:37:39 +03:00
Serkan Reis
f8232b27ea Implement Badges welcome screen 2023-02-10 16:04:59 +03:00
Serkan Reis
0f4dd53ad2 Improve Badge Hub dashboard > Execute UI 2023-02-10 15:49:42 +03:00
Serkan Reis
876c271b9c Generate QR Code for badge claim on Badge Hub Dashboard > Execute 2023-02-10 14:03:20 +03:00
Serkan Reis
04781069b7 Calculate badge creation fee 2023-02-10 10:46:41 +03:00
Serkan Reis
c626864f0b Generate public & private key pair for badge creation 2023-02-06 19:49:11 +03:00
Serkan Reis
cb30fbf13c Complete createBadge related fields on Badge Hub dashboard > Execute 2023-02-06 16:02:40 +03:00
Serkan Reis
6d0adcc355
Merge pull request #101 from public-awesome/develop
Sync development > main
2023-02-05 14:38:00 +03:00
Serkan Reis
dd695b1305
Merge pull request #100 from public-awesome/fixes-and-improvements
Fix and improvements
2023-02-04 22:53:48 +03:00
Serkan Reis
5a903f691e Execute Create Badge init 2023-02-04 19:18:41 +03:00
Serkan Reis
59cbe7cb73 Bump Studio version 2023-02-04 11:30:35 +03:00
Serkan Reis
56e7986fbd Metadata file count check prior to batch minting for 1/1 collections 2023-02-04 11:28:41 +03:00
Serkan Reis
5d888dd8d6 Update minter instantiation error toast durations as 10s 2023-02-04 11:10:06 +03:00
Serkan Reis
dc2e0c421e Remove withdraw{} from collection actions 2023-02-04 11:00:55 +03:00
Serkan Reis
a3e54a29a3 Fix: Using an existing token URI triggers batch minting for 1/1 collections 2023-02-04 10:47:38 +03:00
Serkan Reis
5c5ccbe392 Badge Hub dashboard > Migrate tab 2023-02-03 19:20:26 +03:00
Serkan Reis
0d7d87f254 Badge Hub dashboard > Query tab init 2023-02-03 18:13:15 +03:00
Serkan Reis
b17c4172a3 Implement metadata/attributes input logic for Badge Hub dashboard execute tab 2023-02-02 18:14:57 +03:00
Serkan Reis
b6f46236b6 Badge Hub Dashboard > Execute Tab init 2023-02-02 16:39:00 +03:00
Serkan Reis
5a15d7935e Folder rename 2023-02-02 11:34:28 +03:00
Serkan Reis
3870159797
Merge pull request #99 from public-awesome/develop
Sync development > main
2023-02-01 16:30:35 +03:00
Serkan Reis
2d3ccfd770
Merge pull request #94 from public-awesome/warm-up
Warming Module
2023-02-01 16:26:55 +03:00
Serkan Reis
5a8dbcba4f Remove Lister.tsx 2023-02-01 16:22:15 +03:00
Serkan Reis
12479437fd Update maintainer list - add new line 2023-02-01 16:20:42 +03:00
Serkan Reis
55067c9256 Update maintainer list 2023-02-01 16:17:39 +03:00
Serkan Reis
798ab3e071 Badge Hub instantiate.tsx init 2023-02-01 15:57:00 +03:00
Serkan Reis
34ff4bf973 Implement badge-hub queries & actions 2023-02-01 15:55:52 +03:00
Serkan Reis
5a00d329c1 Update LinkTabs to include badgeHubLinkTabs 2023-02-01 15:54:18 +03:00
Serkan Reis
9169c10d97 Environment variable updates for badge-hub & badge-nft 2023-02-01 15:47:33 +03:00
Serkan Reis
c6b503f01a Init badge-hub contract helpers 2023-01-31 20:31:06 +03:00
name-user1
94d7d1571c Minor changes 2023-01-31 17:14:58 +03:00
Serkan Reis
c0ba8d4715
Merge pull request #98 from public-awesome/develop
Sync development > main
2023-01-24 14:30:07 +03:00
Serkan Reis
af3aba109b
Merge pull request #97 from public-awesome/html-file-support
Upload support for .html files during collection creation
2023-01-24 14:26:46 +03:00
Serkan Reis
db4d8dd929 Bump Studio version 2023-01-24 14:15:21 +03:00
Serkan Reis
e59d62aafc Update upload logic for .html files 2023-01-24 14:14:29 +03:00
Serkan Reis
2ad2cd11ec Update SingleAssetPreview for .html files 2023-01-24 14:04:29 +03:00
Serkan Reis
70ce08a98a Update MetadataModal asset preview for .html files 2023-01-24 13:45:47 +03:00
Serkan Reis
db44f85fdc Update AssetsPreview for .html files 2023-01-24 11:16:29 +03:00
Serkan Reis
7ec8f94fb6 Update valid file types for asset selection 2023-01-24 10:23:41 +03:00
Serkan Reis
f39e06508e Update getAssetType() 2023-01-24 09:48:50 +03:00
Serkan Reis
600a5e063c
Merge pull request #96 from public-awesome/develop
Sync development > main
2023-01-21 15:04:24 +03:00
Serkan Reis
03c8a86417
Merge pull request #95 from public-awesome/update-collection-creation-fee
Update collection creation fee
2023-01-20 16:58:15 +03:00
name-user1
de263c604e Progress bar added 2023-01-20 16:57:09 +03:00
Serkan Reis
2cdada33e0 Update eslint exceptions 2023-01-20 16:53:52 +03:00
Serkan Reis
55a759fa56 Bump Studio version 2023-01-20 16:48:03 +03:00
Serkan Reis
823e6de84a Update collection creation fee 2023-01-20 16:44:42 +03:00
Serkan Reis
aeec57bdef Query update 2023-01-19 13:30:16 +03:00
name-user1
de79ffa1db Routing 2023-01-19 12:12:57 +03:00
name-user1
fc20a541e2 Small fixes 2023-01-19 10:59:55 +03:00
name-user1
3b6224db13 Warming Module 2023-01-19 10:34:28 +03:00
Serkan Reis
489c53ed23
Merge pull request #93 from public-awesome/update_check_wallet_balance
Update checkWalletBalance() to cover Base Minter instantiation
2023-01-18 10:29:42 +03:00
Serkan Reis
06239329c4 Update checkWalletBalance() to cover Base Minter instantiation 2023-01-18 10:24:25 +03:00
Serkan Reis
940cf06d6b
Merge pull request #92 from public-awesome/update-vending-minter-init-fee
Update vending minter instantiation fee
2023-01-18 09:24:07 +03:00
Serkan Reis
c384f2ca72 Bump Studio version 2023-01-18 07:48:19 +03:00
Serkan Reis
26899000b7 Update vending minter instantiation fee 2023-01-18 07:47:03 +03:00
Serkan Reis
4c6442595c
Merge pull request #91 from public-awesome/develop
Sync development > main
2023-01-17 16:48:15 +03:00
Serkan Reis
6ed323290b
Merge pull request #90 from public-awesome/batch-1-1-minting
Batch minting support for 1/1 Collections
2023-01-17 16:45:37 +03:00
Serkan Reis
c1f716637c Bump Studio version 2023-01-17 16:41:23 +03:00
Serkan Reis
75bbcb011b Update asset selection checks for 1/1 collections - 2 2023-01-17 16:39:05 +03:00
Serkan Reis
ff726f6a27 Update asset selection checks for 1/1 collections 2023-01-17 16:26:56 +03:00
Serkan Reis
a372f70e31 Implement batch minting for appending tokens 2023-01-17 16:23:17 +03:00
Serkan Reis
5ff7f6f649 Implement batch minting for 1/1 Collection Creation 2023-01-16 16:48:38 +03:00
Serkan Reis
3821e89c53 Implement batchMint() for contracts/baseMinter 2023-01-16 15:16:34 +03:00
Serkan Reis
e5cb8faf51 Enable multiple asset selection & preview for 1/1 Minting 2023-01-16 13:24:23 +03:00
Jorge Hernandez
e6b37bb95d
update version 2023-01-12 16:57:24 -06:00
Serkan Reis
a57af69c49
Merge pull request #89 from public-awesome/develop
Sync development > main
2023-01-12 14:45:58 +03:00
Serkan Reis
a01fc71755
Merge pull request #88 from public-awesome/additional-names-support
Additional Stargaze Names support
2023-01-12 14:44:16 +03:00
Serkan Reis
74ee927fac Bump Studio version 2023-01-12 14:40:04 +03:00
Serkan Reis
7f67b91103 Names support for Whitelist contract dashboard > Query 2023-01-12 14:04:33 +03:00
Serkan Reis
c9d9a32034 Names support for SG721 contract dashboard > Execute 2023-01-12 14:01:15 +03:00
Serkan Reis
7e343657a4 Names support for SG721 contract dashboard > Query 2023-01-12 13:51:03 +03:00
Serkan Reis
3706102a39 Names support for Vending Minter dashboard > Execute 2023-01-12 13:46:14 +03:00
Serkan Reis
c28d1bbd09 Names support for Vending Minter dashboard > Query 2023-01-12 13:39:25 +03:00
Serkan Reis
57a8ba4c92 Names support for Vending Minter dashboard > Instantiate 2023-01-12 13:31:49 +03:00
Serkan Reis
84aa8823ff Names support for Base Minter dashboard > Instantiate 2023-01-12 13:26:05 +03:00
Serkan Reis
664237d628 Names support for Create Collection > Royalty Payment Address 2023-01-12 13:01:11 +03:00
Serkan Reis
7669c5ebfe Names support for Collection Actions > Queries 2023-01-12 11:45:57 +03:00
Serkan Reis
468bf95cc2 Names support for Collection Actions > Airdrop Tokens 2023-01-12 10:18:10 +03:00
Serkan Reis
f1a2c153e0 Names support for Collection Actions > Recipient Address, Royalty Payment Address 2023-01-10 13:37:45 +03:00
Serkan Reis
813d0e27f8
Merge pull request #87 from public-awesome/develop
Sync development > main
2023-01-09 10:18:18 +03:00
Serkan Reis
c6ea1baf04
Merge pull request #86 from public-awesome/file-support-for-WL-execute-tab
File selection option for WL dashboard > Execute > Add/Remove Members
2023-01-09 10:16:47 +03:00
Serkan Reis
cb8eeb0dfd Added check for external_link protocol 2023-01-09 09:38:13 +03:00
Serkan Reis
ef80449b91 Minor UI update on UploadDetails 2023-01-08 20:27:37 +03:00
Serkan Reis
123cbea66c Bump Studio version 2023-01-08 20:16:56 +03:00
Serkan Reis
6e1f1905b3 Update mint & append token success toasts 2023-01-08 20:12:15 +03:00
Serkan Reis
0066fddae8 Rename mint & append token UI elements 2023-01-08 20:09:05 +03:00
Serkan Reis
ce38980eff Perform description length check with checkCollectionDetails() 2023-01-08 19:53:58 +03:00
Serkan Reis
fd54468d06 Add instructions for WL > Execute > Add/Remove Members 2023-01-08 19:41:02 +03:00
Serkan Reis
80bec18cc7 Include file selection support for WL > Add Members 2023-01-06 17:19:43 +03:00
Serkan Reis
3d40de95b1
Merge pull request #85 from public-awesome/develop
Sync development > main
2023-01-05 14:46:06 +03:00
Serkan Reis
7bec3b1a70
Merge pull request #84 from public-awesome/improve-base-minter-ui
Base Minter UI improvements
2023-01-05 14:44:52 +03:00
Serkan Reis
29e3c31864 Bump Studio version 2023-01-05 14:40:11 +03:00
Serkan Reis
0def7e0aad Update Vending Minter tab name & description 2023-01-05 14:38:01 +03:00
Serkan Reis
c7b26d133d Update Append New Token description on Collection Actions 2023-01-05 14:17:46 +03:00
Serkan Reis
fa32649633 Update Mint Token URI name & description on Collection Actions 2023-01-05 14:14:42 +03:00
Serkan Reis
67d414a55f Update 1/1 Collection tab name and description 2023-01-05 14:09:57 +03:00
Serkan Reis
950d5d46c8 Add wallet connection check for collection retrieval 2023-01-05 13:31:48 +03:00
Serkan Reis
2ad0d893c6 Append a new token UI update 2023-01-05 13:00:10 +03:00
Serkan Reis
6e2f41342a Updated names for 1/1 minting tabs 2023-01-02 13:35:06 +03:00
Serkan Reis
045679bbd2
Merge pull request #83 from public-awesome/develop
Sync development > main
2022-12-26 16:23:54 +03:00
Serkan Reis
0cb5e0e6b6
Merge pull request #82 from public-awesome/manual-metadata-input
Manual metadata input option for 1/1 minting
2022-12-26 16:22:40 +03:00
Serkan Reis
b4337df3cc Bump Studio version to 0.3.5 2022-12-26 16:18:14 +03:00
Serkan Reis
a6abf4a2fc Implement manual metadata input functionality for 1/1 minting 2022-12-26 16:17:09 +03:00
Serkan Reis
aff06448a5
Merge pull request #81 from public-awesome/develop
Sync development > main
2022-12-21 10:51:41 +03:00
Serkan Reis
e1eaec89b9
Merge pull request #80 from public-awesome/whitelist-per-address-limit-check
New checks to perform during colection creation
2022-12-21 10:50:37 +03:00
Serkan Reis
d2a0b40f09 Bump Studio version 2022-12-21 10:39:33 +03:00
Serkan Reis
9e462c0343 Update the success message for address resolution 2022-12-21 10:38:12 +03:00
Serkan Reis
6b6d0a34a1 Add wallet balance check for WL and/or Vending Minter instantiation 2022-12-21 10:36:43 +03:00
Serkan Reis
eaf3485bab Add per address limit check for new and existing WLs 2022-12-21 09:24:54 +03:00
Serkan Reis
9cd952122e
Merge pull request #79 from public-awesome/develop
Sync development > main
2022-12-20 15:01:53 +03:00
Serkan Reis
ff0a910d37
Merge pull request #78 from public-awesome/update-mint-price-changes
Implement minting start time check prior to update_mint_price()
2022-12-20 15:00:33 +03:00
Serkan Reis
b3614095e2 Bump Studio version to 0.3.3 2022-12-20 14:55:55 +03:00
Serkan Reis
f7eac26f98 Check minting price & start time before update_mint_price() on Vending Minter/execute tab 2022-12-20 14:54:49 +03:00
Serkan Reis
6046d899b3 Check minting price & start time before update_mint_price() on Collection Actions 2022-12-20 14:36:52 +03:00
Serkan Reis
27500f8474
Merge pull request #77 from public-awesome/develop
Sync development > mainnet
2022-12-19 17:59:39 +03:00
Serkan Reis
ca7283a2ec
Merge pull request #76 from public-awesome/whitelisting-names
Stargaze Names support for WL addresses on Collection Creation Page & WL contract dashboard
2022-12-19 17:52:46 +03:00
Serkan Reis
7c93d7f73f Collection Creation, Sidebar & Dashboard landing page changes for mainnet compatibility 2022-12-19 17:38:51 +03:00
Serkan Reis
9f167b647f Bump Studio version to 0.3.2 2022-12-19 16:09:24 +03:00
Serkan Reis
7a3b3b763f Stargaze Names support for Whitelist Execute Tab - Add/Remove member 2022-12-19 16:08:20 +03:00
Serkan Reis
66e865277d Stargaze Names WL support on Collection Creation 2022-12-19 15:01:00 +03:00
Serkan Reis
03ad2f816e
Merge pull request #75 from public-awesome/ui-improvements
Base Minter related UI improvements
2022-12-15 15:10:00 +03:00
Serkan Reis
285097870e Bump Studio version 2022-12-15 15:00:04 +03:00
Serkan Reis
2ee9f1e73d Update Base Minter creation process 2022-12-15 14:59:05 +03:00
Serkan Reis
eab5a4a36c Update contract dashboard landing page 2022-12-15 13:58:38 +03:00
Serkan Reis
2edbea9245 Update asset preview for the Base Minter option 2022-12-15 12:03:42 +03:00
Serkan Reis
64932a617e
Merge pull request #73 from public-awesome/base-contract-support
Initial base-minter contract support
2022-12-14 14:54:43 +03:00
Serkan Reis
b881ffa5b0 Update colors for combobox options 2022-12-14 10:16:27 +03:00
Serkan Reis
35309e220a Update react-hot-toast version 2022-12-13 22:21:05 +03:00
Serkan Reis
8993b1b9c0 Update checkUploadDetails() 2022-12-13 21:58:40 +03:00
Serkan Reis
fe4da95566 Implement 1/1 minting UI 2022-12-13 21:50:42 +03:00
Serkan Reis
5c6c87eb9e Isolate Base & Vending Minter creation UI 2022-12-13 15:52:43 +03:00
Serkan Reis
637294c9b6 Update minter type retrieval dependencies 2022-12-09 23:26:43 +03:00
Serkan Reis
f19c6348e1 Update mint() subtitle for baseMinter/execute 2022-12-09 22:58:07 +03:00
Serkan Reis
1e6d6c4330 Update .env.example 2022-12-09 22:18:08 +03:00
Serkan Reis
6da7d8ff4e Bump Studio version to v0.3.0 2022-12-09 21:49:16 +03:00
Serkan Reis
c9d4734417 Implement Collection Actions changes for Base Minter contract 2022-12-09 21:47:03 +03:00
Serkan Reis
a8c2548554 Implement Base Minter Contract dashboard 2022-12-09 11:27:50 +03:00
Serkan Reis
9258432d50
Merge pull request #72 from public-awesome/develop
Sync development > main
2022-12-01 10:26:07 +03:00
Serkan Reis
b690022655
Merge pull request #71 from public-awesome/migrate-option
Add Migrate tab to Minter & SG721 contract dashboards
2022-12-01 10:24:18 +03:00
Serkan Reis
cfdb83f314 Fix Execute tab highlighting for Whitelist contract dashboard 2022-11-30 21:43:50 +03:00
Serkan Reis
412a8b18c2 Add migrate tab to Minter & SG721 contract dashboards 2022-11-30 21:35:01 +03:00
Serkan Reis
d40ed12c5e
Merge pull request #69 from public-awesome/develop
Sync development > main
2022-11-24 09:25:58 +03:00
Serkan Reis
2221684f74
Merge pull request #68 from public-awesome/base-token-uri-scheme-check
Convert base token URI and cover image URL schemes to lowercase
2022-11-24 09:24:43 +03:00
Serkan Reis
fdabfac8e0 Convert base token URI and cover image URL schemes to lowercase 2022-11-24 09:22:32 +03:00
Jorge Hernandez
7ace365327
Merge pull request #66 from public-awesome/develop
revert ipfs lowercase
2022-11-23 14:03:29 -06:00
jhernandezb
281a70d778 revert ipfs lowercase 2022-11-23 14:02:33 -06:00
Serkan Reis
3edd21502a
Merge pull request #65 from public-awesome/develop
Sync development > main
2022-11-18 19:59:26 +03:00
Serkan Reis
6e5f06708f
Merge pull request #64 from public-awesome/clean-existing-urls
Implement new checks & user input cleaners
2022-11-18 19:56:28 +03:00
Serkan Reis
381bbb35d4 Clean royalty payment address 2022-11-18 19:54:02 +03:00
Serkan Reis
572becd4fc Clean existing whitelist address 2022-11-18 19:50:33 +03:00
Serkan Reis
8033d36aa4 Bump Studio version to 0.2.7 2022-11-18 19:15:49 +03:00
Serkan Reis
75c77e5a53 Check if existing/new whitelist start time = minting start time 2022-11-18 19:15:06 +03:00
Serkan Reis
b4848506f3 Clean whitelist addresses 2022-11-18 19:10:52 +03:00
Serkan Reis
e22ed6b321 Clean existing Base Token URI & Cover Image URL 2022-11-18 19:10:16 +03:00
Serkan Reis
fa108fe746
Merge pull request #62 from public-awesome/develop
Sync development > main
2022-11-08 16:11:18 +03:00
Serkan Reis
bac11411c0
Merge pull request #61 from public-awesome/fix-reversed-metadata-order
Fix: metadata gets sorted in reverse on some browsers
2022-11-08 16:09:47 +03:00
Serkan Reis
73fc03aa10 Fix: metadata gets sorted in reverse in some browsers 2022-11-08 16:03:46 +03:00
Serkan Reis
f5d3906b41
Merge pull request #60 from public-awesome/develop
Sync development > main
2022-11-08 14:51:35 +03:00
Serkan Reis
4044221b0d
Merge pull request #59 from public-awesome/metadata-validity-check
Add validity check for metadata files upon selection
2022-11-08 14:48:55 +03:00
Serkan Reis
bbacfcc3bc Bump Studio version 2022-11-08 12:20:21 +03:00
Serkan Reis
23674ac819 Add validity check for metadata files upon selection 2022-11-08 12:15:45 +03:00
Serkan Reis
996469d556
Merge pull request #58 from public-awesome/develop
Sync development > main
2022-11-07 10:52:12 +03:00
Serkan Reis
a4f7138154
Merge pull request #57 from public-awesome/whitelist-file-clean-up
Clean up whitelist file contents prior to collection creation
2022-11-07 10:49:26 +03:00
Serkan Reis
68e6b80d2d Selection reset logic update 2022-11-04 16:25:31 +03:00
Serkan Reis
2579e449be Clean up whitelist file contents prior to collection creation 2022-11-04 14:53:11 +03:00
Serkan Reis
2f571d547b
Merge pull request #55 from public-awesome/develop
Sync development > main
2022-11-02 10:58:15 +03:00
Serkan Reis
a29cd50886
Merge pull request #54 from public-awesome/wider-error-messages
Remove maxWidth for error messages
2022-11-02 10:56:24 +03:00
Serkan Reis
fdec20798e Remove maxWidth for error messages 2022-11-02 10:53:17 +03:00
Serkan Reis
aac3665781
Merge pull request #53 from public-awesome/develop
Merge development > main
2022-10-31 10:12:20 +03:00
Serkan Reis
9eb2fdf302
Merge pull request #52 from public-awesome/metadata-modal-update
Metadata modal update
2022-10-31 10:06:59 +03:00
Serkan Reis
c026a3ca32 Update metadata modal behavior when no metadata selected 2022-10-31 10:03:21 +03:00
Serkan Reis
920ad1965d Fix: external_url updates do not persist 2022-10-31 09:31:23 +03:00
jhernandezb
1dc6f63996 remove validation 2022-10-28 12:56:54 -06:00
Jorge Hernandez
6386429693
Merge pull request #51 from public-awesome/develop
dev>main
2022-10-28 07:57:56 -06:00
Jorge Hernandez
84bba503e8
Merge pull request #50 from public-awesome/batch-mint-for
Add batch mint_for functionality to collection actions
2022-10-28 07:57:33 -06:00
Serkan Reis
12ba37e5c3 Update Studio version 2022-10-28 13:06:41 +03:00
Serkan Reis
f86be40cc3 Add batch mint_for() functionality to Collection Actions 2022-10-28 13:05:02 +03:00
Jorge Hernandez
60de59a5ad
Merge pull request #49 from public-awesome/main
Main
2022-10-28 00:10:12 -06:00
Jorge Hernandez
cebfbf48c5
Update package.json 2022-10-28 00:09:46 -06:00
Jorge Hernandez
8d70edd6c0
Merge pull request #48 from public-awesome/develop
dev>main
2022-10-28 00:08:27 -06:00
Jorge Hernandez
9949a50c6f
Merge pull request #44 from public-awesome/update-collection-info
Update & Freeze collection info functionality
2022-10-28 00:07:55 -06:00
Serkan Reis
e575ae3b0a Base Token URI display update 2022-10-27 10:15:26 +03:00
Jorge Hernandez
b77fa98856
Merge pull request #45 from public-awesome/terms-on-confirmation-modal
Add terms and conditions checkbox to collection creation confirmation modal
2022-10-26 11:33:39 -06:00
Serkan Reis
b58610e55e
Merge pull request #46 from public-awesome/yubrew/update-code-ids
update testnet code ids
2022-10-26 05:24:15 +03:00
Jorge Hernandez
c3e5c60a05
Merge pull request #47 from public-awesome/main
Main > dev
2022-10-25 16:19:05 -06:00
John Y
52f26b959a update testnet code ids 2022-10-25 17:33:23 -04:00
Serkan Reis
5ea8127128 Add terms and conditions to confirmation modal 2022-10-25 19:31:36 +03:00
Serkan Reis
eac77b006d Update trading start time subtitle on collection creation page 2022-10-24 19:03:23 +03:00
Serkan Reis
7a2e35a859 Add freeze_collection_info() to collection actions 2022-10-24 15:20:12 +03:00
Serkan Reis
8a07a182e5 Add update_collection_info() to collection actions 2022-10-24 12:09:43 +03:00
Jorge Hernandez
0350ba5f11
Merge pull request #43 from public-awesome/develop 2022-10-22 07:40:57 -06:00
Jorge Hernandez
7e42100567
Merge pull request #42 from public-awesome/fix-update-trading-start-time 2022-10-22 03:10:51 -06:00
Serkan Reis
248c85a5c2 Fix: price input for update_mint_price not being displayed - 2 2022-10-22 11:06:39 +03:00
Serkan Reis
91be311cfe Fix: price input for update_mint_price not being displayed 2022-10-22 08:48:10 +03:00
Serkan Reis
b65fabca43 Fix: non-functional update_start_trading_time() 2022-10-21 22:30:24 +03:00
jhernandezb
b4fe13f763 update version 2022-10-21 12:52:41 -06:00
jhernandezb
0fa90c76c0 Merge branch 'develop' into main 2022-10-21 12:49:09 -06:00
jhernandezb
c691a3e47d Merge branch 'develop' of github.com:public-awesome/stargaze-studio into develop 2022-10-21 12:48:55 -06:00
jhernandezb
c032ff7f39 fix update start time 2022-10-21 12:48:31 -06:00
Jorge Hernandez
55a3f4a970
Merge pull request #41 from public-awesome/develop
dev>main
2022-10-20 22:23:26 -06:00
Jorge Hernandez
6008a4b831
Merge pull request #39 from public-awesome/fix-external-link-issue
Fix: external_link related problem on collection creation
2022-10-20 22:18:58 -06:00
Serkan Reis
b451ac46bf Fix: external_link related problem when creating with an existing base token uri 2022-10-21 07:14:49 +03:00
Jorge Hernandez
477bc49529
Merge pull request #38 from public-awesome/develop
dev>main
2022-10-20 22:02:22 -06:00
jhernandezb
b2bb9c68a3 update version 2022-10-20 19:08:33 -06:00
name-user1
039b8b424b
Launchpad V2 sync (#34)
* V2 Sync

* v2 sync

* Launchpad V2 sync

* Update trading start time description

* Add explicit_content to CollectionDetails update dependencies

* Minor UI changes

* Update MintPriceMessage interface

* Add symbolState.value to CollectionDetails update dependencies

* Add external_link to Collection Details

* Remove the tab Instantiate from the minter contract dashboard

* Add price check for update_minting_price

* Implement dynamic per address limit check

* Add checks for trading start time

* Update Minter Contract Dashboard Instantiate Tab - 1

* Update Minter Contract Dashboard Instantiate Tab - 2

* Remove Instantiate tab from SG721 Contract Dashboard

* Update whitelist contract helpers

* Update whitelist instantiate fee wrt member limit

Co-authored-by: name-user1 <eray@deuslabs.fi>
Co-authored-by: Serkan Reis <serkanreis@gmail.com>
2022-10-20 19:02:52 -06:00
Jorge Hernandez
85c240725f
Merge pull request #37 from public-awesome/develop
dev>main
2022-10-20 11:44:14 -06:00
Serkan Reis
ce875e0c10
Merge pull request #33 from public-awesome/develop
Merge development > main
2022-10-10 13:51:21 +03:00
Serkan Reis
5e23598188
Merge pull request #31 from public-awesome/develop
Merge development > main
2022-10-07 14:50:10 +03:00
Serkan Reis
8b0c5edc38
Merge pull request #29 from public-awesome/develop
Merge development > main
2022-10-06 17:03:21 +03:00
Serkan Reis
beb6a51b08
Merge pull request #26 from public-awesome/develop
Merge development > main
2022-10-05 13:13:10 +03:00
Jorge Hernandez
1e91776c96
Merge pull request #24 from public-awesome/develop 2022-10-02 09:55:32 -05:00
Serkan Reis
f70ee70e59
Merge pull request #22 from public-awesome/develop
Merge development > main
2022-09-27 11:45:54 +03:00
Jorge Hernandez
cb25b03870
Merge pull request #19 from public-awesome/develop
dev>main
2022-09-24 10:59:04 -06:00
Jorge Hernandez
9c664a4eb6
Merge pull request #17 from public-awesome/develop
deploy to main
2022-09-23 08:33:24 -06:00
252 changed files with 53721 additions and 2589 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
CERC_MAX_GENERATE_TIME=180

View File

@ -1,18 +1,129 @@
APP_VERSION=0.1.0 APP_VERSION=0.8.7
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_WHITELIST_CODE_ID=3 NEXT_PUBLIC_SG721_CODE_ID=2595
NEXT_PUBLIC_MINTER_CODE_ID=2 NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=2596
NEXT_PUBLIC_SG721_CODE_ID=1 NEXT_PUBLIC_STRDST_SG721_CODE_ID=2595
NEXT_PUBLIC_BASE_FACTORY_SG721_CODE_ID=2595
NEXT_PUBLIC_OPEN_EDITION_SG721_CODE_ID=2595
NEXT_PUBLIC_OPEN_EDITION_SG721_UPDATABLE_CODE_ID=2596
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=2600
NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID=2601
NEXT_PUBLIC_BASE_MINTER_CODE_ID=2598
NEXT_PUBLIC_OPEN_EDITION_MINTER_CODE_ID=2579
NEXT_PUBLIC_API_URL=https:// NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars18h7ugh8eaug7wr0w4yjw0ls5s937z35pnkg935ucsek2y9xl3gaqqk4jtx"
NEXT_PUBLIC_FEATURED_VENDING_FACTORY_ADDRESS="stars14pd96yk3t6gq9l6uyrkg0n5dr09n8rt5y9v3at8x4wl4lrkxhlzq4trqmh"
NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1h65nms9gwg4vdktyqj84tu50gwlm34e0eczl5w2ezllxuzfxy9esa9qlt0"
NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1hvu2ghqkcnvhtj2fc6wuazxt4dqcftslp2rwkkkcxy269a35a9pq60ug2q"
NEXT_PUBLIC_VENDING_FACTORY_MERKLE_TREE_ADDRESS="stars167tudcsr9n2y9ljgk4cwxhs0cvkfkk0hh6c3dzngsz7m5s9jmqnsdgr3jy"
NEXT_PUBLIC_FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS="stars167tudcsr9n2y9ljgk4cwxhs0cvkfkk0hh6c3dzngsz7m5s9jmqnsdgr3jy"
NEXT_PUBLIC_FEATURED_VENDING_FACTORY_FLEX_ADDRESS="stars1udlmmnmmnnqamh36hy6d7azn3ycv23yymkmg6558ntalvyt2pz7s8lhgcd"
# NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USDC_FACTORY_ADDRESS=
# NEXT_PUBLIC_FEATURED_VENDING_IBC_USDC_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USK_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USK_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_ADDRESS=
# NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS=
# NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS=
# NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS=
NEXT_PUBLIC_VENDING_NATIVE_STARDUST_FACTORY_ADDRESS="stars1mxwf2hjcjvqnlw0v3j7m0u34975qesp325wzrgz0ht7vr8ys2zmsenjutf"
NEXT_PUBLIC_VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS="stars18gjczf88jd4z3a3megwj9g5c9famu654csxfnnq59mkqeszuzy4ssdgr46"
NEXT_PUBLIC_VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS="stars1eluqmr6x78ehl4plrln6khxc0qrspfhc7rt3whmr59escpve0r4swcacjh"
# NEXT_PUBLIC_VENDING_NATIVE_BRNCH_FACTORY_ADDRESS=""
# NEXT_PUBLIC_VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS=""
# NEXT_PUBLIC_VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS=""
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1a45hcxty3spnmm2f0papl8v4dk5ew29s4syhn4efte8u5haex99qlkrtnx"
NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars100xegx2syry4tclkmejjwxk4nfqahvcqhm9qxut5wxuzhj5d9qfsh5nmym"
NEXT_PUBLIC_OPEN_EDITION_FACTORY_ADDRESS="stars1sqweqcxlf2f7qhf27gn5naqusk5q52fkzewmy63c4sglvle3s7ls6k828e"
NEXT_PUBLIC_OPEN_EDITION_FACTORY_FLEX_ADDRESS="stars1nc59ddaa8xcx9mu8jladza82dznhxrta3njal3xylkqlsfqa7g4s9s5q02"
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS="stars1fk5dkzcylam8mcpqrn8y9spauvc3d4navtaqurcc49dc3p9f8d3qdkvymx"
NEXT_PUBLIC_VENDING_IBC_KUJI_FACTORY_ADDRESS="stars1yyje87e0h9mqg34kp3x75yesa78ve4glc3dstdrn6nscw3zjfanqkj95f0"
NEXT_PUBLIC_VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS="stars1jralxqalpw9nf3kdc0s222z3mk343wry60cjaze9xadgfn2te4usf92e9r"
NEXT_PUBLIC_VENDING_IBC_HUAHUA_FACTORY_ADDRESS="stars16luw6rxgr6as9s7eu5auvnk5tnzszjrs34etsw9fmk25yqjfq09qq9gzl4"
NEXT_PUBLIC_VENDING_IBC_HUAHUA_FACTORY_FLEX_ADDRESS="stars1d97h6nfgwqr8eynzdcrsm3p0n6rduvkrcqdjhm5z7heavtgnqg4sgy2yew"
NEXT_PUBLIC_VENDING_IBC_CRBRUS_FACTORY_ADDRESS="stars1z0upxsyxhrvygrsd2t69majd6wl8qw4h8ff2fp27z3nn93m73pwsu4hpdh"
NEXT_PUBLIC_VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS="stars1halhp674yxwgn3p4gpkl8790h07vkm0vjm4vj7y8ql499e3zydzqurt5m3"
# NEXT_PUBLIC_OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS=
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS=
# NEXT_PUBLIC_OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS="stars152a40mmd3k2kk90add606vrqxcvzdp29qrjx4pjv33cjl6svksfscrrtuk"
# NEXT_PUBLIC_OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS="stars10sz9mup3a548l34k83q5w59nrklrnvv2gdsdkr2xref4zl5j3d4q0efamx"
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS=
# NEXT_PUBLIC_OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS="stars1vza7k890fkejxz3mqwau0u2m89k9y76w94vvxe4d42ya9862ryfq0damns"
# NEXT_PUBLIC_OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS="stars1jgn0ntt5tut93yn756rrqa60794qdsrn6dwhl8vhfx0yxgpr44qsfzhmrt"
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS=
NEXT_PUBLIC_OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS="stars1vzffawsjhvspstu5lvtzz2x5n7zh07hnw09c9dfxcj78un05rcms5n3q3e"
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS="stars1tc09vlgdg8rqyapcxwm9qdq8naj4gym9px4ntue9cs0kse5rvess0nee3a"
NEXT_PUBLIC_OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS="stars10sw8fvwtetndy3ctpcvee8yq7t6qp49m5yahm5gf8qz3qt3hzvcq5c2m0s"
NEXT_PUBLIC_OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS="stars1uxdqnu9ysd9q8kd43c52ufy9azfxyuvyt5nnyk4p2gtag30zre3q0cg30z"
NEXT_PUBLIC_OPEN_EDITION_IBC_USK_FACTORY_ADDRESS="stars1vxf9u6a4d5ty00k59zthv7mnpzlrfhqnf4ds0y0eake7lepuamnqymyf3t"
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS="stars1njhkyyv0l8dmq528w67t8dxyg5a3h0hvusk6pfvpm52pspd9gq9s3zmdez"
NEXT_PUBLIC_OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS="stars1yjvfy6fpm4nxl0afm6e8lnx96e6v49e3fxsymsdxxtu0pdeshrxq702zaz"
NEXT_PUBLIC_OPEN_EDITION_IBC_HUAHUA_FACTORY_ADDRESS="stars1grxlqatna07y8f3tzu2l9lmt82uj8gzzshxnz2ruwn6yljpyucnq059rmn"
# NEXT_PUBLIC_OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS=""
NEXT_PUBLIC_OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS="stars1tjzlz2e8pkucgytkjct5drt7x0dysnepqv3nmvxn0fzk2hfv73zsneevyt"
NEXT_PUBLIC_OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS="stars1cd4gykxfq4nc4yx8uzn8yr3ggu86r57chhxme4y7q2jag53cw75qgs96u8"
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS="stars1d57xe77mvcg5q337umf4qz49vumfn6w3wss0t7u8ra6s3cyvezsqyaeejn"
NEXT_PUBLIC_VENDING_IBC_NBTC_FACTORY_ADDRESS="stars1e6t6lp052er2gu3rwjnf434vgh59ydkfg8dm589fxlx593afqmuqh75a0s"
NEXT_PUBLIC_VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS="stars1k6ee8qgwvumguqnqqrvsnwluwk0rp994nkcgdemk0tj3ecc5kk8su2tcr4"
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
NEXT_PUBLIC_ROYALTY_REGISTRY_ADDRESS="stars1crgx0f70fzksa57hq87wtl8f04h0qyk5la0hk0fu8dyhl67ju80qaxzr5z"
NEXT_PUBLIC_INFINITY_SWAP_PROTOCOL_ADDRESS="stars136yp6fl9h66m0cwv8weu4w4aawveuz40992ty0atj5ecjd8z0thqv9xpy5"
NEXT_PUBLIC_WHITELIST_CODE_ID=4008
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=4009
NEXT_PUBLIC_WHITELIST_MERKLE_TREE_CODE_ID=3911
NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336
NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa"
NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337
NEXT_PUBLIC_BADGE_NFT_ADDRESS="stars1vlw4y54dyzt3zg7phj8yey9fg4zj49czknssngwmgrnwymyktztstalg7t"
NEXT_PUBLIC_SPLITS_CODE_ID=4010
NEXT_PUBLIC_CW4_GROUP_CODE_ID=1904
NEXT_PUBLIC_API_URL=https://nft-api.elgafar-1.stargaze-apis.com
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
NEXT_PUBLIC_NETWORK=testnet NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev
NEXT_PUBLIC_WEBSITE_URL=https:// NEXT_PUBLIC_WEBSITE_URL=https://
NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL="https://..."
NEXT_PUBLIC_WHITELIST_MERKLE_TREE_API_URL="https://..."
NEXT_PUBLIC_NFT_STORAGE_DEFAULT_API_KEY="..."
NEXT_PUBLIC_S3_BUCKET= # TODO NEXT_PUBLIC_MEILISEARCH_HOST="https://search.publicawesome.dev"
NEXT_PUBLIC_S3_ENDPOINT= # TODO NEXT_PUBLIC_MEILISEARCH_API_KEY= "..."
NEXT_PUBLIC_S3_KEY= # TODO
NEXT_PUBLIC_S3_REGION= # TODO
NEXT_PUBLIC_S3_SECRET= # TODO

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @findolor @MightOfOaks @name-user1 * @MightOfOaks @name-user1 @Ninjatosba

45
.github/workflows/publish.yaml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Publish ApplicationRecord to Registry
on:
release:
types: [published]
push:
branches:
- main
- '*'
env:
CERC_REGISTRY_USER_KEY: ${{ secrets.CICD_LACONIC_USER_KEY }}
CERC_REGISTRY_BOND_ID: ${{ secrets.CICD_LACONIC_BOND_ID }}
jobs:
cns_publish:
runs-on: ubuntu-latest
steps:
- name: "Clone project repository"
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18 # though you need version 14 with geojson
# - name: "Install exiftool"
# run: |
# apt-get update -y
# apt-get upgrade -y
# apt-get install exiftool -y
#- name: "Exiftool Version"
# run: |
# exiftool -ver
- name: "Install Yarn"
run: npm install -g yarn
- name: "Install registry CLI"
run: |
npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
yarn global add @cerc-io/laconic-registry-cli
- name: "Install jq"
uses: dcarbone/install-jq-action@v2.1.0
- name: "Publish App Record"
run: scripts/publish-app-record.sh
#- name: "Create Metadata Record"
# run: scripts/create-metadata-record.sh
- name: "Request Deployment"
run: scripts/request-app-deployment.sh

View File

@ -1,16 +1,62 @@
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx' import clsx from 'clsx'
import React from 'react' import React, { useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { SG721_NAME_ADDRESS } from 'utils/constants'
import { csvToArray } from 'utils/csvToArray' import { csvToArray } from 'utils/csvToArray'
import type { AirdropAllocation } from 'utils/isValidAccountsFile' import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import { isValidAccountsFile } from 'utils/isValidAccountsFile' import { isValidAccountsFile } from 'utils/isValidAccountsFile'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
interface AirdropUploadProps { interface AirdropUploadProps {
onChange: (data: AirdropAllocation[]) => void onChange: (data: AirdropAllocation[]) => void
} }
export const AirdropUpload = ({ onChange }: AirdropUploadProps) => { export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
const wallet = useWallet()
const [resolvedAllocationData, setResolvedAllocationData] = useState<AirdropAllocation[]>([])
const resolveAllocationData = async (allocationData: AirdropAllocation[]) => {
if (!allocationData.length) return []
await new Promise((resolve) => {
let i = 0
allocationData.map(async (data) => {
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
await (
await wallet.getCosmWasmClient()
)
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(
data.address.trim().substring(0, data.address.lastIndexOf('.stars')),
).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri))
resolvedAllocationData.push({ address: tokenUri, amount: data.amount, tokenId: data.tokenId })
else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`)
})
.catch((e) => {
console.log(e)
toast.error(`Error resolving address for the name: ${data.address}`)
})
i++
if (i === allocationData.length) resolve(resolvedAllocationData)
})
})
return resolvedAllocationData
}
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedAllocationData([])
if (!event.target.files) return toast.error('Error opening file') if (!event.target.files) return toast.error('Error opening file')
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!event.target.files[0]?.name.endsWith('.csv')) { if (!event.target.files[0]?.name.endsWith('.csv')) {
@ -18,18 +64,38 @@ export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
return onChange([]) return onChange([])
} }
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e: ProgressEvent<FileReader>) => { reader.onload = async (e: ProgressEvent<FileReader>) => {
try { try {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
// eslint-disable-next-line @typescript-eslint/no-base-to-string // eslint-disable-next-line @typescript-eslint/no-base-to-string
const accountsData = csvToArray(e.target.result.toString()) const accountsData = csvToArray(e.target.result.toString())
console.log(accountsData)
if (!isValidAccountsFile(accountsData)) { if (!isValidAccountsFile(accountsData)) {
event.target.value = '' event.target.value = ''
return onChange([]) return onChange([])
} }
return onChange(accountsData) await resolveAllocationData(accountsData.filter((data) => data.address.trim().endsWith('.stars'))).finally(
() => {
return onChange(
accountsData
.filter((data) => data.address.startsWith('stars') && !data.address.endsWith('.stars'))
.map((data) => ({
address: data.address.trim(),
amount: data.amount,
tokenId: data.tokenId,
}))
.concat(
resolvedAllocationData.map((data) => ({
address: data.address,
amount: data.amount,
tokenId: data.tokenId,
})),
),
)
},
)
} catch (error: any) { } catch (error: any) {
toast.error(error.message) toast.error(error.message, { style: { maxWidth: 'none' } })
} }
} }
reader.readAsText(event.target.files[0]) reader.readAsText(event.target.files[0])

View File

@ -21,7 +21,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
tempArray.push( tempArray.push(
<video <video
key={assetFile.name} key={assetFile.name}
className="absolute px-1 my-1 max-h-24 thumbnail" className={clsx('absolute px-1 my-1 max-h-24 thumbnail')}
id="video" id="video"
muted muted
onMouseEnter={(e) => { onMouseEnter={(e) => {
@ -50,7 +50,9 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
return assetFilesArray.slice((page - 1) * ITEM_NUMBER, page * ITEM_NUMBER).map((assetSource, index) => ( return assetFilesArray.slice((page - 1) * ITEM_NUMBER, page * ITEM_NUMBER).map((assetSource, index) => (
<button <button
key={assetSource.name} key={assetSource.name}
className="relative p-0 w-[100px] h-[100px] bg-transparent hover:bg-transparent border-0 btn modal-button" className={clsx(
'relative p-0 w-[100px] h-[100px] bg-transparent hover:bg-transparent border-0 btn modal-button',
)}
onClick={() => { onClick={() => {
updateMetadataFileIndex((page - 1) * ITEM_NUMBER + index) updateMetadataFileIndex((page - 1) * ITEM_NUMBER + index)
}} }}
@ -69,9 +71,26 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
> >
{(page - 1) * 12 + (index + 1)} {(page - 1) * 12 + (index + 1)}
</div> </div>
{getAssetType(assetSource.name) === 'audio' && ( {getAssetType(assetSource.name) === 'audio' && (
<div className="flex absolute flex-col items-center mt-4 ml-2"> <div className="flex absolute flex-col items-center mt-4 ml-2">
<img key={`audio-${index}`} alt="audio_icon" className="mb-2 ml-1 w-6 h-6 thumbnail" src="/audio.png" /> <img
key={`audio-${index}`}
alt="audio_icon"
className={clsx('mb-2 ml-1 w-6 h-6 thumbnail')}
src="/audio.png"
/>
<span className="flex self-center ">{assetSource.name}</span>
</div>
)}
{getAssetType(assetSource.name) === 'document' && (
<div className="flex absolute flex-col items-center mt-4 ml-2">
<img
key={`document-${index}`}
alt="document_icon"
className={clsx('mb-2 ml-1 w-6 h-6 thumbnail')}
src="/pdf.png"
/>
<span className="flex self-center ">{assetSource.name}</span> <span className="flex self-center ">{assetSource.name}</span>
</div> </div>
)} )}
@ -83,11 +102,23 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
<img <img
key={`image-${index}`} key={`image-${index}`}
alt="asset" alt="asset"
className="px-1 my-1 max-h-24 thumbnail" className={clsx('px-1 my-1 max-h-24 thumbnail')}
src={URL.createObjectURL(assetSource)} src={URL.createObjectURL(assetSource)}
/> />
</div> </div>
)} )}
{getAssetType(assetSource.name) === 'html' && (
<div className="flex absolute flex-col items-center mt-4 ml-2">
<img
key={`html-${index}`}
alt="html_icon"
className={clsx('mb-2 ml-1 w-10 h-10 thumbnail')}
src="/html.png"
/>
<span className="flex self-center">{assetSource.name.toLowerCase()}</span>
</div>
)}
</label> </label>
</button> </button>
)) ))
@ -116,6 +147,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div> <div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div>
<div className="mt-5 btn-group"> <div className="mt-5 btn-group">
<button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button"> <button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button">
«« ««

View File

@ -0,0 +1,128 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import React, { useState } from 'react'
import { toast } from 'react-hot-toast'
import { useWallet } from 'utils/wallet'
import { SG721_NAME_ADDRESS } from '../utils/constants'
import { isValidAddress } from '../utils/isValidAddress'
interface BadgeAirdropListUploadProps {
onChange: (data: string[]) => void
}
export const BadgeAirdropListUpload = ({ onChange }: BadgeAirdropListUploadProps) => {
const wallet = useWallet()
const [resolvedAddresses, setResolvedAddresses] = useState<string[]>([])
const resolveAddresses = async (names: string[]) => {
await new Promise((resolve) => {
let i = 0
names.map(async (name) => {
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
await (
await wallet.getCosmWasmClient()
)
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri)
else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
})
.catch((e) => {
console.log(e)
toast.error(`Error resolving address for the name: ${name}.stars`)
})
i++
if (i === names.length) resolve(resolvedAddresses)
})
})
return resolvedAddresses
}
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedAddresses([])
if (!event.target.files) return toast.error('Error opening file')
if (event.target.files.length !== 1) {
toast.error('No file selected')
return onChange([])
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]?.type !== 'text/plain') {
toast.error('Invalid file type')
return onChange([])
}
const reader = new FileReader()
reader.onload = async (e: ProgressEvent<FileReader>) => {
const text = e.target?.result?.toString()
let newline = '\n'
if (text?.includes('\r')) newline = '\r'
if (text?.includes('\r\n')) newline = '\r\n'
const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '')
const data = cleanText?.split(newline)
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
const printableData = data?.map((item) => item.replace(regex, ''))
const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars'))
const strippedNames = names?.map((name) => name.split('.')[0])
console.log(names)
if (strippedNames?.length) {
await toast
.promise(resolveAddresses(strippedNames), {
loading: 'Resolving addresses...',
success: 'Address resolution finalized.',
error: 'Address resolution failed!',
})
.then((addresses) => {
console.log(addresses)
})
.catch((error) => {
console.log(error)
})
}
return onChange([
...new Set(
printableData
?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars'))
.concat(resolvedAddresses) || [],
),
])
}
reader.readAsText(event.target.files[0])
}
return (
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept=".txt"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="badge-airdrop-list-file"
multiple={false}
onChange={onFileChange}
type="file"
/>
</div>
)
}

View File

@ -0,0 +1,69 @@
import { useState } from 'react'
import { Button } from './Button'
export interface BadgeConfirmationModalProps {
confirm: () => void
}
export const BadgeConfirmationModal = (props: BadgeConfirmationModalProps) => {
const [isChecked, setIsChecked] = useState(false)
return (
<div>
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-2">
<label
className="absolute top-[23%] bottom-5 left-1/3 max-w-[600px] max-h-[410px] border-2 no-scrollbar modal-box"
htmlFor="temp"
>
{/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold">
<div className="text-sm font-thin">
You represent and warrant that you have, or have obtained, all rights, licenses, consents, permissions,
power and/or authority necessary to grant the rights granted herein for any content that you create,
submit, post, promote, or display on or through the Service. You represent and warrant that such contain
material subject to copyright, trademark, publicity rights, or other intellectual property rights, unless
you have necessary permission or are otherwise legally entitled to post the material and to grant Stargaze
Parties the license described above, and that the content does not violate any laws. Stargaze.zone
reserves the right to exercise its discretion in concealing user-generated content, should such content be
determined to have a detrimental impact on the brand.
</div>
<br />
<div className="flex flex-row pb-4">
<label className="flex flex-col space-y-1" htmlFor="terms">
<span className="text-sm font-light text-white">I agree with the terms above.</span>
</label>
<input
checked={isChecked}
className="p-2 mb-1 ml-2"
id="terms"
name="terms"
onClick={() => setIsChecked(!isChecked)}
type="checkbox"
/>
</div>
<br />
Are you sure to proceed with creating a new badge?
</div>
<div className="flex justify-end w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
<label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 rounded border-0 btn modal-button"
htmlFor="my-modal-2"
>
Go Back
</label>
</Button>
<Button className="px-0 mt-4 mb-4 max-h-12" isDisabled={!isChecked} onClick={props.confirm}>
<label
className="w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Confirm
</label>
</Button>
</div>
</label>
</label>
</div>
)
}

View File

@ -0,0 +1,11 @@
export const BadgeLoadingModal = () => {
return (
<div
className="flex overflow-hidden fixed top-0 right-0 bottom-0 left-0 z-50 flex-col justify-center items-center w-full h-screen bg-gray-900 opacity-80"
style={{ margin: 0 }}
>
<img alt="Pixel Logo" className="mb-5 w-[50px] h-[50px] animate-spin" src="/icon.svg" />
<p className="w-1/3 font-bold text-center text-white">Uploading the image for badge creation, please wait...</p>
</div>
)
}

View File

@ -0,0 +1,57 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable jsx-a11y/img-redundant-alt */
import { truncateAddress } from 'utils/wallet'
export interface ClickableCollection {
contractAddress: string
name: string
media: string
onClick: () => void
}
export function CollectionsTable({ collections }: { collections: ClickableCollection[] }) {
return (
<table className="w-full divide-y divide-zinc-800 table-fixed">
<thead>
<tr>
<th className="py-3.5 pr-3 pl-4 text-sm text-left sm:pl-0 text-infinity-blue" scope="col">
Name
</th>
<th className="py-3.5 px-3 text-sm text-left text-infinity-blue" scope="col">
Address
</th>
</tr>
</thead>
<tbody className=" bg-black">
{collections
? collections?.map((collection) => (
<tr
key={collection.contractAddress}
className="hover:bg-zinc-900 cursor-pointer"
onClick={collection.onClick}
>
<td className="py-2 pr-3 pl-4 whitespace-nowrap sm:pl-0">
<div className="flex items-center">
<div className="shrink-0 w-11 h-11">
<img alt="Collection Image" src={collection.media} />
</div>
<div className="ml-4 font-medium text-white truncate">{collection.name}</div>
</div>
</td>
<td className="py-5 px-3 text-zinc-400 whitespace-nowrap">
<div className="text-left text-white">
{collection.contractAddress?.startsWith('stars')
? truncateAddress(collection.contractAddress)
: collection.contractAddress}
</div>
</td>
</tr>
))
: null}
</tbody>
</table>
)
}

View File

@ -1,33 +1,61 @@
import { useState } from 'react'
import { Button } from './Button' import { Button } from './Button'
export interface ConfirmationModalProps { export interface ConfirmationModalProps {
confirm: () => void confirm: () => void
} }
export const ConfirmationModal = (props: ConfirmationModalProps) => { export const ConfirmationModal = (props: ConfirmationModalProps) => {
const [isChecked, setIsChecked] = useState(false)
return ( return (
<div> <div>
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" /> <input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-2"> <label className="cursor-pointer modal" htmlFor="my-modal-2">
<label <label
className="absolute top-[40%] bottom-5 left-1/3 max-w-xl max-h-40 border-2 no-scrollbar modal-box" className="absolute top-[23%] bottom-5 left-1/3 max-w-[600px] max-h-[440px] border-2 no-scrollbar modal-box"
htmlFor="temp" htmlFor="temp"
> >
{/* <Alert type="warning"></Alert> */} {/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold"> <div className="text-xl font-bold">
Are you sure to create a collection with the specified assets, metadata and parameters? <div className="text-sm font-thin">
You represent and warrant that you have, or have obtained, all rights, licenses, consents, permissions,
power and/or authority necessary to grant the rights granted herein for any content that you create,
submit, post, promote, or display on or through the Service. You represent and warrant that such contain
material subject to copyright, trademark, publicity rights, or other intellectual property rights, unless
you have necessary permission or are otherwise legally entitled to post the material and to grant Stargaze
Parties the license described above, and that the content does not violate any laws. Stargaze.zone
reserves the right to exercise its discretion in concealing user-generated content, should such content be
determined to have a detrimental impact on the brand.
</div>
<br />
<div className="flex flex-row pb-4">
<label className="flex flex-col space-y-1" htmlFor="terms">
<span className="text-sm font-light text-white">I agree with the terms above.</span>
</label>
<input
checked={isChecked}
className="p-2 mb-1 ml-2"
id="terms"
name="terms"
onClick={() => setIsChecked(!isChecked)}
type="checkbox"
/>
</div>
<br />
Are you sure to proceed with the specified assets, metadata and parameters?
</div> </div>
<div className="flex justify-end w-full"> <div className="flex justify-end w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600"> <Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
<label <label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 border-0 btn modal-button" className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 rounded border-0 btn modal-button"
htmlFor="my-modal-2" htmlFor="my-modal-2"
> >
Go Back Go Back
</label> </label>
</Button> </Button>
<Button className="px-0 mt-4 mb-4 max-h-12" onClick={props.confirm}> <Button className="px-0 mt-4 mb-4 max-h-12" isDisabled={!isChecked} onClick={props.confirm}>
<label <label
className="w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button" className="w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2" htmlFor="my-modal-2"
> >
Confirm Confirm

View File

@ -8,7 +8,7 @@ export function FaviconsMetaTags() {
<link href="/assets/manifest.webmanifest" rel="manifest" /> <link href="/assets/manifest.webmanifest" rel="manifest" />
<meta content="yes" name="mobile-web-app-capable" /> <meta content="yes" name="mobile-web-app-capable" />
<meta content="#F0827D" name="theme-color" /> <meta content="#F0827D" name="theme-color" />
<meta content="StargazeStudio" name="application-name" /> <meta content="Stargaze Studio" name="application-name" />
<link href="/assets/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57" /> <link href="/assets/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57" />
<link href="/assets/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60" /> <link href="/assets/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60" />
<link href="/assets/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72" /> <link href="/assets/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72" />
@ -22,7 +22,7 @@ export function FaviconsMetaTags() {
<link href="/assets/apple-touch-icon-1024x1024.png" rel="apple-touch-icon" sizes="1024x1024" /> <link href="/assets/apple-touch-icon-1024x1024.png" rel="apple-touch-icon" sizes="1024x1024" />
<meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="yes" name="apple-mobile-web-app-capable" />
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style" /> <meta content="black-translucent" name="apple-mobile-web-app-status-bar-style" />
<meta content="StargazeStudio" name="apple-mobile-web-app-title" /> <meta content="Stargaze Studio" name="apple-mobile-web-app-title" />
<link <link
href="/assets/apple-touch-startup-image-640x1136.png" href="/assets/apple-touch-startup-image-640x1136.png"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"

79
components/Fieldset.tsx Normal file
View File

@ -0,0 +1,79 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-implicit-coercion */
/* eslint-disable import/no-default-export */
/* eslint-disable tsdoc/syntax */
import type { ReactNode } from 'react'
export interface FieldsetBaseType {
/**
* The input's required id, used to link the label and input, as well as the error message.
*/
id: string
/**
* Error message to show input validation.
*/
error?: string
/**
* Success message to show input validation.
*/
success?: string
/**
* Label to describe the input.
*/
label?: string | ReactNode
/**
* Hint to show optional fields or a hint to the user of what to enter in the input.
*/
hint?: string
}
type FieldsetType = FieldsetBaseType & {
children: ReactNode
}
/**
* @name Fieldset
* @description A fieldset component, used to share markup for labels, hints, and errors for Input components.
*
* @example
* <Fieldset error={error} hint={hint} id={id} label={label}>
* <input id={id} {...props} />
* </Fieldset>
*/
export default function Fieldset({ label, hint, id, children, error, success }: FieldsetType) {
return (
<div>
{!!label && (
<div className="flex justify-between mb-1">
<label className="block w-full text-sm font-medium text-zinc-700 dark:text-zinc-300" htmlFor={id}>
{label}
</label>
{typeof hint === 'string' && (
<span className="text-sm text-zinc-500 dark:text-zinc-400" id={`${id}-optional`}>
{hint}
</span>
)}
</div>
)}
{children}
{error && (
<div className="mt-2">
<p className="text-sm text-zinc-600" id={`${id}-error`}>
{error}
</p>
</div>
)}
{success && (
<div className="mt-2">
<p className="text-sm text-zinc-500" id={`${id}-success`}>
{success}
</p>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,83 @@
import { useRef, useState } from 'react'
import { Button } from './Button'
export interface IncomeDashboardDisclaimerProps {
creatorAddress: string
}
export const IncomeDashboardDisclaimer = (props: IncomeDashboardDisclaimerProps) => {
const [isChecked, setIsChecked] = useState(false)
const checkBoxRef = useRef<HTMLInputElement>(null)
const handleCheckBox = () => {
checkBoxRef.current?.click()
}
return (
<div>
<input className="modal-toggle" defaultChecked={false} id="my-modal-1" ref={checkBoxRef} type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-1">
<label
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[450px] border-2 no-scrollbar modal-box"
htmlFor="temp"
>
{/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold">
<div className="text-sm font-thin">
The tool provided on this website is for informational purposes only and does not constitute tax, legal or
financial advice. The information provided by the tool is not intended to be used for tax planning, tax
avoidance, promoting, marketing or related purposes. Users should consult their own tax, legal or
financial advisors prior to acting on any information provided by the tool. By clicking accept below, you
agree that neither Stargaze Foundation or Public Awesome, LLC or any of its directors, officers,
employees, or advisors shall be responsible for any errors, omissions, or inaccuracies in the information
provided by the tool, and shall not be liable for any damages, losses, or expenses arising out of or in
connection with the use of the tool. Furthermore, you agree to indemnify Stargaze Foundation, Public
Awesome, LLC and any of its directors, officers, employees and advisors against any claims, suits, or
actions related to your use of the tool.
</div>
<br />
<div className="flex flex-row pb-4">
<label className="flex flex-col space-y-1" htmlFor="terms">
<span className="text-sm font-light text-white">I agree with the terms above.</span>
</label>
<input
checked={isChecked}
className="p-2 mb-1 ml-2"
id="terms"
name="terms"
onClick={() => setIsChecked(!isChecked)}
type="checkbox"
/>
</div>
<br />
Are you sure to proceed to the Creator Revenue Dashboard?
</div>
<div className="flex justify-end w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700">
<label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-700 rounded border-0 btn modal-button"
htmlFor="my-modal-1"
>
Go Back
</label>
</Button>
<a
className="my-4"
href={
isChecked
? `https://metabase.constellations.zone/public/dashboard/4d751721-51ab-46ff-ad27-075ec8d47a17?creator_address=${props.creatorAddress}&chart_granularity_(day%252Fweek%252Fmonth)=week`
: undefined
}
rel="noopener"
target="_blank"
>
<Button className="px-5 w-full h-full" isDisabled={!isChecked} onClick={() => handleCheckBox()}>
Confirm
</Button>
</a>
</div>
</label>
</label>
</div>
)
}

173
components/Input.tsx Normal file
View File

@ -0,0 +1,173 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-default-export */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable jsx-a11y/autocomplete-valid */
/* eslint-disable tsdoc/syntax */
import type { PropsOf } from '@headlessui/react/dist/types'
import type { ReactNode } from 'react'
import { forwardRef } from 'react'
import { classNames } from 'utils/css'
import type { FieldsetBaseType } from './Fieldset'
import Fieldset from './Fieldset'
import type { TrailingSelectProps } from './TrailingSelect'
import TrailingSelect from './TrailingSelect'
/**
* Shared styles for all input components.
*/
export const inputClassNames = {
base: [
'block w-full rounded-lg bg-white shadow-sm dark:bg-zinc-900 sm:text-sm',
'text-white placeholder:text-zinc-500 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
],
valid: 'border-zinc-300 focus:border-zinc-300 dark:border-zinc-800 dark:focus:border-zinc-800',
invalid: '!text-red-500 !border-red-500 focus:!border-red-500',
success: 'text-green border-green focus:border-green',
}
type InputProps = Omit<PropsOf<'input'> & FieldsetBaseType, 'className'> & {
directory?: 'true'
mozdirectory?: 'true'
webkitdirectory?: 'true'
leadingAddon?: string
trailingAddon?: string
trailingAddonIcon?: ReactNode
trailingSelectProps?: TrailingSelectProps
autoCompleteOff?: boolean
preventAutoCapitalizeFirstLetter?: boolean
className?: string
icon?: JSX.Element
}
/**
* @name Input
* @description A standard input component, defaults to the text type.
*
* @example
* // Standard input
* <Input id="first-name" name="first-name" />
*
* @example
* // Input component with label, placeholder and type email
* <Input id="email" name="email" type="email" autoComplete="email" label="Email" placeholder="name@email.com" />
*
* @example
* // Input component with label and leading and trailing addons
* <Input
* id="input-label-leading-trailing"
* label="Bid"
* placeholder="0.00"
* leadingAddon="$"
* trailingAddon="USD"
* />
*
* @example
* // Input component with label and trailing select
* const [trailingSelectValue, trailingSelectValueSet] = useState('USD');
*
* <Input
* id="input-label-trailing-select"
* label="Bid"
* placeholder="0.00"
* trailingSelectProps={{
* id: 'currency',
* label: 'Currency',
* value: trailingSelectValue,
* onChange: (event) => trailingSelectValueSet(event.target.value),
* options: ['USD', 'CAD', 'EUR'],
* }}
* />
*/
const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
error,
success,
hint,
label,
leadingAddon,
trailingAddon,
trailingAddonIcon,
trailingSelectProps,
id,
className,
type = 'text',
autoCompleteOff = false,
preventAutoCapitalizeFirstLetter,
icon,
...rest
},
ref,
) => {
const cachedClassNames = classNames(
...inputClassNames.base,
className,
error ? inputClassNames.invalid : inputClassNames.valid,
success ? inputClassNames.success : inputClassNames.valid,
leadingAddon && 'pl-7',
trailingAddon && 'pr-12',
trailingSelectProps && 'pr-16',
icon && 'pl-10',
)
const describedBy = [
...(error ? [`${id}-error`] : []),
...(success ? [`${id}-success`] : []),
...(typeof hint === 'string' ? [`${id}-optional`] : []),
...(typeof trailingAddon === 'string' ? [`${id}-addon`] : []),
].join(' ')
return (
<Fieldset error={error} hint={hint} id={id} label={label} success={success}>
<div className="relative rounded-md shadow-sm">
{leadingAddon && (
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm">{leadingAddon}</span>
</div>
)}
{icon && (
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
<span className="pr-10 text-zinc-500 dark:text-zinc-400 sm:text-sm">{icon}</span>
</div>
)}
<input
aria-describedby={describedBy}
aria-invalid={error ? 'true' : undefined}
autoCapitalize={`${preventAutoCapitalizeFirstLetter ?? 'off'}`}
autoComplete={`${autoCompleteOff ? 'off' : 'on'}`}
className={cachedClassNames}
id={id}
ref={ref}
type={type}
{...rest}
/>
{!trailingAddon && trailingSelectProps && <TrailingSelect {...trailingSelectProps} />}
{trailingAddon && (
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addon`}>
{trailingAddon}
</span>
</div>
)}
{trailingAddonIcon && (
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addonicon`}>
{trailingAddonIcon}
</span>
</div>
)}
</div>
</Fieldset>
)
},
)
Input.displayName = 'Input'
export default Input

View File

@ -66,7 +66,7 @@ export const JsonPreview = ({
</div> </div>
{show && ( {show && (
<div className="overflow-auto p-2 font-mono text-sm"> <div className="overflow-auto p-2 font-mono text-sm">
<pre>{JSON.stringify(content, null, 2).trim()}</pre> <pre>{content ? JSON.stringify(content, null, 2).trim() : '{}'}</pre>
</div> </div>
)} )}
</div> </div>

View File

@ -47,9 +47,9 @@ export const Layout = ({ children, metadata = {} }: LayoutProps) => {
<FaDesktop size={48} /> <FaDesktop size={48} />
<h1 className="text-2xl font-bold">Unsupported Viewport</h1> <h1 className="text-2xl font-bold">Unsupported Viewport</h1>
<p> <p>
StargazeStudio is best viewed on the big screen. Stargaze Studio is best viewed on the big screen.
<br /> <br />
Please open StargazeStudio on your tablet or desktop browser. Please open Stargaze Studio on your tablet or desktop browser.
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import clsx from 'clsx' import clsx from 'clsx'
import { Anchor } from 'components/Anchor' import { Anchor } from 'components/Anchor'
import { useRouter } from 'next/router'
export interface LinkTabProps { export interface LinkTabProps {
title: string title: string
@ -11,6 +12,10 @@ export interface LinkTabProps {
export const LinkTab = (props: LinkTabProps) => { export const LinkTab = (props: LinkTabProps) => {
const { title, description, href, isActive } = props const { title, description, href, isActive } = props
// get contract address from the router
const router = useRouter()
const { contractAddress } = router.query
return ( return (
<Anchor <Anchor
className={clsx( className={clsx(
@ -19,7 +24,7 @@ export const LinkTab = (props: LinkTabProps) => {
isActive ? 'border-plumbus' : 'border-transparent', isActive ? 'border-plumbus' : 'border-transparent',
isActive ? 'bg-plumbus/5 hover:bg-plumbus/10' : 'hover:bg-white/5', isActive ? 'bg-plumbus/5 hover:bg-plumbus/10' : 'hover:bg-white/5',
)} )}
href={href} href={href + (contractAddress ? `?contractAddress=${contractAddress as string}` : '')}
> >
<h4 className="font-bold">{title}</h4> <h4 className="font-bold">{title}</h4>
<span className="text-sm text-white/80 line-clamp-2">{description}</span> <span className="text-sm text-white/80 line-clamp-2">{description}</span>

View File

@ -1,11 +1,6 @@
import type { LinkTabProps } from './LinkTab' import type { LinkTabProps } from './LinkTab'
export const sg721LinkTabs: LinkTabProps[] = [ export const sg721LinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Create a new SG721 contract`,
href: '/contracts/sg721/instantiate',
},
{ {
title: 'Query', title: 'Query',
description: `Dispatch queries with your SG721 contract`, description: `Dispatch queries with your SG721 contract`,
@ -16,23 +11,74 @@ export const sg721LinkTabs: LinkTabProps[] = [
description: `Execute SG721 contract actions`, description: `Execute SG721 contract actions`,
href: '/contracts/sg721/execute', href: '/contracts/sg721/execute',
}, },
{
title: 'Migrate',
description: `Migrate SG721 contract`,
href: '/contracts/sg721/migrate',
},
] ]
export const minterLinkTabs: LinkTabProps[] = [ export const vendingMinterLinkTabs: LinkTabProps[] = [
{ {
title: 'Instantiate', title: 'Instantiate',
description: `Initialize a new Minter contract`, description: `Initialize a new Vending Minter contract`,
href: '/contracts/minter/instantiate', href: '/contracts/vendingMinter/instantiate',
}, },
{ {
title: 'Query', title: 'Query',
description: `Dispatch queries with your Minter contract`, description: `Dispatch queries for your Vending Minter contract`,
href: '/contracts/minter/query', href: '/contracts/vendingMinter/query',
}, },
{ {
title: 'Execute', title: 'Execute',
description: `Execute Minter contract actions`, description: `Execute Vending Minter contract actions`,
href: '/contracts/minter/execute', href: '/contracts/vendingMinter/execute',
},
{
title: 'Migrate',
description: `Migrate Vending Minter contract`,
href: '/contracts/vendingMinter/migrate',
},
]
export const openEditionMinterLinkTabs: LinkTabProps[] = [
{
title: 'Query',
description: `Dispatch queries for your Open Edition Minter contract`,
href: '/contracts/openEditionMinter/query',
},
{
title: 'Execute',
description: `Execute Open Edition Minter contract actions`,
href: '/contracts/openEditionMinter/execute',
},
{
title: 'Migrate',
description: `Migrate Open Edition Minter contract`,
href: '/contracts/openEditionMinter/migrate',
},
]
export const baseMinterLinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Initialize a new Base Minter contract`,
href: '/contracts/baseMinter/instantiate',
},
{
title: 'Query',
description: `Dispatch queries for your Base Minter contract`,
href: '/contracts/baseMinter/query',
},
{
title: 'Execute',
description: `Execute Base Minter contract actions`,
href: '/contracts/baseMinter/execute',
},
{
title: 'Migrate',
description: `Migrate Base Minter contract`,
href: '/contracts/baseMinter/migrate',
}, },
] ]
@ -44,7 +90,7 @@ export const whitelistLinkTabs: LinkTabProps[] = [
}, },
{ {
title: 'Query', title: 'Query',
description: `Dispatch queries with your Whitelist contract`, description: `Dispatch queries for your Whitelist contract`,
href: '/contracts/whitelist/query', href: '/contracts/whitelist/query',
}, },
{ {
@ -53,3 +99,88 @@ export const whitelistLinkTabs: LinkTabProps[] = [
href: '/contracts/whitelist/execute', href: '/contracts/whitelist/execute',
}, },
] ]
export const badgeHubLinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Initialize a new Badge Hub contract`,
href: '/contracts/badgeHub/instantiate',
},
{
title: 'Query',
description: `Dispatch queries for your Badge Hub contract`,
href: '/contracts/badgeHub/query',
},
{
title: 'Execute',
description: `Execute Badge Hub contract actions`,
href: '/contracts/badgeHub/execute',
},
{
title: 'Migrate',
description: `Migrate Badge Hub contract`,
href: '/contracts/badgeHub/migrate',
},
]
export const splitsLinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Initialize a new Splits contract`,
href: '/contracts/splits/instantiate',
},
{
title: 'Query',
description: `Dispatch queries for your Splits contract`,
href: '/contracts/splits/query',
},
{
title: 'Execute',
description: `Execute Splits contract actions`,
href: '/contracts/splits/execute',
},
{
title: 'Migrate',
description: `Migrate Splits contract`,
href: '/contracts/splits/migrate',
},
]
export const royaltyRegistryLinkTabs: LinkTabProps[] = [
{
title: 'Query',
description: `Dispatch queries for your Royalty Registry contract`,
href: '/contracts/royaltyRegistry/query',
},
{
title: 'Execute',
description: `Execute Royalty Registry contract actions`,
href: '/contracts/royaltyRegistry/execute',
},
]
export const authzLinkTabs: LinkTabProps[] = [
{
title: 'Grant',
description: `Grant authorizations to a given address`,
href: '/authz/grant',
},
{
title: 'Revoke',
description: `Revoke already granted authorizations`,
href: '/authz/revoke',
},
]
export const snapshotLinkTabs: LinkTabProps[] = [
{
title: 'Collection Holders',
description: `Take a snapshot of collection holders`,
href: '/snapshots/holders',
},
{
title: 'Chain Snapshots',
description: `Export a list of users fulfilling a given condition`,
href: '/snapshots/chain',
},
]

155
components/LogModal.tsx Normal file
View File

@ -0,0 +1,155 @@
import { useLogStore } from 'contexts/log'
import { useRef, useState } from 'react'
import { FaCopy, FaEraser } from 'react-icons/fa'
import { copy } from 'utils/clipboard'
import type { LogItem } from '../contexts/log'
import { removeLogItem, setLogItemList } from '../contexts/log'
import { Button } from './Button'
import { Tooltip } from './Tooltip'
export interface LogModalProps {
tempLogItem?: LogItem
}
export const LogModal = (props: LogModalProps) => {
const logs = useLogStore()
const [isChecked, setIsChecked] = useState(false)
const checkBoxRef = useRef<HTMLInputElement>(null)
const handleCheckBox = () => {
checkBoxRef.current?.click()
}
return (
<div>
<input className="modal-toggle" defaultChecked={false} id="my-modal-8" ref={checkBoxRef} type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-8">
<label
className={`absolute top-[15%] bottom-5 left-[21.5%] lg:max-w-[70%] ${
logs.itemList.length > 4 ? 'max-h-[750px]' : 'max-h-[500px]'
} border-2 no-scrollbar modal-box`}
htmlFor="temp"
>
<div className="text-xl font-bold">
<table className="table w-full h-1/2">
<thead className="sticky inset-x-0 top-0 bg-blue-400/20 backdrop-blur-sm">
<tr>
<th className="text-lg font-bold bg-transparent">#</th>
<th className="text-lg font-bold bg-transparent">Type</th>
<th className="text-lg font-bold bg-transparent">Message</th>
<th className="text-lg font-bold bg-transparent">
Time (UTC +{-new Date().getTimezoneOffset() / 60}){' '}
</th>
<th className="bg-transparent" />
</tr>
</thead>
<tbody>
{logs.itemList.length > 0 &&
logs.itemList.map((logItem, index) => (
<tr key={logItem.id} className="p-0 border-b-2 border-teal-200/10 border-collapse">
<td className="ml-8 w-[5%] font-mono text-base font-bold bg-transparent">{index + 1}</td>
<td
className={`w-[5%] font-mono text-base font-bold bg-transparent ${
logItem.type === 'Error' ? 'text-red-400' : ''
}`}
>
{logItem.type || 'Info'}
</td>
<td className="w-[70%] text-sm font-bold bg-transparent">
<Tooltip backgroundColor="bg-transparent" label="" placement="bottom">
<button
className="group flex overflow-auto space-x-2 max-w-xl font-mono text-base text-white/80 hover:underline no-scrollbar"
onClick={() => void copy(logItem.message)}
type="button"
>
<span>{logItem.message}</span>
<FaCopy className="opacity-0 group-hover:opacity-100" />
</button>
</Tooltip>
</td>
<td className="w-[20%] font-mono text-base bg-transparent">
{logItem.timestamp ? new Date(logItem.timestamp).toLocaleString() : ''}
</td>
<th className="bg-transparent">
<div className="flex items-center space-x-8">
<Button
className="bg-clip-text border-blue-200"
onClick={(e) => {
e.preventDefault()
removeLogItem(logItem.id)
}}
variant="outline"
>
<FaEraser />
</Button>
</div>
</th>
</tr>
//line break
))}
</tbody>
</table>
<br />
</div>
<div className="flex flex-row">
<div className="flex justify-start ml-4 w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700">
<label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-700 rounded border-0 btn modal-button"
htmlFor="my-modal-8"
>
Go Back
</label>
</Button>
<Button
className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700"
onClick={() => {
window.localStorage.setItem('error_list', '')
setLogItemList([])
}}
>
<label
className="w-full h-full text-white bg-blue-400 hover:bg-blue-500 rounded border-0 btn modal-button"
htmlFor="my-modal-8"
>
Clear
</label>
</Button>
</div>
<div className="flex justify-end w-full">
<Button
className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700"
onClick={() => {
const csv = logs.itemList
.map((logItem) => {
return `${logItem.type as string},${logItem.message},${
logItem.timestamp ? new Date(logItem.timestamp).toUTCString().replace(',', '') : ''
}`
})
.join('\n')
const blob = new Blob([csv], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('hidden', '')
a.setAttribute('href', url)
a.setAttribute('download', 'studio_logs.csv')
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}}
>
<label
className="w-full h-full text-white bg-stargaze hover:bg-stargaze/80 rounded border-0 btn modal-button"
htmlFor="my-modal-8"
>
Download
</label>
</Button>
</div>
</div>
</label>
</label>
</div>
)
}

View File

@ -1,7 +1,8 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */ /* eslint-disable jsx-a11y/media-has-caption */
import clsx from 'clsx'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { getAssetType } from 'utils/getAssetType' import { getAssetType } from 'utils/getAssetType'
export interface MetadataFormGroupProps { export interface MetadataFormGroupProps {
@ -13,6 +14,7 @@ export interface MetadataFormGroupProps {
export const MetadataFormGroup = (props: MetadataFormGroupProps) => { export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
const { title, subtitle, relatedAsset, children } = props const { title, subtitle, relatedAsset, children } = props
const [htmlContents, setHtmlContents] = useState<string>('')
const videoPreview = useMemo( const videoPreview = useMemo(
() => ( () => (
@ -40,6 +42,27 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
[relatedAsset], [relatedAsset],
) )
const documentPreview = useMemo(
() => (
<div className="flex flex-col items-center mt-4 ml-2">
<img key="document-key" alt="document_icon" className={clsx('mb-2 ml-2 w-24 h-24 thumbnail')} src="/pdf.png" />
<span className="flex self-center ">{relatedAsset?.name}</span>
</div>
),
[relatedAsset],
)
useEffect(() => {
if (getAssetType(relatedAsset?.name as string) !== 'html') return
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
setHtmlContents(e.target.result)
}
}
reader.readAsText(new Blob([relatedAsset as File]))
}, [relatedAsset])
return ( return (
<div className="flex p-4 pt-0 space-x-4 w-full"> <div className="flex p-4 pt-0 space-x-4 w-full">
<div className="flex flex-col w-1/3"> <div className="flex flex-col w-1/3">
@ -48,12 +71,20 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
{subtitle && <span className="text-sm text-white/50">{subtitle}</span>} {subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
<div> <div>
{relatedAsset && ( {relatedAsset && (
<div className="flex flex-row items-center mt-2 mr-4 border-2 border-dashed"> <div
className={`flex flex-row items-center mt-2 mr-4 ${
getAssetType(relatedAsset.name) === 'document' ? '' : `border-2 border-dashed`
}`}
>
{getAssetType(relatedAsset.name) === 'audio' && audioPreview} {getAssetType(relatedAsset.name) === 'audio' && audioPreview}
{getAssetType(relatedAsset.name) === 'video' && videoPreview} {getAssetType(relatedAsset.name) === 'video' && videoPreview}
{getAssetType(relatedAsset.name) === 'document' && documentPreview}
{getAssetType(relatedAsset.name) === 'image' && ( {getAssetType(relatedAsset.name) === 'image' && (
<img alt="preview" src={URL.createObjectURL(relatedAsset)} /> <img alt="preview" src={URL.createObjectURL(relatedAsset)} />
)} )}
{getAssetType(relatedAsset.name) === 'html' && (
<iframe allowFullScreen height="420px" srcDoc={htmlContents} title="Preview" width="100%" />
)}
</div> </div>
)} )}
</div> </div>

View File

@ -0,0 +1,237 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { useEffect, useMemo, useState } from 'react'
import { TextInput } from './forms/FormInput'
import { useInputState } from './forms/FormInput.hooks'
import { MetadataAttributes } from './forms/MetadataAttributes'
export interface MetadataInputProps {
selectedAssetFile: File
selectedMetadataFile: File
updateMetadataToUpload: (metadataFile: File) => void
onChange?: (metadata: any) => void
importedMetadata?: any
}
export const MetadataInput = (props: MetadataInputProps) => {
const emptyMetadataFile = new File(
[JSON.stringify({})],
`${props.selectedAssetFile?.name
.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))
.replaceAll('#', '')}.json`,
{ type: 'application/json' },
)
const [metadata, setMetadata] = useState<any>(null)
const nameState = useInputState({
id: 'name',
name: 'name',
title: 'Name',
placeholder: 'Token name',
})
const descriptionState = useInputState({
id: 'description',
name: 'description',
title: 'Description',
placeholder: 'Token description',
})
const externalUrlState = useInputState({
id: 'externalUrl',
name: 'externalUrl',
title: 'External URL',
placeholder: 'https://',
})
const youtubeUrlState = useInputState({
id: 'youtubeUrl',
name: 'youtubeUrl',
title: 'Youtube URL',
placeholder: 'https://',
})
const attributesState = useMetadataAttributesState()
let parsedMetadata: any
const parseMetadata = async (metadataFile: File) => {
console.log(metadataFile.size)
console.log(`Parsing metadataFile...`)
if (metadataFile) {
attributesState.reset()
try {
parsedMetadata = JSON.parse(await metadataFile.text())
} catch (e) {
console.log(e)
return
}
console.log('Parsed metadata: ', parsedMetadata)
if (!parsedMetadata.attributes || parsedMetadata.attributes?.length === 0) {
attributesState.reset()
attributesState.add({
trait_type: '',
value: '',
})
} else {
attributesState.reset()
for (let i = 0; i < parsedMetadata.attributes?.length; i++) {
attributesState.add({
trait_type: parsedMetadata.attributes[i].trait_type,
value: parsedMetadata.attributes[i].value,
})
}
}
if (!parsedMetadata.name) {
nameState.onChange('')
} else {
nameState.onChange(parsedMetadata.name)
}
if (!parsedMetadata.description) {
descriptionState.onChange('')
} else {
descriptionState.onChange(parsedMetadata.description)
}
if (!parsedMetadata.external_url) {
externalUrlState.onChange('')
} else {
externalUrlState.onChange(parsedMetadata.external_url)
}
if (!parsedMetadata.youtube_url) {
youtubeUrlState.onChange('')
} else {
youtubeUrlState.onChange(parsedMetadata.youtube_url)
}
setMetadata(parsedMetadata)
} else {
attributesState.reset()
nameState.onChange('')
descriptionState.onChange('')
externalUrlState.onChange('')
youtubeUrlState.onChange('')
attributesState.add({
trait_type: '',
value: '',
})
setMetadata(null)
props.updateMetadataToUpload(emptyMetadataFile)
}
}
const generateUpdatedMetadata = () => {
metadata.attributes = Object.values(attributesState)[1]
metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '')
if (metadata.attributes.length === 0) delete metadata.attributes
if (nameState.value === '') delete metadata.name
else metadata.name = nameState.value
if (descriptionState.value === '') delete metadata.description
else metadata.description = descriptionState.value.replaceAll('\\n', '\n')
if (externalUrlState.value === '') delete metadata.external_url
else metadata.external_url = externalUrlState.value
if (youtubeUrlState.value === '') delete metadata.youtube_url
else metadata.youtube_url = youtubeUrlState.value
const metadataFileBlob = new Blob([JSON.stringify(metadata)], {
type: 'application/json',
})
const editedMetadataFile = new File(
[metadataFileBlob],
props.selectedMetadataFile?.name
? props.selectedMetadataFile?.name.replaceAll('#', '')
: `${props.selectedAssetFile?.name
.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))
.replaceAll('#', '')}.json`,
{ type: 'application/json' },
)
props.updateMetadataToUpload(editedMetadataFile)
//console.log(editedMetadataFile)
//console.log(`${props.assetFile?.name.substring(0, props.assetFile?.name.lastIndexOf('.'))}.json`)
}
useEffect(() => {
console.log(props.selectedMetadataFile?.name)
if (props.selectedMetadataFile) {
void parseMetadata(props.selectedMetadataFile)
} else if (!props.importedMetadata) {
void parseMetadata(emptyMetadataFile)
}
}, [props.selectedMetadataFile?.name, props.importedMetadata])
const nameStateMemo = useMemo(() => nameState, [nameState.value])
const descriptionStateMemo = useMemo(() => descriptionState, [descriptionState.value])
const externalUrlStateMemo = useMemo(() => externalUrlState, [externalUrlState.value])
const youtubeUrlStateMemo = useMemo(() => youtubeUrlState, [youtubeUrlState.value])
const attributesStateMemo = useMemo(() => attributesState, [attributesState.entries])
useEffect(() => {
console.log('Update metadata')
if (metadata) {
generateUpdatedMetadata()
if (props.onChange) props.onChange(metadata)
}
console.log(metadata)
}, [
nameStateMemo.value,
descriptionStateMemo.value,
externalUrlStateMemo.value,
youtubeUrlStateMemo.value,
attributesStateMemo.entries,
])
useEffect(() => {
if (props.importedMetadata) {
void parseMetadata(emptyMetadataFile).then(() => {
console.log('Imported metadata: ', props.importedMetadata)
nameState.onChange(props.importedMetadata.name || '')
descriptionState.onChange(props.importedMetadata.description || '')
externalUrlState.onChange(props.importedMetadata.external_url || '')
youtubeUrlState.onChange(props.importedMetadata.youtube_url || '')
if (props.importedMetadata?.attributes && props.importedMetadata?.attributes?.length > 0) {
attributesState.reset()
props.importedMetadata?.attributes?.forEach((attribute: { trait_type: string; value: string }) => {
attributesState.add({
trait_type: attribute.trait_type,
value: attribute.value,
})
})
} else {
attributesState.reset()
attributesState.add({
trait_type: '',
value: '',
})
}
})
}
}, [props.importedMetadata])
return (
<div>
<div className="grid grid-cols-2 mt-4 mr-4 ml-8 w-full max-w-6xl max-h-full no-scrollbar">
<div className="mr-4">
<div className="mb-7 text-xl font-bold underline underline-offset-4">NFT Metadata</div>
<TextInput {...nameState} />
<TextInput className="mt-2" {...descriptionState} />
<TextInput className="mt-2" {...externalUrlState} />
<TextInput className="mt-2" {...youtubeUrlState} />
</div>
<div className="mt-6">
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
title="Attributes"
/>
</div>
</div>
</div>
)
}

View File

@ -1,12 +1,14 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Alert } from './Alert'
import { Button } from './Button' import { Button } from './Button'
import { Conditional } from './Conditional'
import { TextInput } from './forms/FormInput' import { TextInput } from './forms/FormInput'
import { useInputState } from './forms/FormInput.hooks' import { useInputState } from './forms/FormInput.hooks'
import { MetadataAttributes } from './forms/MetadataAttributes' import { MetadataAttributes } from './forms/MetadataAttributes'
@ -64,6 +66,13 @@ export const MetadataModal = (props: MetadataModalProps) => {
} }
setMetadata(parsedMetadata) setMetadata(parsedMetadata)
} else {
attributesState.reset()
nameState.onChange('')
descriptionState.onChange('')
externalUrlState.onChange('')
youtubeUrlState.onChange('')
setMetadata(null)
} }
} }
@ -102,9 +111,6 @@ export const MetadataModal = (props: MetadataModalProps) => {
const attributesState = useMetadataAttributesState() const attributesState = useMetadataAttributesState()
const generateUpdatedMetadata = () => { const generateUpdatedMetadata = () => {
console.log(`Current parsed data: ${parsedMetadata}`)
console.log('Updating...')
metadata.attributes = Object.values(attributesState)[1] metadata.attributes = Object.values(attributesState)[1]
metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '') metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '')
@ -113,7 +119,7 @@ export const MetadataModal = (props: MetadataModalProps) => {
if (descriptionState.value === '') delete metadata.description if (descriptionState.value === '') delete metadata.description
else metadata.description = descriptionState.value else metadata.description = descriptionState.value
if (externalUrlState.value === '') delete metadata.external_url if (externalUrlState.value === '') delete metadata.external_url
else metadata.externalUrl = externalUrlState.value else metadata.external_url = externalUrlState.value
if (youtubeUrlState.value === '') delete metadata.youtube_url if (youtubeUrlState.value === '') delete metadata.youtube_url
else metadata.youtube_url = youtubeUrlState.value else metadata.youtube_url = youtubeUrlState.value
@ -121,10 +127,11 @@ export const MetadataModal = (props: MetadataModalProps) => {
type: 'application/json', type: 'application/json',
}) })
const editedMetadataFile = new File([metadataFileBlob], metadataFile.name, { type: 'application/json' }) const editedMetadataFile = new File([metadataFileBlob], metadataFile.name.replaceAll('#', ''), {
type: 'application/json',
})
props.updateMetadata(editedMetadataFile) props.updateMetadata(editedMetadataFile)
toast.success('Metadata updated successfully.') toast.success('Metadata updated successfully.')
console.log(editedMetadataFile)
} }
useEffect(() => { useEffect(() => {
@ -144,21 +151,42 @@ export const MetadataModal = (props: MetadataModalProps) => {
subtitle={`Asset filename: ${props.assetFile?.name}`} subtitle={`Asset filename: ${props.assetFile?.name}`}
title="Update Metadata" title="Update Metadata"
> >
<TextInput {...nameState} onChange={(e) => nameState.onChange(e.target.value)} /> <TextInput
<TextInput {...descriptionState} onChange={(e) => descriptionState.onChange(e.target.value)} /> {...nameState}
<TextInput {...externalUrlState} onChange={(e) => externalUrlState.onChange(e.target.value)} /> disabled={!props.metadataFile}
<TextInput {...youtubeUrlState} onChange={(e) => youtubeUrlState.onChange(e.target.value)} /> onChange={(e) => nameState.onChange(e.target.value)}
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
subtitle="Enter trait types and values"
title="Attributes"
/> />
<TextInput
{...descriptionState}
disabled={!props.metadataFile}
onChange={(e) => descriptionState.onChange(e.target.value)}
/>
<TextInput
{...externalUrlState}
disabled={!props.metadataFile}
onChange={(e) => externalUrlState.onChange(e.target.value)}
/>
<TextInput
{...youtubeUrlState}
disabled={!props.metadataFile}
onChange={(e) => youtubeUrlState.onChange(e.target.value)}
/>
<Conditional test={props.metadataFile !== null}>
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
subtitle="Enter trait types and values"
title="Attributes"
/>
</Conditional>
<Button isDisabled={!props.metadataFile} onClick={generateUpdatedMetadata}> <Button isDisabled={!props.metadataFile} onClick={generateUpdatedMetadata}>
Update Metadata Update Metadata
</Button> </Button>
<Conditional test={Boolean(!props.metadataFile)}>
<Alert type="info">No metadata file to preview. Please select metadata files.</Alert>
</Conditional>
</MetadataFormGroup> </MetadataFormGroup>
</label> </label>
</label> </label>

View File

@ -0,0 +1,63 @@
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import Input from 'components/Input'
import useSearch from 'hooks/useSearch'
import { useMemo, useState } from 'react'
import { useDebounce } from 'utils/debounce'
import { CollectionsTable } from './CollectionsTable'
export function SelectCollection({ selectCollection }: { selectCollection: (collectionAddress: string) => void }) {
const [search, setSearch] = useState('')
const [isInputFocused, setInputFocus] = useState(false)
const debouncedQuery = useDebounce<string>(search, 200)
const debouncedIsInputFocused = useDebounce<boolean>(isInputFocused, 200)
const collectionsQuery = useSearch(debouncedQuery, ['collections'], 5)
const collectionsResults = useMemo(() => {
return collectionsQuery.data?.find((searchResult) => searchResult.indexUid === 'collections')
}, [collectionsQuery.data])
const clickableCollections = useMemo(() => {
return (
collectionsResults?.hits.map((hit) => ({
contractAddress: hit.id,
name: hit.name,
media: hit.thumbnail_url || hit.image_url,
onClick: () => {
selectCollection(hit.id)
setSearch(hit.name)
},
})) ?? []
)
}, [collectionsResults, selectCollection, setSearch])
const handleInputFocus = () => {
setInputFocus(true)
}
const handleInputBlur = () => {
setInputFocus(false)
}
return (
<div className="flex flex-col p-4 space-y-4 w-3/4 h-full bg-black rounded-md border-2 border-gray-600 border-solid md:p-6">
<p className="text-base font-bold text-white text-start">Select the NFT collection to take a snapshot for</p>
<Input
className="py-2 w-full text-black dark:text-white rounded-sm md:w-72"
icon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
id="collection-search"
onBlur={handleInputBlur}
onChange={(e) => setSearch(e.target.value)}
onFocus={handleInputFocus}
placeholder="Search Collections..."
value={search}
/>
{debouncedIsInputFocused && (
<div className="overflow-auto w-full">
<CollectionsTable collections={clickableCollections} />
</div>
)}
</div>
)
}

View File

@ -0,0 +1,74 @@
import type { Timezone } from 'contexts/globalSettings'
import { useGlobalSettings } from 'contexts/globalSettings'
import { useRef, useState } from 'react'
import { setTimezone } from '../contexts/globalSettings'
import { Button } from './Button'
export interface SettingsModalProps {
timezone?: Timezone
}
export const SettingsModal = (props: SettingsModalProps) => {
const globalSettings = useGlobalSettings()
const [isChecked, setIsChecked] = useState(false)
const checkBoxRef = useRef<HTMLInputElement>(null)
return (
<div>
<input className="modal-toggle" defaultChecked={false} id="my-modal-9" ref={checkBoxRef} type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-9">
<label
className={`absolute top-[42%] bottom-5 left-[260px] max-w-[450px] max-h-[250px]
border-[1px] no-scrollbar modal-box`}
htmlFor="temp"
>
<div className="flex flex-col justify-between h-full">
<div className="flex flex-col">
<h1 className="text-2xl font-bold underline underline-offset-2">Settings</h1>
<div className="flex justify-start w-full">
<div className="flex-row mt-2 w-full form-control">
<h1 className="mt-[5px] text-lg font-bold">Time & Date: </h1>
<label className="justify-start ml-6 cursor-pointer label">
<span className="mr-2 font-bold">Local</span>
<input
checked={globalSettings.timezone === 'Local'}
className={`${globalSettings.timezone === 'Local' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
setTimezone('Local' as Timezone)
window.localStorage.setItem('timezone', 'Local')
}}
type="checkbox"
/>
</label>
<label className="justify-start ml-4 cursor-pointer label">
<span className="mr-2 font-bold">UTC</span>
<input
checked={globalSettings.timezone === 'UTC'}
className={`${globalSettings.timezone === 'UTC' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
setTimezone('UTC' as Timezone)
window.localStorage.setItem('timezone', 'UTC')
}}
type="checkbox"
/>
</label>
</div>
</div>
</div>
<Button
className="w-[40%] max-h-12 bg-blue-500 hover:bg-blue-600"
isWide
onClick={() => {
setTimezone('UTC' as Timezone)
window.localStorage.setItem('timezone', 'UTC')
}}
>
Use Defaults
</Button>
</div>
</label>
</label>
</div>
)
}

View File

@ -1,84 +1,379 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import clsx from 'clsx' import clsx from 'clsx'
import { Anchor } from 'components/Anchor' import { Anchor } from 'components/Anchor'
import { useWallet } from 'contexts/wallet' import type { Timezone } from 'contexts/globalSettings'
import { setTimezone } from 'contexts/globalSettings'
import { setLogItemList, useLogStore } from 'contexts/log'
import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { FaCog } from 'react-icons/fa'
// import BrandText from 'public/brand/brand-text.svg' // import BrandText from 'public/brand/brand-text.svg'
import { footerLinks, socialsLinks } from 'utils/links' import { footerLinks, socialsLinks } from 'utils/links'
import { useWallet } from 'utils/wallet'
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS, NETWORK, OPEN_EDITION_FACTORY_ADDRESS } from '../utils/constants'
import { Conditional } from './Conditional'
import { IncomeDashboardDisclaimer } from './IncomeDashboardDisclaimer'
import { LogModal } from './LogModal'
import { SettingsModal } from './SettingsModal'
import { SidebarLayout } from './SidebarLayout' import { SidebarLayout } from './SidebarLayout'
import { WalletLoader } from './WalletLoader' import { WalletLoader } from './WalletLoader'
const routes = [
{ text: 'Collections', href: `/collections/`, isChild: false },
{ text: 'Create a Collection', href: `/collections/create/`, isChild: true },
{ text: 'My Collections', href: `/collections/myCollections/`, isChild: true },
{ text: 'Collection Actions', href: `/collections/actions/`, isChild: true },
{ text: 'Contract Dashboards', href: `/contracts/`, isChild: false },
{ text: 'Minter Contract', href: `/contracts/minter/`, isChild: true },
{ text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true },
{ text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true },
]
export const Sidebar = () => { export const Sidebar = () => {
const router = useRouter() const router = useRouter()
const wallet = useWallet() const wallet = useWallet()
const logs = useLogStore()
const [isTallWindow, setIsTallWindow] = useState(false)
useEffect(() => {
if (logs.itemList.length === 0) return
console.log('Stringified log item list: ', JSON.stringify(logs.itemList))
window.localStorage.setItem('logs', JSON.stringify(logs.itemList))
}, [logs])
useEffect(() => {
console.log(window.localStorage.getItem('logs'))
setLogItemList(JSON.parse(window.localStorage.getItem('logs') || '[]'))
setTimezone(
(window.localStorage.getItem('timezone') as Timezone)
? (window.localStorage.getItem('timezone') as Timezone)
: 'UTC',
)
}, [])
const handleResize = () => {
setIsTallWindow(window.innerHeight > 768)
}
useEffect(() => {
handleResize()
window.addEventListener('resize', handleResize)
// return () => {
// window.removeEventListener('resize', handleResize)
// }
}, [])
return ( return (
<SidebarLayout> <SidebarLayout>
{/* Stargaze brand as home button */} {/* Stargaze brand as home button */}
<Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}> <Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}>
<img alt="Brand Text" className="w-full" src="/stargaze_logo_800.svg" /> <img alt="Brand Text" className="ml-6 w-3/4" src="/studio-logo.png" />
</Anchor> </Anchor>
{/* wallet button */} {/* wallet button */}
<WalletLoader /> <WalletLoader />
{/* main navigation routes */} {/* main navigation routes */}
{routes.map(({ text, href, isChild }) => (
<Anchor <div className={clsx('absolute left-[5%] mt-2', isTallWindow ? 'top-[20%]' : 'top-[30%]')}>
key={href} <ul className="group py-1 px-2 w-full bg-transparent menu rounded-box">
className={clsx( <li tabIndex={0}>
'px-4 -mx-5 font-extrabold uppercase rounded-lg', // styling <div
'hover:bg-white/5 transition-colors', // hover styling className={clsx(
{ 'py-0 ml-2 text-sm font-bold': isChild }, 'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
{ 'hover:bg-white/5 transition-colors',
'text-gray hover:text-white': router.asPath.includes('/collections/') ? 'text-white' : 'text-gray',
router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) !== href && isChild, )}
}, >
{ 'text-plumbus': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) === href && isChild }, // active route styling <Link href="/collections/" passHref>
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling Collections
)} </Link>
href={href} </div>
> <ul className="z-50 p-2 bg-base-200">
{text} <li
</Anchor> className={clsx(
))} 'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/collections/create') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/collections/create/">Create a Collection</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/collections/myCollections/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/collections/myCollections/">My Collections</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/collections/actions/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/collections/actions/">Collection Actions</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/snapshots') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/snapshots">Snapshots</Link>
</li>
<Conditional test={NETWORK === 'mainnet'}>
<li className={clsx('text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded')} tabIndex={-1}>
<label
className="w-full h-full text-lg font-bold text-gray hover:text-white normal-case bg-clip-text bg-transparent border-none animate-none btn modal-button"
htmlFor="my-modal-1"
>
Revenue Dashboard
</label>
</li>
</Conditional>
</ul>
</li>
</ul>
<Conditional test={BADGE_HUB_ADDRESS !== undefined}>
<ul className="group py-1 px-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<span
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/badges/') ? 'text-white' : 'text-gray',
)}
>
<Link href="/badges/"> Badges </Link>
</span>
<ul className="z-50 p-2 rounded-box bg-base-200">
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/badges/create/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/badges/create/">Create a Badge</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/badges/myBadges/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/badges/myBadges/">My Badges</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/badges/actions/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/badges/actions/">Badge Actions</Link>
</li>
</ul>
</li>
</ul>
</Conditional>
<ul className="group p-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<span
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/tokenfactory') ? 'text-white' : 'text-gray',
)}
>
<Link href="/tokenfactory/">Tokens</Link>
</span>
<ul className="z-50 p-2 rounded-box bg-base-200">
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/tokenfactory/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/tokenfactory/">Token Factory</Link>
</li>
<li
className={clsx(
'disabled',
'text-lg font-bold hover:text-white',
router.asPath.includes('/airdrop-tokens/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/">Airdrop Tokens</Link>
</li>
</ul>
</li>
</ul>
<ul className="group py-1 px-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<span
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/contracts/') ? 'text-white' : 'text-gray',
)}
>
<Link href="/contracts/"> Contract Dashboards </Link>
</span>
<ul className="z-50 p-2 bg-base-200">
<Conditional test={BASE_FACTORY_ADDRESS !== undefined}>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/baseMinter/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/baseMinter/">Base Minter Contract</Link>
</li>
</Conditional>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/vendingMinter/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/vendingMinter/">Vending Minter Contract</Link>
</li>
<Conditional test={OPEN_EDITION_FACTORY_ADDRESS !== undefined}>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/openEditionMinter/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/openEditionMinter/">Open Edition Minter Contract</Link>
</li>
</Conditional>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/sg721/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/sg721/">SG721 Contract</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/whitelist/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/whitelist/">Whitelist Contract</Link>
</li>
<Conditional test={BADGE_HUB_ADDRESS !== undefined}>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/badgeHub/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/badgeHub/">Badge Hub Contract</Link>
</li>
</Conditional>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/splits/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/splits/">Splits Contract</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/royaltyRegistry/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/royaltyRegistry/">Royalty Registry</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/upload/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/upload/">Upload Contract</Link>
</li>
</ul>
</li>
</ul>
<ul className="group py-1 px-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<span
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/authz/') ? 'text-white' : 'text-gray',
)}
>
<Link href="/authz/"> Authz </Link>
</span>
</li>
</ul>
</div>
<IncomeDashboardDisclaimer creatorAddress={wallet.address ? wallet.address : ''} />
<LogModal />
<SettingsModal />
<div className="flex-grow" /> <div className="flex-grow" />
{isTallWindow && (
<div className="flex-row w-full h-full">
<label
className="absolute mb-8 w-[25%] text-lg font-bold text-white normal-case bg-zinc-500 hover:bg-zinc-600 border-none animate-none btn modal-button"
htmlFor="my-modal-9"
>
<FaCog className="justify-center align-bottom" size={20} />
</label>
<label
className="ml-16 w-[65%] text-lg font-bold text-white normal-case bg-blue-500 hover:bg-blue-600 border-none animate-none btn modal-button"
htmlFor="my-modal-8"
>
View Logs
</label>
</div>
)}
{/* Stargaze network status */} {/* Stargaze network status */}
<div className="text-sm capitalize">Network: {wallet.network}</div> {isTallWindow && <div className="text-sm capitalize">Network: {wallet.chain.pretty_name}</div>}
{/* footer reference links */} {/* footer reference links */}
<ul className="text-sm list-disc list-inside"> <ul className="text-sm list-disc list-inside">
{footerLinks.map(({ href, text }) => ( {isTallWindow &&
<li key={href}> footerLinks.map(({ href, text }) => (
<Anchor className="hover:text-plumbus hover:underline" href={href}> <li key={href}>
{text} <Anchor className="hover:text-plumbus hover:underline" href={href}>
</Anchor> {text}
</li> </Anchor>
))} </li>
))}
</ul> </ul>
{/* footer attribution */} {/* footer attribution */}
<div className="text-xs text-white/50"> <div className="text-xs text-white/50">
Stargaze Studio {process.env.APP_VERSION} <br /> Stargaze Studio {process.env.APP_VERSION} <br />
Made by{' '} Powered by{' '}
<Anchor className="text-plumbus hover:underline" href="https://deuslabs.fi"> <Anchor className="text-plumbus hover:underline" href="https://stargaze.zone">
deus labs Stargaze
</Anchor> </Anchor>
</div> </div>
{/* footer social links */} {/* footer social links */}
<div className="flex gap-x-6 items-center text-white/75"> <div className="flex gap-x-6 items-center text-white/75">
{socialsLinks.map(({ Icon, href, text }) => ( {socialsLinks.map(({ Icon, href, text }) => (
<Anchor key={href} className="hover:text-plumbus" href={href}> <Anchor key={href} className="hover:text-plumbus" href={href}>

View File

@ -15,7 +15,7 @@ export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
{/* fixed component */} {/* fixed component */}
<div <div
className={clsx( className={clsx(
'overflow-auto fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar', 'overflow-x-visible fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
'border-r-[1px] border-r-plumbus-light', 'border-r-[1px] border-r-plumbus-light',
{ 'translate-x-[-230px]': !isOpen }, { 'translate-x-[-230px]': !isOpen },
)} )}

View File

@ -0,0 +1,95 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */
import clsx from 'clsx'
import type { ReactNode } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { getAssetType } from 'utils/getAssetType'
export interface SingleAssetPreviewProps {
subtitle: ReactNode
relatedAsset?: File
updateMetadataFileIndex?: (index: number) => void
children?: ReactNode
}
export const SingleAssetPreview = (props: SingleAssetPreviewProps) => {
const { subtitle, relatedAsset, updateMetadataFileIndex, children } = props
const [htmlContents, setHtmlContents] = useState<string>('')
const videoPreview = useMemo(
() => (
<video
controls
id="video"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={relatedAsset ? URL.createObjectURL(relatedAsset) : ''}
/>
),
[relatedAsset],
)
const audioPreview = useMemo(
() => (
<audio
controls
id="audio"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={relatedAsset ? URL.createObjectURL(relatedAsset) : ''}
/>
),
[relatedAsset],
)
const documentPreview = useMemo(
() => (
<div className="flex flex-col items-center mt-4 ml-2">
<img key="document-key" alt="document_icon" className={clsx('mb-2 ml-1 w-20 h-20 thumbnail')} src="/pdf.png" />
<span className="flex self-center ">{relatedAsset?.name}</span>
</div>
),
[relatedAsset],
)
useEffect(() => {
if (getAssetType(relatedAsset?.name as string) !== 'html') return
const reader = new FileReader()
reader.onload = (e) => {
if (typeof e.target?.result === 'string') {
setHtmlContents(e.target.result)
}
}
reader.readAsText(new Blob([relatedAsset as File]))
}, [relatedAsset])
return (
<div className="flex p-4 pt-0 mt-11 ml-24 space-x-4 w-full">
<div className="flex flex-col w-full">
<label className="flex flex-col space-y-1">
<div>
{/* {subtitle && <span className="text-sm text-white/50">{subtitle}</span>} */}
{relatedAsset && (
<div
className={`flex flex-row items-center mt-2 mr-4 ${
getAssetType(relatedAsset.name) === 'document' ? '' : `border-2 border-dashed`
}`}
>
{getAssetType(relatedAsset.name) === 'audio' && audioPreview}
{getAssetType(relatedAsset.name) === 'video' && videoPreview}
{getAssetType(relatedAsset.name) === 'document' && documentPreview}
{getAssetType(relatedAsset.name) === 'image' && (
<img alt="preview" src={URL.createObjectURL(relatedAsset)} />
)}
{getAssetType(relatedAsset.name) === 'html' && (
<iframe allowFullScreen height="300px" srcDoc={htmlContents} title="Preview" width="100%" />
)}
</div>
)}
</div>
</label>
</div>
<div className="space-y-4 w-2/3">{children}</div>
</div>
)
}

View File

@ -6,6 +6,8 @@ import { usePopper } from 'react-popper'
export interface TooltipProps extends ComponentProps<'div'> { export interface TooltipProps extends ComponentProps<'div'> {
label: ReactNode label: ReactNode
children: ReactElement children: ReactElement
placement?: 'top' | 'bottom' | 'left' | 'right'
backgroundColor?: string
} }
export const Tooltip = ({ label, children, ...props }: TooltipProps) => { export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
@ -14,7 +16,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const { styles, attributes } = usePopper(referenceElement, popperElement, { const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'top', placement: props.placement ? props.placement : 'top',
}) })
return ( return (
@ -32,7 +34,11 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
<div <div
{...props} {...props}
{...attributes.popper} {...attributes.popper}
className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)} className={clsx(
'py-1 px-2 m-1 text-sm rounded shadow-md',
props.backgroundColor ? props.backgroundColor : 'bg-slate-900',
props.className,
)}
ref={setPopperElement} ref={setPopperElement}
style={{ ...styles.popper, ...props.style }} style={{ ...styles.popper, ...props.style }}
> >

View File

@ -0,0 +1,37 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-default-export */
import type { ChangeEvent } from 'react'
import { classNames } from 'utils/css'
export interface TrailingSelectProps {
id: string
label: string
options: string[]
value: string
onChange: (event: ChangeEvent<HTMLSelectElement>) => void
}
export default function TrailingSelect({ id, label, value, onChange, options }: TrailingSelectProps) {
const cachedClassNames = classNames(
'h-full rounded-md border-transparent bg-transparent py-0 pl-2 pr-7 text-zinc-500 dark:text-zinc-400 sm:text-sm',
'focus:border-transparent focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
)
return (
<div className="flex absolute inset-y-0 right-0 items-center">
<label className="sr-only" htmlFor={id}>
{label}
</label>
<select className={cachedClassNames} id={id} name={id} onChange={onChange} value={value}>
{/* TODO - Option values in a select are supposed to be unique, remove this comment during PR review */}
{options.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
</div>
)
}

View File

@ -1,36 +1,63 @@
import type { Coin } from '@cosmjs/proto-signing'
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { useWallet, useWalletStore } from 'contexts/wallet' import { tokensList } from 'config/token'
import { Fragment } from 'react' import { Fragment, useEffect, useState } from 'react'
import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa' import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa'
import { copy } from 'utils/clipboard' import { copy } from 'utils/clipboard'
import { convertDenomToReadable } from 'utils/convertDenomToReadable' import { convertDenomToReadable } from 'utils/convertDenomToReadable'
import { getShortAddress } from 'utils/getShortAddress' import { getShortAddress } from 'utils/getShortAddress'
import { truncateMiddle } from 'utils/text'
import { useWallet } from 'utils/wallet'
import { WalletButton } from './WalletButton' import { WalletButton } from './WalletButton'
import { WalletPanelButton } from './WalletPanelButton' import { WalletPanelButton } from './WalletPanelButton'
export const WalletLoader = () => { export const WalletLoader = () => {
const { address, balance, connect, disconnect, initializing: isLoading, initialized: isReady } = useWallet() const {
address = '',
username,
connect,
disconnect,
isWalletConnecting,
isWalletConnected,
getStargateClient,
} = useWallet()
const displayName = useWalletStore((store) => store.name || getShortAddress(store.address)) // Once wallet connects, load balances.
const [balances, setBalances] = useState<readonly Coin[] | undefined>()
useEffect(() => {
if (!isWalletConnected) {
setBalances(undefined)
return
}
const loadBalances = async () => {
const client = await getStargateClient()
setBalances(await client.getAllBalances(address))
}
loadBalances().catch(console.error)
}, [isWalletConnected, getStargateClient, address])
return ( return (
<Popover className="my-8"> <Popover className="mt-4 mb-2">
{({ close }) => ( {({ close }) => (
<> <>
<div className="grid -mx-4"> <div className="grid -mx-4">
{!isReady && ( {isWalletConnected ? (
<WalletButton className="w-full" isLoading={isLoading} onClick={() => void connect()}> <Popover.Button as={WalletButton} className="w-full">
{username || address}
</Popover.Button>
) : (
<WalletButton
className="w-full"
isLoading={isWalletConnecting}
onClick={() => void connect().catch(console.error)}
>
Connect Wallet Connect Wallet
</WalletButton> </WalletButton>
)} )}
{isReady && (
<Popover.Button as={WalletButton} className="w-full" isLoading={isLoading}>
{displayName}
</Popover.Button>
)}
</div> </div>
<Transition <Transition
@ -44,7 +71,7 @@ export const WalletLoader = () => {
> >
<Popover.Panel <Popover.Panel
className={clsx( className={clsx(
'absolute inset-x-4 mt-2', 'absolute inset-x-4 z-50 mt-2',
'bg-stone-800/80 rounded shadow-lg shadow-black/90 backdrop-blur-sm', 'bg-stone-800/80 rounded shadow-lg shadow-black/90 backdrop-blur-sm',
'flex flex-col items-stretch text-sm divide-y divide-white/10', 'flex flex-col items-stretch text-sm divide-y divide-white/10',
)} )}
@ -54,9 +81,12 @@ export const WalletLoader = () => {
{getShortAddress(address)} {getShortAddress(address)}
</span> </span>
<div className="font-bold">Your Balances</div> <div className="font-bold">Your Balances</div>
{balance.map((val) => ( {balances?.map((val) => (
<span key={`balance-${val.denom}`}> <span key={`balance-${val.denom}`}>
{convertDenomToReadable(val.amount)} {val.denom.slice(1, val.denom.length)} {convertDenomToReadable(val.amount)}{' '}
{tokensList.find((t) => t.denom === val.denom)?.displayName
? tokensList.find((t) => t.denom === val.denom)?.displayName
: truncateMiddle(val.denom ? val.denom : '', 28)}
</span> </span>
))} ))}
</div> </div>

View File

@ -0,0 +1,48 @@
// Styles required for @cosmos-kit/react modal
import '@interchain-ui/react/styles'
import { GasPrice } from '@cosmjs/stargate'
import { wallets as keplrExtensionWallets } from '@cosmos-kit/keplr-extension'
import { wallets as leapExtensionWallets } from '@cosmos-kit/leap-extension'
import { ChainProvider } from '@cosmos-kit/react'
import { assets, chains } from 'chain-registry'
import { getConfig } from 'config'
import type { ReactNode } from 'react'
import { NETWORK } from 'utils/constants'
export const WalletProvider = ({ children }: { children: ReactNode }) => {
const { gasPrice, feeToken } = getConfig(NETWORK)
return (
<ChainProvider
assetLists={assets}
chains={chains}
endpointOptions={{
endpoints: {
stargaze: {
rpc: ['https://rpc.stargaze-apis.com/'],
rest: ['https://rest.stargaze-apis.com/'],
},
stargazetestnet: {
rpc: ['https://rpc.elgafar-1.stargaze-apis.com/'],
rest: ['https://rest.elgafar-1.stargaze-apis.com/'],
},
},
isLazy: true,
}}
sessionOptions={{
duration: 1000 * 60 * 60 * 12, // 12 hours
}}
signerOptions={{
signingCosmwasm: () => ({
gasPrice: GasPrice.fromString(`${gasPrice}${feeToken}`),
}),
signingStargate: () => ({
gasPrice: GasPrice.fromString(`${gasPrice}${feeToken}`),
}),
}}
wallets={[...keplrExtensionWallets, ...leapExtensionWallets]}
>
{children}
</ChainProvider>
)
}

View File

@ -0,0 +1,118 @@
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import React, { useState } from 'react'
import { toast } from 'react-hot-toast'
import { SG721_NAME_ADDRESS } from 'utils/constants'
import { csvToFlexList } from 'utils/csvToFlexList'
import { isValidAddress } from 'utils/isValidAddress'
import { isValidFlexListFile } from 'utils/isValidFlexListFile'
import { useWallet } from 'utils/wallet'
export interface WhitelistFlexMember {
address: string
mint_count: number
}
interface WhitelistFlexUploadProps {
onChange: (data: WhitelistFlexMember[]) => void
}
export const WhitelistFlexUpload = ({ onChange }: WhitelistFlexUploadProps) => {
const wallet = useWallet()
const [resolvedMemberData, setResolvedMemberData] = useState<WhitelistFlexMember[]>([])
const resolveMemberData = async (memberData: WhitelistFlexMember[]) => {
if (!memberData.length) return []
await new Promise((resolve) => {
let i = 0
memberData.map(async (data) => {
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
await (
await wallet.getCosmWasmClient()
)
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(
data.address.trim().substring(0, data.address.lastIndexOf('.stars')),
).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri))
resolvedMemberData.push({ address: tokenUri, mint_count: Number(data.mint_count) })
else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`)
})
.catch((e) => {
console.log(e)
toast.error(`Error resolving address for the name: ${data.address}`)
})
i++
if (i === memberData.length) resolve(resolvedMemberData)
})
})
return resolvedMemberData
}
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedMemberData([])
if (!event.target.files) return toast.error('Error opening file')
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!event.target.files[0]?.name.endsWith('.csv')) {
toast.error('Please select a .csv file!')
return onChange([])
}
const reader = new FileReader()
reader.onload = async (e: ProgressEvent<FileReader>) => {
try {
if (!e.target?.result) return toast.error('Error parsing file.')
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const memberData = csvToFlexList(e.target.result.toString())
console.log(memberData)
if (!isValidFlexListFile(memberData)) {
event.target.value = ''
return onChange([])
}
await resolveMemberData(memberData.filter((data) => data.address.trim().endsWith('.stars'))).finally(() => {
return onChange(
memberData
.filter((data) => data.address.startsWith('stars') && !data.address.endsWith('.stars'))
.map((data) => ({
address: data.address.trim(),
mint_count: Number(data.mint_count),
}))
.concat(resolvedMemberData),
)
})
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}
reader.readAsText(event.target.files[0])
}
return (
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept=".csv"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="whitelist-flex-file"
onChange={onFileChange}
type="file"
/>
</div>
)
}

View File

@ -1,24 +1,106 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx' import clsx from 'clsx'
import React from 'react' import React, { useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useWallet } from 'utils/wallet'
import { SG721_NAME_ADDRESS } from '../utils/constants'
import { isValidAddress } from '../utils/isValidAddress'
interface WhitelistUploadProps { interface WhitelistUploadProps {
onChange: (data: string[]) => void onChange: (data: string[]) => void
} }
export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => { export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => {
const wallet = useWallet()
const [resolvedAddresses, setResolvedAddresses] = useState<string[]>([])
const resolveAddresses = async (names: string[]) => {
await new Promise((resolve) => {
let i = 0
names.map(async (name) => {
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
await (
await wallet.getCosmWasmClient()
)
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri)
else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
})
.catch((e) => {
console.log(e)
toast.error(`Error resolving address for the name: ${name}.stars`)
})
i++
if (i === names.length) resolve(resolvedAddresses)
})
})
return resolvedAddresses
}
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedAddresses([])
if (!event.target.files) return toast.error('Error opening file') if (!event.target.files) return toast.error('Error opening file')
if (event.target.files[0].type !== 'text/plain') return toast.error('Invalid file type') if (event.target.files.length !== 1) {
toast.error('No file selected')
return onChange([])
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]?.type !== 'text/plain') {
toast.error('Invalid file type')
return onChange([])
}
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e: ProgressEvent<FileReader>) => { reader.onload = async (e: ProgressEvent<FileReader>) => {
const text = e.target?.result?.toString() const text = e.target?.result?.toString()
let newline = '\n' let newline = '\n'
if (text?.includes('\r')) newline = '\r' if (text?.includes('\r')) newline = '\r'
if (text?.includes('\r\n')) newline = '\r\n' if (text?.includes('\r\n')) newline = '\r\n'
const data = text?.split(newline)
return onChange([...new Set(data?.filter((address) => address !== '') || [])]) const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '')
const data = cleanText?.split(newline)
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
const printableData = data?.map((item) => item.replace(regex, ''))
const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars'))
const strippedNames = names?.map((name) => name.split('.')[0])
console.log('names: ', names)
if (strippedNames?.length) {
await toast
.promise(resolveAddresses(strippedNames), {
loading: 'Resolving addresses...',
success: 'Address resolution finalized.',
error: 'Address resolution failed!',
})
.then((addresses) => {
console.log(addresses)
})
.catch((error) => {
console.log(error)
})
}
return onChange([
...new Set(
printableData
?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars'))
.concat(resolvedAddresses) || [],
),
])
} }
reader.readAsText(event.target.files[0]) reader.readAsText(event.target.files[0])
} }
@ -37,7 +119,7 @@ export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => {
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)} )}
id="whitelist-file" id="whitelist-file"
multiple multiple={false}
onChange={onFileChange} onChange={onFileChange}
type="file" type="file"
/> />

View File

@ -0,0 +1,639 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// import { AirdropUpload } from 'components/AirdropUpload'
import { toUtf8 } from '@cosmjs/encoding'
import { Alert } from 'components/Alert'
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
import { ActionsCombobox } from 'components/badges/actions/Combobox'
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
import sizeof from 'object-sizeof'
import type { FormEvent } from 'react'
import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query'
import * as secp256k1 from 'secp256k1'
import { generateKeyPairs, sha256 } from 'utils/hash'
import { isValidAddress } from 'utils/isValidAddress'
import { resolveAddress } from 'utils/resolveAddress'
import { useWallet } from 'utils/wallet'
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
import type { MintRule } from '../creation/ImageUploadDetails'
interface BadgeActionsProps {
badgeHubContractAddress: string
badgeId: number
badgeHubMessages: BadgeHubInstance | undefined
mintRule: MintRule
}
type TransferrableType = true | false | undefined
export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeActionsProps) => {
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [airdropAllocationArray, setAirdropAllocationArray] = useState<string[]>([])
const [badge, setBadge] = useState<Badge>()
const [transferrable, setTransferrable] = useState<TransferrableType>(undefined)
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
const [editFee, setEditFee] = useState<number | undefined>(undefined)
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
const [signature, setSignature] = useState<string>('')
const [ownerList, setOwnerList] = useState<string[]>([])
const [numberOfKeys, setNumberOfKeys] = useState(0)
const actionComboboxState = useActionsComboboxState()
const type = actionComboboxState.value?.id
const maxSupplyState = useNumberInputState({
id: 'max-supply',
name: 'max-supply',
title: 'Max Supply',
subtitle: 'Maximum number of badges that can be minted',
})
// Metadata related fields
const managerState = useInputState({
id: 'manager-address',
name: 'manager',
title: 'Manager',
subtitle: 'Badge Hub Manager',
defaultValue: wallet.address,
})
const nameState = useInputState({
id: 'metadata-name',
name: 'metadata-name',
title: 'Name',
subtitle: 'Name of the badge',
})
const descriptionState = useInputState({
id: 'metadata-description',
name: 'metadata-description',
title: 'Description',
subtitle: 'Description of the badge',
})
const imageState = useInputState({
id: 'metadata-image',
name: 'metadata-image',
title: 'Image',
subtitle: 'Badge Image URL',
})
const imageDataState = useInputState({
id: 'metadata-image-data',
name: 'metadata-image-data',
title: 'Image Data',
subtitle: 'Raw SVG image data',
})
const externalUrlState = useInputState({
id: 'metadata-external-url',
name: 'metadata-external-url',
title: 'External URL',
subtitle: 'External URL for the badge',
})
const attributesState = useMetadataAttributesState()
const backgroundColorState = useInputState({
id: 'metadata-background-color',
name: 'metadata-background-color',
title: 'Background Color',
subtitle: 'Background color of the badge',
})
const animationUrlState = useInputState({
id: 'metadata-animation-url',
name: 'metadata-animation-url',
title: 'Animation URL',
subtitle: 'Animation URL for the badge',
})
const youtubeUrlState = useInputState({
id: 'metadata-youtube-url',
name: 'metadata-youtube-url',
title: 'YouTube URL',
subtitle: 'YouTube URL for the badge',
})
// Rules related fields
const keyState = useInputState({
id: 'key',
name: 'key',
title: 'Key',
subtitle: 'The key generated for the badge',
})
const ownerState = useInputState({
id: 'owner-address',
name: 'owner',
title: 'Owner',
subtitle: 'The owner of the badge',
defaultValue: wallet.address,
})
const ownerListState = useAddressListState()
const pubKeyState = useInputState({
id: 'pubKey',
name: 'pubKey',
title: 'Public Key',
subtitle:
type === 'mint_by_keys'
? 'The whitelisted public key authorized to mint a badge'
: 'The public key to check whether it can be used to mint a badge',
})
const privateKeyState = useInputState({
id: 'privateKey',
name: 'privateKey',
title: 'Private Key',
subtitle:
type === 'mint_by_keys'
? 'The corresponding private key for the whitelisted public key'
: 'The private key that was generated during badge creation',
})
const nftState = useInputState({
id: 'nft-address',
name: 'nft-address',
title: 'NFT Contract Address',
subtitle: 'The NFT Contract Address for the badge',
})
const limitState = useNumberInputState({
id: 'limit',
name: 'limit',
title: 'Limit',
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
})
const showMetadataField = isEitherType(type, ['edit_badge'])
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'mint_by_minter'])
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
const showOwnerList = isEitherType(type, ['mint_by_minter'])
const showPubKeyField = isEitherType(type, ['mint_by_keys'])
const showLimitState = isEitherType(type, ['purge_keys', 'purge_owners'])
const payload: DispatchExecuteArgs = {
badge: {
manager: badge?.manager || managerState.value,
metadata: {
name: nameState.value || undefined,
description: descriptionState.value || undefined,
image: imageState.value || undefined,
image_data: imageDataState.value || undefined,
external_url: externalUrlState.value || undefined,
attributes:
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
? attributesState.values
.map((attr) => ({
trait_type: attr.trait_type,
value: attr.value,
}))
.filter((attr) => attr.trait_type && attr.value)
: undefined,
background_color: backgroundColorState.value || undefined,
animation_url: animationUrlState.value || undefined,
youtube_url: youtubeUrlState.value || undefined,
},
transferrable: transferrable === true,
rule: {
by_key: keyState.value,
},
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
},
metadata: {
name: nameState.value || undefined,
description: descriptionState.value || undefined,
image: imageState.value || undefined,
image_data: imageDataState.value || undefined,
external_url: externalUrlState.value || undefined,
attributes:
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
? attributesState.values
.map((attr) => ({
trait_type: attr.trait_type,
value: attr.value,
}))
.filter((attr) => attr.trait_type && attr.value)
: undefined,
background_color: backgroundColorState.value || undefined,
animation_url: animationUrlState.value || undefined,
youtube_url: youtubeUrlState.value || undefined,
},
id: badgeId,
editFee,
owner: resolvedOwnerAddress,
pubkey: pubKeyState.value,
signature,
keys: keyPairs.map((keyPair) => keyPair.publicKey),
limit: limitState.value || undefined,
owners: [
...new Set(
ownerListState.values
.map((a) => a.address.trim())
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
.concat(ownerList),
),
],
recipients: airdropAllocationArray,
privateKey: privateKeyState.value,
nft: nftState.value,
badgeHubMessages,
badgeHubContract: badgeHubContractAddress,
txSigner: wallet.address || '',
type,
}
const resolveOwnerAddress = async () => {
await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => {
setResolvedOwnerAddress(resolvedAddress)
})
}
useEffect(() => {
void resolveOwnerAddress()
}, [ownerState.value])
const resolveManagerAddress = async () => {
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
setBadge({
manager: resolvedAddress,
metadata: {
name: nameState.value || undefined,
description: descriptionState.value || undefined,
image: imageState.value || undefined,
image_data: imageDataState.value || undefined,
external_url: externalUrlState.value || undefined,
attributes:
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
? attributesState.values
.map((attr) => ({
trait_type: attr.trait_type,
value: attr.value,
}))
.filter((attr) => attr.trait_type && attr.value)
: undefined,
background_color: backgroundColorState.value || undefined,
animation_url: animationUrlState.value || undefined,
youtube_url: youtubeUrlState.value || undefined,
},
transferrable: transferrable === true,
rule: {
by_key: keyState.value,
},
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
})
})
}
useEffect(() => {
void resolveManagerAddress()
}, [managerState.value])
useEffect(() => {
setBadge({
manager: managerState.value,
metadata: {
name: nameState.value || undefined,
description: descriptionState.value || undefined,
image: imageState.value || undefined,
image_data: imageDataState.value || undefined,
external_url: externalUrlState.value || undefined,
attributes:
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
? attributesState.values
.map((attr) => ({
trait_type: attr.trait_type,
value: attr.value,
}))
.filter((attr) => attr.trait_type && attr.value)
: undefined,
background_color: backgroundColorState.value || undefined,
animation_url: animationUrlState.value || undefined,
youtube_url: youtubeUrlState.value || undefined,
},
transferrable: transferrable === true,
rule: {
by_key: keyState.value,
},
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
})
}, [
nameState.value,
descriptionState.value,
imageState.value,
imageDataState.value,
externalUrlState.value,
attributesState.values,
backgroundColorState.value,
animationUrlState.value,
youtubeUrlState.value,
transferrable,
keyState.value,
timestamp,
maxSupplyState.value,
])
useEffect(() => {
if (attributesState.values.length === 0)
attributesState.add({
trait_type: '',
value: '',
})
}, [])
useEffect(() => {
void dispatchEditBadgeMessage().catch((err) => {
toast.error(String(err), { style: { maxWidth: 'none' } })
})
}, [triggerDispatch])
useEffect(() => {
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
}, [privateKeyState.value, resolvedOwnerAddress])
useEffect(() => {
if (numberOfKeys > 0) {
setKeyPairs(generateKeyPairs(numberOfKeys))
}
}, [numberOfKeys])
const handleDownloadKeys = () => {
const element = document.createElement('a')
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
element.href = URL.createObjectURL(file)
element.download = `badge-${badgeId.toString()}-keys.json`
document.body.appendChild(element)
element.click()
}
const { isLoading, mutate } = useMutation(
async (event: FormEvent) => {
if (!wallet.isWalletConnected) {
throw new Error('Please connect your wallet.')
}
event.preventDefault()
if (!type) {
throw new Error('Please select an action.')
}
if (badgeHubContractAddress === '') {
throw new Error('Please enter the Badge Hub contract addresses.')
}
if (type === 'mint_by_key' && privateKeyState.value.length !== 64) {
throw new Error('Please enter a valid private key.')
}
if (type === 'edit_badge') {
const feeRateRaw = await (
await wallet.getCosmWasmClient()
).queryContractRaw(
badgeHubContractAddress,
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
await toast
.promise(
(
await wallet.getCosmWasmClient()
).queryContractSmart(badgeHubContractAddress, {
badge: { id: badgeId },
}),
{
error: `Edit Fee calculation failed!`,
loading: 'Calculating Edit Fee...',
success: (currentBadge) => {
console.log('Current badge: ', currentBadge)
return `Current metadata is ${
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes))
} bytes in size.`
},
},
)
.then((currentBadge) => {
// TODO - Go over the calculation
const currentBadgeMetadataSize =
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2)
console.log('Current badge metadata size: ', currentBadgeMetadataSize)
const newBadgeMetadataSize =
Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2
console.log('New badge metadata size: ', newBadgeMetadataSize)
if (newBadgeMetadataSize > currentBadgeMetadataSize) {
const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2
setEditFee(calculatedFee)
setTriggerDispatch(!triggerDispatch)
} else {
setEditFee(undefined)
setTriggerDispatch(!triggerDispatch)
}
})
.catch((error) => {
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
})
} else {
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
}
},
{
onError: (error) => {
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)
const dispatchEditBadgeMessage = async () => {
if (type) {
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
}
}
const airdropFileOnChange = (data: string[]) => {
console.log(data)
setAirdropAllocationArray(data)
}
const handleGenerateSignature = (id: number, owner: string, privateKey: string) => {
try {
const message = `claim badge ${id} for user ${owner}`
const privKey = Buffer.from(privateKey, 'hex')
// const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true))
const msgBytes = Buffer.from(message, 'utf8')
const msgHashBytes = sha256(msgBytes)
const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey)
setSignature(Buffer.from(signedMessage.signature).toString('hex'))
} catch (error) {
console.log(error)
toast.error('Error generating signature.')
}
}
return (
<form>
<div className="grid grid-cols-2 mt-4">
<div className="mr-2">
<ActionsCombobox mintRule={mintRule} {...actionComboboxState} />
{showMetadataField && (
<div className="p-4 mt-2 rounded-md border-2 border-gray-800">
<span className="text-gray-400">Metadata</span>
<TextInput className="mt-2" {...nameState} />
<TextInput className="mt-2" {...descriptionState} />
<TextInput className="mt-2" {...imageState} />
<TextInput className="mt-2" {...imageDataState} />
<TextInput className="mt-2" {...externalUrlState} />
<div className="mt-2">
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
title="Traits"
/>
</div>
<TextInput className="mt-2" {...backgroundColorState} />
<TextInput className="mt-2" {...animationUrlState} />
<TextInput className="mt-2" {...youtubeUrlState} />
</div>
)}
{showOwnerField && (
<AddressInput
className="mt-2"
{...ownerState}
subtitle="The address that the badge will be minted to"
title="Owner"
/>
)}
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
{showLimitState && <NumberInput className="mt-2" {...limitState} />}
<Conditional test={isEitherType(type, ['purge_owners', 'purge_keys'])}>
<Alert className="mt-4" type="info">
This action is only available if the badge with the specified id is either minted out or expired.
</Alert>
</Conditional>
<Conditional test={type === 'add_keys'}>
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
<div className="grid grid-cols-2 gap-24">
<div className="flex flex-col ml-4">
<span className="font-bold">Number of Keys</span>
<span className="text-sm text-white/80">
The number of public keys to be whitelisted for minting badges
</span>
</div>
<input
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
required
type="number"
value={numberOfKeys}
/>
</div>
</div>
</Conditional>
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
<Alert type="info">
<div className="pt-2">
<span className="mt-2">
Make sure to download the whitelisted public keys together with their private key counterparts.
</span>
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
Download Key Pairs
</Button>
</div>
</Alert>
</Conditional>
<Conditional test={showOwnerList}>
<div className="mt-4">
<AddressList
entries={ownerListState.entries}
isRequired
onAdd={ownerListState.add}
onChange={ownerListState.update}
onRemove={ownerListState.remove}
subtitle="Enter the owner addresses"
title="Addresses"
/>
<Alert className="mt-8" type="info">
You may optionally choose a text file of additional owner addresses.
</Alert>
<WhitelistUpload onChange={setOwnerList} />
</div>
</Conditional>
{showAirdropFileField && (
<FormGroup
subtitle="TXT file that contains the addresses to airdrop a badge for"
title="Badge Airdrop List File"
>
<BadgeAirdropListUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
</div>
<div className="-mt-6">
<div className="relative mb-2">
<Button
className="absolute top-0 right-0"
isLoading={isLoading}
onClick={mutate}
rightIcon={<FaArrowRight />}
>
Execute
</Button>
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
<TransactionHash hash={lastTx} />
</FormControl>
</div>
<FormControl subtitle="View current message to be sent" title="Payload Preview">
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
</FormControl>
</div>
</div>
</form>
)
}

View File

@ -0,0 +1,8 @@
import { useState } from 'react'
import type { ActionListItem } from './actions'
export const useActionsComboboxState = () => {
const [value, setValue] = useState<ActionListItem | null>(null)
return { value, onChange: (item: ActionListItem) => setValue(item) }
}

View File

@ -0,0 +1,106 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MintRule } from '../creation/ImageUploadDetails'
import type { ActionListItem } from './actions'
import { BY_KEY_ACTION_LIST, BY_KEYS_ACTION_LIST, BY_MINTER_ACTION_LIST } from './actions'
export interface ActionsComboboxProps {
value: ActionListItem | null
onChange: (item: ActionListItem) => void
mintRule?: MintRule
}
export const ActionsCombobox = ({ value, onChange, mintRule }: ActionsComboboxProps) => {
const [search, setSearch] = useState('')
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(BY_KEY_ACTION_LIST)
useEffect(() => {
if (mintRule === 'by_keys') {
SET_ACTION_LIST(BY_KEYS_ACTION_LIST)
} else if (mintRule === 'by_minter') {
SET_ACTION_LIST(BY_MINTER_ACTION_LIST)
} else {
SET_ACTION_LIST(BY_KEY_ACTION_LIST)
}
}, [mintRule])
const filtered =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="action"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Badge actions"
title=""
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ActionListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select action"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Action not found
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,206 @@
import type { Badge, BadgeHubInstance, Metadata } from 'contracts/badgeHub'
import { useBadgeHubContract } from 'contracts/badgeHub'
export type ActionType = typeof ACTION_TYPES[number]
export const ACTION_TYPES = [
'create_badge',
'edit_badge',
'add_keys',
'purge_keys',
'purge_owners',
'mint_by_minter',
'mint_by_key',
'airdrop_by_key',
'mint_by_keys',
'set_nft',
] as const
export interface ActionListItem {
id: ActionType
name: string
description?: string
}
export const BY_KEY_ACTION_LIST: ActionListItem[] = [
{
id: 'edit_badge',
name: 'Edit Badge',
description: `Edit badge metadata for the badge with the specified ID`,
},
{
id: 'mint_by_key',
name: 'Mint by Key',
description: `Mint a badge to a specified address`,
},
{
id: 'airdrop_by_key',
name: 'Airdrop by Key',
description: `Airdrop badges to a list of specified addresses`,
},
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
]
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
{
id: 'edit_badge',
name: 'Edit Badge',
description: `Edit badge metadata for the badge with the specified ID`,
},
{
id: 'mint_by_keys',
name: 'Mint by Keys',
description: `Mint a new badge with a whitelisted private key`,
},
{
id: 'add_keys',
name: 'Add Keys',
description: `Add keys to the badge with the specified ID`,
},
{
id: 'purge_keys',
name: 'Purge Keys',
description: `Purge keys from the badge with the specified ID`,
},
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
]
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
{
id: 'edit_badge',
name: 'Edit Badge',
description: `Edit badge metadata for the badge with the specified ID`,
},
{
id: 'mint_by_minter',
name: 'Mint by Minter',
description: `Mint a new badge to specified owner addresses`,
},
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
]
export interface DispatchExecuteProps {
type: ActionType
[k: string]: unknown
}
type Select<T extends ActionType> = T
/** @see {@link BadgeHubInstance}*/
export type DispatchExecuteArgs = {
badgeHubContract: string
badgeHubMessages?: BadgeHubInstance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'create_badge'>; badge: Badge }
| { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number }
| { type: Select<'add_keys'>; id: number; keys: string[] }
| { type: Select<'purge_keys'>; id: number; limit?: number }
| { type: Select<'purge_owners'>; id: number; limit?: number }
| { type: Select<'mint_by_minter'>; id: number; owners: string[] }
| { type: Select<'mint_by_key'>; id: number; owner: string; signature: string }
| { type: Select<'airdrop_by_key'>; id: number; recipients: string[]; privateKey: string }
| { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
| { type: Select<'set_nft'>; nft: string }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { badgeHubMessages, txSigner } = args
if (!badgeHubMessages) {
throw new Error('Cannot execute actions')
}
switch (args.type) {
case 'create_badge': {
return badgeHubMessages.createBadge(txSigner, args.badge)
}
case 'edit_badge': {
return badgeHubMessages.editBadge(txSigner, args.id, args.metadata, args.editFee)
}
case 'add_keys': {
return badgeHubMessages.addKeys(txSigner, args.id, args.keys)
}
case 'purge_keys': {
return badgeHubMessages.purgeKeys(txSigner, args.id, args.limit)
}
case 'purge_owners': {
return badgeHubMessages.purgeOwners(txSigner, args.id, args.limit)
}
case 'mint_by_minter': {
return badgeHubMessages.mintByMinter(txSigner, args.id, args.owners)
}
case 'mint_by_key': {
return badgeHubMessages.mintByKey(txSigner, args.id, args.owner, args.signature)
}
case 'airdrop_by_key': {
return badgeHubMessages.airdropByKey(txSigner, args.id, args.recipients, args.privateKey)
}
case 'mint_by_keys': {
return badgeHubMessages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return badgeHubMessages.setNft(txSigner, args.nft)
}
default: {
throw new Error('Unknown action')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: badgeHubMessages } = useBadgeHubContract()
const { badgeHubContract } = args
switch (args.type) {
case 'create_badge': {
return badgeHubMessages(badgeHubContract)?.createBadge(args.badge)
}
case 'edit_badge': {
return badgeHubMessages(badgeHubContract)?.editBadge(args.id, args.metadata)
}
case 'add_keys': {
return badgeHubMessages(badgeHubContract)?.addKeys(args.id, args.keys)
}
case 'purge_keys': {
return badgeHubMessages(badgeHubContract)?.purgeKeys(args.id, args.limit)
}
case 'purge_owners': {
return badgeHubMessages(badgeHubContract)?.purgeOwners(args.id, args.limit)
}
case 'mint_by_minter': {
return badgeHubMessages(badgeHubContract)?.mintByMinter(args.id, args.owners)
}
case 'mint_by_key': {
return badgeHubMessages(badgeHubContract)?.mintByKey(args.id, args.owner, args.signature)
}
case 'airdrop_by_key': {
return badgeHubMessages(badgeHubContract)?.airdropByKey(args.id, args.recipients, args.privateKey)
}
case 'mint_by_keys': {
return badgeHubMessages(badgeHubContract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return badgeHubMessages(badgeHubContract)?.setNft(args.nft)
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ActionType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}

View File

@ -0,0 +1,382 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { useGlobalSettings } from 'contexts/globalSettings'
import type { Trait } from 'contracts/badgeHub'
import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { BADGE_HUB_ADDRESS } from 'utils/constants'
import { useWallet } from 'utils/wallet'
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
import { MetadataAttributes } from '../../forms/MetadataAttributes'
import { Tooltip } from '../../Tooltip'
import type { MintRule, UploadMethod } from './ImageUploadDetails'
interface BadgeDetailsProps {
onChange: (data: BadgeDetailsDataProps) => void
uploadMethod: UploadMethod | undefined
mintRule: MintRule
metadataSize: number
}
export interface BadgeDetailsDataProps {
manager: string
name?: string
description?: string
attributes?: Trait[]
expiry?: number
transferrable: boolean
max_supply?: number
image_data?: string
external_url?: string
background_color?: string
animation_url?: string
youtube_url?: string
}
export const BadgeDetails = ({ metadataSize, onChange, uploadMethod }: BadgeDetailsProps) => {
const { address = '', isWalletConnected, getCosmWasmClient } = useWallet()
const { timezone } = useGlobalSettings()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [transferrable, setTransferrable] = useState<boolean>(false)
const [metadataFile, setMetadataFile] = useState<File>()
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
const metadataFileRef = useRef<HTMLInputElement | null>(null)
const managerState = useInputState({
id: 'manager-address',
name: 'manager',
title: 'Manager',
subtitle: 'Badge Hub Manager',
defaultValue: address,
})
const nameState = useInputState({
id: 'name',
name: 'name',
title: 'Name',
placeholder: 'My Awesome Collection',
})
const descriptionState = useInputState({
id: 'description',
name: 'description',
title: 'Description',
placeholder: 'My Awesome Collection Description',
})
const imageDataState = useInputState({
id: 'metadata-image-data',
name: 'metadata-image-data',
title: 'Image Data',
subtitle: 'Raw SVG image data',
})
const externalUrlState = useInputState({
id: 'metadata-external-url',
name: 'metadata-external-url',
title: 'External URL',
subtitle: 'External URL for the badge',
})
const attributesState = useMetadataAttributesState()
const maxSupplyState = useNumberInputState({
id: 'max-supply',
name: 'max-supply',
title: 'Max Supply',
subtitle: 'Maximum number of badges that can be minted',
})
const backgroundColorState = useInputState({
id: 'metadata-background-color',
name: 'metadata-background-color',
title: 'Background Color',
subtitle: 'Background color of the badge',
})
const animationUrlState = useInputState({
id: 'metadata-animation-url',
name: 'metadata-animation-url',
title: 'Animation URL',
subtitle: 'Animation URL for the badge',
})
const youtubeUrlState = useInputState({
id: 'metadata-youtube-url',
name: 'metadata-youtube-url',
title: 'YouTube URL',
subtitle: 'YouTube URL for the badge',
})
const parseMetadata = async () => {
try {
let parsedMetadata: any
if (metadataFile) {
attributesState.reset()
parsedMetadata = JSON.parse(await metadataFile.text())
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
attributesState.add({
trait_type: '',
value: '',
})
} else {
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
attributesState.add({
trait_type: parsedMetadata.attributes[i].trait_type,
value: parsedMetadata.attributes[i].value,
})
}
}
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
backgroundColorState.onChange(parsedMetadata.background_color ? parsedMetadata.background_color : '')
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
} else {
attributesState.reset()
nameState.onChange('')
descriptionState.onChange('')
externalUrlState.onChange('')
youtubeUrlState.onChange('')
animationUrlState.onChange('')
backgroundColorState.onChange('')
imageDataState.onChange('')
}
} catch (error) {
toast.error('Error parsing metadata file: Invalid JSON format.')
if (metadataFileRef.current) metadataFileRef.current.value = ''
setMetadataFile(undefined)
}
}
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
type: 'application/json',
})
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setMetadataFile(selectedFile)
}
}
useEffect(() => {
void parseMetadata()
if (!metadataFile)
attributesState.add({
trait_type: '',
value: '',
})
}, [metadataFile])
useEffect(() => {
animationUrlState.onChange('')
}, [uploadMethod])
useEffect(() => {
try {
const data: BadgeDetailsDataProps = {
manager: managerState.value,
name: nameState.value || undefined,
description: descriptionState.value || undefined,
attributes:
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
? attributesState.values
.map((attr) => ({
trait_type: attr.trait_type,
value: attr.value,
}))
.filter((attr) => attr.trait_type && attr.value)
: undefined,
expiry: timestamp ? timestamp.getTime() / 1000 : undefined,
max_supply: maxSupplyState.value || undefined,
transferrable,
image_data: imageDataState.value || undefined,
external_url: externalUrlState.value || undefined,
background_color: backgroundColorState.value || undefined,
animation_url: animationUrlState.value || undefined,
youtube_url: youtubeUrlState.value || undefined,
}
onChange(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
managerState.value,
nameState.value,
descriptionState.value,
timestamp,
maxSupplyState.value,
transferrable,
imageDataState.value,
externalUrlState.value,
attributesState.values,
backgroundColorState.value,
animationUrlState.value,
youtubeUrlState.value,
])
useEffect(() => {
const retrieveFeeRate = async () => {
try {
if (isWalletConnected) {
const feeRateRaw = await (
await getCosmWasmClient()
).queryContractRaw(
BADGE_HUB_ADDRESS,
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
)
console.log('Fee Rate Raw: ', feeRateRaw)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
setMetadataFeeRate(Number(feeRate.metadata))
}
} catch (error) {
toast.error('Error retrieving metadata fee rate.')
setMetadataFeeRate(0)
console.log('Error retrieving fee rate: ', error)
}
}
void retrieveFeeRate()
}, [isWalletConnected, getCosmWasmClient])
return (
<div>
<div className={clsx('grid grid-cols-2 ml-4 max-w-5xl')}>
<div className={clsx('mt-2')}>
<AddressInput {...managerState} isRequired />
<TextInput className="mt-2" {...nameState} />
<TextInput className="mt-2" {...descriptionState} />
<NumberInput className="mt-2" {...maxSupplyState} />
{uploadMethod === 'existing' ? <TextInput className="mt-2" {...animationUrlState} /> : null}
<TextInput className="mt-2" {...externalUrlState} />
<FormControl
className="mt-2"
htmlId="expiry-date"
subtitle={`Badge minting expiry date ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
title="Expiry Date"
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
setTimestamp(
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
}
value={
timezone === 'Local'
? timestamp
: timestamp
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
<div className="grid grid-cols-2">
<div className="mt-2 w-1/3 form-control">
<label className="justify-start cursor-pointer label">
<span className="mr-4 font-bold">Transferrable</span>
<input
checked={transferrable}
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setTransferrable(!transferrable)}
type="checkbox"
/>
</label>
</div>
<Conditional test={managerState.value !== ''}>
<Tooltip
backgroundColor="bg-stargaze"
className="bg-yellow-600"
label="This is only an estimate. Be sure to check the final amount before signing the transaction."
placement="bottom"
>
<div className="grid grid-cols-2 ml-12 w-full">
<div className="mt-4 font-bold">Fee Estimate:</div>
<span className="mt-4">{(metadataSize * Number(metadataFeeRate)) / 1000000} stars</span>
</div>
</Tooltip>
</Conditional>
</div>
</div>
<div className={clsx('ml-10')}>
<div>
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
title="Traits"
/>
</div>
<div className="w-full">
<Tooltip
backgroundColor="bg-blue-500"
label="A metadata file can be selected to automatically fill in the related fields."
placement="bottom"
>
<div>
<label
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFile"
>
Metadata File Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="application/json"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="metadataFile"
onChange={selectMetadata}
ref={metadataFileRef}
type="file"
/>
</div>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,352 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
import clsx from 'clsx'
import { Anchor } from 'components/Anchor'
import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { SingleAssetPreview } from 'components/SingleAssetPreview'
import type { ChangeEvent } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
import { getAssetType } from 'utils/getAssetType'
export type UploadMethod = 'new' | 'existing'
export type MintRule = 'by_key' | 'by_minter' | 'by_keys' | 'not_resolved'
interface ImageUploadDetailsProps {
onChange: (value: ImageUploadDetailsDataProps) => void
mintRule: MintRule
}
export interface ImageUploadDetailsDataProps {
assetFile: File | undefined
uploadService: UploadServiceType
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
imageUrl?: string
}
export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsProps) => {
const [assetFile, setAssetFile] = useState<File>()
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
const assetFileRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key',
name: 'nftStorageApiKey',
title: 'NFT.Storage API Key',
placeholder: 'Enter NFT.Storage API Key',
defaultValue: '',
})
const pinataApiKeyState = useInputState({
id: 'pinata-api-key',
name: 'pinataApiKey',
title: 'Pinata API Key',
placeholder: 'Enter Pinata API Key',
defaultValue: '',
})
const pinataSecretKeyState = useInputState({
id: 'pinata-secret-key',
name: 'pinataSecretKey',
title: 'Pinata Secret Key',
placeholder: 'Enter Pinata Secret Key',
defaultValue: '',
})
const imageUrlState = useInputState({
id: 'imageUrl',
name: 'imageUrl',
title: 'Image URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/jpg' })
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setAssetFile(selectedFile)
}
}
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
useEffect(() => {
try {
const data: ImageUploadDetailsDataProps = {
assetFile,
uploadService,
nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value,
uploadMethod,
imageUrl: imageUrlState.value
.replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(regex, '')
.trim(),
}
onChange(data)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}, [
assetFile,
uploadService,
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
imageUrlState.value,
])
useEffect(() => {
if (assetFileRef.current) assetFileRef.current.value = ''
setAssetFile(undefined)
imageUrlState.onChange('')
}, [uploadMethod, mintRule])
useEffect(() => {
if (useDefaultApiKey) {
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
} else {
nftStorageApiKeyState.onChange('')
}
}, [useDefaultApiKey])
const videoPreview = useMemo(
() => (
<video
className="ml-4"
controls
id="video"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={
imageUrlState.value ? imageUrlState.value.replace('ipfs://', 'https://ipfs-gw.stargaze-apis.com/ipfs/') : ''
}
/>
),
[imageUrlState.value],
)
return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'new'}
className="peer sr-only"
id="inlineRadio2"
name="inlineRadioOptions2"
onClick={() => {
setUploadMethod('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2"
>
Upload New Image
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'existing'}
className="peer sr-only"
id="inlineRadio1"
name="inlineRadioOptions1"
onClick={() => {
setUploadMethod('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1"
>
Use an existing Image URL
</label>
</div>
</div>
<div className="p-3 py-5 pb-4">
<Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column">
<p className="mb-5 ml-5">
Though the Badge Hub contract allows for off-chain image storage, it is recommended to use a decentralized
storage solution, such as IPFS. <br /> You may head over to{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
NFT.Storage
</Anchor>{' '}
or{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
Pinata
</Anchor>{' '}
and upload your image manually to get an image URL for your badge.
</p>
<div className="flex flex-row w-full">
<TextInput {...imageUrlState} className="mt-2 ml-6 w-full max-w-2xl" />
<Conditional test={imageUrlState.value !== ''}>
{getAssetType(imageUrlState.value) === 'image' && (
<div className="mt-2 ml-4 w-1/4 border-2 border-dashed">
<img
alt="badge-preview"
className="w-full"
src={imageUrlState.value.replace('IPFS://', 'ipfs://').replace(/,/g, '').replace(/"/g, '').trim()}
/>
</div>
)}
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
</Conditional>
</div>
</div>
</Conditional>
<Conditional test={uploadMethod === 'new'}>
<div>
<div className="flex flex-col items-center px-8 w-full">
<div className="flex justify-items-start mb-5 w-full font-bold">
<div className="form-check form-check-inline">
<input
checked={uploadService === 'nft-storage'}
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
setUploadService('nft-storage')
}}
type="radio"
value="nft-storage"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Upload using NFT.Storage
</label>
</div>
<div className="ml-2 form-check form-check-inline">
<input
checked={uploadService === 'pinata'}
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
setUploadService('pinata')
}}
type="radio"
value="pinata"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio4"
>
Upload using Pinata
</label>
</div>
</div>
<div className="flex w-full">
<Conditional test={uploadService === 'nft-storage'}>
<div className="flex-col w-full">
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
<div className="flex-row mt-2 w-full form-control">
<label className="cursor-pointer label">
<span className="mr-2 font-bold">Use Default API Key</span>
<input
checked={useDefaultApiKey}
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
setUseDefaultApiKey(!useDefaultApiKey)
}}
type="checkbox"
/>
</label>
</div>
</div>
</Conditional>
<Conditional test={uploadService === 'pinata'}>
<TextInput {...pinataApiKeyState} className="w-full" />
<div className="w-[20px]" />
<TextInput {...pinataSecretKeyState} className="w-full" />
</Conditional>
</div>
</div>
<div className="mt-6">
<div className="grid grid-cols-2">
<div>
<div className="w-full">
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFile"
>
Image Selection
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*, video/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="assetFile"
onChange={selectAsset}
ref={assetFileRef}
type="file"
/>
</div>
</div>
</div>
</div>
<Conditional test={assetFile !== undefined}>
<SingleAssetPreview
relatedAsset={assetFile}
subtitle={`Asset filename: ${assetFile?.name as string}`}
/>
</Conditional>
</div>
</div>
</div>
</Conditional>
</div>
</div>
)
}

View File

@ -0,0 +1,8 @@
import { useState } from 'react'
import type { QueryListItem } from './query'
export const useQueryComboboxState = () => {
const [value, setValue] = useState<QueryListItem | null>(null)
return { value, onChange: (item: QueryListItem) => setValue(item) }
}

View File

@ -0,0 +1,105 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MintRule } from '../creation/ImageUploadDetails'
import type { QueryListItem } from './query'
import { BY_KEY_QUERY_LIST, BY_KEYS_QUERY_LIST, BY_MINTER_QUERY_LIST } from './query'
export interface QueryComboboxProps {
value: QueryListItem | null
onChange: (item: QueryListItem) => void
mintRule?: MintRule
}
export const QueryCombobox = ({ value, onChange, mintRule }: QueryComboboxProps) => {
const [search, setSearch] = useState('')
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(BY_KEY_QUERY_LIST)
useEffect(() => {
if (mintRule === 'by_keys') {
SET_QUERY_LIST(BY_KEYS_QUERY_LIST)
} else if (mintRule === 'by_minter') {
SET_QUERY_LIST(BY_MINTER_QUERY_LIST)
} else {
SET_QUERY_LIST(BY_KEY_QUERY_LIST)
}
}, [mintRule])
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="query"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Badge queries"
title=""
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: QueryListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select query"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Query not found
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,113 @@
import { QueryCombobox } from 'components/badges/queries/Combobox'
import { useQueryComboboxState } from 'components/badges/queries/Combobox.hooks'
import { dispatchQuery } from 'components/badges/queries/query'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { NumberInput, TextInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import type { BadgeHubInstance } from 'contracts/badgeHub'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'
import type { MintRule } from '../creation/ImageUploadDetails'
interface BadgeQueriesProps {
badgeHubContractAddress: string
badgeId: number
badgeHubMessages: BadgeHubInstance | undefined
mintRule: MintRule
}
export const BadgeQueries = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeQueriesProps) => {
const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id
const pubkeyState = useInputState({
id: 'pubkey',
name: 'pubkey',
title: 'Public Key',
subtitle: 'The public key to check whether it can be used to mint a badge',
})
const startAfterNumberState = useNumberInputState({
id: 'start-after-number',
name: 'start-after-number',
title: 'Start After (optional)',
subtitle: 'The id to start the pagination after',
})
const startAfterStringState = useInputState({
id: 'start-after-string',
name: 'start-after-string',
title: 'Start After (optional)',
subtitle: 'The public key to start the pagination after',
})
const paginationLimitState = useNumberInputState({
id: 'pagination-limit',
name: 'pagination-limit',
title: 'Pagination Limit (optional)',
subtitle: 'The number of items to return (max: 30)',
defaultValue: 5,
})
const { data: response } = useQuery(
[
badgeHubMessages,
type,
badgeId,
pubkeyState.value,
startAfterNumberState.value,
startAfterStringState.value,
paginationLimitState.value,
] as const,
async ({ queryKey }) => {
const [_badgeHubMessages, _type, _badgeId, _pubKey, _startAfterNumber, _startAfterString, _limit] = queryKey
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result = await dispatchQuery({
badgeHubMessages: _badgeHubMessages,
id: _badgeId,
startAfterNumber: _startAfterNumber,
startAfterString: _startAfterString,
limit: _limit,
type: _type,
pubkey: _pubKey,
})
return result
},
{
placeholderData: null,
onError: (error: any) => {
toast.error(error.message, { style: { maxWidth: 'none' } })
},
enabled: Boolean(badgeHubContractAddress && type && badgeId),
retry: false,
},
)
return (
<div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8">
<QueryCombobox mintRule={mintRule} {...comboboxState} />
<Conditional test={type === 'getKey'}>
<TextInput {...pubkeyState} />
</Conditional>
<Conditional test={type === 'getBadges'}>
<NumberInput {...startAfterNumberState} />
</Conditional>
<Conditional test={type === 'getBadges' || type === 'getKeys'}>
<NumberInput {...paginationLimitState} />
</Conditional>
<Conditional test={type === 'getKeys'}>
<TextInput {...startAfterStringState} />
</Conditional>
</div>
<div className="space-y-8">
<FormControl title="Query Response">
<JsonPreview content={response || {}} isCopyable />
</FormControl>
</div>
</div>
)
}

View File

@ -0,0 +1,76 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import type { BadgeHubInstance } from 'contracts/badgeHub'
export type QueryType = typeof QUERY_TYPES[number]
export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const
export interface QueryListItem {
id: QueryType
name: string
description?: string
}
export const BY_KEY_QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
]
export const BY_KEYS_QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
{ id: 'getKey', name: 'Query Key', description: "Query a key by ID to see if it's whitelisted" },
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
]
export const BY_MINTER_QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
]
export interface DispatchExecuteProps {
type: QueryType
[k: string]: unknown
}
type Select<T extends QueryType> = T
export type DispatchQueryArgs = {
badgeHubMessages?: BadgeHubInstance
} & (
| { type: undefined }
| { type: Select<'config'> }
| { type: Select<'getBadge'>; id: number }
| { type: Select<'getBadges'>; startAfterNumber: number; limit: number }
| { type: Select<'getKey'>; id: number; pubkey: string }
| { type: Select<'getKeys'>; id: number; startAfterString: string; limit: number }
)
export const dispatchQuery = async (args: DispatchQueryArgs) => {
const { badgeHubMessages } = args
if (!badgeHubMessages) {
throw new Error('Cannot perform a query. Please connect your wallet first.')
}
switch (args.type) {
case 'config': {
return badgeHubMessages?.getConfig()
}
case 'getBadge': {
return badgeHubMessages?.getBadge(args.id)
}
case 'getBadges': {
return badgeHubMessages?.getBadges(args.startAfterNumber, args.limit)
}
case 'getKey': {
return badgeHubMessages?.getKey(args.id, args.pubkey)
}
case 'getKeys': {
return badgeHubMessages?.getKeys(args.id, args.startAfterString, args.limit)
}
default: {
throw new Error('Unknown action')
}
}
}

View File

@ -1,4 +1,9 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import { AirdropUpload } from 'components/AirdropUpload' import { AirdropUpload } from 'components/AirdropUpload'
import { Alert } from 'components/Alert'
import { Button } from 'components/Button' import { Button } from 'components/Button'
import type { DispatchExecuteArgs } from 'components/collections/actions/actions' import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions' import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions'
@ -11,44 +16,72 @@ import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import { Tooltip } from 'components/Tooltip'
import { TransactionHash } from 'components/TransactionHash' import { TransactionHash } from 'components/TransactionHash'
import { useWallet } from 'contexts/wallet' import { useGlobalSettings } from 'contexts/globalSettings'
import type { MinterInstance } from 'contracts/minter' import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import type { SG721Instance } from 'contracts/sg721' import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import type { FormEvent } from 'react' import type { FormEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa' import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
import { ROYALTY_REGISTRY_ADDRESS } from 'utils/constants'
import type { AirdropAllocation } from 'utils/isValidAccountsFile' import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import { resolveAddress } from 'utils/resolveAddress'
import { useWallet } from 'utils/wallet'
import type { CollectionInfo } from '../../../contracts/sg721/contract'
import { TextInput } from '../../forms/FormInput' import { TextInput } from '../../forms/FormInput'
import type { MinterType, Sg721Type } from './Combobox'
interface CollectionActionsProps { interface CollectionActionsProps {
minterContractAddress: string minterContractAddress: string
sg721ContractAddress: string sg721ContractAddress: string
sg721Messages: SG721Instance | undefined sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined vendingMinterMessages: VendingMinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
openEditionMinterMessages: OpenEditionMinterInstance | undefined
royaltyRegistryMessages: RoyaltyRegistryInstance | undefined
minterType: MinterType
sg721Type: Sg721Type
} }
type ExplicitContentType = true | false | undefined
export const CollectionActions = ({ export const CollectionActions = ({
sg721ContractAddress, sg721ContractAddress,
sg721Messages, sg721Messages,
minterContractAddress, minterContractAddress,
minterMessages, vendingMinterMessages,
baseMinterMessages,
openEditionMinterMessages,
royaltyRegistryMessages,
minterType,
sg721Type,
}: CollectionActionsProps) => { }: CollectionActionsProps) => {
const wallet = useWallet() const wallet = useWallet()
const [lastTx, setLastTx] = useState('') const [lastTx, setLastTx] = useState('')
const { timezone } = useGlobalSettings()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined) const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([]) const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
const [airdropArray, setAirdropArray] = useState<string[]>([]) const [airdropArray, setAirdropArray] = useState<string[]>([])
const [collectionInfo, setCollectionInfo] = useState<CollectionInfo>()
const [explicitContent, setExplicitContent] = useState<ExplicitContentType>(undefined)
const [resolvedRecipientAddress, setResolvedRecipientAddress] = useState<string>('')
const [jsonExtensions, setJsonExtensions] = useState<boolean>(false)
const [decrement, setDecrement] = useState<boolean>(false)
const actionComboboxState = useActionsComboboxState() const actionComboboxState = useActionsComboboxState()
const type = actionComboboxState.value?.id const type = actionComboboxState.value?.id
const limitState = useNumberInputState({ const limitState = useNumberInputState({
id: 'per-address-limi', id: 'per-address-limit',
name: 'perAddressLimit', name: 'perAddressLimit',
title: 'Per Address Limit', title: 'Per Address Limit',
subtitle: 'Enter the per address limit', subtitle: 'Enter the per address limit',
@ -83,6 +116,29 @@ export const CollectionActions = ({
subtitle: 'Address of the recipient', subtitle: 'Address of the recipient',
}) })
const creatorState = useInputState({
id: 'creator-address',
name: 'creator',
title: 'Creator Address',
subtitle: 'Address of the creator',
})
const tokenURIState = useInputState({
id: 'token-uri',
name: 'tokenURI',
title: 'Token URI',
subtitle: 'URI for the token',
placeholder: 'ipfs://',
})
const baseURIState = useInputState({
id: 'base-uri',
name: 'baseURI',
title: 'Base URI',
subtitle: 'Base URI to batch update token metadata with',
placeholder: 'ipfs://',
})
const whitelistState = useInputState({ const whitelistState = useInputState({
id: 'whitelist-address', id: 'whitelist-address',
name: 'whitelistAddress', name: 'whitelistAddress',
@ -90,31 +146,199 @@ export const CollectionActions = ({
subtitle: 'Address of the whitelist contract', subtitle: 'Address of the whitelist contract',
}) })
const priceState = useNumberInputState({
id: 'update-mint-price',
name: 'updateMintPrice',
title: type === 'update_discount_price' ? 'Discount Price' : 'Update Mint Price',
subtitle: type === 'update_discount_price' ? 'New discount price' : 'New minting price',
})
const descriptionState = useInputState({
id: 'collection-description',
name: 'description',
title: 'Collection Description',
})
const imageState = useInputState({
id: 'collection-cover-image',
name: 'cover_image',
title: 'Collection Cover Image',
subtitle: 'URL for collection cover image.',
})
const externalLinkState = useInputState({
id: 'collection-ext-link',
name: 'external_link',
title: 'External Link',
subtitle: 'External URL for the collection.',
})
const royaltyPaymentAddressState = useInputState({
id: 'royalty-payment-address',
name: 'royaltyPaymentAddress',
title: 'Royalty Payment Address',
subtitle: 'Address to receive royalties.',
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
})
const royaltyShareState = useInputState({
id: 'royalty-share',
name: 'royaltyShare',
title: type !== 'update_royalties_for_infinity_swap' ? 'Share Percentage' : 'Share Delta',
subtitle:
type !== 'update_royalties_for_infinity_swap'
? 'Percentage of royalties to be paid'
: 'Change in share percentage',
placeholder: isEitherType(type, ['set_royalties_for_infinity_swap', 'update_royalties_for_infinity_swap'])
? '0.5%'
: '5%',
})
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
const showWhitelistField = type === 'set_whitelist' const showWhitelistField = type === 'set_whitelist'
const showDateField = type === 'update_start_time' const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time'])
const showEndDateField = type === 'update_end_time'
const showLimitField = type === 'update_per_address_limit' const showLimitField = type === 'update_per_address_limit'
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn']) const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
const showNumberOfTokensField = type === 'batch_mint' const showNumberOfTokensField = isEitherType(type, ['batch_mint', 'batch_mint_open_edition'])
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer']) const showTokenIdListField = isEitherType(type, [
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer']) 'batch_burn',
const showAirdropFileField = type === 'airdrop' 'batch_transfer',
'batch_mint_for',
'batch_update_token_metadata',
])
const showRecipientField = isEitherType(type, [
'transfer',
'mint_to',
'mint_to_open_edition',
'mint_for',
'batch_mint',
'batch_mint_open_edition',
'batch_transfer',
'batch_mint_for',
])
const showAirdropFileField = isEitherType(type, [
'airdrop',
'airdrop_open_edition',
'airdrop_specific',
'batch_transfer_multi_address',
])
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const showDescriptionField = type === 'update_collection_info'
const showCreatorField = type === 'update_collection_info'
const showImageField = type === 'update_collection_info'
const showExternalLinkField = type === 'update_collection_info'
const showRoyaltyRelatedFields =
type === 'update_collection_info' ||
type === 'set_royalties_for_infinity_swap' ||
type === 'update_royalties_for_infinity_swap'
const showExplicitContentField = type === 'update_collection_info'
const showBaseUriField = type === 'batch_update_token_metadata'
const payload: DispatchExecuteArgs = { const payload: DispatchExecuteArgs = {
whitelist: whitelistState.value, whitelist: whitelistState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
limit: limitState.value, limit: limitState.value,
minterContract: minterContractAddress, minterContract: minterContractAddress,
sg721Contract: sg721ContractAddress, sg721Contract: sg721ContractAddress,
royaltyRegistryContract: ROYALTY_REGISTRY_ADDRESS,
tokenId: tokenIdState.value, tokenId: tokenIdState.value,
tokenIds: tokenIdListState.value, tokenIds: tokenIdListState.value,
tokenUri: tokenURIState.value.trim().endsWith('/')
? tokenURIState.value.trim().slice(0, -1)
: tokenURIState.value.trim(),
batchNumber: batchNumberState.value, batchNumber: batchNumberState.value,
minterMessages, vendingMinterMessages,
baseMinterMessages,
openEditionMinterMessages,
sg721Messages, sg721Messages,
recipient: recipientState.value, royaltyRegistryMessages,
recipient: resolvedRecipientAddress,
recipients: airdropArray, recipients: airdropArray,
txSigner: wallet.address, tokenRecipients: airdropAllocationArray,
txSigner: wallet.address || '',
type, type,
price: priceState.value.toString(),
baseUri: baseURIState.value.trim().endsWith('/')
? baseURIState.value.trim().slice(0, -1)
: baseURIState.value.trim(),
collectionInfo,
jsonExtensions,
decrement,
} }
const resolveRecipientAddress = async () => {
await resolveAddress(recipientState.value.trim(), wallet).then((resolvedAddress) => {
setResolvedRecipientAddress(resolvedAddress)
})
}
useEffect(() => {
void resolveRecipientAddress()
}, [recipientState.value])
const resolveRoyaltyPaymentAddress = async () => {
await resolveAddress(royaltyPaymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
setCollectionInfo({
description: descriptionState.value.replaceAll('\\n', '\n') || undefined,
image: imageState.value || undefined,
explicit_content: explicitContent,
external_link: externalLinkState.value || undefined,
royalty_info:
royaltyPaymentAddressState.value && royaltyShareState.value
? {
payment_address: resolvedAddress,
share: (Number(royaltyShareState.value) / 100).toString(),
}
: undefined,
})
})
}
useEffect(() => {
void resolveRoyaltyPaymentAddress()
}, [royaltyPaymentAddressState.value])
const resolveCreatorAddress = async () => {
await resolveAddress(creatorState.value.trim(), wallet).then((resolvedAddress) => {
creatorState.onChange(resolvedAddress)
})
}
useEffect(() => {
void resolveCreatorAddress()
}, [creatorState.value])
useEffect(() => {
setCollectionInfo({
description: descriptionState.value.replaceAll('\\n', '\n') || undefined,
image: imageState.value || undefined,
explicit_content: explicitContent,
external_link: externalLinkState.value || undefined,
royalty_info:
royaltyPaymentAddressState.value && royaltyShareState.value
? {
payment_address: royaltyPaymentAddressState.value.trim(),
share: (Number(royaltyShareState.value) / 100).toString(),
}
: undefined,
creator: creatorState.value || undefined,
})
}, [
descriptionState.value,
imageState.value,
explicitContent,
externalLinkState.value,
royaltyPaymentAddressState.value,
royaltyShareState.value,
creatorState.value,
])
useEffect(() => {
if (isEitherType(type, ['set_royalties_for_infinity_swap']) && Number(royaltyShareState.value) > 5) {
royaltyShareState.onChange('5')
toast.error('Royalty share cannot be greater than 5% for Infinity Swap')
}
}, [royaltyShareState.value])
useEffect(() => { useEffect(() => {
const addresses: string[] = [] const addresses: string[] = []
@ -134,12 +358,128 @@ export const CollectionActions = ({
const { isLoading, mutate } = useMutation( const { isLoading, mutate } = useMutation(
async (event: FormEvent) => { async (event: FormEvent) => {
event.preventDefault() event.preventDefault()
if (!wallet.isWalletConnected) {
throw new Error('Please connect your wallet first.')
}
if (!type) { if (!type) {
throw new Error('Please select an action!') throw new Error('Please select an action.')
} }
if (minterContractAddress === '' && sg721ContractAddress === '') { if (minterContractAddress === '' && sg721ContractAddress === '') {
throw new Error('Please enter minter and sg721 contract addresses!') throw new Error('Please enter minter and sg721 contract addresses!')
} }
if (wallet.isWalletConnected && type === 'update_mint_price') {
const contractConfig = (await wallet.getCosmWasmClient()).queryContractSmart(minterContractAddress, {
config: {},
})
await toast
.promise(
(
await wallet.getCosmWasmClient()
).queryContractSmart(minterContractAddress, {
mint_price: {},
}),
{
error: `Querying mint price failed!`,
loading: 'Querying current mint price...',
success: (price) => {
console.log('Current mint price: ', price)
return `Current mint price is ${Number(price.public_price.amount) / 1000000} STARS`
},
},
)
.then(async (price) => {
if (Number(price.public_price.amount) / 1000000 <= priceState.value) {
await contractConfig
.then((config) => {
console.log(config.start_time, Date.now() * 1000000)
if (Number(config.start_time) < Date.now() * 1000000) {
throw new Error(
`Minting has already started on ${new Date(
Number(config.start_time) / 1000000,
).toLocaleString()}. Updated mint price cannot be higher than the current price of ${
Number(price.public_price.amount) / 1000000
} STARS`,
)
}
})
.catch((error) => {
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
})
} else {
await contractConfig.then(async (config) => {
const factoryParameters = await (
await wallet.getCosmWasmClient()
).queryContractSmart(config.factory, {
params: {},
})
if (
factoryParameters.params.min_mint_price.amount &&
priceState.value < Number(factoryParameters.params.min_mint_price.amount) / 1000000
) {
throw new Error(
`Updated mint price cannot be lower than the minimum mint price of ${
Number(factoryParameters.params.min_mint_price.amount) / 1000000
} STARS`,
)
}
})
}
})
}
if (
type === 'update_collection_info' &&
(royaltyShareState.value ? !royaltyPaymentAddressState.value : royaltyPaymentAddressState.value)
) {
throw new Error('Royalty payment address and share percentage are both required')
}
if (
type === 'update_collection_info' &&
royaltyPaymentAddressState.value &&
!royaltyPaymentAddressState.value.trim().endsWith('.stars')
) {
const contractInfoResponse = await (await wallet.getCosmWasmClient())
.queryContractRaw(
royaltyPaymentAddressState.value.trim(),
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
)
.catch((e) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (e.message.includes('bech32')) throw new Error('Invalid royalty payment address.')
console.log(e.message)
})
if (contractInfoResponse !== undefined) {
const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array))
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (contractInfo && (contractInfo.contract.includes('minter') || contractInfo.contract.includes('sg721')))
throw new Error('The provided royalty payment address does not belong to a compatible contract.')
else console.log(contractInfo)
}
}
if (type === 'update_collection_info' && creatorState.value) {
const resolvedCreatorAddress = await resolveAddress(creatorState.value.trim(), wallet)
const contractInfoResponse = await (await wallet.getCosmWasmClient())
.queryContractRaw(
resolvedCreatorAddress,
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
)
.catch((e) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (e.message.includes('bech32')) throw new Error('Invalid creator address.')
console.log(e.message)
})
if (contractInfoResponse !== undefined) {
const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array))
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (contractInfo && !contractInfo.contract.includes('dao'))
throw new Error('The provided creator address does not belong to a compatible contract.')
else console.log(contractInfo)
}
}
const txHash = await toast.promise(dispatchExecute(payload), { const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...', loading: 'Executing message...',
@ -151,40 +491,233 @@ export const CollectionActions = ({
}, },
{ {
onError: (error) => { onError: (error) => {
toast.error(String(error)) toast.error(String(error), { style: { maxWidth: 'none' } })
}, },
}, },
) )
const airdropFileOnChange = (data: AirdropAllocation[]) => { const airdropFileOnChange = (data: AirdropAllocation[]) => {
setAirdropAllocationArray(data) setAirdropAllocationArray(data)
console.log(data) }
const downloadSampleAirdropTokensFile = () => {
const csvData =
'address,amount\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
const blob = new Blob([csvData], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'airdrop_tokens.csv')
a.click()
}
const downloadSampleAirdropSpecificTokensFile = () => {
const csvData =
'address,tokenId\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,214\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,683\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,102'
const blob = new Blob([csvData], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'airdrop_specific_tokens.csv')
a.click()
} }
return ( return (
<form> <form>
<div className="grid grid-cols-2 mt-4"> <div className="grid grid-cols-2 mt-4">
<div className="mr-2"> <div className="mr-2">
<ActionsCombobox {...actionComboboxState} /> <ActionsCombobox minterType={minterType} sg721Type={sg721Type} {...actionComboboxState} />
{showRecipientField && <AddressInput {...recipientState} />} {showRecipientField && <AddressInput {...recipientState} />}
{showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
{showWhitelistField && <AddressInput {...whitelistState} />} {showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />} {showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput {...tokenIdState} />} {showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />} {showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />} {showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
{showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />}
{showPriceField && <NumberInput className="mt-2" {...priceState} />}
{showCreatorField && <AddressInput className="mt-2" {...creatorState} />}
{showDescriptionField && <TextInput className="my-2" {...descriptionState} />}
{showImageField && <TextInput className="mb-2" {...imageState} />}
{showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />}
{showRoyaltyRelatedFields && (
<div className="p-2 my-4 rounded border-2 border-gray-500/50">
<TextInput className="mb-2" {...royaltyPaymentAddressState} />
<NumberInput className="mb-2" {...royaltyShareState} />
<Conditional test={type === 'update_royalties_for_infinity_swap'}>
<div className="flex flex-row space-y-2 w-1/4">
<div className={clsx('flex flex-col space-y-2 w-full form-control')}>
<label className="justify-start cursor-pointer label">
<div className="flex flex-col">
<span className="mr-4 font-bold">Increment</span>
</div>
<input
checked={decrement}
className={`toggle ${decrement ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setDecrement(!decrement)}
type="checkbox"
/>
</label>
</div>
<span className="mx-4 font-bold">Decrement</span>
</div>
</Conditional>
</div>
)}
{showExplicitContentField && (
<div className="flex flex-col space-y-2">
<div>
<div className="flex">
<span className="mt-1 text-sm first-letter:capitalize">
Does the collection contain explicit content?
</span>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={explicitContent === true}
className="peer sr-only"
id="explicitRadio1"
name="explicitRadioOptions1"
onClick={() => {
setExplicitContent(true)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio1"
>
YES
</label>
</div>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={explicitContent === false}
className="peer sr-only"
id="explicitRadio2"
name="explicitRadioOptions2"
onClick={() => {
setExplicitContent(false)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio2"
>
NO
</label>
</div>
</div>
</div>
</div>
)}
{showAirdropFileField && ( {showAirdropFileField && (
<FormGroup <div>
subtitle="CSV file that contains the airdrop addresses and the amount of tokens allocated for each address. Should start with the following header row: address,amount" <FormGroup
title="Airdrop File" subtitle={`CSV file that contains the ${
> type === 'batch_transfer_multi_address' ? '' : 'airdrop'
<AirdropUpload onChange={airdropFileOnChange} /> } addresses and the ${
</FormGroup> type === 'airdrop' || type === 'airdrop_open_edition' ? 'amount of tokens' : 'token ID'
} allocated for each address. Should start with the following header row: ${
type === 'airdrop' || type === 'airdrop_open_edition' ? 'address,amount' : 'address,tokenId'
}`}
title={`${type === 'batch_transfer_multi_address' ? 'Multi-Recipient Transfer File' : 'Airdrop File'}`}
>
<AirdropUpload onChange={airdropFileOnChange} />
</FormGroup>
<Button
className="ml-4 text-sm"
onClick={
type === 'airdrop' || type === 'airdrop_open_edition'
? downloadSampleAirdropTokensFile
: downloadSampleAirdropSpecificTokensFile
}
>
Download Sample File
</Button>
</div>
)} )}
<Conditional test={showDateField}> <Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time"> <FormControl
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} /> className="mt-2"
htmlId="start-date"
title={`Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
setTimestamp(
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
}
value={
timezone === 'Local'
? timestamp
: timestamp
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl> </FormControl>
</Conditional> </Conditional>
<Conditional test={showEndDateField}>
<FormControl
className="mt-2"
htmlId="end-date"
title={`End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
setEndTimestamp(
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
}
value={
timezone === 'Local'
? endTimestamp
: endTimestamp
? new Date(endTimestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
</Conditional>
<Conditional test={showBaseUriField}>
<Tooltip
backgroundColor="bg-blue-500"
className="ml-7"
label="Please toggle this on if the IPFS folder contains files with .json extensions."
placement="bottom"
>
<div className="mt-2 w-3/4 form-control">
<label className="justify-start cursor-pointer label">
<span className="mr-4 font-bold">Metadata files with .json extensions?</span>
<input
checked={jsonExtensions}
className={`toggle ${jsonExtensions ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setJsonExtensions(!jsonExtensions)}
type="checkbox"
/>
</label>
</div>
</Tooltip>
</Conditional>
<Conditional test={type === 'update_collection_info'}>
<Alert className="mt-2 text-sm" type="info">
Please note that you are only required to fill in the fields you want to update.
</Alert>
</Conditional>
<Conditional test={type === 'update_discount_price'}>
<Alert className="mt-2 text-sm" type="warning">
Please note that discount price can only be updated every 24 hours and be removed 12 hours after its last
update.
</Alert>
</Conditional>
</div> </div>
<div className="-mt-6"> <div className="-mt-6">
<div className="relative mb-2"> <div className="relative mb-2">

View File

@ -2,19 +2,38 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react' import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { ActionListItem } from './actions' import type { ActionListItem } from './actions'
import { ACTION_LIST } from './actions' import { BASE_ACTION_LIST, OPEN_EDITION_ACTION_LIST, SG721_UPDATABLE_ACTION_LIST, VENDING_ACTION_LIST } from './actions'
export type MinterType = 'base' | 'vending' | 'openEdition'
export type Sg721Type = 'updatable' | 'base'
export interface ActionsComboboxProps { export interface ActionsComboboxProps {
value: ActionListItem | null value: ActionListItem | null
onChange: (item: ActionListItem) => void onChange: (item: ActionListItem) => void
minterType?: MinterType
sg721Type?: Sg721Type
} }
export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => { export const ActionsCombobox = ({ value, onChange, minterType, sg721Type }: ActionsComboboxProps) => {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(VENDING_ACTION_LIST)
useEffect(() => {
if (minterType === 'base') {
if (sg721Type === 'updatable') SET_ACTION_LIST(BASE_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
else SET_ACTION_LIST(BASE_ACTION_LIST)
} else if (minterType === 'vending') {
if (sg721Type === 'updatable') SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
else SET_ACTION_LIST(VENDING_ACTION_LIST)
} else if (minterType === 'openEdition') {
if (sg721Type === 'updatable') SET_ACTION_LIST(OPEN_EDITION_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
else SET_ACTION_LIST(OPEN_EDITION_ACTION_LIST)
} else SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
}, [minterType, sg721Type])
const filtered = const filtered =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] }) search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
@ -68,7 +87,7 @@ export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active }) clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
} }
value={entry} value={entry}
> >

View File

@ -1,24 +1,54 @@
import type { MinterInstance } from 'contracts/minter' /* eslint-disable eslint-comments/disable-enable-pair */
import { useMinterContract } from 'contracts/minter' import { useBaseMinterContract } from 'contracts/baseMinter'
import type { SG721Instance } from 'contracts/sg721' import { useOpenEditionMinterContract } from 'contracts/openEditionMinter'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import { useRoyaltyRegistryContract } from 'contracts/royaltyRegistry'
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
import { useSG721Contract } from 'contracts/sg721' import { useSG721Contract } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { useVendingMinterContract } from 'contracts/vendingMinter'
import { INFINITY_SWAP_PROTOCOL_ADDRESS } from 'utils/constants'
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
import type { OpenEditionMinterInstance } from '../../../contracts/openEditionMinter/contract'
export type ActionType = typeof ACTION_TYPES[number] export type ActionType = typeof ACTION_TYPES[number]
export const ACTION_TYPES = [ export const ACTION_TYPES = [
'mint_token_uri',
'update_mint_price',
'update_discount_price',
'remove_discount_price',
'mint_to', 'mint_to',
'mint_to_open_edition',
'mint_for', 'mint_for',
'batch_mint', 'batch_mint',
'batch_mint_open_edition',
'set_whitelist', 'set_whitelist',
'update_start_time', 'update_start_time',
'update_end_time',
'update_start_trading_time',
'update_per_address_limit', 'update_per_address_limit',
'withdraw', 'update_collection_info',
'freeze_collection_info',
'set_royalties_for_infinity_swap',
'update_royalties_for_infinity_swap',
'transfer', 'transfer',
'batch_transfer', 'batch_transfer',
'batch_transfer_multi_address',
'burn', 'burn',
'batch_burn', 'batch_burn',
'batch_mint_for',
'shuffle', 'shuffle',
'airdrop', 'airdrop',
'airdrop_open_edition',
'airdrop_specific',
'burn_remaining',
'update_token_metadata',
'batch_update_token_metadata',
'freeze_token_metadata',
'enable_updatable',
] as const ] as const
export interface ActionListItem { export interface ActionListItem {
@ -27,41 +57,36 @@ export interface ActionListItem {
description?: string description?: string
} }
export const ACTION_LIST: ActionListItem[] = [ export const BASE_ACTION_LIST: ActionListItem[] = [
{ {
id: 'mint_to', id: 'mint_token_uri',
name: 'Mint To', name: 'Add New Token',
description: `Mint a token to a user`, description: `Mint a new token and add it to the collection`,
}, },
{ {
id: 'mint_for', id: 'update_start_trading_time',
name: 'Mint For', name: 'Update Trading Start Time',
description: `Mint a token for a user with given token ID`, description: `Update start time for trading`,
}, },
{ {
id: 'batch_mint', id: 'update_collection_info',
name: 'Batch Mint', name: 'Update Collection Info',
description: `Mint multiple tokens to a user with given token amount`, description: `Update Collection Info`,
}, },
{ {
id: 'set_whitelist', id: 'freeze_collection_info',
name: 'Set Whitelist', name: 'Freeze Collection Info',
description: `Set whitelist contract address`, description: `Freeze collection info to prevent further updates`,
}, },
{ {
id: 'update_start_time', id: 'set_royalties_for_infinity_swap',
name: 'Update Start Time', name: 'Set Royalty Details for Infinity Swap',
description: `Update start time for minting`, description: `Set royalty details for Infinity Swap`,
}, },
{ {
id: 'update_per_address_limit', id: 'update_royalties_for_infinity_swap',
name: 'Update Tokens Per Address Limit', name: 'Update Royalty Details for Infinity Swap',
description: `Update token per address limit`, description: `Update royalty details for Infinity Swap`,
},
{
id: 'withdraw',
name: 'Withdraw Tokens',
description: `Withdraw tokens from the contract`,
}, },
{ {
id: 'transfer', id: 'transfer',
@ -73,6 +98,114 @@ export const ACTION_LIST: ActionListItem[] = [
name: 'Batch Transfer Tokens', name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`, description: `Transfer a list of tokens to a recipient`,
}, },
{
id: 'batch_transfer_multi_address',
name: 'Transfer Tokens to Multiple Recipients',
description: `Transfer a list of tokens to multiple addresses`,
},
{
id: 'burn',
name: 'Burn Token',
description: `Burn a specified token from the collection`,
},
{
id: 'batch_burn',
name: 'Batch Burn Tokens',
description: `Burn a list of tokens from the collection`,
},
]
export const VENDING_ACTION_LIST: ActionListItem[] = [
{
id: 'update_mint_price',
name: 'Update Mint Price',
description: `Update mint price`,
},
{
id: 'update_discount_price',
name: 'Update Discount Price',
description: `Update discount price`,
},
{
id: 'remove_discount_price',
name: 'Remove Discount Price',
description: `Remove discount price`,
},
{
id: 'mint_to',
name: 'Mint To',
description: `Mint a token to a user`,
},
{
id: 'batch_mint',
name: 'Batch Mint To',
description: `Mint multiple tokens to a user`,
},
{
id: 'mint_for',
name: 'Mint For',
description: `Mint a token for a user with the given token ID`,
},
{
id: 'batch_mint_for',
name: 'Batch Mint For',
description: `Mint a specific range of tokens from the collection to a specific address`,
},
{
id: 'set_whitelist',
name: 'Set Whitelist',
description: `Set whitelist contract address`,
},
{
id: 'update_start_time',
name: 'Update Minting Start Time',
description: `Update start time for minting`,
},
{
id: 'update_start_trading_time',
name: 'Update Trading Start Time',
description: `Update start time for trading`,
},
{
id: 'update_per_address_limit',
name: 'Update Tokens Per Address Limit',
description: `Update token per address limit`,
},
{
id: 'update_collection_info',
name: 'Update Collection Info',
description: `Update Collection Info`,
},
{
id: 'freeze_collection_info',
name: 'Freeze Collection Info',
description: `Freeze collection info to prevent further updates`,
},
{
id: 'set_royalties_for_infinity_swap',
name: 'Set Royalty Details for Infinity Swap',
description: `Set royalty details for Infinity Swap`,
},
{
id: 'update_royalties_for_infinity_swap',
name: 'Update Royalty Details for Infinity Swap',
description: `Update royalty details for Infinity Swap`,
},
{
id: 'transfer',
name: 'Transfer Tokens',
description: `Transfer tokens from one address to another`,
},
{
id: 'batch_transfer',
name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`,
},
{
id: 'batch_transfer_multi_address',
name: 'Transfer Tokens to Multiple Recipients',
description: `Transfer a list of tokens to multiple addresses`,
},
{ {
id: 'burn', id: 'burn',
name: 'Burn Token', name: 'Burn Token',
@ -93,6 +226,127 @@ export const ACTION_LIST: ActionListItem[] = [
name: 'Airdrop Tokens', name: 'Airdrop Tokens',
description: 'Airdrop tokens to given addresses', description: 'Airdrop tokens to given addresses',
}, },
{
id: 'airdrop_specific',
name: 'Airdrop Specific Tokens',
description: 'Airdrop specific tokens to given addresses',
},
{
id: 'burn_remaining',
name: 'Burn Remaining Tokens',
description: 'Burn remaining tokens',
},
]
export const OPEN_EDITION_ACTION_LIST: ActionListItem[] = [
{
id: 'update_mint_price',
name: 'Update Mint Price',
description: `Update mint price`,
},
{
id: 'mint_to_open_edition',
name: 'Mint To',
description: `Mint a token to a user`,
},
{
id: 'batch_mint_open_edition',
name: 'Batch Mint To',
description: `Mint multiple tokens to a user`,
},
{
id: 'update_start_time',
name: 'Update Minting Start Time',
description: `Update the start time for minting`,
},
{
id: 'update_end_time',
name: 'Update Minting End Time',
description: `Update the end time for minting`,
},
{
id: 'update_start_trading_time',
name: 'Update Trading Start Time',
description: `Update start time for trading`,
},
{
id: 'update_per_address_limit',
name: 'Update Tokens Per Address Limit',
description: `Update token per address limit`,
},
{
id: 'update_collection_info',
name: 'Update Collection Info',
description: `Update Collection Info`,
},
{
id: 'freeze_collection_info',
name: 'Freeze Collection Info',
description: `Freeze collection info to prevent further updates`,
},
{
id: 'set_royalties_for_infinity_swap',
name: 'Set Royalty Details for Infinity Swap',
description: `Set royalty details for Infinity Swap`,
},
{
id: 'update_royalties_for_infinity_swap',
name: 'Update Royalty Details for Infinity Swap',
description: `Update royalty details for Infinity Swap`,
},
{
id: 'transfer',
name: 'Transfer Tokens',
description: `Transfer tokens from one address to another`,
},
{
id: 'batch_transfer',
name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`,
},
{
id: 'batch_transfer_multi_address',
name: 'Transfer Tokens to Multiple Recipients',
description: `Transfer a list of tokens to multiple addresses`,
},
{
id: 'burn',
name: 'Burn Token',
description: `Burn a specified token from the collection`,
},
{
id: 'batch_burn',
name: 'Batch Burn Tokens',
description: `Burn a list of tokens from the collection`,
},
{
id: 'airdrop_open_edition',
name: 'Airdrop Tokens',
description: 'Airdrop tokens to given addresses',
},
]
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
{
id: 'update_token_metadata',
name: 'Update Token Metadata',
description: `Update the metadata URI for a token`,
},
{
id: 'batch_update_token_metadata',
name: 'Batch Update Token Metadata',
description: `Update the metadata URI for a range of tokens`,
},
{
id: 'freeze_token_metadata',
name: 'Freeze Token Metadata',
description: `Render the metadata for tokens no longer updatable`,
},
{
id: 'enable_updatable',
name: 'Enable Updatable',
description: `Render a collection updatable following a migration`,
},
] ]
export interface DispatchExecuteProps { export interface DispatchExecuteProps {
@ -100,61 +354,134 @@ export interface DispatchExecuteProps {
[k: string]: unknown [k: string]: unknown
} }
type Select<T extends ActionType> = T /** @see {@link VendingMinterInstance}{@link BaseMinterInstance} */
export interface DispatchExecuteArgs {
/** @see {@link MinterInstance} */
export type DispatchExecuteArgs = {
minterContract: string minterContract: string
sg721Contract: string sg721Contract: string
minterMessages?: MinterInstance royaltyRegistryContract: string
vendingMinterMessages?: VendingMinterInstance
baseMinterMessages?: BaseMinterInstance
openEditionMinterMessages?: OpenEditionMinterInstance
sg721Messages?: SG721Instance sg721Messages?: SG721Instance
royaltyRegistryMessages?: RoyaltyRegistryInstance
txSigner: string txSigner: string
} & ( type: string | undefined
| { type: undefined } tokenUri: string
| { type: Select<'mint_to'>; recipient: string } price: string
| { type: Select<'mint_for'>; recipient: string; tokenId: number } recipient: string
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number } tokenId: number
| { type: Select<'set_whitelist'>; whitelist: string } batchNumber: number
| { type: Select<'update_start_time'>; startTime: string } whitelist: string
| { type: Select<'update_per_address_limit'>; limit: number } startTime: string | undefined
| { type: Select<'shuffle'> } endTime: string | undefined
| { type: Select<'withdraw'> } limit: number
| { type: Select<'transfer'>; recipient: string; tokenId: number } tokenIds: string
| { type: Select<'batch_transfer'>; recipient: string; tokenIds: string } recipients: string[]
| { type: Select<'burn'>; tokenId: number } tokenRecipients: AirdropAllocation[]
| { type: Select<'batch_burn'>; tokenIds: string } collectionInfo: CollectionInfo | undefined
| { type: Select<'airdrop'>; recipients: string[] } baseUri: string
) jsonExtensions: boolean
decrement: boolean
}
export const dispatchExecute = async (args: DispatchExecuteArgs) => { export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { minterMessages, sg721Messages, txSigner } = args const {
if (!minterMessages || !sg721Messages) { vendingMinterMessages,
baseMinterMessages,
openEditionMinterMessages,
sg721Messages,
royaltyRegistryMessages,
txSigner,
} = args
if (
!vendingMinterMessages ||
!baseMinterMessages ||
!openEditionMinterMessages ||
!sg721Messages ||
!royaltyRegistryMessages
) {
throw new Error('Cannot execute actions') throw new Error('Cannot execute actions')
} }
switch (args.type) { switch (args.type) {
case 'mint_token_uri': {
return baseMinterMessages.mint(txSigner, args.tokenUri)
}
case 'update_mint_price': {
return vendingMinterMessages.updateMintPrice(txSigner, args.price)
}
case 'update_discount_price': {
return vendingMinterMessages.updateDiscountPrice(txSigner, args.price)
}
case 'remove_discount_price': {
return vendingMinterMessages.removeDiscountPrice(txSigner)
}
case 'mint_to': { case 'mint_to': {
return minterMessages.mintTo(txSigner, args.recipient) return vendingMinterMessages.mintTo(txSigner, args.recipient)
}
case 'mint_to_open_edition': {
return openEditionMinterMessages.mintTo(txSigner, args.recipient)
} }
case 'mint_for': { case 'mint_for': {
return minterMessages.mintFor(txSigner, args.recipient, args.tokenId) return vendingMinterMessages.mintFor(txSigner, args.recipient, args.tokenId)
} }
case 'batch_mint': { case 'batch_mint': {
return minterMessages.batchMint(txSigner, args.recipient, args.batchNumber) return vendingMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
}
case 'batch_mint_open_edition': {
return openEditionMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
} }
case 'set_whitelist': { case 'set_whitelist': {
return minterMessages.setWhitelist(txSigner, args.whitelist) return vendingMinterMessages.setWhitelist(txSigner, args.whitelist)
} }
case 'update_start_time': { case 'update_start_time': {
return minterMessages.updateStartTime(txSigner, args.startTime) return vendingMinterMessages.updateStartTime(txSigner, args.startTime as string)
}
case 'update_end_time': {
return openEditionMinterMessages.updateEndTime(txSigner, args.endTime as string)
}
case 'update_start_trading_time': {
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
} }
case 'update_per_address_limit': { case 'update_per_address_limit': {
return minterMessages.updatePerAddressLimit(txSigner, args.limit) return vendingMinterMessages.updatePerAddressLimit(txSigner, args.limit)
}
case 'update_collection_info': {
return sg721Messages.updateCollectionInfo(args.collectionInfo as CollectionInfo)
}
case 'freeze_collection_info': {
return sg721Messages.freezeCollectionInfo()
}
case 'update_token_metadata': {
return sg721Messages.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
}
case 'batch_update_token_metadata': {
return sg721Messages.batchUpdateTokenMetadata(args.tokenIds, args.baseUri, args.jsonExtensions)
}
case 'freeze_token_metadata': {
return sg721Messages.freezeTokenMetadata()
}
case 'enable_updatable': {
return sg721Messages.enableUpdatable()
} }
case 'shuffle': { case 'shuffle': {
return minterMessages.shuffle(txSigner) return vendingMinterMessages.shuffle(txSigner)
} }
case 'withdraw': { case 'set_royalties_for_infinity_swap': {
return minterMessages.withdraw(txSigner) return royaltyRegistryMessages.setCollectionRoyaltyProtocol(
args.sg721Contract,
INFINITY_SWAP_PROTOCOL_ADDRESS,
args.collectionInfo?.royalty_info?.payment_address as string,
Number(args.collectionInfo?.royalty_info?.share) * 100,
)
}
case 'update_royalties_for_infinity_swap': {
return royaltyRegistryMessages.updateCollectionRoyaltyProtocol(
args.sg721Contract,
INFINITY_SWAP_PROTOCOL_ADDRESS,
args.collectionInfo?.royalty_info?.payment_address as string,
Number(args.collectionInfo?.royalty_info?.share) * 100,
args.decrement,
)
} }
case 'transfer': { case 'transfer': {
return sg721Messages.transferNft(args.recipient, args.tokenId.toString()) return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
@ -162,14 +489,29 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'batch_transfer': { case 'batch_transfer': {
return sg721Messages.batchTransfer(args.recipient, args.tokenIds) return sg721Messages.batchTransfer(args.recipient, args.tokenIds)
} }
case 'batch_transfer_multi_address': {
return sg721Messages.batchTransferMultiAddress(txSigner, args.tokenRecipients)
}
case 'burn': { case 'burn': {
return sg721Messages.burn(args.tokenId.toString()) return sg721Messages.burn(args.tokenId.toString())
} }
case 'batch_burn': { case 'batch_burn': {
return sg721Messages.batchBurn(args.tokenIds) return sg721Messages.batchBurn(args.tokenIds)
} }
case 'batch_mint_for': {
return vendingMinterMessages.batchMintFor(txSigner, args.recipient, args.tokenIds)
}
case 'airdrop': { case 'airdrop': {
return minterMessages.airdrop(txSigner, args.recipients) return vendingMinterMessages.airdrop(txSigner, args.recipients)
}
case 'airdrop_open_edition': {
return openEditionMinterMessages.airdrop(txSigner, args.recipients)
}
case 'airdrop_specific': {
return vendingMinterMessages.airdropSpecificTokens(txSigner, args.tokenRecipients)
}
case 'burn_remaining': {
return vendingMinterMessages.burnRemaining(txSigner)
} }
default: { default: {
throw new Error('Unknown action') throw new Error('Unknown action')
@ -179,34 +521,97 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
export const previewExecutePayload = (args: DispatchExecuteArgs) => { export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: minterMessages } = useMinterContract() const { messages: vendingMinterMessages } = useVendingMinterContract()
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: sg721Messages } = useSG721Contract() const { messages: sg721Messages } = useSG721Contract()
const { minterContract, sg721Contract } = args // eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: baseMinterMessages } = useBaseMinterContract()
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: openEditionMinterMessages } = useOpenEditionMinterContract()
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: royaltyRegistryMessages } = useRoyaltyRegistryContract()
const { minterContract, sg721Contract, royaltyRegistryContract } = args
switch (args.type) { switch (args.type) {
case 'mint_token_uri': {
return baseMinterMessages(minterContract)?.mint(args.tokenUri)
}
case 'update_mint_price': {
return vendingMinterMessages(minterContract)?.updateMintPrice(args.price)
}
case 'update_discount_price': {
return vendingMinterMessages(minterContract)?.updateDiscountPrice(args.price)
}
case 'remove_discount_price': {
return vendingMinterMessages(minterContract)?.removeDiscountPrice()
}
case 'mint_to': { case 'mint_to': {
return minterMessages(minterContract)?.mintTo(args.recipient) return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
}
case 'mint_to_open_edition': {
return openEditionMinterMessages(minterContract)?.mintTo(args.recipient)
} }
case 'mint_for': { case 'mint_for': {
return minterMessages(minterContract)?.mintFor(args.recipient, args.tokenId) return vendingMinterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
} }
case 'batch_mint': { case 'batch_mint': {
return minterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber) return vendingMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
}
case 'batch_mint_open_edition': {
return openEditionMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
} }
case 'set_whitelist': { case 'set_whitelist': {
return minterMessages(minterContract)?.setWhitelist(args.whitelist) return vendingMinterMessages(minterContract)?.setWhitelist(args.whitelist)
} }
case 'update_start_time': { case 'update_start_time': {
return minterMessages(minterContract)?.updateStartTime(args.startTime) return vendingMinterMessages(minterContract)?.updateStartTime(args.startTime as string)
}
case 'update_end_time': {
return openEditionMinterMessages(minterContract)?.updateEndTime(args.endTime as string)
}
case 'update_start_trading_time': {
return vendingMinterMessages(minterContract)?.updateStartTradingTime(args.startTime as string)
} }
case 'update_per_address_limit': { case 'update_per_address_limit': {
return minterMessages(minterContract)?.updatePerAddressLimit(args.limit) return vendingMinterMessages(minterContract)?.updatePerAddressLimit(args.limit)
}
case 'update_collection_info': {
return sg721Messages(sg721Contract)?.updateCollectionInfo(args.collectionInfo as CollectionInfo)
}
case 'freeze_collection_info': {
return sg721Messages(sg721Contract)?.freezeCollectionInfo()
}
case 'update_token_metadata': {
return sg721Messages(sg721Contract)?.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
}
case 'batch_update_token_metadata': {
return sg721Messages(sg721Contract)?.batchUpdateTokenMetadata(args.tokenIds, args.baseUri, args.jsonExtensions)
}
case 'freeze_token_metadata': {
return sg721Messages(sg721Contract)?.freezeTokenMetadata()
}
case 'enable_updatable': {
return sg721Messages(sg721Contract)?.enableUpdatable()
} }
case 'shuffle': { case 'shuffle': {
return minterMessages(minterContract)?.shuffle() return vendingMinterMessages(minterContract)?.shuffle()
} }
case 'withdraw': { case 'set_royalties_for_infinity_swap': {
return minterMessages(minterContract)?.withdraw() return royaltyRegistryMessages(royaltyRegistryContract)?.setCollectionRoyaltyProtocol(
args.sg721Contract,
INFINITY_SWAP_PROTOCOL_ADDRESS,
args.collectionInfo?.royalty_info?.payment_address as string,
Number(args.collectionInfo?.royalty_info?.share) * 100,
)
}
case 'update_royalties_for_infinity_swap': {
return royaltyRegistryMessages(royaltyRegistryContract)?.updateCollectionRoyaltyProtocol(
args.sg721Contract,
INFINITY_SWAP_PROTOCOL_ADDRESS,
args.collectionInfo?.royalty_info?.payment_address as string,
Number(args.collectionInfo?.royalty_info?.share) * 100,
args.decrement,
)
} }
case 'transfer': { case 'transfer': {
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString()) return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
@ -214,14 +619,29 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'batch_transfer': { case 'batch_transfer': {
return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds) return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds)
} }
case 'batch_transfer_multi_address': {
return sg721Messages(sg721Contract)?.batchTransferMultiAddress(args.tokenRecipients)
}
case 'burn': { case 'burn': {
return sg721Messages(sg721Contract)?.burn(args.tokenId.toString()) return sg721Messages(sg721Contract)?.burn(args.tokenId.toString())
} }
case 'batch_burn': { case 'batch_burn': {
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds) return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
} }
case 'batch_mint_for': {
return vendingMinterMessages(minterContract)?.batchMintFor(args.recipient, args.tokenIds)
}
case 'airdrop': { case 'airdrop': {
return minterMessages(minterContract)?.airdrop(args.recipients) return vendingMinterMessages(minterContract)?.airdrop(args.recipients)
}
case 'airdrop_open_edition': {
return openEditionMinterMessages(minterContract)?.airdrop(args.recipients)
}
case 'airdrop_specific': {
return vendingMinterMessages(minterContract)?.airdropSpecificTokens(args.tokenRecipients)
}
case 'burn_remaining': {
return vendingMinterMessages(minterContract)?.burnRemaining()
} }
default: { default: {
return {} return {}

View File

@ -0,0 +1,301 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { toUtf8 } from '@cosmjs/encoding'
import axios from 'axios'
import clsx from 'clsx'
import { Alert } from 'components/Alert'
import { Conditional } from 'components/Conditional'
import { useInputState } from 'components/forms/FormInput.hooks'
import React, { useCallback, useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { API_URL } from 'utils/constants'
import { useWallet } from 'utils/wallet'
import { useDebounce } from '../../../utils/debounce'
import { TextInput } from '../../forms/FormInput'
import type { MinterType } from '../actions/Combobox'
export type BaseMinterAcquisitionMethod = 'existing' | 'new'
export interface MinterInfo {
name: string
minter: string
contractAddress: string
}
interface BaseMinterDetailsProps {
onChange: (data: BaseMinterDetailsDataProps) => void
minterType: MinterType
importedBaseMinterDetails?: BaseMinterDetailsDataProps
}
export interface BaseMinterDetailsDataProps {
baseMinterAcquisitionMethod: BaseMinterAcquisitionMethod
existingBaseMinter: string | undefined
selectedCollectionAddress: string | undefined
collectionTokenCount: number | undefined
}
export const BaseMinterDetails = ({ onChange, minterType, importedBaseMinterDetails }: BaseMinterDetailsProps) => {
const wallet = useWallet()
const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([])
const [baseMinterAcquisitionMethod, setBaseMinterAcquisitionMethod] = useState<BaseMinterAcquisitionMethod>('new')
const [selectedCollectionAddress, setSelectedCollectionAddress] = useState<string | undefined>(undefined)
const [collectionTokenCount, setCollectionTokenCount] = useState<number | undefined>(undefined)
const existingBaseMinterState = useInputState({
id: 'existingMinter',
name: 'existingMinter',
title: 'Existing Base Minter Contract Address',
subtitle: '',
placeholder: 'stars1...',
})
const fetchMinterContracts = async (): Promise<MinterInfo[]> => {
const contracts: MinterInfo[] = await axios
.get(`${API_URL}/api/v1beta/collections/${wallet.address || ''}`)
.then((response) => {
const collectionData = response.data
const minterContracts = collectionData.map((collection: any) => {
return { name: collection.name, minter: collection.minter, contractAddress: collection.contractAddress }
})
return minterContracts
})
.catch(console.error)
console.log(contracts)
return contracts
}
async function getMinterContractType(minterContractAddress: string) {
if (wallet.isWalletConnected && minterContractAddress.length > 0) {
const client = await wallet.getCosmWasmClient()
const data = await client.queryContractRaw(
minterContractAddress,
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
)
const contractType: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract
return contractType
}
}
const filterBaseMinterContracts = async () => {
await fetchMinterContracts()
.then((minterContracts) =>
minterContracts.map(async (minterContract: any) => {
await getMinterContractType(minterContract.minter)
.then((contractType) => {
if (contractType?.includes('sg-base-minter')) {
setMyBaseMinterContracts((prevState) => [...prevState, minterContract])
}
})
.catch((err) => {
console.log(err)
console.log('Unable to retrieve contract type')
})
}),
)
.catch((err) => {
console.log(err)
console.log('Unable to fetch base minter contracts')
})
}
const debouncedMyBaseMinterContracts = useDebounce(myBaseMinterContracts, 500)
const renderBaseMinterContracts = useCallback(() => {
return debouncedMyBaseMinterContracts.map((baseMinterContract, index) => {
return (
<option key={index} className="mt-2 text-lg bg-[#1A1A1A]">
{`${baseMinterContract.name} - ${baseMinterContract.minter}`}
</option>
)
})
}, [debouncedMyBaseMinterContracts])
const debouncedWalletAddress = useDebounce(wallet.address, 300)
const debouncedExistingBaseMinterContract = useDebounce(existingBaseMinterState.value, 300)
const displayToast = async () => {
await toast.promise(filterBaseMinterContracts(), {
loading: 'Retrieving previous 1/1 collections...',
success: 'Collection retrieval finalized.',
error: 'Unable to retrieve any 1/1 collections.',
})
}
const fetchSg721Address = async () => {
if (debouncedExistingBaseMinterContract.length === 0) return
await (
await wallet.getCosmWasmClient()
)
.queryContractSmart(debouncedExistingBaseMinterContract, {
config: {},
})
.then((response) => {
console.log(response.collection_address)
setSelectedCollectionAddress(response.collection_address)
})
.catch((err) => {
console.log(err)
console.log('Unable to retrieve collection address')
})
}
const fetchCollectionTokenCount = async () => {
if (selectedCollectionAddress === undefined) return
await (
await wallet.getCosmWasmClient()
)
.queryContractSmart(selectedCollectionAddress, {
num_tokens: {},
})
.then((response) => {
console.log(response)
setCollectionTokenCount(Number(response.count))
})
.catch((err) => {
console.log(err)
console.log('Unable to retrieve collection token count')
})
}
useEffect(() => {
if (debouncedWalletAddress && baseMinterAcquisitionMethod === 'existing') {
setMyBaseMinterContracts([])
existingBaseMinterState.onChange('')
void displayToast()
} else if (baseMinterAcquisitionMethod === 'new' || !wallet.isWalletConnected) {
setMyBaseMinterContracts([])
existingBaseMinterState.onChange('')
}
}, [debouncedWalletAddress, baseMinterAcquisitionMethod])
useEffect(() => {
if (baseMinterAcquisitionMethod === 'existing') {
void fetchSg721Address()
}
}, [debouncedExistingBaseMinterContract])
useEffect(() => {
if (baseMinterAcquisitionMethod === 'existing') {
void fetchCollectionTokenCount()
}
}, [selectedCollectionAddress])
useEffect(() => {
const data: BaseMinterDetailsDataProps = {
baseMinterAcquisitionMethod,
existingBaseMinter: existingBaseMinterState.value,
selectedCollectionAddress,
collectionTokenCount,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
existingBaseMinterState.value,
baseMinterAcquisitionMethod,
wallet.isWalletConnected,
selectedCollectionAddress,
collectionTokenCount,
])
useEffect(() => {
if (importedBaseMinterDetails) {
setBaseMinterAcquisitionMethod(importedBaseMinterDetails.baseMinterAcquisitionMethod)
existingBaseMinterState.onChange(
importedBaseMinterDetails.existingBaseMinter ? importedBaseMinterDetails.existingBaseMinter : '',
)
}
}, [importedBaseMinterDetails])
return (
<div className="mx-10 mb-4 rounded border-2 border-white/20">
<div className="flex justify-center mb-2">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={baseMinterAcquisitionMethod === 'new'}
className="peer sr-only"
id="inlineRadio5"
name="inlineRadioOptions5"
onClick={() => {
setBaseMinterAcquisitionMethod('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio5"
>
Create a New 1/1 Collection
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={baseMinterAcquisitionMethod === 'existing'}
className="peer sr-only"
id="inlineRadio6"
name="inlineRadioOptions6"
onClick={() => {
setBaseMinterAcquisitionMethod('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio6"
>
Add a New Token to an Existing 1/1 Collection
</label>
</div>
</div>
{baseMinterAcquisitionMethod === 'existing' && (
<div>
<div className={clsx('my-4 mx-10')}>
<Conditional test={myBaseMinterContracts.length !== 0}>
<select
className="mt-4 w-full max-w-3xl text-base bg-white/10 select select-bordered"
onChange={(e) => {
existingBaseMinterState.onChange(e.target.value.slice(e.target.value.indexOf('stars1')))
e.preventDefault()
}}
>
<option className="mt-2 text-lg bg-[#1A1A1A]" disabled selected>
Select one of your existing 1/1 collections
</option>
{renderBaseMinterContracts()}
</select>
</Conditional>
<Conditional test={myBaseMinterContracts.length === 0}>
<div className="flex flex-col">
<Conditional test={wallet.isWalletConnected}>
<Alert className="my-2 w-[90%]" type="info">
No previous 1/1 collections were found. You may create a new 1/1 collection or fill in the minter
contract address manually.
</Alert>
<TextInput
className="w-3/5"
defaultValue={existingBaseMinterState.value}
{...existingBaseMinterState}
isRequired
/>
</Conditional>
<Conditional test={!wallet.isWalletConnected}>
<Alert className="my-2 w-[90%]" type="warning">
Please connect your wallet first.
</Alert>
</Conditional>
</div>
</Conditional>
</div>
</div>
)}
</div>
)
}

View File

@ -1,23 +1,34 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import clsx from 'clsx' import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { Tooltip } from 'components/Tooltip'
import { useGlobalSettings } from 'contexts/globalSettings'
import { addLogItem } from 'contexts/log'
import type { ChangeEvent } from 'react' import type { ChangeEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { SG721_UPDATABLE_CODE_ID } from 'utils/constants'
import { uid } from 'utils/random'
import { TextInput } from '../../forms/FormInput' import { TextInput } from '../../forms/FormInput'
import type { MinterType } from '../actions/Combobox'
import type { UploadMethod } from './UploadDetails' import type { UploadMethod } from './UploadDetails'
interface CollectionDetailsProps { interface CollectionDetailsProps {
onChange: (data: CollectionDetailsDataProps) => void onChange: (data: CollectionDetailsDataProps) => void
uploadMethod: UploadMethod uploadMethod: UploadMethod
coverImageUrl: string coverImageUrl: string
minterType: MinterType
importedCollectionDetails?: CollectionDetailsDataProps
} }
export interface CollectionDetailsDataProps { export interface CollectionDetailsDataProps {
@ -26,10 +37,25 @@ export interface CollectionDetailsDataProps {
symbol: string symbol: string
imageFile: File[] imageFile: File[]
externalLink?: string externalLink?: string
startTradingTime?: string
explicit: boolean
updatable: boolean
} }
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: CollectionDetailsProps) => { export const CollectionDetails = ({
onChange,
uploadMethod,
coverImageUrl,
minterType,
importedCollectionDetails,
}: CollectionDetailsProps) => {
const [coverImage, setCoverImage] = useState<File | null>(null) const [coverImage, setCoverImage] = useState<File | null>(null)
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [explicit, setExplicit] = useState<boolean>(false)
const [updatable, setUpdatable] = useState<boolean>(false)
const { timezone } = useGlobalSettings()
const initialRender = useRef(true)
const nameState = useInputState({ const nameState = useInputState({
id: 'name', id: 'name',
@ -66,15 +92,45 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
description: descriptionState.value, description: descriptionState.value,
symbol: symbolState.value, symbol: symbolState.value,
imageFile: coverImage ? [coverImage] : [], imageFile: coverImage ? [coverImage] : [],
externalLink: externalLinkState.value, externalLink: externalLinkState.value || undefined,
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
explicit,
updatable,
} }
onChange(data) onChange(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
toast.error(error.message) toast.error(error.message, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [nameState.value, descriptionState.value, coverImage, externalLinkState.value]) }, [
nameState.value,
descriptionState.value,
symbolState.value,
externalLinkState.value,
coverImage,
timestamp,
explicit,
updatable,
])
useEffect(() => {
if (importedCollectionDetails) {
nameState.onChange(importedCollectionDetails.name)
descriptionState.onChange(importedCollectionDetails.description)
symbolState.onChange(importedCollectionDetails.symbol)
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
setTimestamp(
importedCollectionDetails.startTradingTime
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
: undefined,
)
setExplicit(importedCollectionDetails.explicit)
setUpdatable(importedCollectionDetails.updatable)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedCollectionDetails])
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => { const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) return toast.error('Error selecting cover image') if (event.target.files === null) return toast.error('Error selecting cover image')
@ -86,24 +142,166 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
reader.onload = (e) => { reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.') if (!event.target.files) return toast.error('No files selected.')
const imageFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' }) const imageFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
type: 'image/jpg',
})
setCoverImage(imageFile) setCoverImage(imageFile)
} }
reader.readAsArrayBuffer(event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
} }
useEffect(() => {
if (initialRender.current) {
initialRender.current = false
} else if (updatable) {
toast.success('Token metadata will be updatable upon collection creation.', {
style: { maxWidth: 'none' },
icon: '✅📝',
})
} else {
toast.error('Token metadata will not be updatable upon collection creation.', {
style: { maxWidth: 'none' },
icon: '⛔🔏',
})
}
}, [updatable])
return ( return (
<div> <div>
<FormGroup subtitle="Information about your collection" title="Collection Details"> <FormGroup subtitle="Information about your collection" title="Collection Details">
<TextInput {...nameState} isRequired /> <div className={clsx(minterType === 'base' ? 'grid grid-cols-2 -ml-16 max-w-5xl' : '')}>
<TextInput {...descriptionState} isRequired /> <div className={clsx(minterType === 'base' ? 'ml-0' : '')}>
<TextInput {...symbolState} isRequired /> <TextInput {...nameState} isRequired />
<TextInput className="mt-2" {...descriptionState} isRequired />
<TextInput className="mt-2" {...symbolState} isRequired />
</div>
<div className={clsx(minterType === 'base' ? 'ml-10' : '')}>
<TextInput className={clsx(minterType === 'base' ? 'mt-0' : 'mt-2')} {...externalLinkState} />
{/* Currently trading starts immediately for 1/1 Collections */}
<Conditional test={minterType !== 'base'}>
<FormControl
className={clsx(minterType === 'base' ? 'mt-10' : 'mt-2')}
htmlId="timestamp"
subtitle="Trading start time offset will be set as 2 weeks by default."
title={`Trading Start Time (optional | ${timezone === 'Local' ? 'local)' : 'UTC)'}`}
>
<InputDateTime
minDate={
timezone === 'Local'
? new Date()
: new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setTimestamp(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setTimestamp(undefined)
}
value={
timezone === 'Local'
? timestamp
: timestamp
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
</Conditional>
<Conditional test={minterType === 'base'}>
<div
className={clsx(minterType === 'base' ? 'flex flex-col -ml-6 space-y-2' : 'flex flex-col space-y-2')}
>
<div>
<div className="flex mt-9 ml-6">
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
Does the collection contain explicit content?
</span>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={explicit}
className="peer sr-only"
id="explicitRadio1"
name="explicitRadioOptions1"
onClick={() => {
setExplicit(true)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio1"
>
YES
</label>
</div>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={!explicit}
className="peer sr-only"
id="explicitRadio2"
name="explicitRadioOptions2"
onClick={() => {
setExplicit(false)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio2"
>
NO
</label>
</div>
</div>
</div>
</div>
<Conditional test={false && SG721_UPDATABLE_CODE_ID > 0}>
<Tooltip
backgroundColor="bg-blue-500"
label={
<div className="grid grid-flow-row">
<span>
When enabled, the metadata for tokens can be updated after the collection is created until
the collection is frozen by the creator.
</span>
</div>
}
placement="bottom"
>
<div className={clsx('flex flex-col mt-11 space-y-2 w-full form-control')}>
<label className="justify-start cursor-pointer label">
<div className="flex flex-col">
<span className="mr-4 font-bold">Updatable Token Metadata</span>
<span className="mr-4">(Price: 2000 STARS)</span>
</div>
<input
checked={updatable}
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setUpdatable(!updatable)}
type="checkbox"
/>
</label>
</div>
</Tooltip>
</Conditional>
</Conditional>
</div>
</div>
<FormControl isRequired={uploadMethod === 'new'} title="Cover Image"> <FormControl
className={clsx(minterType === 'base' ? '-ml-16' : '')}
isRequired={uploadMethod === 'new'}
title="Cover Image"
>
{uploadMethod === 'new' && ( {uploadMethod === 'new' && (
<input <input
accept="image/*" accept="image/*"
className={clsx( className={clsx(
minterType === 'base' ? 'w-1/2' : 'w-full',
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer', 'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:hover:bg-white/5 before:transition', 'before:hover:bg-white/5 before:transition',
)} )}
@ -122,7 +320,9 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
<div className="max-w-[200px] max-h-[200px] rounded border-2"> <div className="max-w-[200px] max-h-[200px] rounded border-2">
<img <img
alt="no-preview-available" alt="no-preview-available"
src={`https://ipfs.io/ipfs/${coverImageUrl.substring(coverImageUrl.lastIndexOf('ipfs://') + 7)}`} src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
coverImageUrl.lastIndexOf('ipfs://') + 7,
)}`}
/> />
</div> </div>
)} )}
@ -135,8 +335,88 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
<span className="italic font-light ">Waiting for cover image URL to be specified.</span> <span className="italic font-light ">Waiting for cover image URL to be specified.</span>
)} )}
</FormControl> </FormControl>
<Conditional test={minterType !== 'base'}>
<TextInput {...externalLinkState} /> <div className={clsx(minterType === 'base' ? 'flex flex-col -ml-6 space-y-2' : 'flex flex-col space-y-2')}>
<div>
<div className="flex mt-4">
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
Does the collection contain explicit content?
</span>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={explicit}
className="peer sr-only"
id="explicitRadio1"
name="explicitRadioOptions1"
onClick={() => {
setExplicit(true)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio1"
>
YES
</label>
</div>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={!explicit}
className="peer sr-only"
id="explicitRadio2"
name="explicitRadioOptions2"
onClick={() => {
setExplicit(false)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio2"
>
NO
</label>
</div>
</div>
</div>
</div>
<Conditional test={false && SG721_UPDATABLE_CODE_ID > 0}>
<Tooltip
backgroundColor="bg-blue-500"
label={
<div className="grid grid-flow-row">
<span>
When enabled, the metadata for tokens can be updated after the collection is created until the
collection is frozen by the creator.
</span>
</div>
}
placement="bottom"
>
<div
className={clsx(
minterType === 'base'
? 'flex flex-col -ml-16 space-y-2 w-1/2 form-control'
: 'flex flex-col space-y-2 w-full form-control',
)}
>
<label className="justify-start cursor-pointer label">
<div className="flex flex-col">
<span className="mr-4 font-bold">Updatable Token Metadata</span>
<span className="mr-4">(Price: 2000 STARS)</span>
</div>
<input
checked={updatable}
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setUpdatable(!updatable)}
type="checkbox"
/>
</label>
</div>
</Tooltip>
</Conditional>
</Conditional>
</FormGroup> </FormGroup>
</div> </div>
) )

View File

@ -1,16 +1,30 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable no-nested-ternary */
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import { vendingMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { stars, tokensList } from 'config/token'
import { useGlobalSettings } from 'contexts/globalSettings'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
import { useWallet } from 'utils/wallet'
import { NumberInput } from '../../forms/FormInput' import { NumberInput, TextInput } from '../../forms/FormInput'
import type { UploadMethod } from './UploadDetails' import type { UploadMethod } from './UploadDetails'
interface MintingDetailsProps { interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void onChange: (data: MintingDetailsDataProps) => void
numberOfTokens: number | undefined numberOfTokens: number | undefined
uploadMethod: UploadMethod uploadMethod: UploadMethod
minimumMintPrice: number
mintingTokenFromFactory?: TokenInfo
importedMintingDetails?: MintingDetailsDataProps
isPresale: boolean
whitelistStartDate?: string
} }
export interface MintingDetailsDataProps { export interface MintingDetailsDataProps {
@ -18,10 +32,24 @@ export interface MintingDetailsDataProps {
unitPrice: string unitPrice: string
perAddressLimit: number perAddressLimit: number
startTime: string startTime: string
paymentAddress?: string
selectedMintToken?: TokenInfo
} }
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => { export const MintingDetails = ({
onChange,
numberOfTokens,
uploadMethod,
minimumMintPrice,
mintingTokenFromFactory,
importedMintingDetails,
isPresale,
whitelistStartDate,
}: MintingDetailsProps) => {
const wallet = useWallet()
const { timezone } = useGlobalSettings()
const [timestamp, setTimestamp] = useState<Date | undefined>() const [timestamp, setTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const numberOfTokensState = useNumberInputState({ const numberOfTokensState = useNumberInputState({
id: 'numberoftokens', id: 'numberoftokens',
@ -35,7 +63,9 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
id: 'unitPrice', id: 'unitPrice',
name: 'unitPrice', name: 'unitPrice',
title: 'Unit Price', title: 'Unit Price',
subtitle: 'Price of each token (min. 50 STARS)', subtitle: `Price of each token (min. ${minimumMintPrice} ${
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '50', placeholder: '50',
}) })
@ -47,17 +77,68 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
placeholder: '1', placeholder: '1',
}) })
const paymentAddressState = useInputState({
id: 'payment-address',
name: 'paymentAddress',
title: 'Payment Address (optional)',
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
})
const resolvePaymentAddress = async () => {
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
paymentAddressState.onChange(resolvedAddress)
})
}
useEffect(() => {
void resolvePaymentAddress()
}, [paymentAddressState.value])
useEffect(() => { useEffect(() => {
if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens) if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens)
const data: MintingDetailsDataProps = { const data: MintingDetailsDataProps = {
numTokens: numberOfTokensState.value, numTokens: numberOfTokensState.value,
unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '', unitPrice: unitPriceState.value
? (Number(unitPriceState.value) * 1_000_000).toString()
: unitPriceState.value === 0
? '0'
: '',
perAddressLimit: perAddressLimitState.value, perAddressLimit: perAddressLimitState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
} }
console.log('Timestamp:', timestamp?.getTime())
onChange(data) onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [numberOfTokens, numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp]) }, [
numberOfTokens,
numberOfTokensState.value,
unitPriceState.value,
perAddressLimitState.value,
timestamp,
paymentAddressState.value,
selectedMintToken,
])
useEffect(() => {
if (importedMintingDetails) {
numberOfTokensState.onChange(importedMintingDetails.numTokens)
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1_000_000)
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedMintingDetails])
useEffect(() => {
if (isPresale) {
setTimestamp(whitelistStartDate ? new Date(Number(whitelistStartDate) / 1_000_000) : undefined)
}
}, [whitelistStartDate, isPresale])
return ( return (
<div> <div>
@ -68,12 +149,58 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
isRequired isRequired
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value} value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
/> />
<NumberInput {...unitPriceState} isRequired /> <div className="flex flex-row items-end">
<NumberInput {...unitPriceState} isRequired />
<select
className="py-[9px] px-4 ml-2 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
value={selectedMintToken?.displayName}
>
{vendingMinterList
.filter(
(minter) =>
minter.factoryAddress !== undefined && minter.updatable === false && minter.featured === false,
)
.map((minter) => (
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
{minter.supportedToken.displayName}
</option>
))}
</select>
</div>
<NumberInput {...perAddressLimitState} isRequired /> <NumberInput {...perAddressLimitState} isRequired />
<FormControl htmlId="timestamp" isRequired subtitle="Minting start time (local)" title="Start Time"> <FormControl
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} /> htmlId="timestamp"
isRequired
subtitle={`Minting start time ${isPresale ? '(is dictated by whitelist start time)' : ''} ${
timezone === 'Local' ? '(local)' : '(UTC)'
}`}
title="Start Time"
>
<InputDateTime
disabled={isPresale}
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setTimestamp(
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setTimestamp(undefined)
}
value={
timezone === 'Local'
? timestamp
: timestamp
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<TextInput className="p-4 mt-5" {...paymentAddressState} />
</div> </div>
) )
} }

View File

@ -2,11 +2,14 @@ import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useWallet } from 'utils/wallet'
import { resolveAddress } from '../../../utils/resolveAddress'
import { NumberInput, TextInput } from '../../forms/FormInput' import { NumberInput, TextInput } from '../../forms/FormInput'
interface RoyaltyDetailsProps { interface RoyaltyDetailsProps {
onChange: (data: RoyaltyDetailsDataProps) => void onChange: (data: RoyaltyDetailsDataProps) => void
importedRoyaltyDetails?: RoyaltyDetailsDataProps
} }
export interface RoyaltyDetailsDataProps { export interface RoyaltyDetailsDataProps {
@ -17,7 +20,8 @@ export interface RoyaltyDetailsDataProps {
type RoyaltyState = 'none' | 'new' type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => { export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none') const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
const royaltyPaymentAddressState = useInputState({ const royaltyPaymentAddressState = useInputState({
@ -33,19 +37,39 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '8%', placeholder: '5%',
}) })
useEffect(() => { useEffect(() => {
const data: RoyaltyDetailsDataProps = { void resolveAddress(
royaltyType: royaltyState, royaltyPaymentAddressState.value
paymentAddress: royaltyPaymentAddressState.value, .toLowerCase()
share: Number(royaltyShareState.value), .replace(/,/g, '')
} .replace(/"/g, '')
onChange(data) .replace(/'/g, '')
.replace(/ /g, ''),
wallet,
).then((royaltyPaymentAddress) => {
royaltyPaymentAddressState.onChange(royaltyPaymentAddress)
const data: RoyaltyDetailsDataProps = {
royaltyType: royaltyState,
paymentAddress: royaltyPaymentAddressState.value,
share: Number(royaltyShareState.value),
}
onChange(data)
})
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value]) }, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
useEffect(() => {
if (importedRoyaltyDetails) {
setRoyaltyState(importedRoyaltyDetails.royaltyType)
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress)
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedRoyaltyDetails])
return ( return (
<div className="py-3 px-8 rounded border-2 border-white/20"> <div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center"> <div className="flex justify-center">

View File

@ -1,4 +1,8 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable array-callback-return */
/* eslint-disable no-nested-ternary */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
/* eslint-disable @typescript-eslint/no-loop-func */ /* eslint-disable @typescript-eslint/no-loop-func */
import clsx from 'clsx' import clsx from 'clsx'
import { Alert } from 'components/Alert' import { Alert } from 'components/Alert'
@ -7,22 +11,38 @@ import { AssetsPreview } from 'components/AssetsPreview'
import { Conditional } from 'components/Conditional' import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput' import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import { MetadataInput } from 'components/MetadataInput'
import { MetadataModal } from 'components/MetadataModal' import { MetadataModal } from 'components/MetadataModal'
import { SingleAssetPreview } from 'components/SingleAssetPreview'
import { Tooltip } from 'components/Tooltip'
import { addLogItem } from 'contexts/log'
import type { ChangeEvent } from 'react' import type { ChangeEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload' import type { UploadServiceType } from 'services/upload'
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
import type { AssetType } from 'utils/getAssetType'
import { getAssetType } from 'utils/getAssetType'
import { uid } from 'utils/random'
import { naturalCompare } from 'utils/sort' import { naturalCompare } from 'utils/sort'
import type { MinterType } from '../actions/Combobox'
import type { BaseMinterAcquisitionMethod } from './BaseMinterDetails'
export type UploadMethod = 'new' | 'existing' export type UploadMethod = 'new' | 'existing'
interface UploadDetailsProps { interface UploadDetailsProps {
onChange: (value: UploadDetailsDataProps) => void onChange: (value: UploadDetailsDataProps) => void
minterType: MinterType
baseMinterAcquisitionMethod?: BaseMinterAcquisitionMethod
importedUploadDetails?: UploadDetailsDataProps
} }
export interface UploadDetailsDataProps { export interface UploadDetailsDataProps {
assetFiles: File[] assetFiles: File[]
metadataFiles: File[] metadataFiles: File[]
thumbnailFiles?: File[]
thumbnailCompatibleAssetFileNames?: string[]
uploadService: UploadServiceType uploadService: UploadServiceType
nftStorageApiKey?: string nftStorageApiKey?: string
pinataApiKey?: string pinataApiKey?: string
@ -30,15 +50,30 @@ export interface UploadDetailsDataProps {
uploadMethod: UploadMethod uploadMethod: UploadMethod
baseTokenURI?: string baseTokenURI?: string
imageUrl?: string imageUrl?: string
baseMinterMetadataFile?: File
} }
export const UploadDetails = ({ onChange }: UploadDetailsProps) => { export const UploadDetails = ({
onChange,
minterType,
baseMinterAcquisitionMethod,
importedUploadDetails,
}: UploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([]) const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([]) const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [thumbnailCompatibleAssetFileNames, setThumbnailCompatibleAssetFileNames] = useState<string[]>([])
const [thumbnailFilesArray, setThumbnailFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new') const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage') const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false) const [refreshMetadata, setRefreshMetadata] = useState(false)
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
const [baseMinterMetadataFile, setBaseMinterMetadataFile] = useState<File | undefined>()
const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
const thumbnailFilesRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({ const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key', id: 'nft-storage-api-key',
@ -65,7 +100,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const baseTokenUriState = useInputState({ const baseTokenUriState = useInputState({
id: 'baseTokenUri', id: 'baseTokenUri',
name: 'baseTokenUri', name: 'baseTokenUri',
title: 'Base Token URI', title: minterType === 'vending' ? 'Base Token URI' : 'Token URI',
placeholder: 'ipfs://', placeholder: 'ipfs://',
defaultValue: '', defaultValue: '',
}) })
@ -81,19 +116,78 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => { const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFilesArray([]) setAssetFilesArray([])
setMetadataFilesArray([]) setMetadataFilesArray([])
setThumbnailFilesArray([])
setThumbnailCompatibleAssetFileNames([])
if (event.target.files === null) return if (event.target.files === null) return
//sort the files const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html', 'document']
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) const thumbnailCompatibleFileNamesList: string[] = []
//check if the sorted file names are in numerical order if (minterType === 'vending') {
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) //sort the files
for (let i = 0; i < sortedFileNames.length; i++) { const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) { //check if the sorted file names are in numerical order
toast.error('The file names should be in numerical order starting from 1.') const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
//clear the input sortedFiles.map((file) => {
event.target.value = '' if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) {
return thumbnailCompatibleFileNamesList.push(file.name.split('.')[0])
}
})
setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList)
console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList)
for (let i = 0; i < sortedFileNames.length; i++) {
if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.')
setThumbnailCompatibleAssetFileNames([])
addLogItem({
id: uid(),
message: 'The file names should be in numerical order starting from 1.',
type: 'Error',
timestamp: new Date(),
})
//clear the input
event.target.value = ''
return
}
} }
} else if (minterType === 'base' && event.target.files.length > 1) {
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
sortedFiles.map((file) => {
if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) {
thumbnailCompatibleFileNamesList.push(file.name.split('.')[0])
}
})
setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList)
console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList)
for (let i = 0; i < sortedFileNames.length - 1; i++) {
if (
isNaN(Number(sortedFileNames[i])) ||
isNaN(Number(sortedFileNames[i + 1])) ||
parseInt(sortedFileNames[i]) !== parseInt(sortedFileNames[i + 1]) - 1
) {
toast.error('The file names should be in numerical order.')
setThumbnailCompatibleAssetFileNames([])
addLogItem({
id: uid(),
message: 'The file names should be in numerical order.',
type: 'Error',
timestamp: new Date(),
})
//clear the input
event.target.value = ''
return
}
}
} else if (minterType === 'base' && event.target.files.length === 1) {
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
thumbnailCompatibleFileNamesList.push(event.target.files[0].name.split('.')[0])
}
setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList)
console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList)
} }
let loadedFileCount = 0 let loadedFileCount = 0
const files: File[] = [] const files: File[] = []
let reader: FileReader let reader: FileReader
@ -102,7 +196,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
reader.onload = (e) => { reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.') if (!event.target.files) return toast.error('No files selected.')
const assetFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' }) const assetFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
type: 'image/jpg',
})
files.push(assetFile) files.push(assetFile)
} }
reader.readAsArrayBuffer(event.target.files[i]) reader.readAsArrayBuffer(event.target.files[i])
@ -119,20 +215,68 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => { const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFilesArray([]) setMetadataFilesArray([])
if (event.target.files === null) return toast.error('No files selected.') if (event.target.files === null) return toast.error('No files selected.')
if (event.target.files.length !== assetFilesArray.length) { if (
(minterType === 'vending' || (minterType === 'base' && assetFilesArray.length > 1)) &&
event.target.files.length !== assetFilesArray.length
) {
event.target.value = '' event.target.value = ''
return toast.error('The number of metadata files should be equal to the number of asset files.') return toast.error('The number of metadata files should be equal to the number of asset files.')
} }
//sort the files // compare the first file name for asset and metadata files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) if (
//check if the sorted file names are in numerical order minterType === 'base' &&
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) assetFilesArray.length > 1 &&
for (let i = 0; i < sortedFileNames.length; i++) { event.target.files[0].name.split('.')[0] !== assetFilesArray[0].name.split('.')[0]
if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) { ) {
toast.error('The file names should be in numerical order starting from 1.') event.target.value = ''
//clear the input toast.error('The metadata file names should match the asset file names.')
event.target.value = '' addLogItem({
return id: uid(),
message: 'The metadata file names should match the asset file names.',
type: 'Error',
timestamp: new Date(),
})
return
}
if (minterType === 'vending') {
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
for (let i = 0; i < sortedFileNames.length; i++) {
if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.')
addLogItem({
id: uid(),
message: 'The file names should be in numerical order starting from 1.',
type: 'Error',
timestamp: new Date(),
})
event.target.value = ''
return
}
}
} else if (minterType === 'base' && assetFilesArray.length > 1) {
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
for (let i = 0; i < sortedFileNames.length - 1; i++) {
if (
isNaN(Number(sortedFileNames[i])) ||
isNaN(Number(sortedFileNames[i + 1])) ||
parseInt(sortedFileNames[i]) !== parseInt(sortedFileNames[i + 1]) - 1
) {
toast.error('The file names should be in numerical order.')
addLogItem({
id: uid(),
message: 'The file names should be in numerical order.',
type: 'Error',
timestamp: new Date(),
})
event.target.value = ''
return
}
} }
} }
let loadedFileCount = 0 let loadedFileCount = 0
@ -140,11 +284,26 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
let reader: FileReader let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) { for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader() reader = new FileReader()
reader.onload = (e) => { reader.onload = async (e) => {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.') if (!event.target.files) return toast.error('No files selected.')
const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' }) const metadataFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
type: 'application/json',
})
files.push(metadataFile) files.push(metadataFile)
try {
const parsedMetadata = JSON.parse(await metadataFile.text())
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
event.target.value = ''
setMetadataFilesArray([])
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
}
} catch (error: any) {
event.target.value = ''
setMetadataFilesArray([])
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
}
} }
reader.readAsText(event.target.files[i], 'utf8') reader.readAsText(event.target.files[i], 'utf8')
reader.onloadend = () => { reader.onloadend = () => {
@ -162,10 +321,64 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
setRefreshMetadata((prev) => !prev) setRefreshMetadata((prev) => !prev)
} }
const updateMetadataFileArray = async (updatedMetadataFile: File) => { const updateMetadataFileArray = (updatedMetadataFile: File) => {
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
console.log('Updated Metadata File:') }
console.log(JSON.parse(await metadataFilesArray[metadataFileArrayIndex]?.text()))
const updateBaseMinterMetadataFile = (updatedMetadataFile: File) => {
setBaseMinterMetadataFile(updatedMetadataFile)
console.log('Updated Base Minter Metadata File:')
console.log(baseMinterMetadataFile)
}
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
const selectThumbnails = (event: ChangeEvent<HTMLInputElement>) => {
setThumbnailFilesArray([])
if (event.target.files === null) return
// if (minterType === 'vending' || (minterType === 'base' && thumbnailCompatibleAssetFileNames.length > 1)) {
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
// make sure the sorted file names match thumbnail compatible asset file names
for (let i = 0; i < thumbnailCompatibleAssetFileNames.length; i++) {
if (minterType === 'base' && assetFilesArray.length === 1) break
if (sortedFileNames[i] !== thumbnailCompatibleAssetFileNames[i]) {
toast.error('The thumbnail file names should match the thumbnail compatible asset file names.')
addLogItem({
id: uid(),
message: 'The thumbnail file names should match the thumbnail compatible asset file names.',
type: 'Error',
timestamp: new Date(),
})
//clear the input
event.target.value = ''
return
}
}
// }
let loadedFileCount = 0
const files: File[] = []
let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader()
reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.')
const thumbnailFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
type: 'image/jpg',
})
files.push(thumbnailFile)
}
reader.readAsArrayBuffer(event.target.files[i])
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
loadedFileCount++
if (loadedFileCount === event.target.files.length) {
setThumbnailFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
}
}
}
} }
useEffect(() => { useEffect(() => {
@ -173,21 +386,39 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const data: UploadDetailsDataProps = { const data: UploadDetailsDataProps = {
assetFiles: assetFilesArray, assetFiles: assetFilesArray,
metadataFiles: metadataFilesArray, metadataFiles: metadataFilesArray,
thumbnailFiles: thumbnailFilesArray,
thumbnailCompatibleAssetFileNames,
uploadService, uploadService,
nftStorageApiKey: nftStorageApiKeyState.value, nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value, pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value, pinataSecretKey: pinataSecretKeyState.value,
uploadMethod, uploadMethod,
baseTokenURI: baseTokenUriState.value, baseTokenURI: baseTokenUriState.value
imageUrl: coverImageUrlState.value, .replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(regex, '')
.trim(),
imageUrl: coverImageUrlState.value
.replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(regex, '')
.trim(),
baseMinterMetadataFile,
} }
onChange(data) onChange(data)
} catch (error: any) { } catch (error: any) {
toast.error(error.message) toast.error(error.message, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
} }
}, [ }, [
assetFilesArray, assetFilesArray,
metadataFilesArray, metadataFilesArray,
thumbnailFilesArray,
thumbnailCompatibleAssetFileNames,
uploadService, uploadService,
nftStorageApiKeyState.value, nftStorageApiKeyState.value,
pinataApiKeyState.value, pinataApiKeyState.value,
@ -195,17 +426,53 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
uploadMethod, uploadMethod,
baseTokenUriState.value, baseTokenUriState.value,
coverImageUrlState.value, coverImageUrlState.value,
refreshMetadata,
baseMinterMetadataFile,
]) ])
useEffect(() => { useEffect(() => {
setAssetFilesArray([]) if (metadataFilesRef.current) metadataFilesRef.current.value = ''
setMetadataFilesArray([]) setMetadataFilesArray([])
baseTokenUriState.onChange('') if (assetFilesRef.current) assetFilesRef.current.value = ''
coverImageUrlState.onChange('') setAssetFilesArray([])
}, [uploadMethod]) if (thumbnailFilesRef.current) thumbnailFilesRef.current.value = ''
setThumbnailFilesArray([])
setThumbnailCompatibleAssetFileNames([])
if (!importedUploadDetails || minterType === 'base') {
baseTokenUriState.onChange('')
coverImageUrlState.onChange('')
}
}, [uploadMethod, minterType, baseMinterAcquisitionMethod])
useEffect(() => {
if (importedUploadDetails) {
if (importedUploadDetails.uploadMethod === 'new') {
setUploadMethod('new')
setUploadService(importedUploadDetails.uploadService)
nftStorageApiKeyState.onChange(importedUploadDetails.nftStorageApiKey || '')
pinataApiKeyState.onChange(importedUploadDetails.pinataApiKey || '')
pinataSecretKeyState.onChange(importedUploadDetails.pinataSecretKey || '')
baseTokenUriState.onChange(importedUploadDetails.baseTokenURI || '')
coverImageUrlState.onChange(importedUploadDetails.imageUrl || '')
} else if (importedUploadDetails.uploadMethod === 'existing') {
setUploadMethod('existing')
baseTokenUriState.onChange(importedUploadDetails.baseTokenURI || '')
coverImageUrlState.onChange(importedUploadDetails.imageUrl || '')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedUploadDetails])
useEffect(() => {
if (useDefaultApiKey) {
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
} else {
nftStorageApiKeyState.onChange('')
}
}, [useDefaultApiKey])
return ( return (
<div className="justify-items-start mt-5 mb-3 rounded border border-2 border-white/20 flex-column"> <div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline"> <div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input <input
@ -223,7 +490,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label" className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2" htmlFor="inlineRadio2"
> >
Upload assets & metadata {minterType === 'base' ? 'Upload assets & metadata' : 'Upload assets & metadata'}
</label> </label>
</div> </div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline"> <div className="mt-3 ml-2 font-bold form-check form-check-inline">
@ -242,13 +509,13 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label" className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1" htmlFor="inlineRadio1"
> >
Use an existing base URI {minterType === 'base' ? 'Use an existing Token URI' : 'Use an existing base URI'}
</label> </label>
</div> </div>
</div> </div>
<div className="p-3 py-5 pb-8"> <div className="p-3 py-5 pb-8">
<Conditional test={uploadMethod === 'existing'}> <Conditional test={uploadMethod === 'existing' && minterType === 'vending'}>
<div className="ml-3 flex-column"> <div className="ml-3 flex-column">
<p className="mb-5 ml-5"> <p className="mb-5 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a
@ -263,11 +530,53 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
and upload your assets & metadata manually to get a base URI for your collection. and upload your assets & metadata manually to get a base URI for your collection.
</p> </p>
<div> <div>
<TextInput {...baseTokenUriState} className="w-1/2" /> <Tooltip
backgroundColor="bg-blue-500"
className="mb-2 ml-20"
label="The base token URI that points to the IPFS folder containing the metadata files."
placement="top"
>
<TextInput {...baseTokenUriState} className="ml-4 w-1/2" />
</Tooltip>
</div> </div>
<Conditional test={minterType !== 'base'}>
<div>
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
</div>
</Conditional>
</div>
</Conditional>
<Conditional test={uploadMethod === 'existing' && minterType === 'base'}>
<div className="ml-3 flex-column">
<p className="mb-5 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
NFT.Storage
</Anchor>{' '}
or{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
Pinata
</Anchor>{' '}
and upload your asset & metadata manually to get a URI for your token before minting.
</p>
<div> <div>
<TextInput {...coverImageUrlState} className="mt-2 w-1/2" /> <Tooltip
backgroundColor="bg-blue-500"
className="mb-2 ml-4"
label="The token URI that points directly to the metadata file stored on IPFS."
placement="top"
>
<TextInput {...baseTokenUriState} className="ml-4 w-1/2" />
</Tooltip>
</div> </div>
<Conditional
test={minterType !== 'base' || (minterType === 'base' && baseMinterAcquisitionMethod === 'new')}
>
<div>
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
</div>
</Conditional>
</div> </div>
</Conditional> </Conditional>
<Conditional test={uploadMethod === 'new'}> <Conditional test={uploadMethod === 'new'}>
@ -317,7 +626,22 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
<div className="flex w-full"> <div className="flex w-full">
<Conditional test={uploadService === 'nft-storage'}> <Conditional test={uploadService === 'nft-storage'}>
<TextInput {...nftStorageApiKeyState} className="w-full" /> <div className="flex-col w-full">
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
<div className="flex-row mt-2 w-full form-control">
<label className="cursor-pointer label">
<span className="mr-2 font-bold">Use Default API Key</span>
<input
checked={useDefaultApiKey}
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
setUseDefaultApiKey(!useDefaultApiKey)
}}
type="checkbox"
/>
</label>
</div>
</div>
</Conditional> </Conditional>
<Conditional test={uploadService === 'pinata'}> <Conditional test={uploadService === 'pinata'}>
<TextInput {...pinataApiKeyState} className="w-full" /> <TextInput {...pinataApiKeyState} className="w-full" />
@ -356,7 +680,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
)} )}
> >
<input <input
accept="image/*, audio/*, video/*" accept="image/*, audio/*, video/*, .html, .pdf"
className={clsx( className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer', 'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
@ -364,6 +688,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
id="assetFiles" id="assetFiles"
multiple multiple
onChange={selectAssets} onChange={selectAssets}
ref={assetFilesRef}
type="file" type="file"
/> />
</div> </div>
@ -375,7 +700,11 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300" className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="metadataFiles" htmlFor="metadataFiles"
> >
Metadata Selection {minterType === 'vending'
? 'Metadata Selection'
: assetFilesArray.length === 1
? 'Metadata Selection (optional)'
: 'Metadata Selection'}
</label> </label>
<div <div
className={clsx( className={clsx(
@ -392,24 +721,80 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
id="metadataFiles" id="metadataFiles"
multiple multiple
onChange={selectMetadata} onChange={selectMetadata}
ref={metadataFilesRef}
type="file" type="file"
/> />
</div> </div>
</div> </div>
)} )}
<MetadataModal {thumbnailCompatibleAssetFileNames.length > 0 && (
assetFile={assetFilesArray[metadataFileArrayIndex]} <div>
metadataFile={metadataFilesArray[metadataFileArrayIndex]} <label
refresher={refreshMetadata} className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
updateMetadata={updateMetadataFileArray} htmlFor="thumbnailFiles"
/> >
{thumbnailCompatibleAssetFileNames.length > 1
? 'Thumbnail Selection for Compatible Assets (optional)'
: 'Thumbnail Selection (optional)'}
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="thumbnailFiles"
multiple
onChange={selectThumbnails}
ref={thumbnailFilesRef}
type="file"
/>
</div>
</div>
)}
<Conditional test={assetFilesArray.length >= 1}>
<MetadataModal
assetFile={assetFilesArray[metadataFileArrayIndex]}
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
refresher={refreshMetadata}
updateMetadata={updateMetadataFileArray}
/>
</Conditional>
</div> </div>
<Conditional test={assetFilesArray.length > 0}> <Conditional test={assetFilesArray.length > 0 && minterType === 'vending'}>
<AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} /> <AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} />
</Conditional> </Conditional>
<Conditional test={assetFilesArray.length > 0 && minterType === 'base'}>
<Conditional test={assetFilesArray.length === 1}>
<SingleAssetPreview
relatedAsset={assetFilesArray[0]}
subtitle={`Asset filename: ${assetFilesArray[0]?.name}`}
updateMetadataFileIndex={updateMetadataFileIndex}
/>
</Conditional>
<Conditional test={assetFilesArray.length > 1}>
<AssetsPreview
assetFilesArray={assetFilesArray}
updateMetadataFileIndex={updateMetadataFileIndex}
/>
</Conditional>
</Conditional>
</div> </div>
<Conditional test={minterType === 'base' && assetFilesArray.length === 1}>
<MetadataInput
selectedAssetFile={assetFilesArray[0]}
selectedMetadataFile={metadataFilesArray[0]}
updateMetadataToUpload={updateBaseMinterMetadataFile}
/>
</Conditional>
</div> </div>
</div> </div>
</Conditional> </Conditional>

View File

@ -1,8 +1,20 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable no-nested-ternary */
import { Button } from 'components/Button'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import type { TokenInfo } from 'config/token'
import { useGlobalSettings } from 'contexts/globalSettings'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
import { Conditional } from '../../Conditional' import { Conditional } from '../../Conditional'
import { AddressInput, NumberInput } from '../../forms/FormInput' import { AddressInput, NumberInput } from '../../forms/FormInput'
@ -11,26 +23,44 @@ import { WhitelistUpload } from '../../WhitelistUpload'
interface WhitelistDetailsProps { interface WhitelistDetailsProps {
onChange: (data: WhitelistDetailsDataProps) => void onChange: (data: WhitelistDetailsDataProps) => void
mintingTokenFromFactory?: TokenInfo
importedWhitelistDetails?: WhitelistDetailsDataProps
} }
export interface WhitelistDetailsDataProps { export interface WhitelistDetailsDataProps {
whitelistType: WhitelistState whitelistState: WhitelistState
whitelistType: WhitelistType
contractAddress?: string contractAddress?: string
members?: string[] members?: string[] | WhitelistFlexMember[]
unitPrice?: string unitPrice?: string
startTime?: string startTime?: string
endTime?: string endTime?: string
perAddressLimit?: number perAddressLimit?: number
memberLimit?: number memberLimit?: number
admins?: string[]
adminsMutable?: boolean
} }
type WhitelistState = 'none' | 'existing' | 'new' type WhitelistState = 'none' | 'existing' | 'new'
export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { export type WhitelistType = 'standard' | 'flex' | 'merkletree'
export const WhitelistDetails = ({
onChange,
mintingTokenFromFactory,
importedWhitelistDetails,
}: WhitelistDetailsProps) => {
const wallet = useWallet()
const { timezone } = useGlobalSettings()
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none') const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
const [startDate, setStartDate] = useState<Date | undefined>(undefined) const [startDate, setStartDate] = useState<Date | undefined>(undefined)
const [endDate, setEndDate] = useState<Date | undefined>(undefined) const [endDate, setEndDate] = useState<Date | undefined>(undefined)
const [whitelistArray, setWhitelistArray] = useState<string[]>([]) const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
const [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState<string[]>([])
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
const whitelistAddressState = useInputState({ const whitelistAddressState = useInputState({
id: 'whitelist-address', id: 'whitelist-address',
@ -39,11 +69,13 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
defaultValue: '', defaultValue: '',
}) })
const uniPriceState = useNumberInputState({ const unitPriceState = useNumberInputState({
id: 'unit-price', id: 'unit-price',
name: 'unitPrice', name: 'unitPrice',
title: 'Unit Price', title: 'Unit Price',
subtitle: 'Token price for whitelisted addresses \n (min. 25 STARS)', subtitle: `Token price for whitelisted addresses \n (min. 0 ${
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '25', placeholder: '25',
}) })
@ -63,34 +95,161 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
placeholder: '5', placeholder: '5',
}) })
const addressListState = useAddressListState()
const whitelistFileOnChange = (data: string[]) => { const whitelistFileOnChange = (data: string[]) => {
setWhitelistArray(data) if (whitelistType === 'standard') setWhitelistStandardArray(data)
if (whitelistType === 'merkletree') setWhitelistMerkleTreeArray(data)
}
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
setWhitelistFlexArray(whitelistData)
}
const downloadSampleWhitelistFlexFile = () => {
const csvData =
'address,mint_count\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
const blob = new Blob([csvData], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'sample_whitelist_flex.csv')
a.click()
}
const downloadSampleWhitelistFile = () => {
const txtData =
'stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3'
const blob = new Blob([txtData], { type: 'text/txt' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'sample_whitelist.txt')
a.click()
} }
useEffect(() => {
if (!importedWhitelistDetails) {
setWhitelistStandardArray([])
setWhitelistFlexArray([])
setWhitelistMerkleTreeArray([])
}
}, [whitelistType])
useEffect(() => { useEffect(() => {
const data: WhitelistDetailsDataProps = { const data: WhitelistDetailsDataProps = {
whitelistType: whitelistState, whitelistState,
contractAddress: whitelistAddressState.value, whitelistType,
members: whitelistArray, contractAddress: whitelistAddressState.value
unitPrice: uniPriceState.value ? (Number(uniPriceState.value) * 1_000_000).toString() : '', .toLowerCase()
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(/ /g, ''),
members:
whitelistType === 'standard'
? whitelistStandardArray
: whitelistType === 'merkletree'
? whitelistMerkleTreeArray
: whitelistFlexArray,
unitPrice: unitPriceState.value
? (Number(unitPriceState.value) * 1_000_000).toString()
: unitPriceState.value === 0
? '0'
: undefined,
startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '', startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '',
endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '', endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '',
perAddressLimit: perAddressLimitState.value, perAddressLimit: perAddressLimitState.value,
memberLimit: memberLimitState.value, memberLimit: memberLimitState.value,
admins: [
...new Set(
addressListState.values
.map((a) => a.address.trim())
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')),
),
],
adminsMutable,
} }
onChange(data) onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
whitelistAddressState.value, whitelistAddressState.value,
uniPriceState.value, unitPriceState.value,
memberLimitState.value, memberLimitState.value,
perAddressLimitState.value, perAddressLimitState.value,
startDate, startDate,
endDate, endDate,
whitelistArray, whitelistStandardArray,
whitelistFlexArray,
whitelistMerkleTreeArray,
whitelistState, whitelistState,
whitelistType,
addressListState.values,
adminsMutable,
]) ])
// make the necessary changes with respect to imported whitelist details
useEffect(() => {
if (importedWhitelistDetails) {
setWhitelistState(importedWhitelistDetails.whitelistState)
setWhitelistType(importedWhitelistDetails.whitelistType)
whitelistAddressState.onChange(
importedWhitelistDetails.contractAddress ? importedWhitelistDetails.contractAddress : '',
)
unitPriceState.onChange(
importedWhitelistDetails.unitPrice ? Number(importedWhitelistDetails.unitPrice) / 1000000 : 0,
)
memberLimitState.onChange(importedWhitelistDetails.memberLimit ? importedWhitelistDetails.memberLimit : 0)
perAddressLimitState.onChange(
importedWhitelistDetails.perAddressLimit ? importedWhitelistDetails.perAddressLimit : 0,
)
setStartDate(
importedWhitelistDetails.startTime
? new Date(Number(importedWhitelistDetails.startTime) / 1_000_000)
: undefined,
)
setEndDate(
importedWhitelistDetails.endTime ? new Date(Number(importedWhitelistDetails.endTime) / 1_000_000) : undefined,
)
setAdminsMutable(importedWhitelistDetails.adminsMutable ? importedWhitelistDetails.adminsMutable : true)
importedWhitelistDetails.admins?.forEach((admin) => {
addressListState.reset()
addressListState.add({ address: admin })
})
if (importedWhitelistDetails.whitelistType === 'standard') {
setWhitelistStandardArray([])
importedWhitelistDetails.members?.forEach((member) => {
setWhitelistStandardArray((standardArray) => [...standardArray, member as string])
})
} else if (importedWhitelistDetails.whitelistType === 'merkletree') {
setWhitelistMerkleTreeArray([])
// importedWhitelistDetails.members?.forEach((member) => {
// setWhitelistMerkleTreeArray((merkleTreeArray) => [...merkleTreeArray, member as string])
// })
} else if (importedWhitelistDetails.whitelistType === 'flex') {
setWhitelistFlexArray([])
importedWhitelistDetails.members?.forEach((member) => {
setWhitelistFlexArray((flexArray) => [
...flexArray,
{
address: (member as WhitelistFlexMember).address,
mint_count: (member as WhitelistFlexMember).mint_count,
},
])
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedWhitelistDetails])
useEffect(() => {
if (whitelistState === 'new' && wallet.address) {
addressListState.reset()
addressListState.add({ address: wallet.address })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [whitelistState, wallet.address])
return ( return (
<div className="py-3 px-8 rounded border-2 border-white/20"> <div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center"> <div className="flex justify-center">
@ -102,6 +261,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
name="whitelistRadioOptions1" name="whitelistRadioOptions1"
onClick={() => { onClick={() => {
setWhitelistState('none') setWhitelistState('none')
setWhitelistType('standard')
}} }}
type="radio" type="radio"
value="None" value="None"
@ -158,34 +318,207 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
</Conditional> </Conditional>
<Conditional test={whitelistState === 'new'}> <Conditional test={whitelistState === 'new'}>
<div className="flex justify-between mb-5 ml-6 max-w-[500px] text-lg font-bold">
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'standard'}
className="peer sr-only"
id="inlineRadio7"
name="inlineRadioOptions7"
onClick={() => {
setWhitelistType('standard')
}}
type="radio"
value="standard"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio7"
>
Standard Whitelist
</label>
</div>
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'flex'}
className="peer sr-only"
id="inlineRadio8"
name="inlineRadioOptions8"
onClick={() => {
setWhitelistType('flex')
}}
type="radio"
value="flex"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio8"
>
Whitelist Flex
</label>
</div>
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'merkletree'}
className="peer sr-only"
id="inlineRadio9"
name="inlineRadioOptions9"
onClick={() => {
setWhitelistType('merkletree')
}}
type="radio"
value="merkletree"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio9"
>
Whitelist Merkle Tree
</label>
</div>
</div>
<div className="grid grid-cols-2"> <div className="grid grid-cols-2">
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details"> <FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
<NumberInput isRequired {...uniPriceState} /> <NumberInput isRequired {...unitPriceState} />
<NumberInput isRequired {...memberLimitState} /> <Conditional test={whitelistType !== 'merkletree'}>
<NumberInput isRequired {...perAddressLimitState} /> <NumberInput isRequired {...memberLimitState} />
</Conditional>
<Conditional test={whitelistType === 'standard' || whitelistType === 'merkletree'}>
<NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<FormControl <FormControl
htmlId="start-date" htmlId="start-date"
isRequired isRequired
subtitle="Start time for minting tokens to whitelisted addresses" subtitle="Start time for minting tokens to whitelisted addresses"
title="Start Time" title={`Whitelist Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
> >
<InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} /> <InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setStartDate(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setStartDate(undefined)
}
value={
timezone === 'Local'
? startDate
: startDate
? new Date(startDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl> </FormControl>
<FormControl <FormControl
htmlId="end-date" htmlId="end-date"
isRequired isRequired
subtitle="End time for minting tokens to whitelisted addresses" subtitle="Whitelist End Time dictates when public sales will start"
title="End Time" title={`Whitelist End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
> >
<InputDateTime minDate={new Date()} onChange={(date) => setEndDate(date)} value={endDate} /> <InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setEndDate(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setEndDate(undefined)
}
value={
timezone === 'Local'
? endDate
: endDate
? new Date(endDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
<div> <div>
<FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File"> <div className="mt-2 ml-3 w-[65%] form-control">
<WhitelistUpload onChange={whitelistFileOnChange} /> <label className="justify-start cursor-pointer label">
</FormGroup> <span className="mr-4 font-bold">Mutable Administrator Addresses</span>
<Conditional test={whitelistArray.length > 0}> <input
<JsonPreview content={whitelistArray} initialState title="File Contents" /> checked={adminsMutable}
className={`toggle ${adminsMutable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setAdminsMutable(!adminsMutable)}
type="checkbox"
/>
</label>
</div>
<div className="my-4 ml-4">
<AddressList
entries={addressListState.entries}
onAdd={addressListState.add}
onChange={addressListState.update}
onRemove={addressListState.remove}
subtitle="The list of administrator addresses"
title="Administrator Addresses"
/>
</div>
<Conditional test={whitelistType === 'standard'}>
<FormGroup
subtitle={
<div>
<span>TXT file that contains the whitelisted addresses</span>
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
Download Sample File
</Button>
</div>
}
title="Whitelist File"
>
<WhitelistUpload onChange={whitelistFileOnChange} />
</FormGroup>
<Conditional test={whitelistStandardArray.length > 0}>
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
</Conditional>
</Conditional>
<Conditional test={whitelistType === 'flex'}>
<FormGroup
subtitle={
<div>
<span>CSV file that contains the whitelisted addresses and corresponding mint counts</span>
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFlexFile}>
Download Sample File
</Button>
</div>
}
title="Whitelist File"
>
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
</FormGroup>
<Conditional test={whitelistFlexArray.length > 0}>
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
</Conditional>
</Conditional>
<Conditional test={whitelistType === 'merkletree'}>
<FormGroup
subtitle={
<div>
<span>TXT file that contains the whitelisted addresses</span>
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
Download Sample File
</Button>
</div>
}
title="Whitelist File"
>
<WhitelistUpload onChange={whitelistFileOnChange} />
</FormGroup>
<Conditional test={whitelistStandardArray.length > 0}>
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
</Conditional>
</Conditional> </Conditional>
</div> </div>
</div> </div>

View File

@ -2,19 +2,32 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react' import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MinterType } from '../actions/Combobox'
import type { QueryListItem } from './query' import type { QueryListItem } from './query'
import { QUERY_LIST } from './query' import { BASE_QUERY_LIST, OPEN_EDITION_QUERY_LIST, VENDING_QUERY_LIST } from './query'
export interface QueryComboboxProps { export interface QueryComboboxProps {
value: QueryListItem | null value: QueryListItem | null
onChange: (item: QueryListItem) => void onChange: (item: QueryListItem) => void
minterType?: MinterType
} }
export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => { export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProps) => {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(VENDING_QUERY_LIST)
useEffect(() => {
if (minterType === 'base') {
SET_QUERY_LIST(BASE_QUERY_LIST)
} else if (minterType === 'openEdition') {
SET_QUERY_LIST(OPEN_EDITION_QUERY_LIST)
} else {
SET_QUERY_LIST(VENDING_QUERY_LIST)
}
}, [minterType])
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] }) const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })
@ -67,7 +80,7 @@ export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active }) clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
} }
value={entry} value={entry}
> >

View File

@ -5,23 +5,41 @@ import { FormControl } from 'components/FormControl'
import { AddressInput, TextInput } from 'components/forms/FormInput' import { AddressInput, TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import type { MinterInstance } from 'contracts/minter' import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import type { SG721Instance } from 'contracts/sg721' import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useWallet } from 'utils/wallet'
import { resolveAddress } from '../../../utils/resolveAddress'
import type { MinterType } from '../actions/Combobox'
interface CollectionQueriesProps { interface CollectionQueriesProps {
minterContractAddress: string minterContractAddress: string
sg721ContractAddress: string sg721ContractAddress: string
royaltyRegistryContractAddress: string
sg721Messages: SG721Instance | undefined sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined vendingMinterMessages: VendingMinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
openEditionMinterMessages: OpenEditionMinterInstance | undefined
royaltyRegistryMessages: RoyaltyRegistryInstance | undefined
minterType: MinterType
} }
export const CollectionQueries = ({ export const CollectionQueries = ({
sg721ContractAddress, sg721ContractAddress,
sg721Messages, sg721Messages,
minterContractAddress, minterContractAddress,
minterMessages, vendingMinterMessages,
openEditionMinterMessages,
baseMinterMessages,
minterType,
royaltyRegistryMessages,
}: CollectionQueriesProps) => { }: CollectionQueriesProps) => {
const wallet = useWallet()
const comboboxState = useQueryComboboxState() const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id const type = comboboxState.value?.id
@ -43,27 +61,60 @@ export const CollectionQueries = ({
const address = addressState.value const address = addressState.value
const showTokenIdField = type === 'token_info' const showTokenIdField = type === 'token_info'
const showAddressField = type === 'tokens_minted_to_user' const showAddressField = type === 'tokens_minted_to_user' || type === 'tokens'
const { data: response } = useQuery( const { data: response } = useQuery(
[sg721Messages, minterMessages, type, tokenId, address] as const, [
sg721Messages,
baseMinterMessages,
vendingMinterMessages,
openEditionMinterMessages,
royaltyRegistryMessages,
type,
tokenId,
address,
sg721ContractAddress,
] as const,
async ({ queryKey }) => { async ({ queryKey }) => {
const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey const [
const result = await dispatchQuery({ _sg721Messages,
tokenId: _tokenId, _baseMinterMessages_,
minterMessages: _minterMessages, _vendingMinterMessages,
sg721Messages: _sg721Messages, _openEditionMinterMessages,
address: _address, _royaltyRegistryMessages,
type: _type, _type,
_tokenId,
_address,
_sg721ContractAddress,
] = queryKey
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const res = await resolveAddress(_address, wallet).then(async (resolvedAddress) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result = await dispatchQuery({
tokenId: _tokenId,
vendingMinterMessages: _vendingMinterMessages,
baseMinterMessages: _baseMinterMessages_,
openEditionMinterMessages: _openEditionMinterMessages,
sg721Messages: _sg721Messages,
royaltyRegistryMessages: _royaltyRegistryMessages,
address: resolvedAddress,
type: _type,
sg721ContractAddress: _sg721ContractAddress,
})
return result
}) })
return result return res
}, },
{ {
placeholderData: null, placeholderData: null,
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message) if (addressState.value.length > 12 && !addressState.value.includes('.')) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}, },
enabled: Boolean(sg721ContractAddress && minterContractAddress && type), enabled:
Boolean(type && type === 'infinity_swap_royalties' && sg721ContractAddress) ||
Boolean(sg721ContractAddress && minterContractAddress && type),
retry: false, retry: false,
}, },
) )
@ -71,7 +122,7 @@ export const CollectionQueries = ({
return ( return (
<div className="grid grid-cols-2 mt-4"> <div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8"> <div className="mr-2 space-y-8">
<QueryCombobox {...comboboxState} /> <QueryCombobox minterType={minterType} {...comboboxState} />
{showAddressField && <AddressInput {...addressState} />} {showAddressField && <AddressInput {...addressState} />}
{showTokenIdField && <TextInput {...tokenIdState} />} {showTokenIdField && <TextInput {...tokenIdState} />}
</div> </div>

View File

@ -1,5 +1,9 @@
import type { MinterInstance } from 'contracts/minter' import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import type { SG721Instance } from 'contracts/sg721' import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { INFINITY_SWAP_PROTOCOL_ADDRESS } from 'utils/constants'
export type QueryType = typeof QUERY_TYPES[number] export type QueryType = typeof QUERY_TYPES[number]
@ -8,8 +12,13 @@ export const QUERY_TYPES = [
'mint_price', 'mint_price',
'num_tokens', 'num_tokens',
'tokens_minted_to_user', 'tokens_minted_to_user',
'total_mint_count',
'tokens',
// 'token_owners', // 'token_owners',
'infinity_swap_royalties',
'token_info', 'token_info',
'config',
'status',
] as const ] as const
export interface QueryListItem { export interface QueryListItem {
@ -18,7 +27,7 @@ export interface QueryListItem {
description?: string description?: string
} }
export const QUERY_LIST: QueryListItem[] = [ export const VENDING_QUERY_LIST: QueryListItem[] = [
{ {
id: 'collection_info', id: 'collection_info',
name: 'Collection Info', name: 'Collection Info',
@ -29,6 +38,11 @@ export const QUERY_LIST: QueryListItem[] = [
name: 'Mint Price', name: 'Mint Price',
description: `Get the price of minting a token.`, description: `Get the price of minting a token.`,
}, },
{
id: 'infinity_swap_royalties',
name: 'Infinity Swap Royalty Details',
description: `Get the collection's royalty details for Infinity Swap`,
},
{ {
id: 'num_tokens', id: 'num_tokens',
name: 'Mintable Number of Tokens', name: 'Mintable Number of Tokens',
@ -49,6 +63,95 @@ export const QUERY_LIST: QueryListItem[] = [
name: 'Token Info', name: 'Token Info',
description: `Get information about a token in the collection.`, description: `Get information about a token in the collection.`,
}, },
{
id: 'config',
name: 'Minter Config',
description: `Query Minter Config`,
},
{
id: 'status',
name: 'Minter Status',
description: `Query Minter Status`,
},
]
export const BASE_QUERY_LIST: QueryListItem[] = [
{
id: 'collection_info',
name: 'Collection Info',
description: `Get information about the collection.`,
},
{
id: 'tokens',
name: 'Tokens Minted to User',
description: `Get the number of tokens minted in the collection to a user.`,
},
{
id: 'infinity_swap_royalties',
name: 'Infinity Swap Royalty Details',
description: `Get the collection's royalty details for Infinity Swap`,
},
{
id: 'token_info',
name: 'Token Info',
description: `Get information about a token in the collection.`,
},
{
id: 'config',
name: 'Minter Config',
description: `Query Minter Config`,
},
{
id: 'status',
name: 'Minter Status',
description: `Query Minter Status`,
},
]
export const OPEN_EDITION_QUERY_LIST: QueryListItem[] = [
{
id: 'collection_info',
name: 'Collection Info',
description: `Get information about the collection.`,
},
{
id: 'mint_price',
name: 'Mint Price',
description: `Get the price of minting a token.`,
},
{
id: 'infinity_swap_royalties',
name: 'Infinity Swap Royalty Details',
description: `Get the collection's royalty details for Infinity Swap`,
},
{
id: 'tokens_minted_to_user',
name: 'Tokens Minted to User',
description: `Get the number of tokens minted in the collection to a user.`,
},
{
id: 'total_mint_count',
name: 'Total Mint Count',
description: `Get the total number of tokens minted for the collection.`,
},
// {
// id: 'token_owners',
// name: 'Token Owners',
// description: `Get the list of users who own tokens in the collection.`,
// },
{
id: 'token_info',
name: 'Token Info',
description: `Get information about a token in the collection.`,
},
{
id: 'config',
name: 'Minter Config',
description: `Query Minter Config`,
},
{
id: 'status',
name: 'Minter Status',
description: `Query Minter Status`,
},
] ]
export interface DispatchExecuteProps { export interface DispatchExecuteProps {
@ -59,21 +162,36 @@ export interface DispatchExecuteProps {
type Select<T extends QueryType> = T type Select<T extends QueryType> = T
export type DispatchQueryArgs = { export type DispatchQueryArgs = {
minterMessages?: MinterInstance baseMinterMessages?: BaseMinterInstance
vendingMinterMessages?: VendingMinterInstance
openEditionMinterMessages?: OpenEditionMinterInstance
sg721Messages?: SG721Instance sg721Messages?: SG721Instance
royaltyRegistryMessages?: RoyaltyRegistryInstance
sg721ContractAddress?: string
} & ( } & (
| { type: undefined } | { type: undefined }
| { type: Select<'collection_info'> } | { type: Select<'collection_info'> }
| { type: Select<'mint_price'> } | { type: Select<'mint_price'> }
| { type: Select<'num_tokens'> } | { type: Select<'num_tokens'> }
| { type: Select<'tokens_minted_to_user'>; address: string } | { type: Select<'tokens_minted_to_user'>; address: string }
| { type: Select<'total_mint_count'> }
| { type: Select<'tokens'>; address: string }
| { type: Select<'infinity_swap_royalties'> }
// | { type: Select<'token_owners'> } // | { type: Select<'token_owners'> }
| { type: Select<'token_info'>; tokenId: string } | { type: Select<'token_info'>; tokenId: string }
| { type: Select<'config'> }
| { type: Select<'status'> }
) )
export const dispatchQuery = async (args: DispatchQueryArgs) => { export const dispatchQuery = async (args: DispatchQueryArgs) => {
const { minterMessages, sg721Messages } = args const {
if (!minterMessages || !sg721Messages) { baseMinterMessages,
vendingMinterMessages,
openEditionMinterMessages,
sg721Messages,
royaltyRegistryMessages,
} = args
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
throw new Error('Cannot execute actions') throw new Error('Cannot execute actions')
} }
switch (args.type) { switch (args.type) {
@ -81,21 +199,39 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
return sg721Messages.collectionInfo() return sg721Messages.collectionInfo()
} }
case 'mint_price': { case 'mint_price': {
return minterMessages.getMintPrice() return vendingMinterMessages.getMintPrice()
} }
case 'num_tokens': { case 'num_tokens': {
return minterMessages.getMintableNumTokens() return vendingMinterMessages.getMintableNumTokens()
} }
case 'tokens_minted_to_user': { case 'tokens_minted_to_user': {
return minterMessages.getMintCount(args.address) return vendingMinterMessages.getMintCount(args.address)
}
case 'total_mint_count': {
return openEditionMinterMessages.getTotalMintCount()
}
case 'tokens': {
return sg721Messages.tokens(args.address)
} }
// case 'token_owners': { // case 'token_owners': {
// return minterMessages.updateStartTime(txSigner, args.startTime) // return vendingMinterMessages.updateStartTime(txSigner, args.startTime)
// } // }
case 'infinity_swap_royalties': {
return royaltyRegistryMessages?.collectionRoyaltyProtocol(
args.sg721ContractAddress as string,
INFINITY_SWAP_PROTOCOL_ADDRESS,
)
}
case 'token_info': { case 'token_info': {
if (!args.tokenId) return if (!args.tokenId) return
return sg721Messages.allNftInfo(args.tokenId) return sg721Messages.allNftInfo(args.tokenId)
} }
case 'config': {
return baseMinterMessages.getConfig()
}
case 'status': {
return baseMinterMessages.getStatus()
}
default: { default: {
throw new Error('Unknown action') throw new Error('Unknown action')
} }

View File

@ -0,0 +1,7 @@
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
const [value, setValue] = useState<ExecuteListItem | null>(null)
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
}

View File

@ -0,0 +1,92 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
import { EXECUTE_LIST } from 'contracts/badgeHub/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps {
value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void
}
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="message-type"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Contract execute message type"
title="Message Type"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select message type"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Message type not found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,7 @@
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
const [value, setValue] = useState<ExecuteListItem | null>(null)
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
}

View File

@ -0,0 +1,92 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
import { EXECUTE_LIST } from 'contracts/baseMinter/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps {
value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void
}
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="message-type"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Contract execute message type"
title="Message Type"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select message type"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Message type not found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,7 @@
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
const [value, setValue] = useState<ExecuteListItem | null>(null)
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
}

View File

@ -0,0 +1,92 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
import { EXECUTE_LIST } from 'contracts/openEditionMinter/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps {
value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void
}
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="message-type"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Contract execute message type"
title="Message Type"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select message type"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Message type not found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,7 @@
import type { ExecuteListItem } from 'contracts/royaltyRegistry/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
const [value, setValue] = useState<ExecuteListItem | null>(null)
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
}

View File

@ -0,0 +1,92 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/royaltyRegistry/messages/execute'
import { EXECUTE_LIST } from 'contracts/royaltyRegistry/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps {
value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void
}
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="message-type"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Contract execute message type"
title="Message Type"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select message type"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Message type not found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active }) clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
} }
value={entry} value={entry}
> >

View File

@ -1,4 +1,4 @@
import type { ExecuteListItem } from 'contracts/minter/messages/execute' import type { ExecuteListItem } from 'contracts/splits/messages/execute'
import { useState } from 'react' import { useState } from 'react'
export const useExecuteComboboxState = () => { export const useExecuteComboboxState = () => {

View File

@ -1,8 +1,8 @@
import { Combobox, Transition } from '@headlessui/react' import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/minter/messages/execute' import type { ExecuteListItem } from 'contracts/splits/messages/execute'
import { EXECUTE_LIST } from 'contracts/minter/messages/execute' import { EXECUTE_LIST } from 'contracts/splits/messages/execute'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active }) clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
} }
value={entry} value={entry}
> >

View File

@ -0,0 +1,7 @@
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
const [value, setValue] = useState<ExecuteListItem | null>(null)
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
}

View File

@ -0,0 +1,92 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
import { EXECUTE_LIST } from 'contracts/vendingMinter/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps {
value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void
}
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="message-type"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Contract execute message type"
title="Message Type"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select message type"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Message type not found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -1,8 +1,11 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
import { Combobox, Transition } from '@headlessui/react' import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/whitelist/messages/execute' import type { ExecuteListItem } from 'contracts/whitelist/messages/execute'
import { EXECUTE_LIST } from 'contracts/whitelist/messages/execute' import { EXECUTE_LIST } from 'contracts/whitelist/messages/execute'
import { EXECUTE_LIST as WL_MERKLE_TREE_EXECUTE_LIST } from 'contracts/whitelistMerkleTree/messages/execute'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
@ -10,13 +13,20 @@ import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps { export interface ExecuteComboboxProps {
value: ExecuteListItem | null value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void onChange: (item: ExecuteListItem) => void
whitelistType?: 'standard' | 'flex' | 'merkletree'
} }
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => { export const ExecuteCombobox = ({ value, onChange, whitelistType }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const filtered = const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] }) whitelistType !== 'merkletree'
? search === ''
? EXECUTE_LIST
: matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
: search === ''
? WL_MERKLE_TREE_EXECUTE_LIST
: matchSorter(WL_MERKLE_TREE_EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return ( return (
<Combobox <Combobox
@ -67,7 +77,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active }) clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
} }
value={entry} value={entry}
> >

View File

@ -27,5 +27,9 @@ export function useAddressListState() {
}) })
} }
return { entries, values, add, update, remove } function reset() {
setRecord({})
}
return { entries, values, add, update, remove, reset }
} }

View File

@ -1,7 +1,12 @@
import { toUtf8 } from '@cosmjs/encoding'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { AddressInput } from 'components/forms/FormInput' import { AddressInput } from 'components/forms/FormInput'
import { useEffect, useId, useMemo } from 'react' import { useEffect, useId, useMemo } from 'react'
import toast from 'react-hot-toast'
import { FaMinus, FaPlus } from 'react-icons/fa' import { FaMinus, FaPlus } from 'react-icons/fa'
import { SG721_NAME_ADDRESS } from 'utils/constants'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
import { useInputState } from './FormInput.hooks' import { useInputState } from './FormInput.hooks'
@ -26,6 +31,7 @@ export function AddressList(props: AddressListProps) {
{entries.map(([id], i) => ( {entries.map(([id], i) => (
<Address <Address
key={`ib-${id}`} key={`ib-${id}`}
defaultValue={entries[i][1]}
id={id} id={id}
isLast={i === entries.length - 1} isLast={i === entries.length - 1}
onAdd={onAdd} onAdd={onAdd}
@ -43,9 +49,11 @@ export interface AddressProps {
onAdd: AddressListProps['onAdd'] onAdd: AddressListProps['onAdd']
onChange: AddressListProps['onChange'] onChange: AddressListProps['onChange']
onRemove: AddressListProps['onRemove'] onRemove: AddressListProps['onRemove']
defaultValue?: Address
} }
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) { export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }: AddressProps) {
const wallet = useWallet()
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast]) const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
const htmlId = useId() const htmlId = useId()
@ -54,12 +62,45 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
id: `ib-address-${htmlId}`, id: `ib-address-${htmlId}`,
name: `ib-address-${htmlId}`, name: `ib-address-${htmlId}`,
title: ``, title: ``,
defaultValue: defaultValue?.address,
}) })
const resolveAddress = async (name: string) => {
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
await (
await wallet.getCosmWasmClient()
)
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri })
else {
toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
onChange(id, { address: '' })
}
})
.catch((err) => {
toast.error(`Error resolving address for the name: ${name}.stars`)
console.error(err)
onChange(id, { address: '' })
})
}
useEffect(() => { useEffect(() => {
onChange(id, { if (addressState.value.endsWith('.stars')) {
address: addressState.value, void resolveAddress(addressState.value.split('.')[0])
}) } else {
onChange(id, {
address: addressState.value,
})
}
}, [addressState.value, id]) }, [addressState.value, id])
return ( return (
@ -67,7 +108,7 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
<AddressInput {...addressState} /> <AddressInput {...addressState} />
<div className="flex justify-end items-end pb-2 w-8"> <div className="flex justify-end items-end pb-2 w-8">
<button <button
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full" className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
onClick={() => (isLast ? onAdd() : onRemove(id))} onClick={() => (isLast ? onAdd() : onRemove(id))}
type="button" type="button"
> >

View File

@ -0,0 +1,33 @@
import { useMemo, useState } from 'react'
import { uid } from 'utils/random'
import type { DenomUnit } from './DenomUnits'
export function useDenomUnitsState() {
const [record, setRecord] = useState<Record<string, DenomUnit>>(() => ({}))
const entries = useMemo(() => Object.entries(record), [record])
const values = useMemo(() => Object.values(record), [record])
function add(attribute: DenomUnit = { denom: '', exponent: 0, aliases: '' }) {
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
}
function update(key: string, attribute = record[key]) {
setRecord((prev) => ({ ...prev, [key]: attribute }))
}
function remove(key: string) {
return setRecord((prev) => {
const latest = { ...prev }
delete latest[key]
return latest
})
}
function reset() {
setRecord({})
}
return { entries, values, add, update, remove, reset }
}

View File

@ -0,0 +1,106 @@
import { FormControl } from 'components/FormControl'
import { NumberInput, TextInput } from 'components/forms/FormInput'
import { useEffect, useId, useMemo } from 'react'
import { FaMinus, FaPlus } from 'react-icons/fa'
import { useWallet } from 'utils/wallet'
import { useInputState, useNumberInputState } from './FormInput.hooks'
export interface DenomUnit {
denom: string
exponent: number
aliases: string
}
export interface DenomUnitsProps {
title: string
subtitle?: string
isRequired?: boolean
attributes: [string, DenomUnit][]
onAdd: () => void
onChange: (key: string, attribute: DenomUnit) => void
onRemove: (key: string) => void
}
export function DenomUnits(props: DenomUnitsProps) {
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
return (
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
{attributes.map(([id], i) => (
<DenomUnit
key={`ma-${id}`}
defaultAttribute={attributes[i][1]}
id={id}
isLast={i === attributes.length - 1}
onAdd={onAdd}
onChange={onChange}
onRemove={onRemove}
/>
))}
</FormControl>
)
}
export interface DenomUnitProps {
id: string
isLast: boolean
onAdd: DenomUnitsProps['onAdd']
onChange: DenomUnitsProps['onChange']
onRemove: DenomUnitsProps['onRemove']
defaultAttribute: DenomUnit
}
export function DenomUnit({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: DenomUnitProps) {
const wallet = useWallet()
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
const htmlId = useId()
const denomState = useInputState({
id: `ma-denom-${htmlId}`,
name: `ma-denom-${htmlId}`,
title: `Denom`,
defaultValue: defaultAttribute.denom,
})
const exponentState = useNumberInputState({
id: `mint-exponent-${htmlId}`,
name: `mint-exponent-${htmlId}`,
title: `Exponent`,
defaultValue: defaultAttribute.exponent,
})
const aliasesState = useInputState({
id: `ma-aliases-${htmlId}`,
name: `ma-aliases-${htmlId}`,
title: `Aliases`,
defaultValue: defaultAttribute.aliases,
placeholder: 'Comma separated aliases',
})
useEffect(() => {
onChange(id, { denom: denomState.value, exponent: exponentState.value, aliases: aliasesState.value })
}, [id, denomState.value, exponentState.value, aliasesState.value])
return (
<div className="grid relative md:grid-cols-[40%_18%_35_7%] lg:grid-cols-[55%_13%_25%_7%] 2xl:grid-cols-[55%_13%_25%_7%] 2xl:space-x-2">
<TextInput {...denomState} />
<NumberInput className="ml-2" {...exponentState} />
<TextInput className="ml-2" {...aliasesState} />
<div className="flex justify-end items-end pb-2 w-8">
<button
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
onClick={(e) => {
e.preventDefault()
isLast ? onAdd() : onRemove(id)
}}
type="button"
>
<Icon className="w-3 h-3" />
</button>
</div>
</div>
)
}

View File

@ -0,0 +1,32 @@
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { useMemo, useState } from 'react'
import { uid } from 'utils/random'
export function useFlexMemberAttributesState() {
const [record, setRecord] = useState<Record<string, WhitelistFlexMember>>(() => ({}))
const entries = useMemo(() => Object.entries(record), [record])
const values = useMemo(() => Object.values(record), [record])
function add(attribute: WhitelistFlexMember = { address: '', mint_count: 0 }) {
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
}
function update(key: string, attribute = record[key]) {
setRecord((prev) => ({ ...prev, [key]: attribute }))
}
function remove(key: string) {
return setRecord((prev) => {
const latest = { ...prev }
delete latest[key]
return latest
})
}
function reset() {
setRecord({})
}
return { entries, values, add, update, remove, reset }
}

View File

@ -0,0 +1,135 @@
import { toUtf8 } from '@cosmjs/encoding'
import { FormControl } from 'components/FormControl'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { useEffect, useId, useMemo } from 'react'
import toast from 'react-hot-toast'
import { FaMinus, FaPlus } from 'react-icons/fa'
import { SG721_NAME_ADDRESS } from 'utils/constants'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
import { useInputState, useNumberInputState } from './FormInput.hooks'
export interface FlexMemberAttributesProps {
title: string
subtitle?: string
isRequired?: boolean
attributes: [string, WhitelistFlexMember][]
onAdd: () => void
onChange: (key: string, attribute: WhitelistFlexMember) => void
onRemove: (key: string) => void
}
export function FlexMemberAttributes(props: FlexMemberAttributesProps) {
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
return (
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
{attributes.map(([id], i) => (
<FlexMemberAttribute
key={`ma-${id}`}
defaultAttribute={attributes[i][1]}
id={id}
isLast={i === attributes.length - 1}
onAdd={onAdd}
onChange={onChange}
onRemove={onRemove}
/>
))}
</FormControl>
)
}
export interface MemberAttributeProps {
id: string
isLast: boolean
onAdd: FlexMemberAttributesProps['onAdd']
onChange: FlexMemberAttributesProps['onChange']
onRemove: FlexMemberAttributesProps['onRemove']
defaultAttribute: WhitelistFlexMember
}
export function FlexMemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) {
const wallet = useWallet()
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
const htmlId = useId()
const addressState = useInputState({
id: `ma-address-${htmlId}`,
name: `ma-address-${htmlId}`,
title: `Address`,
defaultValue: defaultAttribute.address,
})
const mintCountState = useNumberInputState({
id: `mint-count-${htmlId}`,
name: `mint-count-${htmlId}`,
title: `Mint Count`,
defaultValue: defaultAttribute.mint_count,
})
useEffect(() => {
onChange(id, { address: addressState.value, mint_count: mintCountState.value })
}, [addressState.value, mintCountState.value, id])
const resolveAddress = async (name: string) => {
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
await (
await wallet.getCosmWasmClient()
)
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri, mint_count: mintCountState.value })
else {
toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
onChange(id, { address: '', mint_count: mintCountState.value })
}
})
.catch((err) => {
toast.error(`Error resolving address for the name: ${name}.stars`)
console.error(err)
onChange(id, { address: '', mint_count: mintCountState.value })
})
}
useEffect(() => {
if (addressState.value.endsWith('.stars')) {
void resolveAddress(addressState.value.split('.')[0])
} else {
onChange(id, {
address: addressState.value,
mint_count: mintCountState.value,
})
}
}, [addressState.value, id])
return (
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
<AddressInput {...addressState} />
<NumberInput className="ml-2" {...mintCountState} />
<div className="flex justify-end items-end pb-2 w-8">
<button
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
onClick={(e) => {
e.preventDefault()
isLast ? onAdd() : onRemove(id)
}}
type="button"
>
<Icon className="w-3 h-3" />
</button>
</div>
</div>
)
}

View File

@ -69,6 +69,26 @@ export const TextInput = forwardRef<HTMLInputElement, FormInputProps>(
// //
) )
export const CheckBoxInput = forwardRef<HTMLInputElement, FormInputProps>(
function CheckBoxInput(props, ref) {
return (
<div className="flex flex-col space-y-2">
<label className="flex flex-col space-y-1" htmlFor="explicit">
<span className="font-bold first-letter:capitalize">Explicit Content</span>
</label>
<input
className="placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
id="explicit"
name="explicit"
type="checkbox"
value=""
/>
</div>
)
},
//
)
export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>( export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>(
function UrlInput(props, ref) { function UrlInput(props, ref) {
return <FormInput {...props} ref={ref} type="url" /> return <FormInput {...props} ref={ref} type="url" />

View File

@ -0,0 +1,33 @@
import { useMemo, useState } from 'react'
import { uid } from 'utils/random'
import type { Attribute } from './MemberAttributes'
export function useMemberAttributesState() {
const [record, setRecord] = useState<Record<string, Attribute>>(() => ({}))
const entries = useMemo(() => Object.entries(record), [record])
const values = useMemo(() => Object.values(record), [record])
function add(attribute: Attribute = { address: '', weight: 0 }) {
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
}
function update(key: string, attribute = record[key]) {
setRecord((prev) => ({ ...prev, [key]: attribute }))
}
function remove(key: string) {
return setRecord((prev) => {
const latest = { ...prev }
delete latest[key]
return latest
})
}
function reset() {
setRecord({})
}
return { entries, values, add, update, remove, reset }
}

View File

@ -0,0 +1,111 @@
import { FormControl } from 'components/FormControl'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useEffect, useId, useMemo } from 'react'
import { FaMinus, FaPlus } from 'react-icons/fa'
import { useInputState, useNumberInputState } from './FormInput.hooks'
export interface Attribute {
address: string
weight: number
}
export interface MemberAttributesProps {
title: string
subtitle?: string
isRequired?: boolean
attributes: [string, Attribute][]
onAdd: () => void
onChange: (key: string, attribute: Attribute) => void
onRemove: (key: string) => void
}
export function MemberAttributes(props: MemberAttributesProps) {
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
const calculateMemberPercent = (id: string) => {
const total = attributes.reduce((acc, [, { weight }]) => acc + (weight ? weight : 0), 0)
// return attributes.map(([id, { weight }]) => [id, weight / total])
const memberWeight = attributes.find(([memberId]) => memberId === id)?.[1].weight
return memberWeight ? memberWeight / total : 0
}
return (
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
{attributes.map(([id], i) => (
<MemberAttribute
key={`ma-${id}`}
defaultAttribute={attributes[i][1]}
id={id}
isLast={i === attributes.length - 1}
onAdd={onAdd}
onChange={onChange}
onRemove={onRemove}
percentage={(Number(calculateMemberPercent(id)) * 100).toFixed(2)}
/>
))}
</FormControl>
)
}
export interface MemberAttributeProps {
id: string
isLast: boolean
onAdd: MemberAttributesProps['onAdd']
onChange: MemberAttributesProps['onChange']
onRemove: MemberAttributesProps['onRemove']
defaultAttribute: Attribute
percentage?: string
}
export function MemberAttribute({
id,
isLast,
onAdd,
onChange,
onRemove,
defaultAttribute,
percentage,
}: MemberAttributeProps) {
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
const htmlId = useId()
const addressState = useInputState({
id: `ma-address-${htmlId}`,
name: `ma-address-${htmlId}`,
title: `Address`,
defaultValue: defaultAttribute.address,
})
const weightState = useNumberInputState({
id: `ma-weight-${htmlId}`,
name: `ma-weight-${htmlId}`,
title: `Weight ${percentage ? `: ${percentage}%` : ''}`,
defaultValue: defaultAttribute.weight,
})
useEffect(() => {
onChange(id, { address: addressState.value, weight: weightState.value })
}, [addressState.value, weightState.value, id])
return (
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
<AddressInput {...addressState} />
<NumberInput {...weightState} />
<div className="flex justify-end items-end pb-2 w-8">
<button
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
onClick={(e) => {
e.preventDefault()
isLast ? onAdd() : onRemove(id)
}}
type="button"
>
<Icon className="w-3 h-3" />
</button>
</div>
</div>
)
}

View File

@ -73,12 +73,13 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
}, [traitTypeState.value, traitValueState.value, id]) }, [traitTypeState.value, traitValueState.value, id])
return ( return (
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2"> <div className="grid relative xl:grid-cols-[6fr_6fr_1fr] xl:-space-x-8 2xl:space-x-2">
<TraitTypeInput {...traitTypeState} /> <TraitTypeInput className="lg:w-4/5 2xl:w-full" {...traitTypeState} />
<TraitValueInput {...traitValueState} /> <TraitValueInput className="lg:w-4/5 xl:pr-2 xl:w-full" {...traitValueState} />
<div className="flex justify-end items-end pb-2 w-8"> <div className="flex justify-end items-end pb-2 w-8">
<button <button
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full" className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
isLast ? onAdd() : onRemove(id) isLast ? onAdd() : onRemove(id)

View File

@ -0,0 +1,400 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable jsx-a11y/media-has-caption */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { Tooltip } from 'components/Tooltip'
import { useGlobalSettings } from 'contexts/globalSettings'
import { addLogItem } from 'contexts/log'
import type { ChangeEvent } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, SG721_OPEN_EDITION_UPDATABLE_CODE_ID } from 'utils/constants'
import { getAssetType } from 'utils/getAssetType'
import { uid } from 'utils/random'
import { TextInput } from '../forms/FormInput'
import type { UploadMethod } from './OffChainMetadataUploadDetails'
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
interface CollectionDetailsProps {
onChange: (data: CollectionDetailsDataProps) => void
uploadMethod: UploadMethod
coverImageUrl: string
metadataStorageMethod: MetadataStorageMethod
importedCollectionDetails?: CollectionDetailsDataProps
}
export interface CollectionDetailsDataProps {
name: string
description: string
symbol: string
imageFile: File[]
externalLink?: string
startTradingTime?: string
explicit: boolean
updatable: boolean
}
export const CollectionDetails = ({
onChange,
uploadMethod,
metadataStorageMethod,
coverImageUrl,
importedCollectionDetails,
}: CollectionDetailsProps) => {
const [coverImage, setCoverImage] = useState<File | null>(null)
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [explicit, setExplicit] = useState<boolean>(false)
const [updatable, setUpdatable] = useState<boolean>(false)
const { timezone } = useGlobalSettings()
const initialRender = useRef(true)
const coverImageInputRef = useRef<HTMLInputElement>(null)
const nameState = useInputState({
id: 'name',
name: 'name',
title: 'Name',
placeholder: 'My Awesome Collection',
})
const descriptionState = useInputState({
id: 'description',
name: 'description',
title: 'Description',
placeholder: 'My Awesome Collection Description',
})
const symbolState = useInputState({
id: 'symbol',
name: 'symbol',
title: 'Symbol',
placeholder: 'SYMBOL',
})
const externalLinkState = useInputState({
id: 'external-link',
name: 'externalLink',
title: 'External Link (optional)',
placeholder: 'https://my-collection...',
})
useEffect(() => {
try {
const data: CollectionDetailsDataProps = {
name: nameState.value,
description: descriptionState.value,
symbol: symbolState.value,
imageFile: coverImage ? [coverImage] : [],
externalLink: externalLinkState.value || undefined,
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
explicit,
updatable,
}
onChange(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
nameState.value,
descriptionState.value,
symbolState.value,
externalLinkState.value,
coverImage,
timestamp,
explicit,
updatable,
])
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) return toast.error('Error selecting cover image')
if (event.target.files.length === 0) {
setCoverImage(null)
return toast.error('No files selected.')
}
const reader = new FileReader()
reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.')
const imageFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
type: 'image/jpg',
})
setCoverImage(imageFile)
}
reader.readAsArrayBuffer(event.target.files[0])
}
useEffect(() => {
setCoverImage(null)
// empty the element so that the same file can be selected again
if (coverImageInputRef.current) coverImageInputRef.current.value = ''
}, [metadataStorageMethod])
useEffect(() => {
if (initialRender.current) {
initialRender.current = false
} else if (updatable) {
toast.success('Token metadata will be updatable upon collection creation.', {
style: { maxWidth: 'none' },
icon: '✅📝',
})
} else {
toast.error('Token metadata will not be updatable upon collection creation.', {
style: { maxWidth: 'none' },
icon: '⛔🔏',
})
}
}, [updatable])
useEffect(() => {
if (importedCollectionDetails) {
nameState.onChange(importedCollectionDetails.name)
descriptionState.onChange(importedCollectionDetails.description)
symbolState.onChange(importedCollectionDetails.symbol)
//setCoverImage(importedCollectionDetails.imageFile[0] || null)
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
setTimestamp(
importedCollectionDetails.startTradingTime
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
: undefined,
)
setExplicit(importedCollectionDetails.explicit)
setUpdatable(importedCollectionDetails.updatable)
}
}, [importedCollectionDetails])
const videoPreview = useMemo(() => {
if (uploadMethod === 'new' && coverImage) {
return (
<video
controls
id="video"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={URL.createObjectURL(coverImage)}
/>
)
} else if (uploadMethod === 'existing' && coverImageUrl && coverImageUrl.includes('ipfs://')) {
return (
<video
controls
id="video"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
coverImageUrl.lastIndexOf('ipfs://') + 7,
)}`}
/>
)
} else if (uploadMethod === 'existing' && coverImageUrl && !coverImageUrl.includes('ipfs://')) {
return (
<video
controls
id="video"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={coverImageUrl}
/>
)
}
}, [coverImage, coverImageUrl, uploadMethod])
return (
<div>
<FormGroup subtitle="Information about your collection" title="Collection Details">
<div className={clsx('')}>
<div className="">
<TextInput {...nameState} isRequired />
<TextInput className="mt-2" {...descriptionState} isRequired />
<TextInput className="mt-2" {...symbolState} isRequired />
</div>
<div className={clsx('')}>
<TextInput className={clsx('mt-2')} {...externalLinkState} />
{/* Currently trading starts immediately for 1/1 Collections */}
<FormControl
className={clsx('mt-2')}
htmlId="timestamp"
subtitle="Trading start time offset will be set as 1 week by default."
title={`Trading Start Time (optional | ${timezone === 'Local' ? 'local)' : 'UTC)'}`}
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setTimestamp(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setTimestamp(undefined)
}
value={
timezone === 'Local'
? timestamp
: timestamp
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
</div>
</div>
<FormControl className={clsx('')} isRequired={uploadMethod === 'new'} title="Cover Image">
{uploadMethod === 'new' && (
<input
accept="image/*, video/*"
className={clsx(
'w-full',
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:hover:bg-white/5 before:transition',
)}
id="cover-image"
onChange={selectCoverImage}
ref={coverImageInputRef}
type="file"
/>
)}
<Conditional
test={coverImage !== null && uploadMethod === 'new' && getAssetType(coverImage.name) === 'image'}
>
{coverImage !== null && (
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<img alt="no-preview-available" src={URL.createObjectURL(coverImage)} />
</div>
)}
</Conditional>
<Conditional
test={coverImage !== null && uploadMethod === 'new' && getAssetType(coverImage.name) === 'video'}
>
{coverImage !== null && videoPreview}
</Conditional>
{uploadMethod === 'existing' && coverImageUrl?.includes('ipfs://') && (
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<Conditional test={getAssetType(coverImageUrl) !== 'video'}>
<img
alt="no-preview-available"
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
coverImageUrl.lastIndexOf('ipfs://') + 7,
)}`}
/>
</Conditional>
<Conditional test={getAssetType(coverImageUrl) === 'video'}>{videoPreview}</Conditional>
</div>
)}
{uploadMethod === 'existing' && coverImageUrl && !coverImageUrl?.includes('ipfs://') && (
<div>
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<Conditional test={getAssetType(coverImageUrl) !== 'video'}>
<img alt="no-preview-available" src={coverImageUrl} />
</Conditional>
<Conditional test={getAssetType(coverImageUrl) === 'video'}>{videoPreview}</Conditional>
</div>
</div>
)}
{uploadMethod === 'existing' && !coverImageUrl && (
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
)}
</FormControl>
<div className={clsx('flex flex-col space-y-2')}>
<div>
<div className="flex mt-4">
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
Does the collection contain explicit content?
</span>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={explicit}
className="peer sr-only"
id="explicitRadio1"
name="explicitRadioOptions1"
onClick={() => {
setExplicit(true)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio1"
>
YES
</label>
</div>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={!explicit}
className="peer sr-only"
id="explicitRadio2"
name="explicitRadioOptions2"
onClick={() => {
setExplicit(false)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="explicitRadio2"
>
NO
</label>
</div>
</div>
</div>
</div>
<Conditional
test={
false && SG721_OPEN_EDITION_UPDATABLE_CODE_ID > 0 && OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS !== undefined
}
>
<Tooltip
backgroundColor="bg-blue-500"
label={
<div className="grid grid-flow-row">
<span>
When enabled, the metadata for tokens can be updated after the collection is created until the
collection is frozen by the creator.
</span>
</div>
}
placement="bottom"
>
<div className={clsx('flex flex-col space-y-2 w-full form-control')}>
<label className="justify-start cursor-pointer label">
<div className="flex flex-col">
<span className="mr-4 font-bold">Updatable Token Metadata</span>
<span className="mr-4">(Price: 2000 STARS)</span>
</div>
<input
checked={updatable}
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setUpdatable(!updatable)}
type="checkbox"
/>
</label>
</div>
</Tooltip>
</Conditional>
</FormGroup>
</div>
)
}

View File

@ -0,0 +1,454 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
import clsx from 'clsx'
import { Anchor } from 'components/Anchor'
import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { SingleAssetPreview } from 'components/SingleAssetPreview'
import type { ChangeEvent } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
import type { AssetType } from 'utils/getAssetType'
import { getAssetType } from 'utils/getAssetType'
export type UploadMethod = 'new' | 'existing'
interface ImageUploadDetailsProps {
onChange: (value: ImageUploadDetailsDataProps) => void
importedImageUploadDetails?: ImageUploadDetailsDataProps
}
export interface ImageUploadDetailsDataProps {
assetFile: File | undefined
thumbnailFile?: File | undefined
isThumbnailCompatible?: boolean
uploadService: UploadServiceType
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
imageUrl?: string
coverImageUrl?: string
}
export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => {
const [assetFile, setAssetFile] = useState<File>()
const [thumbnailFile, setThumbnailFile] = useState<File>()
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
const assetFileRef = useRef<HTMLInputElement | null>(null)
const thumbnailFileRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key',
name: 'nftStorageApiKey',
title: 'NFT.Storage API Key',
placeholder: 'Enter NFT.Storage API Key',
defaultValue: '',
})
const pinataApiKeyState = useInputState({
id: 'pinata-api-key',
name: 'pinataApiKey',
title: 'Pinata API Key',
placeholder: 'Enter Pinata API Key',
defaultValue: '',
})
const pinataSecretKeyState = useInputState({
id: 'pinata-secret-key',
name: 'pinataSecretKey',
title: 'Pinata Secret Key',
placeholder: 'Enter Pinata Secret Key',
defaultValue: '',
})
const imageUrlState = useInputState({
id: 'imageUrl',
name: 'imageUrl',
title: 'Asset URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const coverImageUrlState = useInputState({
id: 'coverImageUrl',
name: 'coverImageUrl',
title: 'Cover Image URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html']
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFile(undefined)
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/jpg' })
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
setIsThumbnailCompatible(true)
}
setAssetFile(selectedFile)
}
}
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
setThumbnailFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/*' })
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setThumbnailFile(selectedFile)
}
}
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
useEffect(() => {
try {
const data: ImageUploadDetailsDataProps = {
assetFile,
thumbnailFile,
isThumbnailCompatible,
uploadService,
nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value,
uploadMethod,
imageUrl: imageUrlState.value
.replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(regex, '')
.trim(),
coverImageUrl: coverImageUrlState.value.trim(),
}
onChange(data)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}, [
assetFile,
thumbnailFile,
isThumbnailCompatible,
uploadService,
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
imageUrlState.value,
coverImageUrlState.value,
])
useEffect(() => {
if (assetFileRef.current) assetFileRef.current.value = ''
setAssetFile(undefined)
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
imageUrlState.onChange('')
}, [uploadMethod])
useEffect(() => {
if (importedImageUploadDetails) {
setUploadMethod(importedImageUploadDetails.uploadMethod)
setUploadService(importedImageUploadDetails.uploadService)
nftStorageApiKeyState.onChange(importedImageUploadDetails.nftStorageApiKey || '')
pinataApiKeyState.onChange(importedImageUploadDetails.pinataApiKey || '')
pinataSecretKeyState.onChange(importedImageUploadDetails.pinataSecretKey || '')
imageUrlState.onChange(importedImageUploadDetails.imageUrl || '')
coverImageUrlState.onChange(importedImageUploadDetails.coverImageUrl || '')
}
}, [importedImageUploadDetails])
useEffect(() => {
if (useDefaultApiKey) {
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
} else {
nftStorageApiKeyState.onChange('')
}
}, [useDefaultApiKey])
const previewUrl = imageUrlState.value.toLowerCase().trim().startsWith('ipfs://')
? `https://ipfs-gw.stargaze-apis.com/ipfs/${imageUrlState.value.substring(7)}`
: imageUrlState.value
const audioPreview = useMemo(
() => (
<audio
controls
id="audio"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={imageUrlState.value ? previewUrl : ''}
/>
),
[imageUrlState.value],
)
const videoPreview = useMemo(
() => (
<video
controls
id="video"
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
src={imageUrlState.value ? previewUrl : ''}
/>
),
[imageUrlState.value],
)
return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'new'}
className="peer sr-only"
id="inlineRadio2"
name="inlineRadioOptions2"
onClick={() => {
setUploadMethod('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2"
>
Upload New Asset
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'existing'}
className="peer sr-only"
id="inlineRadio1"
name="inlineRadioOptions1"
onClick={() => {
setUploadMethod('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1"
>
Use an existing Asset URL
</label>
</div>
</div>
<div className="p-3 py-5 pb-4">
<Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column">
<p className="mb-5 ml-5">
Though the Open Edition contracts allow for off-chain asset storage, it is recommended to use a
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
NFT.Storage
</Anchor>{' '}
or{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
Pinata
</Anchor>{' '}
and upload your asset manually to get an asset URL for your NFT.
</p>
<div className="flex flex-row w-full">
<div className="flex flex-col w-3/5">
<TextInput {...imageUrlState} className="mt-2 ml-6" />
<TextInput {...coverImageUrlState} className="mt-4 ml-6" />
</div>
<Conditional test={imageUrlState.value !== ''}>
<div className="flex mt-2 ml-8 w-1/3 border-2 border-dashed">
{getAssetType(imageUrlState.value) === 'audio' && audioPreview}
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
{getAssetType(imageUrlState.value) === 'image' && <img alt="asset-preview" src={previewUrl} />}
</div>
</Conditional>
</div>
</div>
</Conditional>
<Conditional test={uploadMethod === 'new'}>
<div>
<div className="flex flex-col items-center px-8 w-full">
<div className="flex justify-items-start mb-5 w-full font-bold">
<div className="form-check form-check-inline">
<input
checked={uploadService === 'nft-storage'}
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
setUploadService('nft-storage')
}}
type="radio"
value="nft-storage"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Upload using NFT.Storage
</label>
</div>
<div className="ml-2 form-check form-check-inline">
<input
checked={uploadService === 'pinata'}
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
setUploadService('pinata')
}}
type="radio"
value="pinata"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio4"
>
Upload using Pinata
</label>
</div>
</div>
<div className="flex w-full">
<Conditional test={uploadService === 'nft-storage'}>
<div className="flex-col w-full">
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
<div className="flex-row mt-2 w-full form-control">
<label className="cursor-pointer label">
<span className="mr-2 font-bold">Use Default API Key</span>
<input
checked={useDefaultApiKey}
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
setUseDefaultApiKey(!useDefaultApiKey)
}}
type="checkbox"
/>
</label>
</div>
</div>
</Conditional>
<Conditional test={uploadService === 'pinata'}>
<TextInput {...pinataApiKeyState} className="w-full" />
<div className="w-[20px]" />
<TextInput {...pinataSecretKeyState} className="w-full" />
</Conditional>
</div>
</div>
<div className="mt-6">
<div className="grid grid-cols-2">
<div>
<div className="w-full">
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFile"
>
Asset Selection
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*, audio/*, video/*, .html, .pdf"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="assetFile"
onChange={selectAsset}
ref={assetFileRef}
type="file"
/>
</div>
</div>
<Conditional test={isThumbnailCompatible}>
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="thumbnailFile"
>
Thumbnail Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="thumbnailFile"
onChange={selectThumbnail}
ref={thumbnailFileRef}
type="file"
/>
</div>
</div>
</Conditional>
</div>
</div>
<Conditional test={assetFile !== undefined}>
<SingleAssetPreview
relatedAsset={assetFile}
subtitle={`Asset filename: ${assetFile?.name as string}`}
/>
</Conditional>
</div>
</div>
</div>
</Conditional>
</div>
</div>
)
}

View File

@ -0,0 +1,280 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable no-nested-ternary */
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { openEditionMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { stars, tokensList } from 'config/token'
import { useGlobalSettings } from 'contexts/globalSettings'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
import { useWallet } from 'utils/wallet'
import { NumberInput, TextInput } from '../forms/FormInput'
import type { UploadMethod } from './OffChainMetadataUploadDetails'
export type LimitType = 'count_limited' | 'time_limited' | 'time_and_count_limited'
interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void
uploadMethod: UploadMethod
minimumMintPrice: number
mintTokenFromFactory?: TokenInfo | undefined
importedMintingDetails?: MintingDetailsDataProps
isPresale: boolean
whitelistStartDate?: string
}
export interface MintingDetailsDataProps {
unitPrice: string
perAddressLimit: number
startTime: string
endTime?: string
tokenCountLimit?: number
paymentAddress?: string
selectedMintToken?: TokenInfo
limitType: LimitType
}
export const MintingDetails = ({
onChange,
uploadMethod,
minimumMintPrice,
mintTokenFromFactory,
importedMintingDetails,
isPresale,
whitelistStartDate,
}: MintingDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const [mintingDetailsImported, setMintingDetailsImported] = useState(false)
const [limitType, setLimitType] = useState<LimitType>('time_limited')
const { timezone } = useGlobalSettings()
const unitPriceState = useNumberInputState({
id: 'unitPrice',
name: 'unitPrice',
title: 'Mint Price',
subtitle: `Price of each token (min. ${minimumMintPrice} ${
mintTokenFromFactory ? mintTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '50',
})
const perAddressLimitState = useNumberInputState({
id: 'peraddresslimit',
name: 'peraddresslimit',
title: 'Per Address Limit',
subtitle: '',
placeholder: '1',
})
const tokenCountLimitState = useNumberInputState({
id: 'tokencountlimit',
name: 'tokencountlimit',
title: 'Maximum Token Count',
subtitle: 'Total number of mintable tokens',
placeholder: '100',
})
const paymentAddressState = useInputState({
id: 'payment-address',
name: 'paymentAddress',
title: 'Payment Address (optional)',
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
})
const resolvePaymentAddress = async () => {
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
paymentAddressState.onChange(resolvedAddress)
})
}
useEffect(() => {
if (!importedMintingDetails || (importedMintingDetails && mintingDetailsImported)) {
void resolvePaymentAddress()
}
}, [paymentAddressState.value])
useEffect(() => {
const data: MintingDetailsDataProps = {
unitPrice: unitPriceState.value
? (Number(unitPriceState.value) * 1_000_000).toString()
: unitPriceState.value === 0
? '0'
: '',
perAddressLimit: perAddressLimitState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
endTime:
limitType === 'time_limited' || limitType === 'time_and_count_limited'
? endTimestamp
? (endTimestamp.getTime() * 1_000_000).toString()
: ''
: undefined,
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
limitType,
tokenCountLimit:
limitType === 'count_limited' || limitType === 'time_and_count_limited'
? tokenCountLimitState.value
: undefined,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
unitPriceState.value,
perAddressLimitState.value,
timestamp,
endTimestamp,
paymentAddressState.value,
selectedMintToken,
tokenCountLimitState.value,
limitType,
])
useEffect(() => {
if (importedMintingDetails) {
console.log('Selected Token ID: ', importedMintingDetails.selectedMintToken?.id)
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1000000)
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
setLimitType(importedMintingDetails.limitType)
tokenCountLimitState.onChange(importedMintingDetails.tokenCountLimit ? importedMintingDetails.tokenCountLimit : 0)
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
setEndTimestamp(new Date(Number(importedMintingDetails.endTime) / 1_000_000))
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
setMintingDetailsImported(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedMintingDetails])
useEffect(() => {
if (isPresale) {
setTimestamp(whitelistStartDate ? new Date(Number(whitelistStartDate) / 1_000_000) : undefined)
}
}, [whitelistStartDate, isPresale])
return (
<div className="border-l-[1px] border-gray-500 border-opacity-20">
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
<div className="flex flex-row items-end">
<NumberInput {...unitPriceState} isRequired />
<select
className="py-[9px] px-4 ml-4 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
value={selectedMintToken?.displayName}
>
{openEditionMinterList
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)
.map((minter) => (
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
{minter.supportedToken.displayName}
</option>
))}
</select>
</div>
<NumberInput {...perAddressLimitState} isRequired />
<FormControl
htmlId="timestamp"
isRequired
subtitle={`Minting start time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
title="Start Time"
>
<InputDateTime
disabled={isPresale}
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setTimestamp(
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setTimestamp(undefined)
}
value={
timezone === 'Local'
? timestamp
: timestamp
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
<div className="flex-row mt-2 w-full form-control">
<h1 className="mt-2 font-bold text-md">Limit Type: </h1>
<label className="justify-start ml-6 cursor-pointer label">
<span className="mr-2">Time</span>
<input
checked={limitType === 'time_limited' || limitType === 'time_and_count_limited'}
className={`${limitType === 'time_limited' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
if (limitType === 'time_and_count_limited') setLimitType('count_limited' as LimitType)
else if (limitType === 'count_limited') setLimitType('time_and_count_limited' as LimitType)
else setLimitType('count_limited' as LimitType)
}}
type="checkbox"
/>
</label>
<label className="justify-start ml-4 cursor-pointer label">
<span className="mr-2">Token Count</span>
<input
checked={limitType === 'count_limited' || limitType === 'time_and_count_limited'}
className={`${limitType === 'count_limited' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
if (limitType === 'time_and_count_limited') setLimitType('time_limited' as LimitType)
else if (limitType === 'time_limited') setLimitType('time_and_count_limited' as LimitType)
else setLimitType('time_limited' as LimitType)
}}
type="checkbox"
/>
</label>
</div>
<Conditional test={limitType === 'time_limited' || limitType === 'time_and_count_limited'}>
<FormControl
htmlId="endTimestamp"
isRequired
subtitle={`Minting end time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
title="End Time"
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setEndTimestamp(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setEndTimestamp(undefined)
}
value={
timezone === 'Local'
? endTimestamp
: endTimestamp
? new Date(endTimestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
</Conditional>
<Conditional test={limitType === 'count_limited' || limitType === 'time_and_count_limited'}>
<NumberInput {...tokenCountLimitState} isRequired />
</Conditional>
</FormGroup>
<TextInput className="pr-4 pl-4 mt-3" {...paymentAddressState} />
</div>
)
}

View File

@ -0,0 +1,579 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
/* eslint-disable @typescript-eslint/no-loop-func */
import clsx from 'clsx'
import { Alert } from 'components/Alert'
import { Anchor } from 'components/Anchor'
import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { MetadataInput } from 'components/MetadataInput'
import { MetadataModal } from 'components/MetadataModal'
import { SingleAssetPreview } from 'components/SingleAssetPreview'
import { Tooltip } from 'components/Tooltip'
import { addLogItem } from 'contexts/log'
import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
import type { AssetType } from 'utils/getAssetType'
import { getAssetType } from 'utils/getAssetType'
import { uid } from 'utils/random'
import { naturalCompare } from 'utils/sort'
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
export type UploadMethod = 'new' | 'existing'
interface OffChainMetadataUploadDetailsProps {
onChange: (value: OffChainMetadataUploadDetailsDataProps) => void
metadataStorageMethod?: MetadataStorageMethod
importedOffChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
}
export interface OffChainMetadataUploadDetailsDataProps {
assetFiles: File[]
metadataFiles: File[]
thumbnailFile?: File
isThumbnailCompatible?: boolean
uploadService: UploadServiceType
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
tokenURI?: string
imageUrl?: string
openEditionMinterMetadataFile?: File
exportedMetadata?: any
}
export const OffChainMetadataUploadDetails = ({
onChange,
metadataStorageMethod,
importedOffChainMetadataUploadDetails,
}: OffChainMetadataUploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [thumbnailFile, setThumbnailFile] = useState<File>()
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false)
const [exportedMetadata, setExportedMetadata] = useState(undefined)
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html', 'document']
const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
const thumbnailFilesRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key',
name: 'nftStorageApiKey',
title: 'NFT.Storage API Key',
placeholder: 'Enter NFT.Storage API Key',
defaultValue: '',
})
const pinataApiKeyState = useInputState({
id: 'pinata-api-key',
name: 'pinataApiKey',
title: 'Pinata API Key',
placeholder: 'Enter Pinata API Key',
defaultValue: '',
})
const pinataSecretKeyState = useInputState({
id: 'pinata-secret-key',
name: 'pinataSecretKey',
title: 'Pinata Secret Key',
placeholder: 'Enter Pinata Secret Key',
defaultValue: '',
})
const tokenUriState = useInputState({
id: 'tokenUri',
name: 'tokenUri',
title: 'Token URI',
placeholder: 'ipfs://',
defaultValue: '',
})
const coverImageUrlState = useInputState({
id: 'coverImageUrl',
name: 'coverImageUrl',
title: 'Cover Image URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFilesArray([])
setMetadataFilesArray([])
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
if (event.target.files === null) return
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
setIsThumbnailCompatible(true)
}
let loadedFileCount = 0
const files: File[] = []
let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader()
reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.')
const assetFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
type: 'image/jpg',
})
files.push(assetFile)
}
reader.readAsArrayBuffer(event.target.files[i])
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
loadedFileCount++
if (loadedFileCount === event.target.files.length) {
setAssetFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
}
}
}
}
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFilesArray([])
if (event.target.files === null) return toast.error('No files selected.')
let loadedFileCount = 0
const files: File[] = []
let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader()
reader.onload = async (e) => {
if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.')
const metadataFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
type: 'application/json',
})
files.push(metadataFile)
try {
const parsedMetadata = JSON.parse(await metadataFile.text())
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
event.target.value = ''
setMetadataFilesArray([])
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
}
} catch (error: any) {
event.target.value = ''
setMetadataFilesArray([])
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
}
}
reader.readAsText(event.target.files[i], 'utf8')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
loadedFileCount++
if (loadedFileCount === event.target.files.length) {
setMetadataFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
}
}
}
}
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
setThumbnailFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/*' })
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setThumbnailFile(selectedFile)
}
}
const updateMetadataFileIndex = (index: number) => {
setMetadataFileArrayIndex(index)
setRefreshMetadata((prev) => !prev)
}
const updateMetadataFileArray = (updatedMetadataFile: File) => {
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
}
const updateOpenEditionMinterMetadataFile = (updatedMetadataFile: File) => {
setOpenEditionMinterMetadataFile(updatedMetadataFile)
console.log('Updated Open Edition Minter Metadata File:')
console.log(openEditionMinterMetadataFile)
}
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
useEffect(() => {
try {
const data: OffChainMetadataUploadDetailsDataProps = {
assetFiles: assetFilesArray,
metadataFiles: metadataFilesArray,
thumbnailFile,
isThumbnailCompatible,
uploadService,
nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value,
uploadMethod,
tokenURI: tokenUriState.value
.replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(regex, '')
.trim(),
imageUrl: coverImageUrlState.value
.replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(regex, '')
.trim(),
openEditionMinterMetadataFile,
exportedMetadata,
}
onChange(data)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
}
}, [
assetFilesArray,
metadataFilesArray,
thumbnailFile,
isThumbnailCompatible,
uploadService,
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
tokenUriState.value,
coverImageUrlState.value,
refreshMetadata,
openEditionMinterMetadataFile,
exportedMetadata,
])
useEffect(() => {
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([])
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
if (!importedOffChainMetadataUploadDetails) {
tokenUriState.onChange('')
coverImageUrlState.onChange('')
}
}, [uploadMethod, metadataStorageMethod])
useEffect(() => {
if (importedOffChainMetadataUploadDetails) {
setUploadService(importedOffChainMetadataUploadDetails.uploadService)
nftStorageApiKeyState.onChange(importedOffChainMetadataUploadDetails.nftStorageApiKey || '')
pinataApiKeyState.onChange(importedOffChainMetadataUploadDetails.pinataApiKey || '')
pinataSecretKeyState.onChange(importedOffChainMetadataUploadDetails.pinataSecretKey || '')
setUploadMethod(importedOffChainMetadataUploadDetails.uploadMethod)
tokenUriState.onChange(importedOffChainMetadataUploadDetails.tokenURI || '')
coverImageUrlState.onChange(importedOffChainMetadataUploadDetails.imageUrl || '')
// setOpenEditionMinterMetadataFile(importedOffChainMetadataUploadDetails.openEditionMinterMetadataFile)
}
}, [importedOffChainMetadataUploadDetails])
useEffect(() => {
if (useDefaultApiKey) {
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
} else {
nftStorageApiKeyState.onChange('')
}
}, [useDefaultApiKey])
return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'new'}
className="peer sr-only"
id="inlineRadio2"
name="inlineRadioOptions2"
onClick={() => {
setUploadMethod('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2"
>
Upload asset & metadata
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'existing'}
className="peer sr-only"
id="inlineRadio1"
name="inlineRadioOptions1"
onClick={() => {
setUploadMethod('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1"
>
Use an existing Token URI
</label>
</div>
</div>
<div className="p-3 py-5 pb-8">
<Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column">
<p className="mb-5 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
NFT.Storage
</Anchor>{' '}
or{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
Pinata
</Anchor>{' '}
and upload your asset & metadata manually to get a URI for your token before minting.
</p>
<div>
<Tooltip
backgroundColor="bg-blue-500"
className="mb-2 ml-4"
label="The token URI that points directly to the metadata file stored on IPFS."
placement="top"
>
<TextInput {...tokenUriState} className="ml-4 w-1/2" />
</Tooltip>
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
</div>
</div>
</Conditional>
<Conditional test={uploadMethod === 'new'}>
<div>
<div className="flex flex-col items-center px-8 w-full">
<div className="flex justify-items-start mb-5 w-full font-bold">
<div className="form-check form-check-inline">
<input
checked={uploadService === 'nft-storage'}
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
setUploadService('nft-storage')
}}
type="radio"
value="nft-storage"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Upload using NFT.Storage
</label>
</div>
<div className="ml-2 form-check form-check-inline">
<input
checked={uploadService === 'pinata'}
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
setUploadService('pinata')
}}
type="radio"
value="pinata"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio4"
>
Upload using Pinata
</label>
</div>
</div>
<div className="flex w-full">
<Conditional test={uploadService === 'nft-storage'}>
<div className="flex-col w-full">
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
<div className="flex-row mt-2 w-full form-control">
<label className="cursor-pointer label">
<span className="mr-2 font-bold">Use Default API Key</span>
<input
checked={useDefaultApiKey}
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
onClick={() => {
setUseDefaultApiKey(!useDefaultApiKey)
}}
type="checkbox"
/>
</label>
</div>
</div>
</Conditional>
<Conditional test={uploadService === 'pinata'}>
<TextInput {...pinataApiKeyState} className="w-full" />
<div className="w-[20px]" />
<TextInput {...pinataSecretKeyState} className="w-full" />
</Conditional>
</div>
</div>
<div className="mt-6">
<div className="grid grid-cols-2">
<div className="w-full">
<Conditional
test={
assetFilesArray.length > 0 &&
metadataFilesArray.length > 0 &&
assetFilesArray.length !== metadataFilesArray.length
}
>
<Alert className="mt-4 ml-8 w-3/4" type="warning">
The number of assets and metadata files should match.
</Alert>
</Conditional>
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFiles"
>
Asset Selection
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*, audio/*, video/*, .html, .pdf"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="assetFiles"
onChange={selectAssets}
ref={assetFilesRef}
type="file"
/>
</div>
</div>
<Conditional test={isThumbnailCompatible}>
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="thumbnailFiles"
>
Thumbnail Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="thumbnailFiles"
onChange={selectThumbnail}
ref={thumbnailFilesRef}
type="file"
/>
</div>
</div>
</Conditional>
{assetFilesArray.length > 0 && (
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="metadataFiles"
>
Metadata Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="application/json"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="metadataFiles"
onChange={selectMetadata}
ref={metadataFilesRef}
type="file"
/>
</div>
</div>
)}
<Conditional test={assetFilesArray.length >= 1}>
<MetadataModal
assetFile={assetFilesArray[metadataFileArrayIndex]}
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
refresher={refreshMetadata}
updateMetadata={updateMetadataFileArray}
/>
</Conditional>
</div>
<SingleAssetPreview
relatedAsset={assetFilesArray[0]}
subtitle={`Asset filename: ${assetFilesArray[0]?.name}`}
updateMetadataFileIndex={updateMetadataFileIndex}
/>
</div>
<MetadataInput
importedMetadata={importedOffChainMetadataUploadDetails?.exportedMetadata}
onChange={setExportedMetadata}
selectedAssetFile={assetFilesArray[0]}
selectedMetadataFile={metadataFilesArray[0]}
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}
/>
</div>
</div>
</Conditional>
</div>
</div>
)
}

View File

@ -0,0 +1,285 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { useInputState } from 'components/forms/FormInput.hooks'
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import type { Trait } from 'contracts/badgeHub'
import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { TextInput } from '../forms/FormInput'
import { MetadataAttributes } from '../forms/MetadataAttributes'
import { Tooltip } from '../Tooltip'
import type { UploadMethod } from './ImageUploadDetails'
interface OnChainMetadataInputDetailsProps {
onChange: (data: OnChainMetadataInputDetailsDataProps) => void
uploadMethod: UploadMethod | undefined
importedOnChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
}
export interface OnChainMetadataInputDetailsDataProps {
name?: string
description?: string
attributes?: Trait[]
image_data?: string
external_url?: string
background_color?: string
animation_url?: string
youtube_url?: string
}
export const OnChainMetadataInputDetails = ({
onChange,
uploadMethod,
importedOnChainMetadataInputDetails,
}: OnChainMetadataInputDetailsProps) => {
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [metadataFile, setMetadataFile] = useState<File>()
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
const metadataFileRef = useRef<HTMLInputElement | null>(null)
const nameState = useInputState({
id: 'name',
name: 'name',
title: 'Name',
placeholder: 'My Awesome Collection',
})
const descriptionState = useInputState({
id: 'description',
name: 'description',
title: 'Description',
placeholder: 'My Awesome Collection Description',
})
const imageDataState = useInputState({
id: 'metadata-image-data',
name: 'metadata-image-data',
title: 'Image Data',
subtitle: 'Raw SVG image data',
})
const externalUrlState = useInputState({
id: 'metadata-external-url',
name: 'metadata-external-url',
title: 'External URL',
subtitle: 'External URL for the token',
placeholder: 'https://',
})
const attributesState = useMetadataAttributesState()
const animationUrlState = useInputState({
id: 'metadata-animation-url',
name: 'metadata-animation-url',
title: 'Animation URL',
subtitle: 'Animation URL for the token',
placeholder: 'https://',
})
const youtubeUrlState = useInputState({
id: 'metadata-youtube-url',
name: 'metadata-youtube-url',
title: 'YouTube URL',
subtitle: 'YouTube URL for the token',
placeholder: 'https://',
})
const parseMetadata = async () => {
try {
let parsedMetadata: any
if (metadataFile) {
attributesState.reset()
parsedMetadata = JSON.parse(await metadataFile.text())
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
attributesState.add({
trait_type: '',
value: '',
})
} else {
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
attributesState.add({
trait_type: parsedMetadata.attributes[i].trait_type,
value: parsedMetadata.attributes[i].value,
})
}
}
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
} else {
attributesState.reset()
nameState.onChange('')
descriptionState.onChange('')
externalUrlState.onChange('')
youtubeUrlState.onChange('')
animationUrlState.onChange('')
imageDataState.onChange('')
}
} catch (error) {
toast.error('Error parsing metadata file: Invalid JSON format.')
if (metadataFileRef.current) metadataFileRef.current.value = ''
setMetadataFile(undefined)
}
}
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
type: 'application/json',
})
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setMetadataFile(selectedFile)
}
}
useEffect(() => {
void parseMetadata()
if (!metadataFile)
attributesState.add({
trait_type: '',
value: '',
})
}, [metadataFile])
useEffect(() => {
try {
const data: OnChainMetadataInputDetailsDataProps = {
name: nameState.value || undefined,
description: descriptionState.value || undefined,
attributes:
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
? attributesState.values
.map((attr) => ({
trait_type: attr.trait_type,
value: attr.value,
}))
.filter((attr) => attr.trait_type && attr.value)
: undefined,
image_data: imageDataState.value || undefined,
external_url: externalUrlState.value || undefined,
animation_url: animationUrlState.value.trim() || undefined,
youtube_url: youtubeUrlState.value || undefined,
}
onChange(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
nameState.value,
descriptionState.value,
timestamp,
imageDataState.value,
externalUrlState.value,
attributesState.values,
animationUrlState.value,
youtubeUrlState.value,
])
useEffect(() => {
if (importedOnChainMetadataInputDetails) {
nameState.onChange(importedOnChainMetadataInputDetails.name || '')
descriptionState.onChange(importedOnChainMetadataInputDetails.description || '')
externalUrlState.onChange(importedOnChainMetadataInputDetails.external_url || '')
youtubeUrlState.onChange(importedOnChainMetadataInputDetails.youtube_url || '')
animationUrlState.onChange(importedOnChainMetadataInputDetails.animation_url || '')
imageDataState.onChange(importedOnChainMetadataInputDetails.image_data || '')
if (importedOnChainMetadataInputDetails.attributes) {
attributesState.reset()
importedOnChainMetadataInputDetails.attributes.forEach((attr) => {
attributesState.add({
trait_type: attr.trait_type,
value: attr.value,
})
})
}
}
}, [importedOnChainMetadataInputDetails])
return (
<div className="py-3 px-8 rounded border-2 border-white/20">
<span className="ml-4 text-xl font-bold underline underline-offset-4">NFT Metadata</span>
<div className={clsx('grid grid-cols-2 mt-4 mb-2 ml-4 max-w-6xl')}>
<div className={clsx('mt-6')}>
<TextInput className="mt-2" {...nameState} />
<TextInput className="mt-2" {...descriptionState} />
<TextInput className="mt-2" {...externalUrlState} />
<Conditional test={uploadMethod === 'existing'}>
<TextInput className="mt-2" {...animationUrlState} />
</Conditional>
<TextInput className="mt-2" {...youtubeUrlState} />
</div>
<div className={clsx('ml-10')}>
<div>
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
title="Traits"
/>
</div>
<div className="w-full">
<Tooltip
backgroundColor="bg-blue-500"
label="A metadata file can be selected to automatically fill in the related fields."
placement="bottom"
>
<div>
<label
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFile"
>
Metadata File Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="application/json"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="metadataFile"
onChange={selectMetadata}
ref={metadataFileRef}
type="file"
/>
</div>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,127 @@
import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
import { useWallet } from 'utils/wallet'
import { NumberInput, TextInput } from '../forms/FormInput'
interface RoyaltyDetailsProps {
onChange: (data: RoyaltyDetailsDataProps) => void
importedRoyaltyDetails?: RoyaltyDetailsDataProps
}
export interface RoyaltyDetailsDataProps {
royaltyType: RoyaltyState
paymentAddress: string
share: number
}
type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
const [royaltyDetailsImported, setRoyaltyDetailsImported] = useState(false)
const royaltyPaymentAddressState = useInputState({
id: 'royalty-payment-address',
name: 'royaltyPaymentAddress',
title: 'Payment Address',
subtitle: 'Address to receive royalties',
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
})
const royaltyShareState = useInputState({
id: 'royalty-share',
name: 'royaltyShare',
title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid',
placeholder: '5%',
})
useEffect(() => {
if (!importedRoyaltyDetails || (importedRoyaltyDetails && royaltyDetailsImported)) {
void resolveAddress(
royaltyPaymentAddressState.value
.toLowerCase()
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(/ /g, ''),
wallet,
).then((royaltyPaymentAddress) => {
royaltyPaymentAddressState.onChange(royaltyPaymentAddress)
const data: RoyaltyDetailsDataProps = {
royaltyType: royaltyState,
paymentAddress: royaltyPaymentAddressState.value,
share: Number(royaltyShareState.value),
}
onChange(data)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
useEffect(() => {
if (importedRoyaltyDetails) {
setRoyaltyState(importedRoyaltyDetails.royaltyType)
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress.toString())
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
setRoyaltyDetailsImported(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedRoyaltyDetails])
return (
<div className="py-3 px-8 mx-10 rounded border-2 border-white/20">
<div className="flex justify-center">
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={royaltyState === 'none'}
className="peer sr-only"
id="royaltyRadio1"
name="royaltyRadioOptions1"
onClick={() => {
setRoyaltyState('none')
}}
type="radio"
value="None"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="royaltyRadio1"
>
No royalty
</label>
</div>
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={royaltyState === 'new'}
className="peer sr-only"
id="royaltyRadio2"
name="royaltyRadioOptions2"
onClick={() => {
setRoyaltyState('new')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="royaltyRadio2"
>
Configure royalty details
</label>
</div>
</div>
<Conditional test={royaltyState === 'new'}>
<FormGroup subtitle="Information about royalty" title="Royalty Details">
<TextInput {...royaltyPaymentAddressState} isRequired />
<NumberInput {...royaltyShareState} isRequired />
</FormGroup>
</Conditional>
</div>
)
}

View File

@ -0,0 +1,528 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable no-nested-ternary */
import { Button } from 'components/Button'
import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import type { TokenInfo } from 'config/token'
import { useGlobalSettings } from 'contexts/globalSettings'
import React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
import { Conditional } from '../Conditional'
import { AddressInput, NumberInput } from '../forms/FormInput'
import { JsonPreview } from '../JsonPreview'
import { WhitelistUpload } from '../WhitelistUpload'
interface WhitelistDetailsProps {
onChange: (data: WhitelistDetailsDataProps) => void
mintingTokenFromFactory?: TokenInfo
importedWhitelistDetails?: WhitelistDetailsDataProps
}
export interface WhitelistDetailsDataProps {
whitelistState: WhitelistState
whitelistType: WhitelistType
contractAddress?: string
members?: string[] | WhitelistFlexMember[]
unitPrice?: string
startTime?: string
endTime?: string
perAddressLimit?: number
memberLimit?: number
admins?: string[]
adminsMutable?: boolean
}
type WhitelistState = 'none' | 'existing' | 'new'
export type WhitelistType = 'standard' | 'flex' | 'merkletree'
export const WhitelistDetails = ({
onChange,
mintingTokenFromFactory,
importedWhitelistDetails,
}: WhitelistDetailsProps) => {
const wallet = useWallet()
const { timezone } = useGlobalSettings()
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
const [startDate, setStartDate] = useState<Date | undefined>(undefined)
const [endDate, setEndDate] = useState<Date | undefined>(undefined)
const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
const [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState<string[]>([])
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
const whitelistAddressState = useInputState({
id: 'whitelist-address',
name: 'whitelistAddress',
title: 'Whitelist Address',
defaultValue: '',
})
const unitPriceState = useNumberInputState({
id: 'unit-price',
name: 'unitPrice',
title: 'Unit Price',
subtitle: `Token price for whitelisted addresses \n (min. 0 ${
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '25',
})
const memberLimitState = useNumberInputState({
id: 'member-limit',
name: 'memberLimit',
title: 'Member Limit',
subtitle: 'Maximum number of whitelisted addresses',
placeholder: '1000',
})
const perAddressLimitState = useNumberInputState({
id: 'per-address-limit',
name: 'perAddressLimit',
title: 'Per Address Limit',
subtitle: 'Maximum number of tokens per whitelisted address',
placeholder: '5',
})
const addressListState = useAddressListState()
const whitelistFileOnChange = (data: string[]) => {
if (whitelistType === 'standard') setWhitelistStandardArray(data)
if (whitelistType === 'merkletree') setWhitelistMerkleTreeArray(data)
}
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
setWhitelistFlexArray(whitelistData)
}
const downloadSampleWhitelistFlexFile = () => {
const csvData =
'address,mint_count\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
const blob = new Blob([csvData], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'sample_whitelist_flex.csv')
a.click()
}
const downloadSampleWhitelistFile = () => {
const txtData =
'stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3'
const blob = new Blob([txtData], { type: 'text/txt' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'sample_whitelist.txt')
a.click()
}
useEffect(() => {
if (!importedWhitelistDetails) {
setWhitelistStandardArray([])
setWhitelistFlexArray([])
setWhitelistMerkleTreeArray([])
}
}, [whitelistType])
useEffect(() => {
const data: WhitelistDetailsDataProps = {
whitelistState,
whitelistType,
contractAddress: whitelistAddressState.value
.toLowerCase()
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(/ /g, ''),
members:
whitelistType === 'standard'
? whitelistStandardArray
: whitelistType === 'merkletree'
? whitelistMerkleTreeArray
: whitelistFlexArray,
unitPrice: unitPriceState.value
? (Number(unitPriceState.value) * 1_000_000).toString()
: unitPriceState.value === 0
? '0'
: undefined,
startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '',
endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '',
perAddressLimit: perAddressLimitState.value,
memberLimit: memberLimitState.value,
admins: [
...new Set(
addressListState.values
.map((a) => a.address.trim())
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')),
),
],
adminsMutable,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
whitelistAddressState.value,
unitPriceState.value,
memberLimitState.value,
perAddressLimitState.value,
startDate,
endDate,
whitelistStandardArray,
whitelistFlexArray,
whitelistMerkleTreeArray,
whitelistState,
whitelistType,
addressListState.values,
adminsMutable,
])
// make the necessary changes with respect to imported whitelist details
useEffect(() => {
if (importedWhitelistDetails) {
setWhitelistState(importedWhitelistDetails.whitelistState)
setWhitelistType(importedWhitelistDetails.whitelistType)
whitelistAddressState.onChange(
importedWhitelistDetails.contractAddress ? importedWhitelistDetails.contractAddress : '',
)
unitPriceState.onChange(
importedWhitelistDetails.unitPrice ? Number(importedWhitelistDetails.unitPrice) / 1000000 : 0,
)
memberLimitState.onChange(importedWhitelistDetails.memberLimit ? importedWhitelistDetails.memberLimit : 0)
perAddressLimitState.onChange(
importedWhitelistDetails.perAddressLimit ? importedWhitelistDetails.perAddressLimit : 0,
)
setStartDate(
importedWhitelistDetails.startTime
? new Date(Number(importedWhitelistDetails.startTime) / 1_000_000)
: undefined,
)
setEndDate(
importedWhitelistDetails.endTime ? new Date(Number(importedWhitelistDetails.endTime) / 1_000_000) : undefined,
)
setAdminsMutable(importedWhitelistDetails.adminsMutable ? importedWhitelistDetails.adminsMutable : true)
importedWhitelistDetails.admins?.forEach((admin) => {
addressListState.reset()
addressListState.add({ address: admin })
})
if (importedWhitelistDetails.whitelistType === 'standard') {
setWhitelistStandardArray([])
importedWhitelistDetails.members?.forEach((member) => {
setWhitelistStandardArray((standardArray) => [...standardArray, member as string])
})
} else if (importedWhitelistDetails.whitelistType === 'merkletree') {
setWhitelistMerkleTreeArray([])
// importedWhitelistDetails.members?.forEach((member) => {
// setWhitelistMerkleTreeArray((merkleTreeArray) => [...merkleTreeArray, member as string])
// })
} else if (importedWhitelistDetails.whitelistType === 'flex') {
setWhitelistFlexArray([])
importedWhitelistDetails.members?.forEach((member) => {
setWhitelistFlexArray((flexArray) => [
...flexArray,
{
address: (member as WhitelistFlexMember).address,
mint_count: (member as WhitelistFlexMember).mint_count,
},
])
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedWhitelistDetails])
useEffect(() => {
if (whitelistState === 'new' && wallet.address) {
addressListState.reset()
addressListState.add({ address: wallet.address })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [whitelistState, wallet.address])
return (
<div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center">
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={whitelistState === 'none'}
className="peer sr-only"
id="whitelistRadio1"
name="whitelistRadioOptions1"
onClick={() => {
setWhitelistState('none')
setWhitelistType('standard')
}}
type="radio"
value="None"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="whitelistRadio1"
>
No whitelist
</label>
</div>
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={whitelistState === 'existing'}
className="peer sr-only"
id="whitelistRadio2"
name="whitelistRadioOptions2"
onClick={() => {
setWhitelistState('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="whitelistRadio2"
>
Existing whitelist
</label>
</div>
<div className="ml-4 font-bold form-check form-check-inline">
<input
checked={whitelistState === 'new'}
className="peer sr-only"
id="whitelistRadio3"
name="whitelistRadioOptions3"
onClick={() => {
setWhitelistState('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="whitelistRadio3"
>
New whitelist
</label>
</div>
</div>
<Conditional test={whitelistState === 'existing'}>
<AddressInput {...whitelistAddressState} className="pb-5" isRequired />
</Conditional>
<Conditional test={whitelistState === 'new'}>
<div className="flex justify-between mb-5 ml-6 max-w-[300px] text-lg font-bold">
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'standard'}
className="peer sr-only"
id="inlineRadio7"
name="inlineRadioOptions7"
onClick={() => {
setWhitelistType('standard')
}}
type="radio"
value="standard"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio7"
>
Standard Whitelist
</label>
</div>
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'flex'}
className="peer sr-only"
id="inlineRadio8"
name="inlineRadioOptions8"
onClick={() => {
setWhitelistType('flex')
}}
type="radio"
value="flex"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio8"
>
Whitelist Flex
</label>
</div>
{/* <div className="form-check form-check-inline">
<input
checked={whitelistType === 'merkletree'}
className="peer sr-only"
id="inlineRadio9"
name="inlineRadioOptions9"
onClick={() => {
setWhitelistType('merkletree')
}}
type="radio"
value="merkletree"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio9"
>
Whitelist Merkle Tree
</label>
</div> */}
</div>
<div className="grid grid-cols-2">
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
<NumberInput isRequired {...unitPriceState} />
<Conditional test={whitelistType !== 'merkletree'}>
<NumberInput isRequired {...memberLimitState} />
</Conditional>
<Conditional test={whitelistType === 'standard' || whitelistType === 'merkletree'}>
<NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<FormControl
htmlId="start-date"
isRequired
subtitle="Start time for minting tokens to whitelisted addresses"
title={`Whitelist Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setStartDate(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setStartDate(undefined)
}
value={
timezone === 'Local'
? startDate
: startDate
? new Date(startDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
<FormControl
htmlId="end-date"
isRequired
subtitle="Whitelist End Time dictates when public sales will start"
title={`Whitelist End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
>
<InputDateTime
minDate={
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
}
onChange={(date) =>
date
? setEndDate(
timezone === 'Local'
? date
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
)
: setEndDate(undefined)
}
value={
timezone === 'Local'
? endDate
: endDate
? new Date(endDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
: undefined
}
/>
</FormControl>
</FormGroup>
<div>
<div className="mt-2 ml-3 w-[65%] form-control">
<label className="justify-start cursor-pointer label">
<span className="mr-4 font-bold">Mutable Administrator Addresses</span>
<input
checked={adminsMutable}
className={`toggle ${adminsMutable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setAdminsMutable(!adminsMutable)}
type="checkbox"
/>
</label>
</div>
<div className="my-4 ml-4">
<AddressList
entries={addressListState.entries}
onAdd={addressListState.add}
onChange={addressListState.update}
onRemove={addressListState.remove}
subtitle="The list of administrator addresses"
title="Administrator Addresses"
/>
</div>
<Conditional test={whitelistType === 'standard'}>
<FormGroup
subtitle={
<div>
<span>TXT file that contains the whitelisted addresses</span>
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
Download Sample File
</Button>
</div>
}
title="Whitelist File"
>
<WhitelistUpload onChange={whitelistFileOnChange} />
</FormGroup>
<Conditional test={whitelistStandardArray.length > 0}>
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
</Conditional>
</Conditional>
<Conditional test={whitelistType === 'flex'}>
<FormGroup
subtitle={
<div>
<span>CSV file that contains the whitelisted addresses and corresponding mint counts</span>
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFlexFile}>
Download Sample File
</Button>
</div>
}
title="Whitelist File"
>
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
</FormGroup>
<Conditional test={whitelistFlexArray.length > 0}>
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
</Conditional>
</Conditional>
<Conditional test={whitelistType === 'merkletree'}>
<FormGroup
subtitle={
<div>
<span>TXT file that contains the whitelisted addresses</span>
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
Download Sample File
</Button>
</div>
}
title="Whitelist File"
>
<WhitelistUpload onChange={whitelistFileOnChange} />
</FormGroup>
<Conditional test={whitelistStandardArray.length > 0}>
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
</Conditional>
</Conditional>
</div>
</div>
</Conditional>
</div>
)
}

324
config/authz.ts Normal file
View File

@ -0,0 +1,324 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable camelcase */
import { GenericAuthorization } from 'cosmjs-types/cosmos/authz/v1beta1/authz'
import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx'
import { SendAuthorization } from 'cosmjs-types/cosmos/bank/v1beta1/authz'
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'
import type { AuthorizationType } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
import { StakeAuthorization, StakeAuthorization_Validators } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
import {
AcceptedMessageKeysFilter,
AllowAllMessagesFilter,
CombinedLimit,
ContractExecutionAuthorization,
ContractMigrationAuthorization,
MaxCallsLimit,
MaxFundsLimit,
} from 'cosmjs-types/cosmwasm/wasm/v1/authz'
import type { AuthorizationMode, GenericAuthorizationType, GrantAuthorizationType } from 'pages/authz/grant'
export interface Msg {
typeUrl: string
value: any
}
export interface AuthzMessage {
authzMode: AuthorizationMode
authzType: GrantAuthorizationType
displayName: string
typeUrl: string
genericAuthzType?: GenericAuthorizationType
}
export const grantGenericStakeAuthorization: AuthzMessage = {
authzMode: 'Grant',
authzType: 'Generic',
displayName: 'Stake',
typeUrl: '/cosmos.staking.v1beta1.MsgDelegate',
genericAuthzType: 'MsgDelegate',
}
export const grantGenericSendAuthorization: AuthzMessage = {
authzMode: 'Grant',
authzType: 'Generic',
displayName: 'Send',
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
genericAuthzType: 'MsgSend',
}
export const authzMessages: AuthzMessage[] = [grantGenericStakeAuthorization, grantGenericSendAuthorization]
const msgAuthzGrantTypeUrl = '/cosmos.authz.v1beta1.MsgGrant'
export function AuthzSendGrantMsg(
granter: string,
grantee: string,
denom: string,
spendLimit: number,
expiration: number,
allowList?: string[],
): Msg {
const sendAuthValue = SendAuthorization.encode(
SendAuthorization.fromPartial({
spendLimit: [
Coin.fromPartial({
amount: String(spendLimit),
denom,
}),
],
// Needs cosmos-sdk >= 0.47
// allowList,
}),
).finish()
const grantValue = MsgGrant.fromPartial({
grant: {
authorization: {
typeUrl: '/cosmos.bank.v1beta1.SendAuthorization',
value: sendAuthValue,
},
expiration: expiration ? { seconds: BigInt(expiration) } : undefined,
},
grantee,
granter,
})
return {
typeUrl: msgAuthzGrantTypeUrl,
value: grantValue,
}
}
export function AuthzExecuteContractGrantMsg(
granter: string,
grantee: string,
contract: string,
expiration: number,
callsRemaining?: number,
amounts?: Coin[],
allowedMessages?: string[],
): Msg {
const sendAuthValue = ContractExecutionAuthorization.encode(
ContractExecutionAuthorization.fromPartial({
grants: [
{
contract,
filter: {
typeUrl: allowedMessages
? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter'
: '/cosmwasm.wasm.v1.AllowAllMessagesFilter',
value: allowedMessages
? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish()
: AllowAllMessagesFilter.encode({}).finish(),
},
limit:
callsRemaining || amounts
? {
typeUrl:
callsRemaining && amounts
? '/cosmwasm.wasm.v1.CombinedLimit'
: callsRemaining
? '/cosmwasm.wasm.v1.MaxCallsLimit'
: '/cosmwasm.wasm.v1.MaxFundsLimit',
value:
callsRemaining && amounts
? CombinedLimit.encode({
callsRemaining: BigInt(callsRemaining),
amounts,
}).finish()
: callsRemaining
? MaxCallsLimit.encode({
remaining: BigInt(callsRemaining),
}).finish()
: MaxFundsLimit.encode({
amounts: amounts || [],
}).finish(),
}
: {
// limit: undefined is not accepted
typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit',
value: MaxCallsLimit.encode({
remaining: BigInt(100000),
}).finish(),
},
},
],
}),
).finish()
const grantValue = MsgGrant.fromPartial({
grant: {
authorization: {
typeUrl: '/cosmwasm.wasm.v1.ContractExecutionAuthorization',
value: sendAuthValue,
},
expiration: expiration ? { seconds: BigInt(expiration), nanos: 0 } : undefined,
},
grantee,
granter,
})
return {
typeUrl: msgAuthzGrantTypeUrl,
value: grantValue,
}
}
export function AuthzMigrateContractGrantMsg(
granter: string,
grantee: string,
contract: string,
expiration: number,
callsRemaining?: number,
amounts?: Coin[],
allowedMessages?: string[],
): Msg {
const sendAuthValue = ContractMigrationAuthorization.encode(
ContractMigrationAuthorization.fromPartial({
grants: [
{
contract,
filter: {
typeUrl: allowedMessages
? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter'
: '/cosmwasm.wasm.v1.AllowAllMessagesFilter',
value: allowedMessages
? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish()
: AllowAllMessagesFilter.encode({}).finish(),
},
limit:
callsRemaining || amounts
? {
typeUrl:
callsRemaining && amounts
? '/cosmwasm.wasm.v1.CombinedLimit'
: callsRemaining
? '/cosmwasm.wasm.v1.MaxCallsLimit'
: '/cosmwasm.wasm.v1.MaxFundsLimit',
value:
callsRemaining && amounts
? CombinedLimit.encode({
callsRemaining: BigInt(callsRemaining),
amounts,
}).finish()
: callsRemaining
? MaxCallsLimit.encode({
remaining: BigInt(callsRemaining),
}).finish()
: MaxFundsLimit.encode({
amounts: amounts || [],
}).finish(),
}
: {
// limit: undefined is not accepted
typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit',
value: MaxCallsLimit.encode({
remaining: BigInt(100000),
}).finish(),
},
},
],
}),
).finish()
const grantValue = MsgGrant.fromPartial({
grant: {
authorization: {
typeUrl: '/cosmwasm.wasm.v1.ContractMigrationAuthorization',
value: sendAuthValue,
},
expiration: expiration ? { seconds: BigInt(expiration), nanos: 0 } : undefined,
},
grantee,
granter,
})
return {
typeUrl: msgAuthzGrantTypeUrl,
value: grantValue,
}
}
export function AuthzGenericGrantMsg(granter: string, grantee: string, typeURL: string, expiration: number): Msg {
return {
typeUrl: msgAuthzGrantTypeUrl,
value: {
grant: {
authorization: {
typeUrl: '/cosmos.authz.v1beta1.GenericAuthorization',
value: GenericAuthorization.encode(
GenericAuthorization.fromPartial({
msg: typeURL,
}),
).finish(),
},
expiration: expiration ? { seconds: expiration } : undefined,
},
grantee,
granter,
},
}
}
export function AuthzStakeGrantMsg({
expiration,
grantee,
granter,
allowList,
denyList,
maxTokens,
denom,
stakeAuthzType,
}: {
granter: string
grantee: string
expiration: number
allowList?: string[]
denyList?: string[]
maxTokens?: string
denom?: string
stakeAuthzType: AuthorizationType
}): Msg {
const allow_list = StakeAuthorization_Validators.encode(
StakeAuthorization_Validators.fromPartial({
address: allowList,
}),
).finish()
const deny_list = StakeAuthorization_Validators.encode(
StakeAuthorization_Validators.fromPartial({
address: denyList,
}),
).finish()
const stakeAuthValue = StakeAuthorization.encode(
StakeAuthorization.fromPartial({
authorizationType: stakeAuthzType,
allowList: allowList?.length ? StakeAuthorization_Validators.decode(allow_list) : undefined,
denyList: denyList?.length ? StakeAuthorization_Validators.decode(deny_list) : undefined,
maxTokens: maxTokens
? Coin.fromPartial({
amount: maxTokens,
denom,
})
: undefined,
}),
).finish()
const grantValue = MsgGrant.fromPartial({
grant: {
authorization: {
typeUrl: '/cosmos.staking.v1beta1.StakeAuthorization',
value: stakeAuthValue,
},
expiration: { seconds: BigInt(expiration) },
},
grantee,
granter,
})
return {
typeUrl: msgAuthzGrantTypeUrl,
value: grantValue,
}
}

View File

@ -1,9 +1,9 @@
{ {
"path": "/assets/", "path": "/assets/",
"appName": "StargazeStudio", "appName": "Stargaze Studio",
"appShortName": "StargazeStudio", "appShortName": "Stargaze Studio",
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.", "appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT collection in no time.",
"developerName": "StargazeStudio", "developerName": "Stargaze Studio",
"developerURL": "https://", "developerURL": "https://",
"background": "#FFC27D", "background": "#FFC27D",
"theme_color": "#FFC27D", "theme_color": "#FFC27D",

View File

@ -1,3 +1,2 @@
export * from './app' export * from './app'
export * from './keplr'
export * from './network' export * from './network'

View File

@ -1,76 +0,0 @@
import type { ChainInfo } from '@keplr-wallet/types'
import type { AppConfig } from './app'
export interface KeplrCoin {
readonly coinDenom: string
readonly coinMinimalDenom: string
readonly coinDecimals: number
}
export interface KeplrConfig {
readonly chainId: string
readonly chainName: string
readonly rpc: string
readonly rest?: string
readonly bech32Config: {
readonly bech32PrefixAccAddr: string
readonly bech32PrefixAccPub: string
readonly bech32PrefixValAddr: string
readonly bech32PrefixValPub: string
readonly bech32PrefixConsAddr: string
readonly bech32PrefixConsPub: string
}
readonly currencies: readonly KeplrCoin[]
readonly feeCurrencies: readonly KeplrCoin[]
readonly stakeCurrency: KeplrCoin
readonly gasPriceStep: {
readonly low: number
readonly average: number
readonly high: number
}
readonly bip44: { readonly coinType: number }
readonly coinType: number
}
export const keplrConfig = (config: AppConfig): ChainInfo => ({
chainId: config.chainId,
chainName: config.chainName,
rpc: config.rpcUrl,
rest: config.httpUrl!,
bech32Config: {
bech32PrefixAccAddr: `${config.addressPrefix}`,
bech32PrefixAccPub: `${config.addressPrefix}pub`,
bech32PrefixValAddr: `${config.addressPrefix}valoper`,
bech32PrefixValPub: `${config.addressPrefix}valoperpub`,
bech32PrefixConsAddr: `${config.addressPrefix}valcons`,
bech32PrefixConsPub: `${config.addressPrefix}valconspub`,
},
currencies: [
{
coinDenom: config.coinMap[config.feeToken].denom,
coinMinimalDenom: config.feeToken,
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
},
],
feeCurrencies: [
{
coinDenom: config.coinMap[config.feeToken].denom,
coinMinimalDenom: config.feeToken,
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
},
],
stakeCurrency: {
coinDenom: config.coinMap[config.stakingToken].denom,
coinMinimalDenom: config.stakingToken,
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
},
gasPriceStep: {
low: config.gasPrice / 2,
average: config.gasPrice,
high: config.gasPrice * 2,
},
bip44: { coinType: 118 },
coinType: 118,
features: ['ibc-transfer', 'cosmwasm', 'ibc-go'],
})

View File

@ -6,6 +6,6 @@ export const meta = {
domain: 'stargaze.tools', domain: 'stargaze.tools',
url: faviconsJson.developerURL, url: faviconsJson.developerURL,
twitter: { twitter: {
username: '@stargazestudio', username: '@StargazeZone',
}, },
} }

844
config/minter.ts Normal file
View File

@ -0,0 +1,844 @@
import {
FEATURED_IBC_TIA_FACTORY_ADDRESS,
FEATURED_IBC_USDC_FACTORY_ADDRESS,
FEATURED_VENDING_FACTORY_ADDRESS,
FEATURED_VENDING_FACTORY_FLEX_ADDRESS,
FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS,
FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
OPEN_EDITION_FACTORY_ADDRESS,
OPEN_EDITION_FACTORY_FLEX_ADDRESS,
OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
OPEN_EDITION_IBC_ATOM_FACTORY_FLEX_ADDRESS,
OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS,
OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS,
OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS,
OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS,
OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS,
OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS,
OPEN_EDITION_IBC_USK_FACTORY_ADDRESS,
OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS,
OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS,
VENDING_FACTORY_ADDRESS,
VENDING_FACTORY_FLEX_ADDRESS,
VENDING_FACTORY_MERKLE_TREE_ADDRESS,
VENDING_FACTORY_UPDATABLE_ADDRESS,
VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
VENDING_IBC_ATOM_FACTORY_ADDRESS,
VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS,
VENDING_IBC_CRBRUS_FACTORY_ADDRESS,
VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS,
VENDING_IBC_KUJI_FACTORY_ADDRESS,
VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS,
VENDING_IBC_NBTC_FACTORY_ADDRESS,
VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS,
VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS,
VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS,
VENDING_IBC_TIA_FACTORY_ADDRESS,
VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS,
VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS,
VENDING_IBC_USDC_FACTORY_ADDRESS,
VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
VENDING_IBC_USK_FACTORY_ADDRESS,
VENDING_IBC_USK_FACTORY_FLEX_ADDRESS,
VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS,
VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS,
VENDING_NATIVE_BRNCH_FACTORY_ADDRESS,
VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS,
VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS,
VENDING_NATIVE_STARDUST_FACTORY_ADDRESS,
VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS,
VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS,
} from 'utils/constants'
import type { TokenInfo } from './token'
import {
ibcAtom,
ibcCrbrus,
ibcFrnz,
// ibcHuahua,
ibcKuji,
ibcNbtc,
ibcTia,
ibcUsdc,
ibcUsk,
nativeBrnch,
nativeStardust,
stars,
} from './token'
export interface MinterInfo {
id: string
factoryAddress: string
supportedToken: TokenInfo
updatable?: boolean
flexible?: boolean
merkleTree?: boolean
featured?: boolean
}
export const openEditionStarsMinter: MinterInfo = {
id: 'open-edition-stars-minter',
factoryAddress: OPEN_EDITION_FACTORY_ADDRESS,
supportedToken: stars,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionUpdatableStarsMinter: MinterInfo = {
id: 'open-edition-updatable-stars-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
supportedToken: stars,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionIbcAtomMinter: MinterInfo = {
id: 'open-edition-ibc-atom-minter',
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionUpdatableIbcAtomMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-atom-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionIbcUsdcMinter: MinterInfo = {
id: 'open-edition-ibc-usdc-minter',
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionIbcTiaMinter: MinterInfo = {
id: 'open-edition-ibc-tia-minter',
factoryAddress: OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS,
supportedToken: ibcTia,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionIbcNbtcMinter: MinterInfo = {
id: 'open-edition-ibc-nbtc-minter',
factoryAddress: OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS,
supportedToken: ibcNbtc,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionUpdatableIbcUsdcMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-usdc-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionUpdatableIbcTiaMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-tia-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS,
supportedToken: ibcTia,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionUpdatableIbcNbtcMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-nbtc-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS,
supportedToken: ibcNbtc,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionIbcFrnzMinter: MinterInfo = {
id: 'open-edition-ibc-frnz-minter',
factoryAddress: OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
supportedToken: ibcFrnz,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionUpdatableIbcFrnzMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-frnz-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
supportedToken: ibcFrnz,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionIbcUskMinter: MinterInfo = {
id: 'open-edition-ibc-usk-minter',
factoryAddress: OPEN_EDITION_IBC_USK_FACTORY_ADDRESS,
supportedToken: ibcUsk,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionUpdatableIbcUskMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-usk-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS,
supportedToken: ibcUsk,
updatable: true,
featured: false,
flexible: false,
}
export const openEditionIbcKujiMinter: MinterInfo = {
id: 'open-edition-ibc-kuji-minter',
factoryAddress: OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS,
supportedToken: ibcKuji,
updatable: false,
featured: false,
flexible: false,
}
// export const openEditionIbcHuahuaMinter: MinterInfo = {
// id: 'open-edition-ibc-huahua-minter',
// factoryAddress: OPEN_EDITION_IBC_HUAHUA_FACTORY_ADDRESS,
// supportedToken: ibcHuahua,
// updatable: false,
// featured: false,
// }
export const openEditionIbcCrbrusMinter: MinterInfo = {
id: 'open-edition-ibc-crbrus-minter',
factoryAddress: OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS,
supportedToken: ibcCrbrus,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionNativeStrdstMinter: MinterInfo = {
id: 'open-edition-native-strdst-minter',
factoryAddress: OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS,
supportedToken: nativeStardust,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionNativeBrnchMinter: MinterInfo = {
id: 'open-edition-native-brnch-minter',
factoryAddress: OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS,
supportedToken: nativeBrnch,
updatable: false,
featured: false,
flexible: false,
}
export const openEditionMinterList = [
openEditionStarsMinter,
openEditionUpdatableStarsMinter,
openEditionUpdatableIbcAtomMinter,
openEditionIbcAtomMinter,
openEditionIbcFrnzMinter,
openEditionUpdatableIbcFrnzMinter,
openEditionIbcUsdcMinter,
openEditionUpdatableIbcUsdcMinter,
openEditionIbcTiaMinter,
openEditionUpdatableIbcTiaMinter,
openEditionIbcNbtcMinter,
openEditionUpdatableIbcNbtcMinter,
openEditionIbcUskMinter,
openEditionUpdatableIbcUskMinter,
openEditionIbcKujiMinter,
// openEditionIbcHuahuaMinter,
openEditionIbcCrbrusMinter,
openEditionNativeStrdstMinter,
openEditionNativeBrnchMinter,
]
export const flexibleOpenEditionStarsMinter: MinterInfo = {
id: 'flexible-open-edition-stars-minter',
factoryAddress: OPEN_EDITION_FACTORY_FLEX_ADDRESS,
supportedToken: stars,
updatable: false,
featured: false,
flexible: true,
}
export const flexibleOpenEditionIbcAtomMinter: MinterInfo = {
id: 'flexible-open-edition-ibc-atom-minter',
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_FLEX_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
featured: false,
flexible: true,
}
export const flexibleOpenEditionIbcUsdcMinter: MinterInfo = {
id: 'flexible-open-edition-ibc-usdc-minter',
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
featured: false,
flexible: true,
}
export const flexibleOpenEditionIbcTiaMinter: MinterInfo = {
id: 'flexible-open-edition-ibc-tia-minter',
factoryAddress: OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS,
supportedToken: ibcTia,
updatable: false,
featured: false,
flexible: true,
}
export const flexibleOpenEditionMinterList = [
flexibleOpenEditionStarsMinter,
flexibleOpenEditionIbcAtomMinter,
flexibleOpenEditionIbcUsdcMinter,
flexibleOpenEditionIbcTiaMinter,
]
export const vendingStarsMinter: MinterInfo = {
id: 'vending-stars-minter',
factoryAddress: VENDING_FACTORY_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingFeaturedStarsMinter: MinterInfo = {
id: 'vending-stars-minter',
factoryAddress: FEATURED_VENDING_FACTORY_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: false,
merkleTree: false,
featured: true,
}
export const vendingUpdatableStarsMinter: MinterInfo = {
id: 'vending-updatable-stars-minter',
factoryAddress: VENDING_FACTORY_UPDATABLE_ADDRESS,
supportedToken: stars,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingIbcAtomMinter: MinterInfo = {
id: 'vending-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableIbcAtomMinter: MinterInfo = {
id: 'vending-updatable-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingIbcUsdcMinter: MinterInfo = {
id: 'vending-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingFeaturedIbcUsdcMinter: MinterInfo = {
id: 'vending-featured-ibc-usdc-minter',
factoryAddress: FEATURED_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
flexible: false,
merkleTree: false,
featured: true,
}
export const vendingIbcTiaMinter: MinterInfo = {
id: 'vending-ibc-tia-minter',
factoryAddress: VENDING_IBC_TIA_FACTORY_ADDRESS,
supportedToken: ibcTia,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingFeaturedIbcTiaMinter: MinterInfo = {
id: 'vending-featured-ibc-tia-minter',
factoryAddress: FEATURED_IBC_TIA_FACTORY_ADDRESS,
supportedToken: ibcTia,
updatable: false,
flexible: false,
merkleTree: false,
featured: true,
}
export const vendingIbcNbtcMinter: MinterInfo = {
id: 'vending-ibc-nbtc-minter',
factoryAddress: VENDING_IBC_NBTC_FACTORY_ADDRESS,
supportedToken: ibcNbtc,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableIbcUsdcMinter: MinterInfo = {
id: 'vending-updatable-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableIbcTiaMinter: MinterInfo = {
id: 'vending-updatable-ibc-tia-minter',
factoryAddress: VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcTia,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableIbcNbtcMinter: MinterInfo = {
id: 'vending-updatable-ibc-nbtc-minter',
factoryAddress: VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcNbtc,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingIbcUskMinter: MinterInfo = {
id: 'vending-ibc-usk-minter',
factoryAddress: VENDING_IBC_USK_FACTORY_ADDRESS,
supportedToken: ibcUsk,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableIbcUskMinter: MinterInfo = {
id: 'vending-updatable-ibc-usk-minter',
factoryAddress: VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcUsk,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingIbcKujiMinter: MinterInfo = {
id: 'vending-ibc-kuji-minter',
factoryAddress: VENDING_IBC_KUJI_FACTORY_ADDRESS,
supportedToken: ibcKuji,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
// export const vendingIbcHuahuaMinter: MinterInfo = {
// id: 'vending-ibc-huahua-minter',
// factoryAddress: VENDING_IBC_HUAHUA_FACTORY_ADDRESS,
// supportedToken: ibcHuahua,
// updatable: false,
// flexible: false,
// merkleTree: false,
// featured: false,
// }
export const vendingIbcCrbrusMinter: MinterInfo = {
id: 'vending-ibc-crbrus-minter',
factoryAddress: VENDING_IBC_CRBRUS_FACTORY_ADDRESS,
supportedToken: ibcCrbrus,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingNativeStardustMinter: MinterInfo = {
id: 'vending-native-stardust-minter',
factoryAddress: VENDING_NATIVE_STARDUST_FACTORY_ADDRESS,
supportedToken: nativeStardust,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableNativeStardustMinter: MinterInfo = {
id: 'vending-native-stardust-minter',
factoryAddress: VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS,
supportedToken: nativeStardust,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingNativeBrnchMinter: MinterInfo = {
id: 'vending-native-brnch-minter',
factoryAddress: VENDING_NATIVE_BRNCH_FACTORY_ADDRESS,
supportedToken: nativeBrnch,
updatable: false,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingUpdatableNativeBrnchMinter: MinterInfo = {
id: 'vending-native-brnch-minter',
factoryAddress: VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS,
supportedToken: nativeBrnch,
updatable: true,
flexible: false,
merkleTree: false,
featured: false,
}
export const vendingMinterList = [
vendingStarsMinter,
vendingFeaturedStarsMinter,
vendingUpdatableStarsMinter,
vendingIbcAtomMinter,
vendingUpdatableIbcAtomMinter,
vendingIbcUsdcMinter,
vendingFeaturedIbcUsdcMinter,
vendingUpdatableIbcUsdcMinter,
vendingIbcTiaMinter,
vendingFeaturedIbcTiaMinter,
vendingUpdatableIbcTiaMinter,
vendingIbcNbtcMinter,
vendingUpdatableIbcNbtcMinter,
vendingIbcUskMinter,
vendingUpdatableIbcUskMinter,
vendingIbcKujiMinter,
// vendingIbcHuahuaMinter,
vendingIbcCrbrusMinter,
vendingNativeStardustMinter,
vendingUpdatableNativeStardustMinter,
vendingNativeBrnchMinter,
vendingUpdatableNativeBrnchMinter,
]
export const flexibleVendingStarsMinter: MinterInfo = {
id: 'flexible-vending-stars-minter',
factoryAddress: VENDING_FACTORY_FLEX_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleFeaturedVendingStarsMinter: MinterInfo = {
id: 'flexible-vending-stars-minter',
factoryAddress: FEATURED_VENDING_FACTORY_FLEX_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: true,
merkleTree: false,
featured: true,
}
export const flexibleVendingUpdatableStarsMinter: MinterInfo = {
id: 'flexible-vending-updatable-stars-minter',
factoryAddress: VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
supportedToken: stars,
updatable: true,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingIbcAtomMinter: MinterInfo = {
id: 'flexible-vending-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingUpdatableIbcAtomMinter: MinterInfo = {
id: 'flexible-vending-updatable-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS,
supportedToken: ibcAtom,
updatable: true,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingIbcUsdcMinter: MinterInfo = {
id: 'flexible-vending-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleFeaturedVendingIbcUsdcMinter: MinterInfo = {
id: 'flexible-featured-vending-ibc-usdc-minter',
factoryAddress: FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
flexible: true,
merkleTree: false,
featured: true,
}
export const flexibleVendingIbcTiaMinter: MinterInfo = {
id: 'flexible-vending-ibc-tia-minter',
factoryAddress: VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
supportedToken: ibcTia,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleFeaturedVendingIbcTiaMinter: MinterInfo = {
id: 'flexible-featured-vending-ibc-tia-minter',
factoryAddress: FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
supportedToken: ibcTia,
updatable: false,
flexible: true,
merkleTree: false,
featured: true,
}
export const flexibleVendingIbcNbtcMinter: MinterInfo = {
id: 'flexible-vending-ibc-nbtc-minter',
factoryAddress: VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS,
supportedToken: ibcNbtc,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingUpdatableIbcUsdcMinter: MinterInfo = {
id: 'flexible-vending-updatable-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsdc,
updatable: true,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingUpdatableIbcTiaMinter: MinterInfo = {
id: 'flexible-vending-updatable-ibc-tia-minter',
factoryAddress: VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS,
supportedToken: ibcTia,
updatable: true,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingUpdatableIbcNbtcMinter: MinterInfo = {
id: 'flexible-vending-updatable-ibc-nbtc-minter',
factoryAddress: VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS,
supportedToken: ibcNbtc,
updatable: true,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingIbcUskMinter: MinterInfo = {
id: 'flexible-vending-ibc-usk-minter',
factoryAddress: VENDING_IBC_USK_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsk,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingUpdatableIbcUskMinter: MinterInfo = {
id: 'flexible-vending-updatable-ibc-usk-minter',
factoryAddress: VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsk,
updatable: true,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingIbcKujiMinter: MinterInfo = {
id: 'flexible-vending-ibc-kuji-minter',
factoryAddress: VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS,
supportedToken: ibcKuji,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
// export const flexibleVendingIbcHuahuaMinter: MinterInfo = {
// id: 'flexible-vending-ibc-huahua-minter',
// factoryAddress: VENDING_IBC_HUAHUA_FACTORY_FLEX_ADDRESS,
// supportedToken: ibcHuahua,
// updatable: false,
// flexible: true,
// merkleTree: false,
// featured: false,
// }
export const flexibleVendingIbcCrbrusMinter: MinterInfo = {
id: 'flexible-vending-ibc-crbrus-minter',
factoryAddress: VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS,
supportedToken: ibcCrbrus,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingStrdstMinter: MinterInfo = {
id: 'flexible-vending-native-strdst-minter',
factoryAddress: VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS,
supportedToken: nativeStardust,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingBrnchMinter: MinterInfo = {
id: 'flexible-vending-native-brnch-minter',
factoryAddress: VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS,
supportedToken: nativeBrnch,
updatable: false,
flexible: true,
merkleTree: false,
featured: false,
}
export const flexibleVendingMinterList = [
flexibleVendingStarsMinter,
flexibleFeaturedVendingStarsMinter,
flexibleVendingUpdatableStarsMinter,
flexibleVendingIbcAtomMinter,
flexibleVendingUpdatableIbcAtomMinter,
flexibleVendingIbcUsdcMinter,
flexibleFeaturedVendingIbcUsdcMinter,
flexibleVendingUpdatableIbcUsdcMinter,
flexibleVendingIbcTiaMinter,
flexibleFeaturedVendingIbcTiaMinter,
flexibleVendingUpdatableIbcTiaMinter,
flexibleVendingIbcNbtcMinter,
flexibleVendingUpdatableIbcNbtcMinter,
flexibleVendingIbcUskMinter,
flexibleVendingUpdatableIbcUskMinter,
flexibleVendingIbcKujiMinter,
// flexibleVendingIbcHuahuaMinter,
flexibleVendingIbcCrbrusMinter,
flexibleVendingStrdstMinter,
flexibleVendingBrnchMinter,
]
export const merkleTreeVendingStarsMinter: MinterInfo = {
id: 'merkletree-vending-stars-minter',
factoryAddress: VENDING_FACTORY_MERKLE_TREE_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: false,
merkleTree: true,
featured: false,
}
export const merkleTreeVendingFeaturedStarsMinter: MinterInfo = {
id: 'merkletree-vending-featured-stars-minter',
factoryAddress: FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: false,
merkleTree: true,
featured: true,
}
export const merkleTreeVendingIbcTiaMinter: MinterInfo = {
id: 'merkletree-vending-ibc-tia-minter',
factoryAddress: VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
supportedToken: ibcTia,
updatable: false,
flexible: false,
merkleTree: true,
featured: false,
}
export const merkleTreeVendingFeaturedIbcTiaMinter: MinterInfo = {
id: 'merkletree-vending-featured-ibc-tia-minter',
factoryAddress: FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
supportedToken: ibcTia,
updatable: false,
flexible: false,
merkleTree: true,
featured: true,
}
export const merkleTreeVendingMinterList = [
merkleTreeVendingStarsMinter,
merkleTreeVendingIbcTiaMinter,
merkleTreeVendingFeaturedStarsMinter,
merkleTreeVendingFeaturedIbcTiaMinter,
]

View File

@ -5,6 +5,7 @@ export const mainnetConfig: AppConfig = {
chainName: 'Stargaze', chainName: 'Stargaze',
addressPrefix: 'stars', addressPrefix: 'stars',
rpcUrl: 'https://rpc.stargaze-apis.com/', rpcUrl: 'https://rpc.stargaze-apis.com/',
httpUrl: 'https://rest.stargaze-apis.com/',
feeToken: 'ustars', feeToken: 'ustars',
stakingToken: 'ustars', stakingToken: 'ustars',
coinMap: { coinMap: {

135
config/token.ts Normal file
View File

@ -0,0 +1,135 @@
import { NETWORK } from 'utils/constants'
export interface TokenInfo {
id: string
denom: string
displayName: string
decimalPlaces: number
imageURL?: string
symbol?: string
}
export const stars: TokenInfo = {
id: 'stars',
denom: 'ustars',
displayName: 'STARS',
decimalPlaces: 6,
}
export const ibcAtom: TokenInfo = {
id: 'ibc-atom',
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
displayName: 'ATOM',
decimalPlaces: 6,
}
export const ibcUsdc: TokenInfo = {
id: 'ibc-usdc',
denom:
NETWORK === 'mainnet'
? 'ibc/4A1C18CA7F50544760CF306189B810CE4C1CB156C7FC870143D401FE7280E591'
: 'factory/stars1paqkeyluuw47pflgwwqaaj8y679zj96aatg5a7/uusdc',
displayName: 'USDC',
decimalPlaces: 6,
}
export const ibcUsk: TokenInfo = {
id: 'ibc-usk',
denom:
NETWORK === 'mainnet'
? 'ibc/938CEB62ABCBA6366AA369A8362E310B2A0B1A54835E4F3FF01D69D860959128'
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uusk',
displayName: 'USK',
decimalPlaces: 6,
}
export const ibcKuji: TokenInfo = {
id: 'ibc-kuji',
denom:
NETWORK === 'mainnet'
? 'ibc/0E57658B71E9CC4BB0F6FE3E01712966713B49E6FD292E6B66E3F111B103D361'
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/ukuji',
displayName: 'KUJI',
decimalPlaces: 6,
}
export const ibcFrnz: TokenInfo = {
id: 'ibc-frnz',
denom:
NETWORK === 'mainnet'
? 'ibc/9C40A8368C0E1CAA4144DBDEBBCE2E7A5CC2D128F0A9F785ECB71ECFF575114C'
: 'factory/stars1paqkeyluuw47pflgwwqaaj8y679zj96aatg5a7/ufrienzies',
displayName: 'FRNZ',
decimalPlaces: 6,
}
export const ibcNbtc: TokenInfo = {
id: 'ibc-nBTC',
denom: NETWORK === 'mainnet' ? 'Not available' : 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/unbtc',
displayName: 'nBTC',
decimalPlaces: 6,
}
// export const ibcHuahua: TokenInfo = {
// id: 'ibc-huahua',
// denom:
// NETWORK === 'mainnet'
// ? 'ibc/CAD8A9F306CAAC55731C66930D6BEE539856DD12E59061C965E44D82AA26A0E7'
// : 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uhuahua',
// displayName: 'HUAHUA',
// decimalPlaces: 6,
// }
export const ibcCrbrus: TokenInfo = {
id: 'ibc-crbrus',
denom:
NETWORK === 'mainnet'
? 'ibc/71CEEB5CC09F75A3ACDC417108C14514351B6B2A540ACE9B37A80BF930845134'
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uCRBRUS',
displayName: 'CRBRUS',
decimalPlaces: 6,
}
export const ibcTia: TokenInfo = {
id: 'ibc-tia',
denom:
NETWORK === 'mainnet'
? 'ibc/14D1406D84227FDF4B055EA5CB2298095BBCA3F3BC3EF583AE6DF36F0FB179C8'
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/utia',
displayName: 'TIA',
decimalPlaces: 6,
}
export const nativeStardust: TokenInfo = {
id: 'native-strdst',
denom:
NETWORK === 'mainnet'
? 'factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust'
: 'factory/stars18vxuarvh44wxltxqsyac36972nvaqc377sdh40/dust',
displayName: 'STRDST',
decimalPlaces: 6,
}
export const nativeBrnch: TokenInfo = {
id: 'native-brnch',
denom:
NETWORK === 'mainnet'
? 'factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/uBRNCH'
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uBRNCH',
displayName: 'BRNCH',
decimalPlaces: 6,
}
export const tokensList = [
stars,
ibcAtom,
ibcUsdc,
ibcUsk,
ibcFrnz,
ibcNbtc,
ibcKuji,
// ibcHuahua,
ibcCrbrus,
ibcTia,
nativeStardust,
nativeBrnch,
]

View File

@ -1,4 +1,4 @@
import create from 'zustand' import { create } from 'zustand'
export const useCollectionStore = create(() => ({ export const useCollectionStore = create(() => ({
name: 'Example', name: 'Example',

View File

@ -1,21 +1,45 @@
import type { UseMinterContractProps } from 'contracts/minter' import type { UseBadgeHubContractProps } from 'contracts/badgeHub'
import { useMinterContract } from 'contracts/minter' import { useBadgeHubContract } from 'contracts/badgeHub'
import type { UseBaseFactoryContractProps } from 'contracts/baseFactory'
import { useBaseFactoryContract } from 'contracts/baseFactory'
import type { UseBaseMinterContractProps } from 'contracts/baseMinter'
import { useBaseMinterContract } from 'contracts/baseMinter'
import { type UseOpenEditionFactoryContractProps, useOpenEditionFactoryContract } from 'contracts/openEditionFactory'
import { type UseOpenEditionMinterContractProps, useOpenEditionMinterContract } from 'contracts/openEditionMinter'
import type { UseRoyaltyRegistryContractProps } from 'contracts/royaltyRegistry'
import { useRoyaltyRegistryContract } from 'contracts/royaltyRegistry'
import type { UseSG721ContractProps } from 'contracts/sg721' import type { UseSG721ContractProps } from 'contracts/sg721'
import { useSG721Contract } from 'contracts/sg721' import { useSG721Contract } from 'contracts/sg721'
import type { UseVendingFactoryContractProps } from 'contracts/vendingFactory'
import { useVendingFactoryContract } from 'contracts/vendingFactory'
import type { UseVendingMinterContractProps } from 'contracts/vendingMinter'
import { useVendingMinterContract } from 'contracts/vendingMinter'
import type { UseWhiteListContractProps } from 'contracts/whitelist' import type { UseWhiteListContractProps } from 'contracts/whitelist'
import { useWhiteListContract } from 'contracts/whitelist' import { useWhiteListContract } from 'contracts/whitelist'
import { type UseWhiteListMerkleTreeContractProps, useWhiteListMerkleTreeContract } from 'contracts/whitelistMerkleTree'
import type { ReactNode, VFC } from 'react' import type { ReactNode, VFC } from 'react'
import { Fragment, useEffect } from 'react' import { Fragment, useEffect } from 'react'
import type { State } from 'zustand' import { create } from 'zustand'
import create from 'zustand'
import type { UseSplitsContractProps } from '../contracts/splits/useContract'
import { useSplitsContract } from '../contracts/splits/useContract'
/** /**
* Contracts store type definitions * Contracts store type definitions
*/ */
export interface ContractsStore extends State { export interface ContractsStore {
sg721: UseSG721ContractProps | null sg721: UseSG721ContractProps | null
minter: UseMinterContractProps | null vendingMinter: UseVendingMinterContractProps | null
baseMinter: UseBaseMinterContractProps | null
openEditionMinter: UseOpenEditionMinterContractProps | null
whitelist: UseWhiteListContractProps | null whitelist: UseWhiteListContractProps | null
whitelistMerkleTree: UseWhiteListMerkleTreeContractProps | null
vendingFactory: UseVendingFactoryContractProps | null
baseFactory: UseBaseFactoryContractProps | null
openEditionFactory: UseOpenEditionFactoryContractProps | null
badgeHub: UseBadgeHubContractProps | null
splits: UseSplitsContractProps | null
royaltyRegistry: UseRoyaltyRegistryContractProps | null
} }
/** /**
@ -23,8 +47,17 @@ export interface ContractsStore extends State {
*/ */
export const defaultValues: ContractsStore = { export const defaultValues: ContractsStore = {
sg721: null, sg721: null,
minter: null, vendingMinter: null,
baseMinter: null,
openEditionMinter: null,
whitelist: null, whitelist: null,
whitelistMerkleTree: null,
vendingFactory: null,
baseFactory: null,
openEditionFactory: null,
badgeHub: null,
splits: null,
royaltyRegistry: null,
} }
/** /**
@ -49,16 +82,47 @@ export const ContractsProvider = ({ children }: { children: ReactNode }) => {
const ContractsSubscription: VFC = () => { const ContractsSubscription: VFC = () => {
const sg721 = useSG721Contract() const sg721 = useSG721Contract()
const minter = useMinterContract() const vendingMinter = useVendingMinterContract()
const baseMinter = useBaseMinterContract()
const openEditionMinter = useOpenEditionMinterContract()
const whitelist = useWhiteListContract() const whitelist = useWhiteListContract()
const whitelistMerkleTree = useWhiteListMerkleTreeContract()
const vendingFactory = useVendingFactoryContract()
const baseFactory = useBaseFactoryContract()
const openEditionFactory = useOpenEditionFactoryContract()
const badgeHub = useBadgeHubContract()
const splits = useSplitsContract()
const royaltyRegistry = useRoyaltyRegistryContract()
useEffect(() => { useEffect(() => {
useContracts.setState({ useContracts.setState({
sg721, sg721,
minter, vendingMinter,
baseMinter,
openEditionMinter,
whitelist, whitelist,
whitelistMerkleTree,
vendingFactory,
baseFactory,
openEditionFactory,
badgeHub,
splits,
royaltyRegistry,
}) })
}, [sg721, minter, whitelist]) }, [
sg721,
vendingMinter,
baseMinter,
whitelist,
whitelistMerkleTree,
vendingFactory,
baseFactory,
badgeHub,
splits,
royaltyRegistry,
openEditionMinter,
openEditionFactory,
])
return null return null
} }

View File

@ -0,0 +1,11 @@
import { create } from 'zustand'
export type Timezone = 'UTC' | 'Local'
export const useGlobalSettings = create(() => ({
timezone: 'UTC' as Timezone,
}))
export const setTimezone = (timezone: Timezone) => {
useGlobalSettings.setState({ timezone })
}

27
contexts/log.ts Normal file
View File

@ -0,0 +1,27 @@
import { create } from 'zustand'
export interface LogItem {
id: string
message: string
type?: string
timestamp?: Date
code?: number
source?: string
connectedWallet?: string
}
export const useLogStore = create(() => ({
itemList: [] as LogItem[],
}))
export const setLogItemList = (list: LogItem[]) => {
useLogStore.setState({ itemList: list })
}
export const addLogItem = (item: LogItem) => {
useLogStore.setState((prev) => ({ itemList: [...prev.itemList, item] }))
}
export const removeLogItem = (id: string) => {
useLogStore.setState((prev) => ({ itemList: prev.itemList.filter((item) => item.id !== id) }))
}

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