Author SHA1 Message Date
Serkan Reis f6bbdecb95 Add selected token type to minting details 2023-08-09 23:11:20 +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 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 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 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
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 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 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
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 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
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
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 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
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
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 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 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 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
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 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 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 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 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 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 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 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 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 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
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
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
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
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
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
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
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
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
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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
jhernandezb 281a70d778 revert ipfs lowercase 2022-11-23 14:02:33 -06: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 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 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 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 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 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
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
183 changed files with 21561 additions and 1112 deletions
+50 -11
View File
@@ -1,18 +1,57 @@
APP_VERSION=0.1.0
APP_VERSION=0.7.3
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_WHITELIST_CODE_ID=3
NEXT_PUBLIC_MINTER_CODE_ID=2
NEXT_PUBLIC_SG721_CODE_ID=1
NEXT_PUBLIC_SG721_CODE_ID=2595
NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=2596
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_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1h65nms9gwg4vdktyqj84tu50gwlm34e0eczl5w2ezllxuzfxy9esa9qlt0"
NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1hvu2ghqkcnvhtj2fc6wuazxt4dqcftslp2rwkkkcxy269a35a9pq60ug2q"
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_VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS=
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_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_UPDATABLE_FACTORY_ADDRESS="stars1fk5dkzcylam8mcpqrn8y9spauvc3d4navtaqurcc49dc3p9f8d3qdkvymx"
# 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=
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS=
# NEXT_PUBLIC_OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS=
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS=
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
NEXT_PUBLIC_WHITELIST_CODE_ID=2602
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2603
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=1905
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_NETWORK=testnet
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev
NEXT_PUBLIC_WEBSITE_URL=https://
NEXT_PUBLIC_S3_BUCKET= # TODO
NEXT_PUBLIC_S3_ENDPOINT= # TODO
NEXT_PUBLIC_S3_KEY= # TODO
NEXT_PUBLIC_S3_REGION= # TODO
NEXT_PUBLIC_S3_SECRET= # TODO
NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL="https://..."
+1 -1
View File
@@ -1 +1 @@
* @findolor @MightOfOaks @name-user1
* @MightOfOaks @name-user1 @Ninjatosba
+68 -4
View File
@@ -1,16 +1,60 @@
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import React from 'react'
import { useWallet } from 'contexts/wallet'
import React, { useState } from 'react'
import { toast } from 'react-hot-toast'
import { SG721_NAME_ADDRESS } from 'utils/constants'
import { csvToArray } from 'utils/csvToArray'
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import { isValidAccountsFile } from 'utils/isValidAccountsFile'
import { isValidAddress } from 'utils/isValidAddress'
interface AirdropUploadProps {
onChange: (data: AirdropAllocation[]) => void
}
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.client) throw new Error('Wallet not connected')
await wallet.client
.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>) => {
setResolvedAllocationData([])
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')) {
@@ -18,18 +62,38 @@ export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
return onChange([])
}
const reader = new FileReader()
reader.onload = (e: ProgressEvent<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 accountsData = csvToArray(e.target.result.toString())
console.log(accountsData)
if (!isValidAccountsFile(accountsData)) {
event.target.value = ''
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) {
toast.error(error.message)
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}
reader.readAsText(event.target.files[0])
+25 -4
View File
@@ -21,7 +21,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
tempArray.push(
<video
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"
muted
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) => (
<button
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={() => {
updateMetadataFileIndex((page - 1) * ITEM_NUMBER + index)
}}
@@ -69,9 +71,15 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
>
{(page - 1) * 12 + (index + 1)}
</div>
{getAssetType(assetSource.name) === 'audio' && (
<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>
)}
@@ -83,11 +91,23 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
<img
key={`image-${index}`}
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)}
/>
</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>
</button>
))
@@ -116,6 +136,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
return (
<div className="flex flex-col items-center">
<div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div>
<div className="mt-5 btn-group">
<button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button">
««
+126
View File
@@ -0,0 +1,126 @@
/* 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 '../contexts/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.client) throw new Error('Wallet not connected')
await wallet.client
.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>
)
}
+67
View File
@@ -0,0 +1,67 @@
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-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[400px] 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.
</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>
)
}
+11
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>
)
}
+31 -5
View File
@@ -1,33 +1,59 @@
import { useState } from 'react'
import { Button } from './Button'
export interface ConfirmationModalProps {
confirm: () => void
}
export const ConfirmationModal = (props: ConfirmationModalProps) => {
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-[40%] bottom-5 left-1/3 max-w-xl max-h-40 border-2 no-scrollbar modal-box"
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[400px] border-2 no-scrollbar modal-box"
htmlFor="temp"
>
{/* <Alert type="warning"></Alert> */}
<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.
</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 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 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"
>
Go Back
</label>
</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
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"
>
Confirm
+83
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>
)
}
+105 -13
View File
@@ -1,11 +1,6 @@
import type { LinkTabProps } from './LinkTab'
export const sg721LinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Create a new SG721 contract`,
href: '/contracts/sg721/instantiate',
},
{
title: 'Query',
description: `Dispatch queries with your SG721 contract`,
@@ -16,23 +11,74 @@ export const sg721LinkTabs: LinkTabProps[] = [
description: `Execute SG721 contract actions`,
href: '/contracts/sg721/execute',
},
{
title: 'Migrate',
description: `Migrate SG721 contract`,
href: '/contracts/sg721/migrate',
},
]
export const minterLinkTabs: LinkTabProps[] = [
export const vendingMinterLinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Initialize a new Minter contract`,
href: '/contracts/minter/instantiate',
description: `Initialize a new Vending Minter contract`,
href: '/contracts/vendingMinter/instantiate',
},
{
title: 'Query',
description: `Dispatch queries with your Minter contract`,
href: '/contracts/minter/query',
description: `Dispatch queries for your Vending Minter contract`,
href: '/contracts/vendingMinter/query',
},
{
title: 'Execute',
description: `Execute Minter contract actions`,
href: '/contracts/minter/execute',
description: `Execute Vending Minter contract actions`,
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',
description: `Dispatch queries with your Whitelist contract`,
description: `Dispatch queries for your Whitelist contract`,
href: '/contracts/whitelist/query',
},
{
@@ -53,3 +99,49 @@ export const whitelistLinkTabs: LinkTabProps[] = [
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',
},
]
+155
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>
)
}
+16 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */
import type { ReactNode } from 'react'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { getAssetType } from 'utils/getAssetType'
export interface MetadataFormGroupProps {
@@ -13,6 +13,7 @@ export interface MetadataFormGroupProps {
export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
const { title, subtitle, relatedAsset, children } = props
const [htmlContents, setHtmlContents] = useState<string>('')
const videoPreview = useMemo(
() => (
@@ -40,6 +41,17 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
[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 space-x-4 w-full">
<div className="flex flex-col w-1/3">
@@ -54,6 +66,9 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
{getAssetType(relatedAsset.name) === 'image' && (
<img alt="preview" src={URL.createObjectURL(relatedAsset)} />
)}
{getAssetType(relatedAsset.name) === 'html' && (
<iframe allowFullScreen height="420px" srcDoc={htmlContents} title="Preview" width="100%" />
)}
</div>
)}
</div>
+198
View File
@@ -0,0 +1,198 @@
/* 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
}
export const MetadataInput = (props: MetadataInputProps) => {
const emptyMetadataFile = new File(
[JSON.stringify({})],
`${props.selectedAssetFile?.name.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))}.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
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
: `${props.selectedAssetFile?.name.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))}.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 void parseMetadata(emptyMetadataFile)
}, [props.selectedMetadataFile?.name])
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()
console.log(metadata)
}, [
nameStateMemo.value,
descriptionStateMemo.value,
externalUrlStateMemo.value,
youtubeUrlStateMemo.value,
attributesStateMemo.entries,
])
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>
)
}
+43 -17
View File
@@ -1,12 +1,14 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { Alert } from './Alert'
import { Button } from './Button'
import { Conditional } from './Conditional'
import { TextInput } from './forms/FormInput'
import { useInputState } from './forms/FormInput.hooks'
import { MetadataAttributes } from './forms/MetadataAttributes'
@@ -64,6 +66,13 @@ export const MetadataModal = (props: MetadataModalProps) => {
}
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 generateUpdatedMetadata = () => {
console.log(`Current parsed data: ${parsedMetadata}`)
console.log('Updating...')
metadata.attributes = Object.values(attributesState)[1]
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
else metadata.description = descriptionState.value
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
else metadata.youtube_url = youtubeUrlState.value
@@ -124,7 +130,6 @@ export const MetadataModal = (props: MetadataModalProps) => {
const editedMetadataFile = new File([metadataFileBlob], metadataFile.name, { type: 'application/json' })
props.updateMetadata(editedMetadataFile)
toast.success('Metadata updated successfully.')
console.log(editedMetadataFile)
}
useEffect(() => {
@@ -144,21 +149,42 @@ export const MetadataModal = (props: MetadataModalProps) => {
subtitle={`Asset filename: ${props.assetFile?.name}`}
title="Update Metadata"
>
<TextInput {...nameState} onChange={(e) => nameState.onChange(e.target.value)} />
<TextInput {...descriptionState} onChange={(e) => descriptionState.onChange(e.target.value)} />
<TextInput {...externalUrlState} onChange={(e) => externalUrlState.onChange(e.target.value)} />
<TextInput {...youtubeUrlState} onChange={(e) => youtubeUrlState.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
{...nameState}
disabled={!props.metadataFile}
onChange={(e) => nameState.onChange(e.target.value)}
/>
<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}>
Update Metadata
</Button>
<Conditional test={Boolean(!props.metadataFile)}>
<Alert type="info">No metadata file to preview. Please select metadata files.</Alert>
</Conditional>
</MetadataFormGroup>
</label>
</label>
+222 -36
View File
@@ -1,60 +1,246 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import clsx from 'clsx'
import { Anchor } from 'components/Anchor'
import { setLogItemList, useLogStore } from 'contexts/log'
import { useWallet } from 'contexts/wallet'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
// import BrandText from 'public/brand/brand-text.svg'
import { footerLinks, socialsLinks } from 'utils/links'
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 { SidebarLayout } from './SidebarLayout'
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 = () => {
const router = useRouter()
const wallet = useWallet()
const logs = useLogStore()
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') || '[]'))
}, [])
return (
<SidebarLayout>
{/* Stargaze brand as home button */}
<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>
{/* wallet button */}
<WalletLoader />
{/* main navigation routes */}
{routes.map(({ text, href, isChild }) => (
<Anchor
key={href}
className={clsx(
'px-4 -mx-5 font-extrabold uppercase rounded-lg', // styling
'hover:bg-white/5 transition-colors', // hover styling
{ 'py-0 ml-2 text-sm font-bold': isChild },
{
'text-gray hover:text-white':
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
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
)}
href={href}
>
{text}
</Anchor>
))}
<div className="absolute top-[20%] left-[5%] mt-2">
<ul className="group p-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<div
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('/collections/') ? 'text-white' : 'text-gray',
)}
>
<Link href="/collections/" passHref>
Collections
</Link>
</div>
<ul className="z-50 p-2 bg-base-200">
<li
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>
<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 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('/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('/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>
</ul>
</li>
</ul>
</div>
<IncomeDashboardDisclaimer creatorAddress={wallet.address ? wallet.address : ''} />
<LogModal />
<div className="flex-grow" />
{logs.itemList.length > 0 && (
<label
className="w-[65%] h-[4px] 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>
)}
{/* Stargaze network status */}
<div className="text-sm capitalize">Network: {wallet.network}</div>
@@ -72,9 +258,9 @@ export const Sidebar = () => {
{/* footer attribution */}
<div className="text-xs text-white/50">
Stargaze Studio {process.env.APP_VERSION} <br />
Made by{' '}
<Anchor className="text-plumbus hover:underline" href="https://deuslabs.fi">
deus labs
Powered by{' '}
<Anchor className="text-plumbus hover:underline" href="https://stargaze.zone">
Stargaze
</Anchor>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
{/* fixed component */}
<div
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',
{ 'translate-x-[-230px]': !isOpen },
)}
+79
View File
@@ -0,0 +1,79 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */
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],
)
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 border-2 border-dashed">
{getAssetType(relatedAsset.name) === 'audio' && audioPreview}
{getAssetType(relatedAsset.name) === 'video' && videoPreview}
{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>
)
}
+8 -2
View File
@@ -6,6 +6,8 @@ import { usePopper } from 'react-popper'
export interface TooltipProps extends ComponentProps<'div'> {
label: ReactNode
children: ReactElement
placement?: 'top' | 'bottom' | 'left' | 'right'
backgroundColor?: string
}
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
@@ -14,7 +16,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
const [show, setShow] = useState(false)
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'top',
placement: props.placement ? props.placement : 'top',
})
return (
@@ -32,7 +34,11 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
<div
{...props}
{...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}
style={{ ...styles.popper, ...props.style }}
>
+2 -2
View File
@@ -16,7 +16,7 @@ export const WalletLoader = () => {
const displayName = useWalletStore((store) => store.name || getShortAddress(store.address))
return (
<Popover className="my-8">
<Popover className="mt-4 mb-2">
{({ close }) => (
<>
<div className="grid -mx-4">
@@ -44,7 +44,7 @@ export const WalletLoader = () => {
>
<Popover.Panel
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',
'flex flex-col items-stretch text-sm divide-y divide-white/10',
)}
+116
View File
@@ -0,0 +1,116 @@
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import { useWallet } from 'contexts/wallet'
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'
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.client) throw new Error('Wallet not connected')
await wallet.client
.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>
)
}
+86 -6
View File
@@ -1,24 +1,104 @@
/* 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 from 'react'
import React, { useState } from 'react'
import { toast } from 'react-hot-toast'
import { useWallet } from '../contexts/wallet'
import { SG721_NAME_ADDRESS } from '../utils/constants'
import { isValidAddress } from '../utils/isValidAddress'
interface WhitelistUploadProps {
onChange: (data: string[]) => void
}
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.client) throw new Error('Wallet not connected')
await wallet.client
.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[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()
reader.onload = (e: ProgressEvent<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 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)
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])
}
@@ -37,7 +117,7 @@ export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => {
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="whitelist-file"
multiple
multiple={false}
onChange={onFileChange}
type="file"
/>
+635
View File
@@ -0,0 +1,635 @@
/* 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 { useWallet } from 'contexts/wallet'
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 { 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.client) {
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 (wallet.client && type === 'edit_badge') {
const feeRateRaw = await wallet.client.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(
wallet.client.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>
)
}
@@ -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) }
}
+106
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>
)
}
+206
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)
}
+349
View File
@@ -0,0 +1,349 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* 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 { useWallet } from 'contexts/wallet'
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 { 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 }: BadgeDetailsProps) => {
const wallet = useWallet()
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: wallet.address ? wallet.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, { 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: 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 (wallet.client) {
const feeRateRaw = await wallet.client.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()
}, [wallet.client])
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} />
<TextInput className="mt-2" {...externalUrlState} />
<FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</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>
)
}
@@ -0,0 +1,307 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* 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, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
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 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, { 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])
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 !== ''}>
<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>
</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'}>
<TextInput {...nftStorageApiKeyState} className="w-full" />
</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/*"
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>
)
}
@@ -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) }
}
+105
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>
)
}
+116
View File
@@ -0,0 +1,116 @@
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 { useWallet } from '../../../contexts/wallet'
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 wallet = useWallet()
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>
)
}
+76
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')
}
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')
}
}
}
+365 -20
View File
@@ -1,3 +1,4 @@
import { toUtf8 } from '@cosmjs/encoding'
import { AirdropUpload } from 'components/AirdropUpload'
import { Button } from 'components/Button'
import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
@@ -11,44 +12,65 @@ import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { Tooltip } from 'components/Tooltip'
import { TransactionHash } from 'components/TransactionHash'
import { useWallet } from 'contexts/wallet'
import type { MinterInstance } from 'contracts/minter'
import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
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 type { AirdropAllocation } from 'utils/isValidAccountsFile'
import { resolveAddress } from 'utils/resolveAddress'
import type { CollectionInfo } from '../../../contracts/sg721/contract'
import { TextInput } from '../../forms/FormInput'
import type { MinterType, Sg721Type } from './Combobox'
interface CollectionActionsProps {
minterContractAddress: string
sg721ContractAddress: string
sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined
vendingMinterMessages: VendingMinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
openEditionMinterMessages: OpenEditionMinterInstance | undefined
minterType: MinterType
sg721Type: Sg721Type
}
type ExplicitContentType = true | false | undefined
export const CollectionActions = ({
sg721ContractAddress,
sg721Messages,
minterContractAddress,
minterMessages,
vendingMinterMessages,
baseMinterMessages,
openEditionMinterMessages,
minterType,
sg721Type,
}: CollectionActionsProps) => {
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
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 actionComboboxState = useActionsComboboxState()
const type = actionComboboxState.value?.id
const limitState = useNumberInputState({
id: 'per-address-limi',
id: 'per-address-limit',
name: 'perAddressLimit',
title: 'Per Address Limit',
subtitle: 'Enter the per address limit',
@@ -83,6 +105,22 @@ export const CollectionActions = ({
subtitle: 'Address of the recipient',
})
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({
id: 'whitelist-address',
name: 'whitelistAddress',
@@ -90,31 +128,163 @@ export const CollectionActions = ({
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 in STARS' : 'New minting price in STARS',
})
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: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid',
placeholder: '5%',
})
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
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 showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn'])
const showNumberOfTokensField = type === 'batch_mint'
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer'])
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
const showAirdropFileField = type === 'airdrop'
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
const showNumberOfTokensField = isEitherType(type, ['batch_mint', 'batch_mint_open_edition'])
const showTokenIdListField = isEitherType(type, [
'batch_burn',
'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'])
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const showDescriptionField = type === 'update_collection_info'
const showImageField = type === 'update_collection_info'
const showExternalLinkField = type === 'update_collection_info'
const showRoyaltyRelatedFields = type === 'update_collection_info'
const showExplicitContentField = type === 'update_collection_info'
const showBaseUriField = type === 'batch_update_token_metadata'
const payload: DispatchExecuteArgs = {
whitelist: whitelistState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
limit: limitState.value,
minterContract: minterContractAddress,
sg721Contract: sg721ContractAddress,
tokenId: tokenIdState.value,
tokenIds: tokenIdListState.value,
tokenUri: tokenURIState.value.trim().endsWith('/')
? tokenURIState.value.trim().slice(0, -1)
: tokenURIState.value.trim(),
batchNumber: batchNumberState.value,
minterMessages,
vendingMinterMessages,
baseMinterMessages,
openEditionMinterMessages,
sg721Messages,
recipient: recipientState.value,
recipient: resolvedRecipientAddress,
recipients: airdropArray,
tokenRecipients: airdropAllocationArray,
txSigner: wallet.address,
type,
price: priceState.value.toString(),
baseUri: baseURIState.value.trim().endsWith('/')
? baseURIState.value.trim().slice(0, -1)
: baseURIState.value.trim(),
collectionInfo,
jsonExtensions,
}
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 || 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])
useEffect(() => {
setCollectionInfo({
description: descriptionState.value || 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,
})
}, [
descriptionState.value,
imageState.value,
explicitContent,
externalLinkState.value,
royaltyPaymentAddressState.value,
royaltyShareState.value,
])
useEffect(() => {
const addresses: string[] = []
@@ -140,6 +310,94 @@ export const CollectionActions = ({
if (minterContractAddress === '' && sg721ContractAddress === '') {
throw new Error('Please enter minter and sg721 contract addresses!')
}
if (wallet.client && type === 'update_mint_price') {
const contractConfig = wallet.client.queryContractSmart(minterContractAddress, {
config: {},
})
await toast
.promise(
wallet.client.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 wallet.client?.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 wallet.client
?.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('splits'))
throw new Error('The provided royalty payment address does not belong to a splits contract.')
else console.log(contractInfo)
}
}
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
@@ -151,40 +409,127 @@ export const CollectionActions = ({
},
{
onError: (error) => {
toast.error(String(error))
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)
const airdropFileOnChange = (data: AirdropAllocation[]) => {
setAirdropAllocationArray(data)
console.log(data)
}
return (
<form>
<div className="grid grid-cols-2 mt-4">
<div className="mr-2">
<ActionsCombobox {...actionComboboxState} />
<ActionsCombobox minterType={minterType} sg721Type={sg721Type} {...actionComboboxState} />
{showRecipientField && <AddressInput {...recipientState} />}
{showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
{showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput {...tokenIdState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
{showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />}
{showPriceField && <NumberInput className="mt-2" {...priceState} />}
{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} />
</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 && (
<FormGroup
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"
subtitle={`CSV file that contains the airdrop addresses and the ${
type === 'airdrop' ? 'amount of tokens' : 'token ID'
} allocated for each address. Should start with the following header row: ${
type === 'airdrop' ? 'address,amount' : 'address,tokenId'
}`}
title="Airdrop File"
>
<AirdropUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
<Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
<FormControl className="mt-2" htmlId="start-date" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
</Conditional>
<Conditional test={showEndDateField}>
<FormControl className="mt-2" htmlId="end-date" title="End Time">
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
</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>
</div>
<div className="-mt-6">
<div className="relative mb-2">
+23 -4
View File
@@ -2,19 +2,38 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
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 {
value: ActionListItem | null
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 [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 =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
@@ -68,7 +87,7 @@ export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => {
<Combobox.Option
key={entry.id}
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}
>
+371 -61
View File
@@ -1,24 +1,48 @@
import type { MinterInstance } from 'contracts/minter'
import { useMinterContract } from 'contracts/minter'
import type { SG721Instance } from 'contracts/sg721'
/* eslint-disable eslint-comments/disable-enable-pair */
import { useBaseMinterContract } from 'contracts/baseMinter'
import { useOpenEditionMinterContract } from 'contracts/openEditionMinter'
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
import { useSG721Contract } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { useVendingMinterContract } from 'contracts/vendingMinter'
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 const ACTION_TYPES = [
'mint_token_uri',
'update_mint_price',
'update_discount_price',
'remove_discount_price',
'mint_to',
'mint_to_open_edition',
'mint_for',
'batch_mint',
'batch_mint_open_edition',
'set_whitelist',
'update_start_time',
'update_end_time',
'update_start_trading_time',
'update_per_address_limit',
'withdraw',
'update_collection_info',
'freeze_collection_info',
'transfer',
'batch_transfer',
'burn',
'batch_burn',
'batch_mint_for',
'shuffle',
'airdrop',
'airdrop_open_edition',
'airdrop_specific',
'burn_remaining',
'update_token_metadata',
'batch_update_token_metadata',
'freeze_token_metadata',
'enable_updatable',
] as const
export interface ActionListItem {
@@ -27,21 +51,84 @@ export interface ActionListItem {
description?: string
}
export const ACTION_LIST: ActionListItem[] = [
export const BASE_ACTION_LIST: ActionListItem[] = [
{
id: 'mint_token_uri',
name: 'Add New Token',
description: `Mint a new token and add it to the collection`,
},
{
id: 'update_start_trading_time',
name: 'Update Trading Start Time',
description: `Update start time for trading`,
},
{
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: '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: '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: 'mint_for',
name: 'Mint For',
description: `Mint a token for a user with given token ID`,
id: 'batch_mint',
name: 'Batch Mint To',
description: `Mint multiple tokens to a user`,
},
{
id: 'batch_mint',
name: 'Batch Mint',
description: `Mint multiple tokens to a user with given token amount`,
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',
@@ -50,18 +137,28 @@ export const ACTION_LIST: ActionListItem[] = [
},
{
id: 'update_start_time',
name: '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: 'withdraw',
name: 'Withdraw Tokens',
description: `Withdraw tokens from the contract`,
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: 'transfer',
@@ -93,6 +190,112 @@ export const ACTION_LIST: ActionListItem[] = [
name: 'Airdrop Tokens',
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: '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: '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 {
@@ -100,61 +303,101 @@ export interface DispatchExecuteProps {
[k: string]: unknown
}
type Select<T extends ActionType> = T
/** @see {@link MinterInstance} */
export type DispatchExecuteArgs = {
/** @see {@link VendingMinterInstance}{@link BaseMinterInstance} */
export interface DispatchExecuteArgs {
minterContract: string
sg721Contract: string
minterMessages?: MinterInstance
vendingMinterMessages?: VendingMinterInstance
baseMinterMessages?: BaseMinterInstance
openEditionMinterMessages?: OpenEditionMinterInstance
sg721Messages?: SG721Instance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'mint_to'>; recipient: string }
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
| { type: Select<'set_whitelist'>; whitelist: string }
| { type: Select<'update_start_time'>; startTime: string }
| { type: Select<'update_per_address_limit'>; limit: number }
| { type: Select<'shuffle'> }
| { type: Select<'withdraw'> }
| { type: Select<'transfer'>; recipient: string; tokenId: number }
| { type: Select<'batch_transfer'>; recipient: string; tokenIds: string }
| { type: Select<'burn'>; tokenId: number }
| { type: Select<'batch_burn'>; tokenIds: string }
| { type: Select<'airdrop'>; recipients: string[] }
)
type: string | undefined
tokenUri: string
price: string
recipient: string
tokenId: number
batchNumber: number
whitelist: string
startTime: string | undefined
endTime: string | undefined
limit: number
tokenIds: string
recipients: string[]
tokenRecipients: AirdropAllocation[]
collectionInfo: CollectionInfo | undefined
baseUri: string
jsonExtensions: boolean
}
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { minterMessages, sg721Messages, txSigner } = args
if (!minterMessages || !sg721Messages) {
const { vendingMinterMessages, baseMinterMessages, openEditionMinterMessages, sg721Messages, txSigner } = args
if (!vendingMinterMessages || !baseMinterMessages || !openEditionMinterMessages || !sg721Messages) {
throw new Error('Cannot execute actions')
}
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': {
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': {
return minterMessages.mintFor(txSigner, args.recipient, args.tokenId)
return vendingMinterMessages.mintFor(txSigner, args.recipient, args.tokenId)
}
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': {
return minterMessages.setWhitelist(txSigner, args.whitelist)
return vendingMinterMessages.setWhitelist(txSigner, args.whitelist)
}
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': {
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': {
return minterMessages.shuffle(txSigner)
}
case 'withdraw': {
return minterMessages.withdraw(txSigner)
return vendingMinterMessages.shuffle(txSigner)
}
case 'transfer': {
return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
@@ -168,8 +411,20 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'batch_burn': {
return sg721Messages.batchBurn(args.tokenIds)
}
case 'batch_mint_for': {
return vendingMinterMessages.batchMintFor(txSigner, args.recipient, args.tokenIds)
}
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: {
throw new Error('Unknown action')
@@ -179,34 +434,77 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// 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
const { messages: sg721Messages } = useSG721Contract()
// 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()
const { minterContract, sg721Contract } = args
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': {
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': {
return minterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
return vendingMinterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
}
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': {
return minterMessages(minterContract)?.setWhitelist(args.whitelist)
return vendingMinterMessages(minterContract)?.setWhitelist(args.whitelist)
}
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': {
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': {
return minterMessages(minterContract)?.shuffle()
}
case 'withdraw': {
return minterMessages(minterContract)?.withdraw()
return vendingMinterMessages(minterContract)?.shuffle()
}
case 'transfer': {
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
@@ -220,8 +518,20 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'batch_burn': {
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
}
case 'batch_mint_for': {
return vendingMinterMessages(minterContract)?.batchMintFor(args.recipient, args.tokenIds)
}
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: {
return {}
@@ -0,0 +1,287 @@
/* 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 { useWallet } from 'contexts/wallet'
import React, { useCallback, useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { API_URL } from 'utils/constants'
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
}
export interface BaseMinterDetailsDataProps {
baseMinterAcquisitionMethod: BaseMinterAcquisitionMethod
existingBaseMinter: string | undefined
selectedCollectionAddress: string | undefined
collectionTokenCount: number | undefined
}
export const BaseMinterDetails = ({ onChange, minterType }: 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.client && minterContractAddress.length > 0) {
const client = wallet.client
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 wallet.client
?.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 wallet.client
?.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.initialized) {
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.initialized,
selectedCollectionAddress,
collectionTokenCount,
])
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.initialized}>
<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.initialized}>
<Alert className="my-2 w-[90%]" type="warning">
Please connect your wallet first.
</Alert>
</Conditional>
</div>
</Conditional>
</div>
</div>
)}
</div>
)
}
@@ -4,20 +4,28 @@
/* 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 { addLogItem } from 'contexts/log'
import type { ChangeEvent } from 'react'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
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 type { MinterType } from '../actions/Combobox'
import type { UploadMethod } from './UploadDetails'
interface CollectionDetailsProps {
onChange: (data: CollectionDetailsDataProps) => void
uploadMethod: UploadMethod
coverImageUrl: string
minterType: MinterType
}
export interface CollectionDetailsDataProps {
@@ -26,10 +34,18 @@ export interface CollectionDetailsDataProps {
symbol: string
imageFile: File[]
externalLink?: string
startTradingTime?: string
explicit: boolean
updatable: boolean
}
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: CollectionDetailsProps) => {
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minterType }: 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 initialRender = useRef(true)
const nameState = useInputState({
id: 'name',
@@ -66,15 +82,28 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
description: descriptionState.value,
symbol: symbolState.value,
imageFile: coverImage ? [coverImage] : [],
externalLink: externalLinkState.value,
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)
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, coverImage, externalLinkState.value])
}, [
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')
@@ -92,18 +121,133 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
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 (
<div>
<FormGroup subtitle="Information about your collection" title="Collection Details">
<TextInput {...nameState} isRequired />
<TextInput {...descriptionState} isRequired />
<TextInput {...symbolState} isRequired />
<div className={clsx(minterType === 'base' ? 'grid grid-cols-2 -ml-16 max-w-5xl' : '')}>
<div className={clsx(minterType === 'base' ? 'ml-0' : '')}>
<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)"
>
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</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={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">
<span className="mr-4 font-bold">Updatable Token Metadata</span>
<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' && (
<input
accept="image/*"
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',
'before:hover:bg-white/5 before:transition',
)}
@@ -122,7 +266,9 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
<div className="max-w-[200px] max-h-[200px] rounded border-2">
<img
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>
)}
@@ -135,8 +281,85 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
)}
</FormControl>
<TextInput {...externalLinkState} />
<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-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={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-3/4 form-control',
)}
>
<label className="justify-start cursor-pointer label">
<span className="mr-4 font-bold">Updatable Token Metadata</span>
<input
checked={updatable}
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setUpdatable(!updatable)}
type="checkbox"
/>
</label>
</div>
</Tooltip>
</Conditional>
</Conditional>
</FormGroup>
</div>
)
@@ -1,27 +1,49 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
import { FormControl } from 'components/FormControl'
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 { vendingMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { stars, tokensList } from 'config/token'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
import { NumberInput } from '../../forms/FormInput'
import { useWallet } from '../../../contexts/wallet'
import { NumberInput, TextInput } from '../../forms/FormInput'
import type { UploadMethod } from './UploadDetails'
interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void
numberOfTokens: number | undefined
uploadMethod: UploadMethod
minimumMintPrice: number
mintingTokenFromFactory?: TokenInfo
}
export type TokenType = 'fungible' | 'non-fungible'
export interface MintingDetailsDataProps {
numTokens: number
unitPrice: string
perAddressLimit: number
startTime: string
paymentAddress?: string
selectedMintToken?: TokenInfo
selectesTokenType?: TokenType
}
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => {
export const MintingDetails = ({
onChange,
numberOfTokens,
uploadMethod,
minimumMintPrice,
mintingTokenFromFactory,
}: MintingDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const numberOfTokensState = useNumberInputState({
id: 'numberoftokens',
@@ -35,7 +57,9 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
id: 'unitPrice',
name: 'unitPrice',
title: 'Unit Price',
subtitle: 'Price of each token (min. 50 STARS)',
subtitle: `Price of each token (min. ${minimumMintPrice} ${
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '50',
})
@@ -47,17 +71,49 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
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(() => {
if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens)
const data: MintingDetailsDataProps = {
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,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
}
onChange(data)
// 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,
])
return (
<div>
@@ -68,12 +124,28 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
isRequired
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
/>
<NumberInput {...unitPriceState} isRequired />
<div className="flex flex-row items-center">
<NumberInput {...unitPriceState} isRequired />
<select
className="py-[9px] px-4 mt-14 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))}
>
{vendingMinterList
.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 (local)" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
</FormGroup>
<TextInput className="p-4 mt-5" {...paymentAddressState} />
</div>
)
}
@@ -1,8 +1,10 @@
import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks'
import { useWallet } from 'contexts/wallet'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from '../../../utils/resolveAddress'
import { NumberInput, TextInput } from '../../forms/FormInput'
interface RoyaltyDetailsProps {
@@ -18,6 +20,7 @@ export interface RoyaltyDetailsDataProps {
type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
const royaltyPaymentAddressState = useInputState({
@@ -33,16 +36,27 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
name: 'royaltyShare',
title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid',
placeholder: '8%',
placeholder: '5%',
})
useEffect(() => {
const data: RoyaltyDetailsDataProps = {
royaltyType: royaltyState,
paymentAddress: royaltyPaymentAddressState.value,
share: Number(royaltyShareState.value),
}
onChange(data)
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])
+195 -50
View File
@@ -1,4 +1,7 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* 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'
@@ -7,17 +10,27 @@ import { AssetsPreview } from 'components/AssetsPreview'
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, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
import { uid } from 'utils/random'
import { naturalCompare } from 'utils/sort'
import type { MinterType } from '../actions/Combobox'
import type { BaseMinterAcquisitionMethod } from './BaseMinterDetails'
export type UploadMethod = 'new' | 'existing'
interface UploadDetailsProps {
onChange: (value: UploadDetailsDataProps) => void
minterType: MinterType
baseMinterAcquisitionMethod?: BaseMinterAcquisitionMethod
}
export interface UploadDetailsDataProps {
@@ -30,9 +43,10 @@ export interface UploadDetailsDataProps {
uploadMethod: UploadMethod
baseTokenURI?: string
imageUrl?: string
baseMinterMetadataFile?: File
}
export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMethod }: UploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
@@ -40,6 +54,11 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false)
const [baseMinterMetadataFile, setBaseMinterMetadataFile] = useState<File | undefined>()
const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key',
name: 'nftStorageApiKey',
@@ -65,7 +84,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const baseTokenUriState = useInputState({
id: 'baseTokenUri',
name: 'baseTokenUri',
title: 'Base Token URI',
title: minterType === 'vending' ? 'Base Token URI' : 'Token URI',
placeholder: 'ipfs://',
defaultValue: '',
})
@@ -82,16 +101,24 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
setAssetFilesArray([])
setMetadataFilesArray([])
if (event.target.files === null) return
//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.')
//clear the input
event.target.value = ''
return
if (minterType === 'vending' || (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])
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(),
})
//clear the input
event.target.value = ''
return
}
}
}
let loadedFileCount = 0
@@ -119,20 +146,30 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFilesArray([])
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 = ''
return toast.error('The number of metadata files should be equal to the number of asset files.')
}
//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.')
//clear the input
event.target.value = ''
return
if (minterType === 'vending' || (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; 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
}
}
}
let loadedFileCount = 0
@@ -140,11 +177,24 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader()
reader.onload = (e) => {
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, { 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 = () => {
@@ -162,12 +212,18 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
setRefreshMetadata((prev) => !prev)
}
const updateMetadataFileArray = async (updatedMetadataFile: File) => {
const updateMetadataFileArray = (updatedMetadataFile: File) => {
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
useEffect(() => {
try {
const data: UploadDetailsDataProps = {
@@ -178,12 +234,26 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value,
uploadMethod,
baseTokenURI: baseTokenUriState.value,
imageUrl: coverImageUrlState.value,
baseTokenURI: baseTokenUriState.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)
} 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,
@@ -195,17 +265,21 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
uploadMethod,
baseTokenUriState.value,
coverImageUrlState.value,
refreshMetadata,
baseMinterMetadataFile,
])
useEffect(() => {
setAssetFilesArray([])
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([])
baseTokenUriState.onChange('')
coverImageUrlState.onChange('')
}, [uploadMethod])
}, [uploadMethod, minterType, baseMinterAcquisitionMethod])
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="mt-3 ml-4 font-bold form-check form-check-inline">
<input
@@ -223,7 +297,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"
htmlFor="inlineRadio2"
>
Upload assets & metadata
{minterType === 'base' ? 'Upload assets & metadata' : 'Upload assets & metadata'}
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
@@ -242,13 +316,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"
htmlFor="inlineRadio1"
>
Use an existing base URI
{minterType === 'base' ? 'Use an existing Token URI' : 'Use an existing base URI'}
</label>
</div>
</div>
<div className="p-3 py-5 pb-8">
<Conditional test={uploadMethod === 'existing'}>
<Conditional test={uploadMethod === 'existing' && minterType === 'vending'}>
<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
@@ -263,11 +337,53 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
and upload your assets & metadata manually to get a base URI for your collection.
</p>
<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>
<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>
<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>
<Conditional
test={minterType !== 'base' || (minterType === 'base' && baseMinterAcquisitionMethod === 'new')}
>
<div>
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
</div>
</Conditional>
</div>
</Conditional>
<Conditional test={uploadMethod === 'new'}>
@@ -356,7 +472,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
)}
>
<input
accept="image/*, audio/*, video/*"
accept="image/*, audio/*, video/*, .html"
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',
@@ -364,6 +480,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
id="assetFiles"
multiple
onChange={selectAssets}
ref={assetFilesRef}
type="file"
/>
</div>
@@ -375,7 +492,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"
htmlFor="metadataFiles"
>
Metadata Selection
{minterType === 'vending'
? 'Metadata Selection'
: assetFilesArray.length === 1
? 'Metadata Selection (optional)'
: 'Metadata Selection'}
</label>
<div
className={clsx(
@@ -392,24 +513,48 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
id="metadataFiles"
multiple
onChange={selectMetadata}
ref={metadataFilesRef}
type="file"
/>
</div>
</div>
)}
<MetadataModal
assetFile={assetFilesArray[metadataFileArrayIndex]}
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
refresher={refreshMetadata}
updateMetadata={updateMetadataFileArray}
/>
<Conditional test={assetFilesArray.length >= 1}>
<MetadataModal
assetFile={assetFilesArray[metadataFileArrayIndex]}
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
refresher={refreshMetadata}
updateMetadata={updateMetadataFileArray}
/>
</Conditional>
</div>
<Conditional test={assetFilesArray.length > 0}>
<Conditional test={assetFilesArray.length > 0 && minterType === 'vending'}>
<AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} />
</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>
<Conditional test={minterType === 'base' && assetFilesArray.length === 1}>
<MetadataInput
selectedAssetFile={assetFilesArray[0]}
selectedMetadataFile={metadataFilesArray[0]}
updateMetadataToUpload={updateBaseMinterMetadataFile}
/>
</Conditional>
</div>
</div>
</Conditional>
@@ -1,8 +1,16 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
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 React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress'
import { Conditional } from '../../Conditional'
import { AddressInput, NumberInput } from '../../forms/FormInput'
@@ -11,26 +19,35 @@ import { WhitelistUpload } from '../../WhitelistUpload'
interface WhitelistDetailsProps {
onChange: (data: WhitelistDetailsDataProps) => void
mintingTokenFromFactory?: TokenInfo
}
export interface WhitelistDetailsDataProps {
whitelistType: WhitelistState
whitelistState: WhitelistState
whitelistType: WhitelistType
contractAddress?: string
members?: string[]
members?: string[] | WhitelistFlexMember[]
unitPrice?: string
startTime?: string
endTime?: string
perAddressLimit?: number
memberLimit?: number
admins?: string[]
adminsMutable?: boolean
}
type WhitelistState = 'none' | 'existing' | 'new'
export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
type WhitelistType = 'standard' | 'flex'
export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: WhitelistDetailsProps) => {
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 [whitelistArray, setWhitelistArray] = useState<string[]>([])
const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
const whitelistAddressState = useInputState({
id: 'whitelist-address',
@@ -39,11 +56,13 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
defaultValue: '',
})
const uniPriceState = useNumberInputState({
const unitPriceState = useNumberInputState({
id: 'unit-price',
name: 'unitPrice',
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',
})
@@ -63,32 +82,64 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
placeholder: '5',
})
const addressListState = useAddressListState()
const whitelistFileOnChange = (data: string[]) => {
setWhitelistArray(data)
setWhitelistStandardArray(data)
}
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
setWhitelistFlexArray(whitelistData)
}
useEffect(() => {
setWhitelistStandardArray([])
setWhitelistFlexArray([])
}, [whitelistType])
useEffect(() => {
const data: WhitelistDetailsDataProps = {
whitelistType: whitelistState,
contractAddress: whitelistAddressState.value,
members: whitelistArray,
unitPrice: uniPriceState.value ? (Number(uniPriceState.value) * 1_000_000).toString() : '',
whitelistState,
whitelistType,
contractAddress: whitelistAddressState.value
.toLowerCase()
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(/ /g, ''),
members: whitelistType === 'standard' ? whitelistStandardArray : 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,
uniPriceState.value,
unitPriceState.value,
memberLimitState.value,
perAddressLimitState.value,
startDate,
endDate,
whitelistArray,
whitelistStandardArray,
whitelistFlexArray,
whitelistState,
addressListState.values,
adminsMutable,
])
return (
@@ -102,6 +153,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
name="whitelistRadioOptions1"
onClick={() => {
setWhitelistState('none')
setWhitelistType('standard')
}}
type="radio"
value="None"
@@ -158,11 +210,54 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
</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="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="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>
<div className="grid grid-cols-2">
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
<NumberInput isRequired {...uniPriceState} />
<NumberInput isRequired {...unitPriceState} />
<NumberInput isRequired {...memberLimitState} />
<NumberInput isRequired {...perAddressLimitState} />
<Conditional test={whitelistType === 'standard'}>
<NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<FormControl
htmlId="start-date"
isRequired
@@ -181,11 +276,46 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
</FormControl>
</FormGroup>
<div>
<FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File">
<WhitelistUpload onChange={whitelistFileOnChange} />
</FormGroup>
<Conditional test={whitelistArray.length > 0}>
<JsonPreview content={whitelistArray} initialState title="File Contents" />
<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}
isRequired
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="TXT file that contains the whitelisted addresses" 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="CSV file that contains the whitelisted addresses and their corresponding mint counts"
title="Whitelist File"
>
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
</FormGroup>
<Conditional test={whitelistFlexArray.length > 0}>
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
</Conditional>
</Conditional>
</div>
</div>
+17 -4
View File
@@ -2,19 +2,32 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MinterType } from '../actions/Combobox'
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 {
value: QueryListItem | null
onChange: (item: QueryListItem) => void
minterType?: MinterType
}
export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProps) => {
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'] })
@@ -67,7 +80,7 @@ export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
<Combobox.Option
key={entry.id}
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}
>
+54 -15
View File
@@ -5,23 +5,37 @@ import { FormControl } from 'components/FormControl'
import { AddressInput, TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
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 { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'
import { useWallet } from '../../../contexts/wallet'
import { resolveAddress } from '../../../utils/resolveAddress'
import type { MinterType } from '../actions/Combobox'
interface CollectionQueriesProps {
minterContractAddress: string
sg721ContractAddress: string
sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined
vendingMinterMessages: VendingMinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
openEditionMinterMessages: OpenEditionMinterInstance | undefined
minterType: MinterType
}
export const CollectionQueries = ({
sg721ContractAddress,
sg721Messages,
minterContractAddress,
minterMessages,
vendingMinterMessages,
openEditionMinterMessages,
baseMinterMessages,
minterType,
}: CollectionQueriesProps) => {
const wallet = useWallet()
const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id
@@ -43,25 +57,50 @@ export const CollectionQueries = ({
const address = addressState.value
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(
[sg721Messages, minterMessages, type, tokenId, address] as const,
[
sg721Messages,
baseMinterMessages,
vendingMinterMessages,
openEditionMinterMessages,
type,
tokenId,
address,
] as const,
async ({ queryKey }) => {
const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey
const result = await dispatchQuery({
tokenId: _tokenId,
minterMessages: _minterMessages,
sg721Messages: _sg721Messages,
address: _address,
type: _type,
const [
_sg721Messages,
_baseMinterMessages_,
_vendingMinterMessages,
_openEditionMinterMessages,
_type,
_tokenId,
_address,
] = 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,
address: resolvedAddress,
type: _type,
})
return result
})
return result
return res
},
{
placeholderData: null,
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),
retry: false,
@@ -71,7 +110,7 @@ export const CollectionQueries = ({
return (
<div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8">
<QueryCombobox {...comboboxState} />
<QueryCombobox minterType={minterType} {...comboboxState} />
{showAddressField && <AddressInput {...addressState} />}
{showTokenIdField && <TextInput {...tokenIdState} />}
</div>
+112 -9
View File
@@ -1,5 +1,7 @@
import type { MinterInstance } from 'contracts/minter'
import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
export type QueryType = typeof QUERY_TYPES[number]
@@ -8,8 +10,12 @@ export const QUERY_TYPES = [
'mint_price',
'num_tokens',
'tokens_minted_to_user',
'total_mint_count',
'tokens',
// 'token_owners',
'token_info',
'config',
'status',
] as const
export interface QueryListItem {
@@ -18,7 +24,7 @@ export interface QueryListItem {
description?: string
}
export const QUERY_LIST: QueryListItem[] = [
export const VENDING_QUERY_LIST: QueryListItem[] = [
{
id: 'collection_info',
name: 'Collection Info',
@@ -49,6 +55,85 @@ export const QUERY_LIST: QueryListItem[] = [
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 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: '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: '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 {
@@ -59,7 +144,9 @@ export interface DispatchExecuteProps {
type Select<T extends QueryType> = T
export type DispatchQueryArgs = {
minterMessages?: MinterInstance
baseMinterMessages?: BaseMinterInstance
vendingMinterMessages?: VendingMinterInstance
openEditionMinterMessages?: OpenEditionMinterInstance
sg721Messages?: SG721Instance
} & (
| { type: undefined }
@@ -67,13 +154,17 @@ export type DispatchQueryArgs = {
| { type: Select<'mint_price'> }
| { type: Select<'num_tokens'> }
| { type: Select<'tokens_minted_to_user'>; address: string }
| { type: Select<'total_mint_count'> }
| { type: Select<'tokens'>; address: string }
// | { type: Select<'token_owners'> }
| { type: Select<'token_info'>; tokenId: string }
| { type: Select<'config'> }
| { type: Select<'status'> }
)
export const dispatchQuery = async (args: DispatchQueryArgs) => {
const { minterMessages, sg721Messages } = args
if (!minterMessages || !sg721Messages) {
const { baseMinterMessages, vendingMinterMessages, openEditionMinterMessages, sg721Messages } = args
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
throw new Error('Cannot execute actions')
}
switch (args.type) {
@@ -81,21 +172,33 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
return sg721Messages.collectionInfo()
}
case 'mint_price': {
return minterMessages.getMintPrice()
return vendingMinterMessages.getMintPrice()
}
case 'num_tokens': {
return minterMessages.getMintableNumTokens()
return vendingMinterMessages.getMintableNumTokens()
}
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': {
// return minterMessages.updateStartTime(txSigner, args.startTime)
// return vendingMinterMessages.updateStartTime(txSigner, args.startTime)
// }
case 'token_info': {
if (!args.tokenId) return
return sg721Messages.allNftInfo(args.tokenId)
}
case 'config': {
return baseMinterMessages.getConfig()
}
case 'status': {
return baseMinterMessages.getStatus()
}
default: {
throw new Error('Unknown action')
}
@@ -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) }
}
@@ -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>
)
}
@@ -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) }
}
@@ -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>
)
}
@@ -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) }
}
@@ -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>
)
}
@@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option
key={entry.id}
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}
>
@@ -1,4 +1,4 @@
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
import type { ExecuteListItem } from 'contracts/splits/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
@@ -1,8 +1,8 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
import { EXECUTE_LIST } from 'contracts/minter/messages/execute'
import type { ExecuteListItem } from 'contracts/splits/messages/execute'
import { EXECUTE_LIST } from 'contracts/splits/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
@@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option
key={entry.id}
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}
>
@@ -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) }
}
@@ -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>
)
}
@@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option
key={entry.id}
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}
>
+5 -1
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 }
}
+40 -4
View File
@@ -1,7 +1,12 @@
import { toUtf8 } from '@cosmjs/encoding'
import { FormControl } from 'components/FormControl'
import { AddressInput } from 'components/forms/FormInput'
import { useWallet } from 'contexts/wallet'
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 { useInputState } from './FormInput.hooks'
@@ -46,6 +51,7 @@ export interface AddressProps {
}
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
const wallet = useWallet()
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
const htmlId = useId()
@@ -56,10 +62,40 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
title: ``,
})
const resolveAddress = async (name: string) => {
if (!wallet.client) throw new Error('Wallet not connected')
await wallet.client
.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(() => {
onChange(id, {
address: addressState.value,
})
if (addressState.value.endsWith('.stars')) {
void resolveAddress(addressState.value.split('.')[0])
} else {
onChange(id, {
address: addressState.value,
})
}
}, [addressState.value, id])
return (
@@ -67,7 +103,7 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
<AddressInput {...addressState} />
<div className="flex justify-end items-end pb-2 w-8">
<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))}
type="button"
>
@@ -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 }
}
+133
View File
@@ -0,0 +1,133 @@
import { toUtf8 } from '@cosmjs/encoding'
import { FormControl } from 'components/FormControl'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { useWallet } from 'contexts/wallet'
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 { 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.client) throw new Error('Wallet not connected')
await wallet.client
.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>
)
}
+20
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>(
function UrlInput(props, ref) {
return <FormInput {...props} ref={ref} type="url" />
@@ -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 }
}
+111
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>
)
}
+5 -4
View File
@@ -73,12 +73,13 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
}, [traitTypeState.value, traitValueState.value, id])
return (
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2">
<TraitTypeInput {...traitTypeState} />
<TraitValueInput {...traitValueState} />
<div className="grid relative xl:grid-cols-[6fr_6fr_1fr] xl:-space-x-8 2xl:space-x-2">
<TraitTypeInput className="lg:w-4/5 2xl:w-full" {...traitTypeState} />
<TraitValueInput className="lg:w-4/5 xl:pr-2 xl:w-full" {...traitValueState} />
<div className="flex justify-end items-end pb-2 w-8">
<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) => {
e.preventDefault()
isLast ? onAdd() : onRemove(id)
@@ -0,0 +1,351 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* 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 { 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
}
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,
}: 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 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, { 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])
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 2 weeks by default."
title="Trading Start Time (optional)"
>
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</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={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-3/4 form-control')}>
<label className="justify-start cursor-pointer label">
<span className="mr-4 font-bold">Updatable Token Metadata</span>
<input
checked={updatable}
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
onClick={() => setUpdatable(!updatable)}
type="checkbox"
/>
</label>
</div>
</Tooltip>
</Conditional>
</FormGroup>
</div>
)
}
@@ -0,0 +1,349 @@
/* 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 { getAssetType } from 'utils/getAssetType'
export type UploadMethod = 'new' | 'existing'
interface ImageUploadDetailsProps {
onChange: (value: ImageUploadDetailsDataProps) => void
}
export interface ImageUploadDetailsDataProps {
assetFile: File | undefined
uploadService: UploadServiceType
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
imageUrl?: string
coverImageUrl?: string
}
export const ImageUploadDetails = ({ onChange }: ImageUploadDetailsProps) => {
const [assetFile, setAssetFile] = useState<File>()
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
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: 'Asset URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const coverImageUrlState = useInputState({
id: 'coverImageUrl',
name: 'coverImageUrl',
title: 'Cover 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, { 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(),
coverImageUrl: coverImageUrlState.value.trim(),
}
onChange(data)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}, [
assetFile,
uploadService,
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
imageUrlState.value,
coverImageUrlState.value,
])
useEffect(() => {
if (assetFileRef.current) assetFileRef.current.value = ''
setAssetFile(undefined)
imageUrlState.onChange('')
}, [uploadMethod])
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'}>
<TextInput {...nftStorageApiKeyState} className="w-full" />
</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"
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>
)
}
+135
View File
@@ -0,0 +1,135 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
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 React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
import { useWallet } from '../../contexts/wallet'
import { NumberInput, TextInput } from '../forms/FormInput'
import type { UploadMethod } from './OffChainMetadataUploadDetails'
interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void
uploadMethod: UploadMethod
minimumMintPrice: number
mintTokenFromFactory?: TokenInfo | undefined
}
export interface MintingDetailsDataProps {
unitPrice: string
perAddressLimit: number
startTime: string
endTime: string
paymentAddress?: string
selectedMintToken?: TokenInfo
}
export const MintingDetails = ({
onChange,
uploadMethod,
minimumMintPrice,
mintTokenFromFactory,
}: MintingDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
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 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(() => {
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: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
unitPriceState.value,
perAddressLimitState.value,
timestamp,
endTimestamp,
paymentAddressState.value,
selectedMintToken,
])
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-center">
<NumberInput {...unitPriceState} isRequired />
<select
className="py-[9px] px-4 mt-14 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))}
>
{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 (local)" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
<FormControl htmlId="endTimestamp" isRequired subtitle="Minting end time (local)" title="End Time">
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
</FormControl>
</FormGroup>
<TextInput className="pr-4 pl-4 mt-3" {...paymentAddressState} />
</div>
)
}
@@ -0,0 +1,460 @@
/* 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 { 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
}
export interface OffChainMetadataUploadDetailsDataProps {
assetFiles: File[]
metadataFiles: File[]
uploadService: UploadServiceType
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
tokenURI?: string
imageUrl?: string
openEditionMinterMetadataFile?: File
}
export const OffChainMetadataUploadDetails = ({
onChange,
metadataStorageMethod,
}: OffChainMetadataUploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false)
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = 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([])
if (event.target.files === null) 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 assetFile = new File([e.target.result], event.target.files[i].name, { 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, { 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 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,
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,
}
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,
uploadService,
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
tokenUriState.value,
coverImageUrlState.value,
refreshMetadata,
openEditionMinterMetadataFile,
])
useEffect(() => {
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([])
tokenUriState.onChange('')
coverImageUrlState.onChange('')
}, [uploadMethod, metadataStorageMethod])
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'}>
<TextInput {...nftStorageApiKeyState} className="w-full" />
</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"
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>
{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
selectedAssetFile={assetFilesArray[0]}
selectedMetadataFile={metadataFilesArray[0]}
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}
/>
</div>
</div>
</Conditional>
</div>
</div>
)
}
@@ -0,0 +1,260 @@
/* 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 { useWallet } from 'contexts/wallet'
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
}
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 }: OnChainMetadataInputDetailsProps) => {
const wallet = useWallet()
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, { 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,
])
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>
)
}
@@ -0,0 +1,752 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { toUtf8 } from '@cosmjs/encoding'
import { coin } from '@cosmjs/proto-signing'
import clsx from 'clsx'
import { Button } from 'components/Button'
import type { MinterType } from 'components/collections/actions/Combobox'
import { Conditional } from 'components/Conditional'
import { ConfirmationModal } from 'components/ConfirmationModal'
import { LoadingModal } from 'components/LoadingModal'
import { openEditionMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { useContracts } from 'contexts/contracts'
import { addLogItem } from 'contexts/log'
import { useWallet } from 'contexts/wallet'
import type { DispatchExecuteArgs as OpenEditionFactoryDispatchExecuteArgs } from 'contracts/openEditionFactory/messages/execute'
import { dispatchExecute as openEditionFactoryDispatchExecute } from 'contracts/openEditionFactory/messages/execute'
import React, { useEffect, useMemo, useState } from 'react'
import { toast } from 'react-hot-toast'
import { upload } from 'services/upload'
import {
OPEN_EDITION_FACTORY_ADDRESS,
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
SG721_OPEN_EDITION_CODE_ID,
SG721_OPEN_EDITION_UPDATABLE_CODE_ID,
} from 'utils/constants'
import { getAssetType } from 'utils/getAssetType'
import { isValidAddress } from 'utils/isValidAddress'
import { checkTokenUri } from 'utils/isValidTokenUri'
import { uid } from 'utils/random'
import { type CollectionDetailsDataProps, CollectionDetails } from './CollectionDetails'
import type { ImageUploadDetailsDataProps } from './ImageUploadDetails'
import { ImageUploadDetails } from './ImageUploadDetails'
import type { MintingDetailsDataProps } from './MintingDetails'
import { MintingDetails } from './MintingDetails'
import type { UploadMethod } from './OffChainMetadataUploadDetails'
import {
type OffChainMetadataUploadDetailsDataProps,
OffChainMetadataUploadDetails,
} from './OffChainMetadataUploadDetails'
import type { OnChainMetadataInputDetailsDataProps } from './OnChainMetadataInputDetails'
import { OnChainMetadataInputDetails } from './OnChainMetadataInputDetails'
import { type RoyaltyDetailsDataProps, RoyaltyDetails } from './RoyaltyDetails'
export type MetadataStorageMethod = 'off-chain' | 'on-chain'
export interface OpenEditionMinterDetailsDataProps {
imageUploadDetails?: ImageUploadDetailsDataProps
collectionDetails?: CollectionDetailsDataProps
royaltyDetails?: RoyaltyDetailsDataProps
onChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
offChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
mintingDetails?: MintingDetailsDataProps
metadataStorageMethod?: MetadataStorageMethod
}
interface OpenEditionMinterCreatorProps {
onChange: (data: OpenEditionMinterCreatorDataProps) => void
onDetailsChange: (data: OpenEditionMinterDetailsDataProps) => void
openEditionMinterUpdatableCreationFee?: string
openEditionMinterCreationFee?: string
minimumMintPrice?: string
minimumUpdatableMintPrice?: string
minterType?: MinterType
mintTokenFromFactory?: TokenInfo | undefined
}
export interface OpenEditionMinterCreatorDataProps {
metadataStorageMethod: MetadataStorageMethod
openEditionMinterContractAddress: string | null
sg721ContractAddress: string | null
transactionHash: string | null
}
export const OpenEditionMinterCreator = ({
onChange,
onDetailsChange,
openEditionMinterCreationFee,
openEditionMinterUpdatableCreationFee,
minimumMintPrice,
minimumUpdatableMintPrice,
minterType,
mintTokenFromFactory,
}: OpenEditionMinterCreatorProps) => {
const wallet = useWallet()
const { openEditionMinter: openEditionMinterContract, openEditionFactory: openEditionFactoryContract } =
useContracts()
const [metadataStorageMethod, setMetadataStorageMethod] = useState<MetadataStorageMethod>('off-chain')
const [imageUploadDetails, setImageUploadDetails] = useState<ImageUploadDetailsDataProps | null>(null)
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
const [onChainMetadataInputDetails, setOnChainMetadataInputDetails] =
useState<OnChainMetadataInputDetailsDataProps | null>(null)
const [offChainMetadataUploadDetails, setOffChainMetadataUploadDetails] =
useState<OffChainMetadataUploadDetailsDataProps | null>(null)
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
const [creationInProgress, setCreationInProgress] = useState(false)
const [readyToCreate, setReadyToCreate] = useState(false)
const [uploading, setUploading] = useState(false)
const [tokenUri, setTokenUri] = useState<string | null>(null)
const [tokenImageUri, setTokenImageUri] = useState<string | null>(null)
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState<string | null>(null)
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null)
const factoryAddressForSelectedDenom =
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === false)
?.factoryAddress || OPEN_EDITION_FACTORY_ADDRESS
const updatableFactoryAddressForSelectedDenom =
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === true)
?.factoryAddress || OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS
const openEditionFactoryMessages = useMemo(
() =>
openEditionFactoryContract?.use(
collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
),
[openEditionFactoryContract, wallet.address],
)
const performOpenEditionMinterChecks = () => {
try {
setReadyToCreate(false)
checkCollectionDetails()
checkMintingDetails()
void checkUploadDetails()
.then(() => {
void checkRoyaltyDetails()
.then(() => {
void checkwalletBalance()
.then(() => {
setReadyToCreate(true)
})
.catch((error: any) => {
toast.error(`Error in Wallet Balance: ${error.message}`, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
setReadyToCreate(false)
})
})
.catch((error: any) => {
toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
setReadyToCreate(false)
})
})
.catch((error: any) => {
toast.error(`Error in Upload Details: ${error.message}`, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
setReadyToCreate(false)
})
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
setUploading(false)
setReadyToCreate(false)
}
}
const checkUploadDetails = async () => {
if (!wallet.initialized) throw new Error('Wallet not connected.')
if (
(metadataStorageMethod === 'off-chain' && !offChainMetadataUploadDetails) ||
(metadataStorageMethod === 'on-chain' && !imageUploadDetails)
) {
throw new Error('Please select assets and metadata')
}
if (
metadataStorageMethod === 'off-chain' &&
offChainMetadataUploadDetails?.uploadMethod === 'new' &&
offChainMetadataUploadDetails.assetFiles.length === 0
) {
throw new Error('Please select the asset file')
}
if (
metadataStorageMethod === 'on-chain' &&
imageUploadDetails?.uploadMethod === 'new' &&
imageUploadDetails.assetFile === undefined
) {
throw new Error('Please select the asset file')
}
if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'new') {
if (
offChainMetadataUploadDetails.uploadService === 'nft-storage' &&
offChainMetadataUploadDetails.nftStorageApiKey === ''
) {
throw new Error('Please enter a valid NFT.Storage API key')
} else if (
offChainMetadataUploadDetails.uploadService === 'pinata' &&
(offChainMetadataUploadDetails.pinataApiKey === '' || offChainMetadataUploadDetails.pinataSecretKey === '')
) {
throw new Error('Please enter valid Pinata API and secret keys')
}
}
if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'new') {
if (imageUploadDetails.uploadService === 'nft-storage' && imageUploadDetails.nftStorageApiKey === '') {
throw new Error('Please enter a valid NFT.Storage API key')
} else if (
imageUploadDetails.uploadService === 'pinata' &&
(imageUploadDetails.pinataApiKey === '' || imageUploadDetails.pinataSecretKey === '')
) {
throw new Error('Please enter valid Pinata API and secret keys')
}
}
if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'existing') {
if (
offChainMetadataUploadDetails.tokenURI === '' ||
!(offChainMetadataUploadDetails.tokenURI as string).includes('ipfs://')
) {
throw new Error('Please enter a valid token URI')
}
if (
offChainMetadataUploadDetails.imageUrl === '' ||
!(offChainMetadataUploadDetails.imageUrl as string).includes('ipfs://')
) {
throw new Error('Please enter a valid image URI')
}
}
if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'existing') {
if (imageUploadDetails.imageUrl === '' || !(imageUploadDetails.imageUrl as string).includes('ipfs://')) {
throw new Error('Please enter a valid asset URI')
}
if (
imageUploadDetails.coverImageUrl === '' ||
!(imageUploadDetails.coverImageUrl as string).includes('ipfs://')
) {
throw new Error('Please enter a valid cover image URL')
}
}
if (offChainMetadataUploadDetails?.uploadMethod === 'existing') {
await checkTokenUri(offChainMetadataUploadDetails.tokenURI as string)
}
}
const checkCollectionDetails = () => {
if (!collectionDetails) throw new Error('Please fill out the collection details')
if (collectionDetails.name === '') throw new Error('Collection name is required')
if (collectionDetails.description === '') throw new Error('Collection description is required')
if (collectionDetails.symbol === '') throw new Error('Collection symbol is required')
if (collectionDetails.description.length > 512)
throw new Error('Collection description cannot exceed 512 characters')
if (
metadataStorageMethod === 'off-chain' &&
offChainMetadataUploadDetails?.uploadMethod === 'new' &&
collectionDetails.imageFile.length === 0
)
throw new Error('Collection cover image is required')
if (
metadataStorageMethod === 'on-chain' &&
imageUploadDetails?.uploadMethod === 'new' &&
collectionDetails.imageFile.length === 0
)
throw new Error('Collection cover image is required')
if (
collectionDetails.startTradingTime &&
Number(collectionDetails.startTradingTime) < new Date().getTime() * 1000000
)
throw new Error('Invalid trading start time')
if (
collectionDetails.startTradingTime &&
Number(collectionDetails.startTradingTime) < Number(mintingDetails?.startTime)
)
throw new Error('Trading start time must be after minting start time')
if (collectionDetails.externalLink) {
try {
const url = new URL(collectionDetails.externalLink)
} catch (e: any) {
throw new Error(`Invalid external link: Make sure to include the protocol (e.g. https://)`)
}
}
}
const checkMintingDetails = () => {
if (!mintingDetails) throw new Error('Please fill out the minting details')
if (mintingDetails.unitPrice === '') throw new Error('Mint price is required')
if (collectionDetails?.updatable) {
if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice))
throw new Error(
`Invalid mint price: The minimum mint price is ${Number(minimumUpdatableMintPrice) / 1000000} ${
mintTokenFromFactory?.displayName
}`,
)
} else if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice))
throw new Error(
`Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} ${
mintTokenFromFactory?.displayName
}`,
)
if (!mintingDetails.perAddressLimit || mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50)
throw new Error('Invalid limit for tokens per address')
if (mintingDetails.startTime === '') throw new Error('Start time is required')
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
if (
mintingDetails.paymentAddress &&
(!isValidAddress(mintingDetails.paymentAddress) || !mintingDetails.paymentAddress.startsWith('stars1'))
)
throw new Error('Invalid payment address')
}
const checkRoyaltyDetails = async () => {
if (!royaltyDetails) throw new Error('Please fill out the royalty details')
if (royaltyDetails.royaltyType === 'new') {
if (royaltyDetails.share === 0) throw new Error('Royalty share percentage is required')
if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share percentage')
if (royaltyDetails.paymentAddress === '') throw new Error('Royalty payment address is required')
if (!isValidAddress(royaltyDetails.paymentAddress.trim())) {
if (royaltyDetails.paymentAddress.trim().endsWith('.stars')) {
throw new Error('Royalty payment address could not be resolved')
}
throw new Error('Invalid royalty payment address')
}
const contractInfoResponse = await wallet.client
?.queryContractRaw(
royaltyDetails.paymentAddress.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('splits'))
throw new Error('The provided royalty payment address does not belong to a splits contract.')
else console.log(contractInfo)
}
}
}
const checkwalletBalance = async () => {
if (!wallet.initialized) throw new Error('Wallet not connected.')
const amountNeeded = collectionDetails?.updatable
? Number(openEditionMinterUpdatableCreationFee)
: Number(openEditionMinterCreationFee)
await wallet.client?.getBalance(wallet.address, 'ustars').then((balance) => {
if (amountNeeded >= Number(balance.amount))
throw new Error(
`Insufficient wallet balance to instantiate the required contracts. Needed amount: ${(
amountNeeded / 1000000
).toString()} STARS`,
)
})
}
const createOpenEditionMinter = async () => {
try {
setCreationInProgress(true)
setTokenUri(null)
setCoverImageUrl(null)
setTokenImageUri(null)
setOpenEditionMinterContractAddress(null)
setSg721ContractAddress(null)
setTransactionHash(null)
if (metadataStorageMethod === 'off-chain') {
if (offChainMetadataUploadDetails?.uploadMethod === 'new') {
setUploading(true)
const metadataUri = await uploadForOffChainStorage()
const coverImageUri = await upload(
collectionDetails?.imageFile as File[],
offChainMetadataUploadDetails.uploadService,
'cover',
offChainMetadataUploadDetails.nftStorageApiKey as string,
offChainMetadataUploadDetails.pinataApiKey as string,
offChainMetadataUploadDetails.pinataSecretKey as string,
)
const metadataUriWithBase = `ipfs://${metadataUri}/${(
offChainMetadataUploadDetails.openEditionMinterMetadataFile as File
).name.substring(
0,
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'),
)}`
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
setTokenUri(metadataUriWithBase)
setCoverImageUrl(coverImageUriWithBase)
setUploading(false)
await instantiateOpenEditionMinter(metadataUriWithBase, coverImageUriWithBase)
} else {
setTokenUri(offChainMetadataUploadDetails?.tokenURI as string)
setCoverImageUrl(offChainMetadataUploadDetails?.imageUrl as string)
await instantiateOpenEditionMinter(
offChainMetadataUploadDetails?.tokenURI as string,
offChainMetadataUploadDetails?.imageUrl as string,
)
}
} else if (metadataStorageMethod === 'on-chain') {
if (imageUploadDetails?.uploadMethod === 'new') {
setUploading(true)
const imageUri = await upload(
[imageUploadDetails.assetFile as File],
imageUploadDetails.uploadService,
'cover',
imageUploadDetails.nftStorageApiKey as string,
imageUploadDetails.pinataApiKey as string,
imageUploadDetails.pinataSecretKey as string,
)
const imageUriWithBase = `ipfs://${imageUri}/${(imageUploadDetails.assetFile as File).name}`
setTokenImageUri(imageUriWithBase)
const coverImageUri = await upload(
collectionDetails?.imageFile as File[],
imageUploadDetails.uploadService,
'cover',
imageUploadDetails.nftStorageApiKey as string,
imageUploadDetails.pinataApiKey as string,
imageUploadDetails.pinataSecretKey as string,
)
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
setCoverImageUrl(coverImageUriWithBase)
setUploading(false)
await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase)
} else if (imageUploadDetails?.uploadMethod === 'existing') {
setTokenImageUri(imageUploadDetails.imageUrl as string)
setCoverImageUrl(imageUploadDetails.coverImageUrl as string)
await instantiateOpenEditionMinter(
imageUploadDetails.imageUrl as string,
imageUploadDetails.coverImageUrl as string,
)
}
}
setCreationInProgress(false)
setReadyToCreate(false)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' }, duration: 10000 })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
setReadyToCreate(false)
setCreationInProgress(false)
setUploading(false)
}
}
const uploadForOffChainStorage = async (): Promise<string> => {
if (!offChainMetadataUploadDetails) throw new Error('Please select the asset and fill in the metadata')
return new Promise((resolve, reject) => {
upload(
offChainMetadataUploadDetails.assetFiles,
offChainMetadataUploadDetails.uploadService,
'assets',
offChainMetadataUploadDetails.nftStorageApiKey as string,
offChainMetadataUploadDetails.pinataApiKey as string,
offChainMetadataUploadDetails.pinataSecretKey as string,
)
.then((assetUri: string) => {
const fileArray: File[] = []
const reader: FileReader = new FileReader()
reader.onload = (e) => {
const data: any = JSON.parse(e.target?.result as string)
if (
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'audio' ||
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'video' ||
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'html'
) {
data.animation_url = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}`
}
if (getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) !== 'html')
data.image = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}`
const metadataFileBlob = new Blob([JSON.stringify(data)], {
type: 'application/json',
})
console.log('Name: ', (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name)
const updatedMetadataFile = new File(
[metadataFileBlob],
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.substring(
0,
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'),
),
{
type: 'application/json',
},
)
fileArray.push(updatedMetadataFile)
}
reader.onloadend = () => {
upload(
fileArray,
offChainMetadataUploadDetails.uploadService,
'metadata',
offChainMetadataUploadDetails.nftStorageApiKey as string,
offChainMetadataUploadDetails.pinataApiKey as string,
offChainMetadataUploadDetails.pinataSecretKey as string,
)
.then(resolve)
.catch(reject)
}
console.log('File: ', offChainMetadataUploadDetails.openEditionMinterMetadataFile)
reader.readAsText(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File, 'utf8')
})
.catch(reject)
})
}
const instantiateOpenEditionMinter = async (uri: string, coverImageUri: string) => {
if (!wallet.initialized) throw new Error('Wallet not connected')
if (!openEditionFactoryContract) throw new Error('Contract not found')
if (!openEditionMinterContract) throw new Error('Contract not found')
let royaltyInfo = null
if (royaltyDetails?.royaltyType === 'new') {
royaltyInfo = {
payment_address: royaltyDetails.paymentAddress.trim(),
share: (Number(royaltyDetails.share) / 100).toString(),
}
}
const msg = {
create_minter: {
init_msg: {
nft_data: {
nft_data_type: metadataStorageMethod === 'off-chain' ? 'off_chain_metadata' : 'on_chain_metadata',
token_uri: metadataStorageMethod === 'off-chain' ? uri : null,
extension:
metadataStorageMethod === 'on-chain'
? {
image: uri,
name: onChainMetadataInputDetails?.name,
description: onChainMetadataInputDetails?.description,
attributes: onChainMetadataInputDetails?.attributes,
external_url: onChainMetadataInputDetails?.external_url,
animation_url:
imageUploadDetails?.uploadMethod === 'existing'
? onChainMetadataInputDetails?.animation_url
: getAssetType(imageUploadDetails?.assetFile?.name as string) === 'video'
? uri
: undefined,
youtube_url: onChainMetadataInputDetails?.youtube_url,
}
: null,
},
start_time: mintingDetails?.startTime,
end_time: mintingDetails?.endTime,
mint_price: {
amount: Number(mintingDetails?.unitPrice).toString(),
denom: (mintTokenFromFactory?.denom as string) || 'ustars',
},
per_address_limit: mintingDetails?.perAddressLimit,
payment_address: mintingDetails?.paymentAddress || null,
},
collection_params: {
code_id: collectionDetails?.updatable ? SG721_OPEN_EDITION_UPDATABLE_CODE_ID : SG721_OPEN_EDITION_CODE_ID,
name: collectionDetails?.name,
symbol: collectionDetails?.symbol,
info: {
creator: wallet.address,
description: collectionDetails?.description,
image: coverImageUri,
explicit_content: collectionDetails?.explicit || false,
royalty_info: royaltyInfo,
start_trading_time: collectionDetails?.startTradingTime || null,
},
},
},
}
console.log('msg: ', msg)
console.log('Using factory address: ', factoryAddressForSelectedDenom)
const payload: OpenEditionFactoryDispatchExecuteArgs = {
contract: collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
messages: openEditionFactoryMessages,
txSigner: wallet.address,
msg,
funds: [
coin(
collectionDetails?.updatable
? (openEditionMinterUpdatableCreationFee as string)
: (openEditionMinterCreationFee as string),
'ustars',
),
],
updatable: collectionDetails?.updatable,
}
await openEditionFactoryDispatchExecute(payload)
.then((data) => {
setTransactionHash(data.transactionHash)
setOpenEditionMinterContractAddress(data.openEditionMinterAddress)
setSg721ContractAddress(data.sg721Address)
})
.catch((error) => {
toast.error(error.message, { style: { maxWidth: 'none' } })
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
setUploading(false)
setCreationInProgress(false)
})
}
useEffect(() => {
if (minterType !== 'openEdition') {
setTransactionHash(null)
setOpenEditionMinterContractAddress(null)
setSg721ContractAddress(null)
setCreationInProgress(false)
setUploading(false)
}
}, [minterType])
useEffect(() => {
const data: OpenEditionMinterCreatorDataProps = {
metadataStorageMethod,
openEditionMinterContractAddress,
sg721ContractAddress,
transactionHash,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [metadataStorageMethod, openEditionMinterContractAddress, sg721ContractAddress, transactionHash])
useEffect(() => {
const data: OpenEditionMinterDetailsDataProps = {
imageUploadDetails: imageUploadDetails ? imageUploadDetails : undefined,
collectionDetails: collectionDetails ? collectionDetails : undefined,
royaltyDetails: royaltyDetails ? royaltyDetails : undefined,
onChainMetadataInputDetails: onChainMetadataInputDetails ? onChainMetadataInputDetails : undefined,
offChainMetadataUploadDetails: offChainMetadataUploadDetails ? offChainMetadataUploadDetails : undefined,
mintingDetails: mintingDetails ? mintingDetails : undefined,
metadataStorageMethod,
}
onDetailsChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
imageUploadDetails,
collectionDetails,
royaltyDetails,
onChainMetadataInputDetails,
offChainMetadataUploadDetails,
mintingDetails,
])
return (
<div>
{/* TODO: Cancel once we're able to index on-chain metadata */}
<Conditional test={false}>
<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={metadataStorageMethod === 'off-chain'}
className="peer sr-only"
id="inlineRadio9"
name="inlineRadioOptions9"
onClick={() => {
setMetadataStorageMethod('off-chain')
}}
type="radio"
value="Off Chain"
/>
<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="inlineRadio9"
>
Off-Chain Metadata
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={metadataStorageMethod === 'on-chain'}
className="peer sr-only"
id="inlineRadio10"
name="inlineRadioOptions10"
onClick={() => {
setMetadataStorageMethod('on-chain')
}}
type="radio"
value="On Chain"
/>
<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="inlineRadio10"
>
On-Chain Metadata
</label>
</div>
</div>
</div>
</Conditional>
<div className={clsx('my-4 mx-10')}>
<Conditional test={metadataStorageMethod === 'off-chain'}>
<div>
<OffChainMetadataUploadDetails onChange={setOffChainMetadataUploadDetails} />
</div>
</Conditional>
<Conditional test={metadataStorageMethod === 'on-chain'}>
<div>
<ImageUploadDetails onChange={setImageUploadDetails} />
<OnChainMetadataInputDetails
onChange={setOnChainMetadataInputDetails}
uploadMethod={imageUploadDetails?.uploadMethod}
/>
</div>
</Conditional>
</div>
<div className="flex justify-between py-3 px-8 mx-10 rounded border-2 border-white/20 grid-col-2">
<CollectionDetails
coverImageUrl={
metadataStorageMethod === 'off-chain'
? (offChainMetadataUploadDetails?.imageUrl as string)
: (imageUploadDetails?.coverImageUrl as string)
}
metadataStorageMethod={metadataStorageMethod}
onChange={setCollectionDetails}
uploadMethod={
metadataStorageMethod === 'off-chain'
? (offChainMetadataUploadDetails?.uploadMethod as UploadMethod)
: (imageUploadDetails?.uploadMethod as UploadMethod)
}
/>
<MintingDetails
minimumMintPrice={
collectionDetails?.updatable
? Number(minimumUpdatableMintPrice) / 1000000
: Number(minimumMintPrice) / 1000000
}
mintTokenFromFactory={mintTokenFromFactory}
onChange={setMintingDetails}
uploadMethod={offChainMetadataUploadDetails?.uploadMethod as UploadMethod}
/>
</div>
<div className="my-6">
<RoyaltyDetails onChange={setRoyaltyDetails} />
</div>
<div className="flex justify-end w-full">
<Button
className="relative justify-center p-2 mr-12 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
isLoading={creationInProgress}
onClick={performOpenEditionMinterChecks}
variant="solid"
>
Create Collection
</Button>
</div>
<Conditional test={uploading}>
<LoadingModal />
</Conditional>
<Conditional test={readyToCreate}>
<ConfirmationModal confirm={createOpenEditionMinter} />
</Conditional>
</div>
)
}
+113
View File
@@ -0,0 +1,113 @@
import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks'
import { useWallet } from 'contexts/wallet'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
import { NumberInput, TextInput } from '../forms/FormInput'
interface RoyaltyDetailsProps {
onChange: (data: RoyaltyDetailsDataProps) => void
}
export interface RoyaltyDetailsDataProps {
royaltyType: RoyaltyState
paymentAddress: string
share: number
}
type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
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(() => {
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])
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>
)
}
+1 -1
View File
@@ -2,7 +2,7 @@
"path": "/assets/",
"appName": "StargazeStudio",
"appShortName": "StargazeStudio",
"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",
"developerURL": "https://",
"background": "#FFC27D",
+214
View File
@@ -0,0 +1,214 @@
import {
OPEN_EDITION_FACTORY_ADDRESS,
OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
OPEN_EDITION_IBC_USDC_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_USDC_FACTORY_ADDRESS,
VENDING_FACTORY_ADDRESS,
VENDING_FACTORY_FLEX_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_USDC_FACTORY_ADDRESS,
VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
} from 'utils/constants'
import type { TokenInfo } from './token'
import { ibcAtom, ibcFrnz, ibcUsdc, stars } from './token'
export interface MinterInfo {
id: string
factoryAddress: string
supportedToken: TokenInfo
updatable?: boolean
flexible?: boolean
}
export const openEditionStarsMinter: MinterInfo = {
id: 'open-edition-stars-minter',
factoryAddress: OPEN_EDITION_FACTORY_ADDRESS,
supportedToken: stars,
updatable: false,
}
export const openEditionUpdatableStarsMinter: MinterInfo = {
id: 'open-edition-updatable-stars-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
supportedToken: stars,
updatable: true,
}
export const openEditionIbcAtomMinter: MinterInfo = {
id: 'open-edition-ibc-atom-minter',
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
}
export const openEditionUpdatableIbcAtomMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-atom-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: true,
}
export const openEditionIbcUsdcMinter: MinterInfo = {
id: 'open-edition-ibc-usdc-minter',
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
}
export const openEditionUpdatableIbcUsdcMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-usdc-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: true,
}
export const openEditionIbcFrnzMinter: MinterInfo = {
id: 'open-edition-ibc-frnz-minter',
factoryAddress: OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
supportedToken: ibcFrnz,
updatable: false,
}
export const openEditionUpdatableIbcFrnzMinter: MinterInfo = {
id: 'open-edition-updatable-ibc-frnz-minter',
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
supportedToken: ibcFrnz,
updatable: true,
}
export const openEditionMinterList = [
openEditionStarsMinter,
openEditionUpdatableStarsMinter,
openEditionUpdatableIbcAtomMinter,
openEditionIbcAtomMinter,
openEditionIbcFrnzMinter,
openEditionUpdatableIbcFrnzMinter,
openEditionIbcUsdcMinter,
openEditionUpdatableIbcUsdcMinter,
]
export const vendingStarsMinter: MinterInfo = {
id: 'vending-stars-minter',
factoryAddress: VENDING_FACTORY_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: false,
}
export const vendingUpdatableStarsMinter: MinterInfo = {
id: 'vending-updatable-stars-minter',
factoryAddress: VENDING_FACTORY_UPDATABLE_ADDRESS,
supportedToken: stars,
updatable: true,
flexible: false,
}
export const vendingIbcAtomMinter: MinterInfo = {
id: 'vending-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
flexible: false,
}
export const vendingUpdatableIbcAtomMinter: MinterInfo = {
id: 'vending-updatable-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcAtom,
updatable: true,
flexible: false,
}
export const vendingIbcUsdcMinter: MinterInfo = {
id: 'vending-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
flexible: false,
}
export const vendingUpdatableIbcUsdcMinter: MinterInfo = {
id: 'vending-updatable-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
supportedToken: ibcUsdc,
updatable: true,
flexible: false,
}
export const vendingMinterList = [
vendingStarsMinter,
vendingUpdatableStarsMinter,
vendingIbcAtomMinter,
vendingUpdatableIbcAtomMinter,
vendingIbcUsdcMinter,
vendingUpdatableIbcUsdcMinter,
]
export const flexibleVendingStarsMinter: MinterInfo = {
id: 'flexible-vending-stars-minter',
factoryAddress: VENDING_FACTORY_FLEX_ADDRESS,
supportedToken: stars,
updatable: false,
flexible: true,
}
export const flexibleVendingUpdatableStarsMinter: MinterInfo = {
id: 'flexible-vending-updatable-stars-minter',
factoryAddress: VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
supportedToken: stars,
updatable: true,
flexible: true,
}
export const flexibleVendingIbcAtomMinter: MinterInfo = {
id: 'flexible-vending-ibc-atom-minter',
factoryAddress: VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
supportedToken: ibcAtom,
updatable: false,
flexible: true,
}
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,
}
export const flexibleVendingIbcUsdcMinter: MinterInfo = {
id: 'flexible-vending-ibc-usdc-minter',
factoryAddress: VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
supportedToken: ibcUsdc,
updatable: false,
flexible: true,
}
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,
}
export const flexibleVendingMinterList = [
flexibleVendingStarsMinter,
flexibleVendingUpdatableStarsMinter,
flexibleVendingIbcAtomMinter,
flexibleVendingUpdatableIbcAtomMinter,
flexibleVendingIbcUsdcMinter,
flexibleVendingUpdatableIbcUsdcMinter,
]
+38
View File
@@ -0,0 +1,38 @@
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: 'ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858',
displayName: 'USDC',
decimalPlaces: 6,
}
export const ibcFrnz: TokenInfo = {
id: 'ibc-frnz',
denom: 'ibc/7FA7EC64490E3BDE5A1A28CBE73CC0AD22522794957BC891C46321E3A6074DB9',
displayName: 'FRNZ',
decimalPlaces: 6,
}
export const tokensList = [stars, ibcAtom, ibcUsdc, ibcFrnz]
+48 -7
View File
@@ -1,7 +1,17 @@
import type { UseMinterContractProps } from 'contracts/minter'
import { useMinterContract } from 'contracts/minter'
import type { UseBadgeHubContractProps } from 'contracts/badgeHub'
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 { UseSG721ContractProps } 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 { useWhiteListContract } from 'contracts/whitelist'
import type { ReactNode, VFC } from 'react'
@@ -9,13 +19,23 @@ import { Fragment, useEffect } from 'react'
import type { State } from 'zustand'
import create from 'zustand'
import type { UseSplitsContractProps } from '../contracts/splits/useContract'
import { useSplitsContract } from '../contracts/splits/useContract'
/**
* Contracts store type definitions
*/
export interface ContractsStore extends State {
sg721: UseSG721ContractProps | null
minter: UseMinterContractProps | null
vendingMinter: UseVendingMinterContractProps | null
baseMinter: UseBaseMinterContractProps | null
openEditionMinter: UseOpenEditionMinterContractProps | null
whitelist: UseWhiteListContractProps | null
vendingFactory: UseVendingFactoryContractProps | null
baseFactory: UseBaseFactoryContractProps | null
openEditionFactory: UseOpenEditionFactoryContractProps | null
badgeHub: UseBadgeHubContractProps | null
splits: UseSplitsContractProps | null
}
/**
@@ -23,8 +43,15 @@ export interface ContractsStore extends State {
*/
export const defaultValues: ContractsStore = {
sg721: null,
minter: null,
vendingMinter: null,
baseMinter: null,
openEditionMinter: null,
whitelist: null,
vendingFactory: null,
baseFactory: null,
openEditionFactory: null,
badgeHub: null,
splits: null,
}
/**
@@ -49,16 +76,30 @@ export const ContractsProvider = ({ children }: { children: ReactNode }) => {
const ContractsSubscription: VFC = () => {
const sg721 = useSG721Contract()
const minter = useMinterContract()
const vendingMinter = useVendingMinterContract()
const baseMinter = useBaseMinterContract()
const openEditionMinter = useOpenEditionMinterContract()
const whitelist = useWhiteListContract()
const vendingFactory = useVendingFactoryContract()
const baseFactory = useBaseFactoryContract()
const openEditionFactory = useOpenEditionFactoryContract()
const badgeHub = useBadgeHubContract()
const splits = useSplitsContract()
useEffect(() => {
useContracts.setState({
sg721,
minter,
vendingMinter,
baseMinter,
openEditionMinter,
whitelist,
vendingFactory,
baseFactory,
openEditionFactory,
badgeHub,
splits,
})
}, [sg721, minter, whitelist])
}, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits])
return null
}
+27
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) }))
}
+2 -2
View File
@@ -83,7 +83,7 @@ export const useWalletStore = create(
init(signer)
if (walletChange) set({ initializing: false })
} catch (err: any) {
toast.error(err?.message)
toast.error(err?.message, { style: { maxWidth: 'none' } })
set({ initializing: false })
}
},
@@ -113,7 +113,7 @@ export const useWalletStore = create(
const client = (await createQueryClient()) as SigningCosmWasmClient
set({ client })
} catch (err: any) {
toast.error(err?.message)
toast.error(err?.message, { style: { maxWidth: 'none' } })
set({ initializing: false })
}
},
+729
View File
@@ -0,0 +1,729 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable camelcase */
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { toUtf8 } from '@cosmjs/encoding'
import type { Coin } from '@cosmjs/proto-signing'
import { coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import sizeof from 'object-sizeof'
import { generateSignature } from '../../utils/hash'
export interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface MigrateResponse {
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface Rule {
by_key?: string
by_minter?: string
by_keys?: string
}
export interface Trait {
display_type?: string
trait_type: string
value: string
}
export interface Metadata {
name?: string
image?: string
image_data?: string
external_url?: string
description?: string
attributes?: Trait[]
background_color?: string
animation_url?: string
youtube_url?: string
}
export interface Badge {
manager: string
metadata: Metadata
transferrable: boolean
rule: Rule | string
expiry?: number
max_supply?: number
}
export interface BadgeHubInstance {
readonly contractAddress: string
//Query
getConfig: () => Promise<any>
getBadge: (id: number) => Promise<any>
getBadges: (start_after?: number, limit?: number) => Promise<any>
getKey: (id: number, pubkey: string) => Promise<any>
getKeys: (id: number, start_after?: string, limit?: number) => Promise<any>
//Execute
createBadge: (senderAddress: string, badge: Badge) => Promise<string>
editBadge: (senderAddress: string, id: number, metadata: Metadata, editFee?: number) => Promise<string>
addKeys: (senderAddress: string, id: number, keys: string[]) => Promise<string>
purgeKeys: (senderAddress: string, id: number, limit?: number) => Promise<string>
purgeOwners: (senderAddress: string, id: number, limit?: number) => Promise<string>
mintByMinter: (senderAddress: string, id: number, owners: string[]) => Promise<string>
mintByKey: (senderAddress: string, id: number, owner: string, signature: string) => Promise<string>
airdropByKey: (senderAddress: string, id: number, recipients: string[], privateKey: string) => Promise<string>
mintByKeys: (senderAddress: string, id: number, owner: string, pubkey: string, signature: string) => Promise<string>
setNft: (senderAddress: string, nft: string) => Promise<string>
}
export interface BadgeHubMessages {
createBadge: (badge: Badge) => CreateBadgeMessage
editBadge: (id: number, metadata: Metadata, editFee?: number) => EditBadgeMessage
addKeys: (id: number, keys: string[]) => AddKeysMessage
purgeKeys: (id: number, limit?: number) => PurgeKeysMessage
purgeOwners: (id: number, limit?: number) => PurgeOwnersMessage
mintByMinter: (id: number, owners: string[]) => MintByMinterMessage
mintByKey: (id: number, owner: string, signature: string) => MintByKeyMessage
airdropByKey: (id: number, recipients: string[], privateKey: string) => CustomMessage
mintByKeys: (id: number, owner: string, pubkey: string, signature: string) => MintByKeysMessage
setNft: (nft: string) => SetNftMessage
}
export interface CreateBadgeMessage {
sender: string
contract: string
msg: {
create_badge: {
manager: string
metadata: Metadata
transferrable: boolean
rule: Rule | string
expiry?: number
max_supply?: number
}
}
funds: Coin[]
}
export interface EditBadgeMessage {
sender: string
contract: string
msg: {
edit_badge: {
id: number
metadata: Metadata
}
}
funds: Coin[]
}
export interface AddKeysMessage {
sender: string
contract: string
msg: {
add_keys: {
id: number
keys: string[]
}
}
funds: Coin[]
}
export interface PurgeKeysMessage {
sender: string
contract: string
msg: {
purge_keys: {
id: number
limit?: number
}
}
funds: Coin[]
}
export interface PurgeOwnersMessage {
sender: string
contract: string
msg: {
purge_owners: {
id: number
limit?: number
}
}
funds: Coin[]
}
export interface MintByMinterMessage {
sender: string
contract: string
msg: {
mint_by_minter: {
id: number
owners: string[]
}
}
funds: Coin[]
}
export interface MintByKeyMessage {
sender: string
contract: string
msg: {
mint_by_key: {
id: number
owner: string
signature: string
}
}
funds: Coin[]
}
export interface CustomMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
funds: Coin[]
}
export interface MintByKeysMessage {
sender: string
contract: string
msg: {
mint_by_keys: {
id: number
owner: string
pubkey: string
signature: string
}
}
funds: Coin[]
}
export interface SetNftMessage {
sender: string
contract: string
msg: {
set_nft: {
nft: string
}
}
funds: Coin[]
}
export interface BadgeHubContract {
instantiate: (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
) => Promise<MigrateResponse>
use: (contractAddress: string) => BadgeHubInstance
messages: (contractAddress: string) => BadgeHubMessages
}
export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): BadgeHubContract => {
const use = (contractAddress: string): BadgeHubInstance => {
//Query
const getConfig = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
config: {},
})
return res
}
const getBadge = async (id: number): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
badge: { id },
})
return res
}
const getBadges = async (start_after?: number, limit?: number): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
badges: { start_after, limit },
})
return res
}
const getKey = async (id: number, pubkey: string): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
key: { id, pubkey },
})
return res
}
const getKeys = async (id: number, start_after?: string, limit?: number): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
keys: { id, start_after, limit },
})
return res
}
//Execute
const createBadge = async (senderAddress: string, badge: Badge): Promise<string> => {
const feeRateRaw = await client.queryContractRaw(
contractAddress,
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))
console.log('Fee Rate:', feeRate)
console.log('badge size: ', sizeof(badge))
console.log('metadata size', sizeof(badge.metadata))
console.log('size of attributes ', sizeof(badge.metadata.attributes))
console.log('Total: ', Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes)))
const res = await client.execute(
senderAddress,
contractAddress,
{
create_badge: {
manager: badge.manager,
metadata: badge.metadata,
transferrable: badge.transferrable,
rule: badge.rule,
expiry: badge.expiry,
max_supply: badge.max_supply,
},
},
'auto',
'',
[
coin(
(Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes))) * Number(feeRate.metadata),
'ustars',
),
],
//[coin(1, 'ustars')],
)
const events = res.logs
.map((log) => log.events)
.flat()
.find(
(event) =>
event.attributes.findIndex((attr) => attr.key === 'action' && attr.value === 'badges/hub/create_badge') > 0,
)!
const id = Number(events.attributes.find((attr) => attr.key === 'id')!.value)
return res.transactionHash.concat(`:${id}`)
}
const editBadge = async (
senderAddress: string,
id: number,
metadata: Metadata,
editFee?: number,
): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
edit_badge: {
id,
metadata,
},
},
'auto',
'',
editFee ? [coin(editFee, 'ustars')] : [],
)
return res.transactionHash
}
const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => {
const feeRateRaw = await client.queryContractRaw(
contractAddress,
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))
console.log('Fee Rate:', feeRate)
console.log('keys size: ', sizeof(keys))
console.log(keys)
const res = await client.execute(
senderAddress,
contractAddress,
{
add_keys: {
id,
keys,
},
},
'auto',
'',
[coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
)
return res.transactionHash
}
const purgeKeys = async (senderAddress: string, id: number, limit?: number): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
purge_keys: {
id,
limit,
},
},
'auto',
'',
)
return res.transactionHash
}
const purgeOwners = async (senderAddress: string, id: number, limit?: number): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
purge_owners: {
id,
limit,
},
},
'auto',
'',
)
return res.transactionHash
}
const mintByMinter = async (senderAddress: string, id: number, owners: string[]): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_by_minter: {
id,
owners,
},
},
'auto',
'',
)
return res.transactionHash
}
const mintByKey = async (senderAddress: string, id: number, owner: string, signature: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_by_key: {
id,
owner,
signature,
},
},
'auto',
'',
)
return res.transactionHash
}
const airdropByKey = async (
senderAddress: string,
id: number,
recipients: string[],
privateKey: string,
): Promise<string> => {
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < recipients.length; i++) {
const msg = {
mint_by_key: { id, owner: recipients[i], signature: generateSignature(id, recipients[i], privateKey) },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop_by_key')
return res.transactionHash
}
const mintByKeys = async (
senderAddress: string,
id: number,
owner: string,
pubkey: string,
signature: string,
): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_by_keys: {
id,
owner,
pubkey,
signature,
},
},
'auto',
'',
)
return res.transactionHash
}
const setNft = async (senderAddress: string, nft: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
set_nft: {
nft,
},
},
'auto',
'',
)
return res.transactionHash
}
return {
contractAddress,
getConfig,
getBadge,
getBadges,
getKey,
getKeys,
createBadge,
editBadge,
addKeys,
purgeKeys,
purgeOwners,
mintByMinter,
mintByKey,
airdropByKey,
mintByKeys,
setNft,
}
}
const migrate = async (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
): Promise<MigrateResponse> => {
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
return {
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const instantiate = async (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
): Promise<InstantiateResponse> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto')
return {
contractAddress: result.contractAddress,
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const messages = (contractAddress: string) => {
const createBadge = (badge: Badge): CreateBadgeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
create_badge: {
manager: badge.manager,
metadata: badge.metadata,
transferrable: badge.transferrable,
rule: badge.rule,
expiry: badge.expiry,
max_supply: badge.max_supply,
},
},
funds: [],
}
}
const editBadge = (id: number, metadata: Metadata, editFee?: number): EditBadgeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
edit_badge: {
id,
metadata,
},
},
funds: editFee ? [coin(editFee, 'ustars')] : [],
}
}
const addKeys = (id: number, keys: string[]): AddKeysMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
add_keys: {
id,
keys,
},
},
funds: [],
}
}
const purgeKeys = (id: number, limit?: number): PurgeKeysMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
purge_keys: {
id,
limit,
},
},
funds: [],
}
}
const purgeOwners = (id: number, limit?: number): PurgeOwnersMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
purge_owners: {
id,
limit,
},
},
funds: [],
}
}
const mintByMinter = (id: number, owners: string[]): MintByMinterMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_by_minter: {
id,
owners,
},
},
funds: [],
}
}
const mintByKey = (id: number, owner: string, signature: string): MintByKeyMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_by_key: {
id,
owner,
signature,
},
},
funds: [],
}
}
const airdropByKey = (id: number, recipients: string[], privateKey: string): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < recipients.length; i++) {
const signature = generateSignature(id, recipients[i], privateKey)
msg.push({
mint_by_key: { id, owner: recipients[i], signature },
})
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const mintByKeys = (id: number, owner: string, pubkey: string, signature: string): MintByKeysMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_by_keys: {
id,
owner,
pubkey,
signature,
},
},
funds: [],
}
}
const setNft = (nft: string): SetNftMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
set_nft: {
nft,
},
},
funds: [],
}
}
return {
createBadge,
editBadge,
addKeys,
purgeKeys,
purgeOwners,
mintByMinter,
mintByKey,
airdropByKey,
mintByKeys,
setNft,
}
}
return { use, instantiate, migrate, messages }
}
+176
View File
@@ -0,0 +1,176 @@
import type { Badge, BadgeHubInstance, Metadata } from '../index'
import { useBadgeHubContract } from '../index'
export type ExecuteType = typeof EXECUTE_TYPES[number]
export const EXECUTE_TYPES = [
'create_badge',
'edit_badge',
'add_keys',
'purge_keys',
'purge_owners',
'mint_by_minter',
'mint_by_key',
'mint_by_keys',
'set_nft',
] as const
export interface ExecuteListItem {
id: ExecuteType
name: string
description?: string
}
export const EXECUTE_LIST: ExecuteListItem[] = [
{
id: 'create_badge',
name: 'Create Badge',
description: `Create a new badge with the specified mint rule and metadata`,
},
{
id: 'edit_badge',
name: 'Edit Badge',
description: ` Edit badge metadata for the badge with the specified ID`,
},
{
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`,
},
{
id: 'mint_by_minter',
name: 'Mint by Minter',
description: `Mint a new token by the minter with the specified ID`,
},
{
id: 'mint_by_key',
name: 'Mint by Key',
description: `Mint a new token by the key with the specified ID`,
},
{
id: 'mint_by_keys',
name: 'Mint by Keys',
description: `Mint a new token by the keys with the specified ID`,
},
{
id: 'set_nft',
name: 'Set NFT',
description: `Set the Badge NFT contract address for the Badge Hub contract`,
},
]
export interface DispatchExecuteProps {
type: ExecuteType
[k: string]: unknown
}
type Select<T extends ExecuteType> = T
/** @see {@link BadgeHubInstance} */
export type DispatchExecuteArgs = {
contract: string
messages?: 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<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
| { type: Select<'set_nft'>; nft: string }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages, txSigner } = args
if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined')
}
switch (args.type) {
case 'create_badge': {
return messages.createBadge(txSigner, args.badge)
}
case 'edit_badge': {
return messages.editBadge(txSigner, args.id, args.metadata, args.editFee)
}
case 'add_keys': {
return messages.addKeys(txSigner, args.id, args.keys)
}
case 'purge_keys': {
return messages.purgeKeys(txSigner, args.id, args.limit)
}
case 'purge_owners': {
return messages.purgeOwners(txSigner, args.id, args.limit)
}
case 'mint_by_minter': {
return messages.mintByMinter(txSigner, args.id, args.owners)
}
case 'mint_by_key': {
return messages.mintByKey(txSigner, args.id, args.owner, args.signature)
}
case 'mint_by_keys': {
return messages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return messages.setNft(txSigner, args.nft)
}
default: {
throw new Error('unknown execute type')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useBadgeHubContract()
const { contract } = args
switch (args.type) {
case 'create_badge': {
return messages(contract)?.createBadge(args.badge)
}
case 'edit_badge': {
return messages(contract)?.editBadge(args.id, args.metadata)
}
case 'add_keys': {
return messages(contract)?.addKeys(args.id, args.keys)
}
case 'purge_keys': {
return messages(contract)?.purgeKeys(args.id, args.limit)
}
case 'purge_owners': {
return messages(contract)?.purgeOwners(args.id, args.limit)
}
case 'mint_by_minter': {
return messages(contract)?.mintByMinter(args.id, args.owners)
}
case 'mint_by_key': {
return messages(contract)?.mintByKey(args.id, args.owner, args.signature)
}
case 'mint_by_keys': {
return messages(contract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return messages(contract)?.setNft(args.nft)
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}
+53
View File
@@ -0,0 +1,53 @@
import type { BadgeHubInstance } from '../contract'
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 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&apos;s whitelisted' },
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
]
export interface DispatchQueryProps {
id: number
pubkey: string
messages: BadgeHubInstance | undefined
type: QueryType
startAfterNumber: number
startAfterString: string
limit: number
}
export const dispatchQuery = (props: DispatchQueryProps) => {
const { id, pubkey, messages, type, startAfterNumber, startAfterString, limit } = props
switch (type) {
case 'config': {
return messages?.getConfig()
}
case 'getBadge': {
return messages?.getBadge(id)
}
case 'getBadges': {
return messages?.getBadges(startAfterNumber, limit)
}
case 'getKey': {
return messages?.getKey(id, pubkey)
}
case 'getKeys': {
return messages?.getKeys(id, startAfterString, limit)
}
default: {
throw new Error('unknown query type')
}
}
}
@@ -3,8 +3,8 @@ import type { logs } from '@cosmjs/stargate'
import { useWallet } from 'contexts/wallet'
import { useCallback, useEffect, useState } from 'react'
import type { MinterContract, MinterInstance, MinterMessages } from './contract'
import { minter as initContract } from './contract'
import type { BadgeHubContract, BadgeHubInstance, BadgeHubMessages, MigrateResponse } from './contract'
import { badgeHub as initContract } from './contract'
/*export interface InstantiateResponse {
/** The address of the newly instantiated contract *-/
@@ -24,7 +24,7 @@ interface InstantiateResponse {
readonly logs: readonly logs.Log[]
}
export interface UseMinterContractProps {
export interface UseBadgeHubContractProps {
instantiate: (
codeId: number,
initMsg: Record<string, unknown>,
@@ -32,25 +32,26 @@ export interface UseMinterContractProps {
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
use: (customAddress: string) => MinterInstance | undefined
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
use: (customAddress: string) => BadgeHubInstance | undefined
updateContractAddress: (contractAddress: string) => void
getContractAddress: () => string | undefined
messages: (contractAddress: string) => MinterMessages | undefined
messages: (contractAddress: string) => BadgeHubMessages | undefined
}
export function useMinterContract(): UseMinterContractProps {
export function useBadgeHubContract(): UseBadgeHubContractProps {
const wallet = useWallet()
const [address, setAddress] = useState<string>('')
const [minter, setMinter] = useState<MinterContract>()
const [badgeHub, setBadgeHub] = useState<BadgeHubContract>()
useEffect(() => {
setAddress(localStorage.getItem('contract_address') || '')
}, [])
useEffect(() => {
const MinterBaseContract = initContract(wallet.getClient(), wallet.address)
setMinter(MinterBaseContract)
const BadgeHubBaseContract = initContract(wallet.getClient(), wallet.address)
setBadgeHub(BadgeHubBaseContract)
}, [wallet])
const updateContractAddress = (contractAddress: string) => {
@@ -60,21 +61,35 @@ export function useMinterContract(): UseMinterContractProps {
const instantiate = useCallback(
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
return new Promise((resolve, reject) => {
if (!minter) {
if (!badgeHub) {
reject(new Error('Contract is not initialized.'))
return
}
minter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
badgeHub.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
})
},
[minter, wallet],
[badgeHub, wallet],
)
const migrate = useCallback(
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
return new Promise((resolve, reject) => {
if (!badgeHub) {
reject(new Error('Contract is not initialized.'))
return
}
console.log(wallet.address, contractAddress, codeId)
badgeHub.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
})
},
[badgeHub, wallet],
)
const use = useCallback(
(customAddress = ''): MinterInstance | undefined => {
return minter?.use(address || customAddress)
(customAddress = ''): BadgeHubInstance | undefined => {
return badgeHub?.use(address || customAddress)
},
[minter, address],
[badgeHub, address],
)
const getContractAddress = (): string | undefined => {
@@ -82,10 +97,10 @@ export function useMinterContract(): UseMinterContractProps {
}
const messages = useCallback(
(customAddress = ''): MinterMessages | undefined => {
return minter?.messages(address || customAddress)
(customAddress = ''): BadgeHubMessages | undefined => {
return badgeHub?.messages(address || customAddress)
},
[minter, address],
[badgeHub, address],
)
return {
@@ -94,5 +109,6 @@ export function useMinterContract(): UseMinterContractProps {
updateContractAddress,
getContractAddress,
messages,
migrate,
}
}
+107
View File
@@ -0,0 +1,107 @@
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import type { Coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import { BASE_FACTORY_ADDRESS } from 'utils/constants'
import { BASE_FACTORY_UPDATABLE_ADDRESS } from '../../utils/constants'
export interface CreateBaseMinterResponse {
readonly baseMinterAddress: string
readonly sg721Address: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface BaseFactoryInstance {
readonly contractAddress: string
//Query
getParams: () => Promise<any>
//Execute
createBaseMinter: (
senderAddress: string,
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
) => Promise<CreateBaseMinterResponse>
}
export interface BaseFactoryMessages {
createBaseMinter: (msg: Record<string, unknown>, funds: Coin[], updatable?: boolean) => CreateBaseMinterMessage
}
export interface CreateBaseMinterMessage {
sender: string
contract: string
msg: Record<string, unknown>
funds: Coin[]
}
export interface BaseFactoryContract {
use: (contractAddress: string) => BaseFactoryInstance
messages: (contractAddress: string) => BaseFactoryMessages
}
export const baseFactory = (client: SigningCosmWasmClient, txSigner: string): BaseFactoryContract => {
const use = (contractAddress: string): BaseFactoryInstance => {
//Query
const getParams = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
params: {},
})
return res
}
//Execute
const createBaseMinter = async (
senderAddress: string,
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
): Promise<CreateBaseMinterResponse> => {
const result = await client.execute(
senderAddress,
updatable ? BASE_FACTORY_UPDATABLE_ADDRESS : BASE_FACTORY_ADDRESS,
msg,
'auto',
'',
funds,
)
return {
baseMinterAddress: result.logs[0].events[5].attributes[0].value,
sg721Address: result.logs[0].events[5].attributes[2].value,
transactionHash: result.transactionHash,
logs: result.logs,
}
}
return {
contractAddress,
getParams,
createBaseMinter,
}
}
const messages = (contractAddress: string) => {
const createBaseMinter = (
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
): CreateBaseMinterMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg,
funds,
}
}
return {
createBaseMinter,
}
}
return { use, messages }
}
+2
View File
@@ -0,0 +1,2 @@
export * from './contract'
export * from './useContract'
+29
View File
@@ -0,0 +1,29 @@
import type { Coin } from '@cosmjs/proto-signing'
import type { BaseFactoryInstance } from '../index'
import { useBaseFactoryContract } from '../index'
/** @see {@link VendingFactoryInstance} */
export interface DispatchExecuteArgs {
contract: string
messages?: BaseFactoryInstance
txSigner: string
msg: Record<string, unknown>
funds: Coin[]
updatable?: boolean
}
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages, txSigner } = args
if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined')
}
return messages.createBaseMinter(txSigner, args.msg, args.funds, args.updatable)
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useBaseFactoryContract()
const { contract } = args
return messages(contract)?.createBaseMinter(args.msg, args.funds, args.updatable)
}
+57
View File
@@ -0,0 +1,57 @@
import { useWallet } from 'contexts/wallet'
import { useCallback, useEffect, useState } from 'react'
import type { BaseFactoryContract, BaseFactoryInstance, BaseFactoryMessages } from './contract'
import { baseFactory as initContract } from './contract'
export interface UseBaseFactoryContractProps {
use: (customAddress: string) => BaseFactoryInstance | undefined
updateContractAddress: (contractAddress: string) => void
getContractAddress: () => string | undefined
messages: (contractAddress: string) => BaseFactoryMessages | undefined
}
export function useBaseFactoryContract(): UseBaseFactoryContractProps {
const wallet = useWallet()
const [address, setAddress] = useState<string>('')
const [baseFactory, setBaseFactory] = useState<BaseFactoryContract>()
useEffect(() => {
setAddress(localStorage.getItem('contract_address') || '')
}, [])
useEffect(() => {
const BaseFactoryBaseContract = initContract(wallet.getClient(), wallet.address)
setBaseFactory(BaseFactoryBaseContract)
}, [wallet])
const updateContractAddress = (contractAddress: string) => {
setAddress(contractAddress)
}
const use = useCallback(
(customAddress = ''): BaseFactoryInstance | undefined => {
return baseFactory?.use(address || customAddress)
},
[baseFactory, address],
)
const getContractAddress = (): string | undefined => {
return address
}
const messages = useCallback(
(customAddress = ''): BaseFactoryMessages | undefined => {
return baseFactory?.messages(address || customAddress)
},
[baseFactory, address],
)
return {
use,
updateContractAddress,
getContractAddress,
messages,
}
}
+306
View File
@@ -0,0 +1,306 @@
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { toUtf8 } from '@cosmjs/encoding'
import type { Coin } from '@cosmjs/proto-signing'
import { coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import toast from 'react-hot-toast'
export interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface MigrateResponse {
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface RoyaltyInfo {
payment_address: string
share: string
}
export interface BaseMinterInstance {
readonly contractAddress: string
//Query
getConfig: () => Promise<any>
getStatus: () => Promise<any>
//Execute
mint: (senderAddress: string, tokenUri: string) => Promise<string>
updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise<string>
batchMint: (senderAddress: string, recipient: string, batchCount: number) => Promise<string>
}
export interface BaseMinterMessages {
mint: (tokenUri: string) => MintMessage
updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage
batchMint: (recipient: string, batchNumber: number) => CustomMessage
}
export interface MintMessage {
sender: string
contract: string
msg: {
mint: {
token_uri: string
}
}
funds: Coin[]
}
export interface UpdateStartTradingTimeMessage {
sender: string
contract: string
msg: {
update_start_trading_time: string
}
funds: Coin[]
}
export interface CustomMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
funds: Coin[]
}
export interface MintPriceMessage {
public_price: {
denom: string
amount: string
}
airdrop_price: {
denom: string
amount: string
}
whitelist_price?: {
denom: string
amount: string
}
current_price: {
denom: string
amount: string
}
}
export interface BaseMinterContract {
instantiate: (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
) => Promise<MigrateResponse>
use: (contractAddress: string) => BaseMinterInstance
messages: (contractAddress: string) => BaseMinterMessages
}
export const baseMinter = (client: SigningCosmWasmClient, txSigner: string): BaseMinterContract => {
const use = (contractAddress: string): BaseMinterInstance => {
//Query
const getFactoryParameters = async (factoryAddress: string): Promise<any> => {
const res = await client.queryContractSmart(factoryAddress, { params: {} })
return res
}
const getConfig = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
config: {},
})
return res
}
const getStatus = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
status: {},
})
return res
}
//Execute
const mint = async (senderAddress: string, tokenUri: string): Promise<string> => {
const txHash = await getConfig().then(async (response) => {
const factoryParameters = await toast.promise(getFactoryParameters(response.config?.factory), {
loading: 'Querying Factory Parameters...',
error: 'Querying Factory Parameters failed!',
success: 'Query successful! Minting...',
})
console.log(factoryParameters.params.mint_fee_bps)
const price = response.config?.mint_price.amount
if (!price) {
throw new Error(
'Unable to retrieve a valid mint price. It may be that the given contract address does not belong to a Base Minter contract.',
)
}
console.log((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100)
const res = await client.execute(
senderAddress,
contractAddress,
{
mint: { token_uri: tokenUri },
},
'auto',
'',
[coin((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100 / 100, 'ustars')],
)
return res.transactionHash
})
return txHash
}
const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_start_trading_time: time || null,
},
'auto',
'',
)
return res.transactionHash
}
const batchMint = async (senderAddress: string, baseUri: string, batchCount: number): Promise<string> => {
const txHash = await getConfig().then(async (response) => {
const factoryParameters = await toast.promise(getFactoryParameters(response?.config?.factory), {
loading: 'Querying Factory Parameters...',
error: 'Querying Factory Parameters failed!',
success: 'Query successful! Minting...',
})
console.log(factoryParameters.params.mint_fee_bps)
const price = response.config?.mint_price.amount
if (!price) {
throw new Error(
'Unable to retrieve a valid mint price. It may be that the given contract address does not belong to a Base Minter contract.',
)
}
console.log((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100)
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < batchCount; i++) {
const msg = {
mint: { token_uri: `${baseUri}/${i + 1}` },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
funds: [coin((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100 / 100, 'ustars')],
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'batch mint')
return res.transactionHash
})
return txHash
}
return {
contractAddress,
getConfig,
getStatus,
mint,
updateStartTradingTime,
batchMint,
}
}
const migrate = async (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
): Promise<MigrateResponse> => {
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
return {
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const instantiate = async (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
): Promise<InstantiateResponse> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
funds: [coin('250000000', 'ustars')],
})
return {
contractAddress: result.contractAddress,
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const messages = (contractAddress: string) => {
const mint = (tokenUri: string): MintMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint: { token_uri: tokenUri },
},
funds: [],
}
}
const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_start_trading_time: startTime,
},
funds: [],
}
}
const batchMint = (baseUri: string, batchCount: number): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < batchCount; i++) {
msg.push({ mint: { token_uri: `${baseUri}/${i + 1}` } })
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
return {
mint,
updateStartTradingTime,
batchMint,
}
}
return { use, instantiate, migrate, messages }
}
+2
View File
@@ -0,0 +1,2 @@
export * from './contract'
export * from './useContract'
+82
View File
@@ -0,0 +1,82 @@
import type { BaseMinterInstance } from '../index'
import { useBaseMinterContract } from '../index'
export type ExecuteType = typeof EXECUTE_TYPES[number]
export const EXECUTE_TYPES = ['mint', 'update_start_trading_time'] as const
export interface ExecuteListItem {
id: ExecuteType
name: string
description?: string
}
export const EXECUTE_LIST: ExecuteListItem[] = [
{
id: 'mint',
name: 'Mint',
description: `Mint a token with the given token URI`,
},
{
id: 'update_start_trading_time',
name: 'Update Start Trading Time',
description: `Update start trading time for minting`,
},
]
export interface DispatchExecuteProps {
type: ExecuteType
[k: string]: unknown
}
type Select<T extends ExecuteType> = T
/** @see {@link BaseMinterInstance} */
export type DispatchExecuteArgs = {
contract: string
messages?: BaseMinterInstance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'mint'>; tokenUri: string }
| { type: Select<'update_start_trading_time'>; startTime?: string }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages, txSigner } = args
if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined')
}
switch (args.type) {
case 'mint': {
return messages.mint(txSigner, args.tokenUri)
}
case 'update_start_trading_time': {
return messages.updateStartTradingTime(txSigner, args.startTime)
}
default: {
throw new Error('unknown execute type')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useBaseMinterContract()
const { contract } = args
switch (args.type) {
case 'mint': {
return messages(contract)?.mint(args.tokenUri)
}
case 'update_start_trading_time': {
return messages(contract)?.updateStartTradingTime(args.startTime as string)
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}
+37
View File
@@ -0,0 +1,37 @@
import type { BaseMinterInstance } from '../contract'
export type QueryType = typeof QUERY_TYPES[number]
export const QUERY_TYPES = ['config', 'status'] as const
export interface QueryListItem {
id: QueryType
name: string
description?: string
}
export const QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'Query current contract config' },
{ id: 'status', name: 'Status', description: 'Query current contract status' },
]
export interface DispatchQueryProps {
address: string
messages: BaseMinterInstance | undefined
type: QueryType
}
export const dispatchQuery = (props: DispatchQueryProps) => {
const { address, messages, type } = props
switch (type) {
case 'config': {
return messages?.getConfig()
}
case 'status': {
return messages?.getStatus()
}
default: {
throw new Error('unknown query type')
}
}
}
+102
View File
@@ -0,0 +1,102 @@
import type { Coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import { useWallet } from 'contexts/wallet'
import { useCallback, useEffect, useState } from 'react'
import type { BaseMinterContract, BaseMinterInstance, BaseMinterMessages, MigrateResponse } from './contract'
import { baseMinter as initContract } from './contract'
interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface UseBaseMinterContractProps {
instantiate: (
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
use: (customAddress: string) => BaseMinterInstance | undefined
updateContractAddress: (contractAddress: string) => void
getContractAddress: () => string | undefined
messages: (contractAddress: string) => BaseMinterMessages | undefined
}
export function useBaseMinterContract(): UseBaseMinterContractProps {
const wallet = useWallet()
const [address, setAddress] = useState<string>('')
const [baseMinter, setBaseMinter] = useState<BaseMinterContract>()
useEffect(() => {
setAddress(localStorage.getItem('contract_address') || '')
}, [])
useEffect(() => {
const BaseMinterBaseContract = initContract(wallet.getClient(), wallet.address)
setBaseMinter(BaseMinterBaseContract)
}, [wallet])
const updateContractAddress = (contractAddress: string) => {
setAddress(contractAddress)
}
const instantiate = useCallback(
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
return new Promise((resolve, reject) => {
if (!baseMinter) {
reject(new Error('Contract is not initialized.'))
return
}
baseMinter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
})
},
[baseMinter, wallet],
)
const migrate = useCallback(
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
return new Promise((resolve, reject) => {
if (!baseMinter) {
reject(new Error('Contract is not initialized.'))
return
}
console.log(wallet.address, contractAddress, codeId)
baseMinter.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
})
},
[baseMinter, wallet],
)
const use = useCallback(
(customAddress = ''): BaseMinterInstance | undefined => {
return baseMinter?.use(address || customAddress)
},
[baseMinter, address],
)
const getContractAddress = (): string | undefined => {
return address
}
const messages = useCallback(
(customAddress = ''): BaseMinterMessages | undefined => {
return baseMinter?.messages(address || customAddress)
},
[baseMinter, address],
)
return {
instantiate,
use,
updateContractAddress,
getContractAddress,
messages,
migrate,
}
}
+97
View File
@@ -0,0 +1,97 @@
/* eslint-disable eslint-comments/disable-enable-pair */
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import type { Coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
export interface CreateOpenEditionMinterResponse {
readonly openEditionMinterAddress: string
readonly sg721Address: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface OpenEditionFactoryInstance {
readonly contractAddress: string
//Query
//Execute
createOpenEditionMinter: (
senderAddress: string,
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
selectedFactoryAddress?: string,
) => Promise<CreateOpenEditionMinterResponse>
}
export interface OpenEditionFactoryMessages {
createOpenEditionMinter: (
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
) => CreateOpenEditionMinterMessage
}
export interface CreateOpenEditionMinterMessage {
sender: string
contract: string
msg: Record<string, unknown>
funds: Coin[]
}
export interface OpenEditionFactoryContract {
use: (contractAddress: string) => OpenEditionFactoryInstance
messages: (contractAddress: string) => OpenEditionFactoryMessages
}
export const openEditionFactory = (client: SigningCosmWasmClient, txSigner: string): OpenEditionFactoryContract => {
const use = (contractAddress: string): OpenEditionFactoryInstance => {
//Query
//Execute
const createOpenEditionMinter = async (
senderAddress: string,
msg: Record<string, unknown>,
funds: Coin[],
): Promise<CreateOpenEditionMinterResponse> => {
console.log('Contract Address: ', contractAddress)
const result = await client.execute(senderAddress, contractAddress, msg, 'auto', '', funds)
return {
openEditionMinterAddress: result.logs[0].events[5].attributes[0].value,
sg721Address: result.logs[0].events[5].attributes[2].value,
transactionHash: result.transactionHash,
logs: result.logs,
}
}
return {
contractAddress,
createOpenEditionMinter,
}
}
const messages = (contractAddress: string) => {
const createOpenEditionMinter = (
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
): CreateOpenEditionMinterMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg,
funds,
}
}
return {
createOpenEditionMinter,
}
}
return { use, messages }
}
+2
View File
@@ -0,0 +1,2 @@
export * from './contract'
export * from './useContract'
@@ -0,0 +1,31 @@
/* eslint-disable eslint-comments/disable-enable-pair */
import type { Coin } from '@cosmjs/proto-signing'
import type { OpenEditionFactoryInstance } from '../index'
import { useOpenEditionFactoryContract } from '../index'
/** @see {@link OpenEditionFactoryInstance} */
export interface DispatchExecuteArgs {
contract: string
messages?: OpenEditionFactoryInstance
txSigner: string
msg: Record<string, unknown>
funds: Coin[]
updatable?: boolean
}
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages, txSigner } = args
if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined')
}
return messages.createOpenEditionMinter(txSigner, args.msg, args.funds, args.updatable)
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useOpenEditionFactoryContract()
const { contract } = args
return messages(contract)?.createOpenEditionMinter(args.msg, args.funds, args.updatable)
}
@@ -0,0 +1,76 @@
import type { logs } from '@cosmjs/stargate'
import { useWallet } from 'contexts/wallet'
import { useCallback, useEffect, useState } from 'react'
import type { OpenEditionFactoryContract, OpenEditionFactoryInstance, OpenEditionFactoryMessages } from './contract'
import { openEditionFactory as initContract } from './contract'
/*export interface InstantiateResponse {
/** The address of the newly instantiated contract *-/
readonly contractAddress: string
readonly logs: readonly logs.Log[]
/** Block height in which the transaction is included *-/
readonly height: number
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/
readonly transactionHash: string
readonly gasWanted: number
readonly gasUsed: number
}*/
interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface UseOpenEditionFactoryContractProps {
use: (customAddress: string) => OpenEditionFactoryInstance | undefined
updateContractAddress: (contractAddress: string) => void
getContractAddress: () => string | undefined
messages: (contractAddress: string) => OpenEditionFactoryMessages | undefined
}
export function useOpenEditionFactoryContract(): UseOpenEditionFactoryContractProps {
const wallet = useWallet()
const [address, setAddress] = useState<string>('')
const [openEditionFactory, setOpenEditionFactory] = useState<OpenEditionFactoryContract>()
useEffect(() => {
setAddress(localStorage.getItem('contract_address') || '')
}, [])
useEffect(() => {
const OpenEditionFactoryBaseContract = initContract(wallet.getClient(), wallet.address)
setOpenEditionFactory(OpenEditionFactoryBaseContract)
}, [wallet])
const updateContractAddress = (contractAddress: string) => {
setAddress(contractAddress)
}
const use = useCallback(
(customAddress = ''): OpenEditionFactoryInstance | undefined => {
return openEditionFactory?.use(address || customAddress)
},
[openEditionFactory, address],
)
const getContractAddress = (): string | undefined => {
return address
}
const messages = useCallback(
(customAddress = ''): OpenEditionFactoryMessages | undefined => {
return openEditionFactory?.messages(address || customAddress)
},
[openEditionFactory, address],
)
return {
use,
updateContractAddress,
getContractAddress,
messages,
}
}
+651
View File
@@ -0,0 +1,651 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { toUtf8 } from '@cosmjs/encoding'
import type { Coin } from '@cosmjs/proto-signing'
import { coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import toast from 'react-hot-toast'
export interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface MigrateResponse {
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface RoyaltyInfo {
payment_address: string
share: string
}
export interface OpenEditionMinterInstance {
readonly contractAddress: string
//Query
getConfig: () => Promise<any>
getStartTime: () => Promise<any>
getEndTime: () => Promise<any>
getMintPrice: () => Promise<any>
getMintCount: (address: string) => Promise<any>
getTotalMintCount: () => Promise<any>
getStatus: () => Promise<any>
//Execute
mint: (senderAddress: string) => Promise<string>
purge: (senderAddress: string) => Promise<string>
updateMintPrice: (senderAddress: string, price: string) => Promise<string>
updateStartTime: (senderAddress: string, time: Timestamp) => Promise<string>
updateEndTime: (senderAddress: string, time: Timestamp) => Promise<string>
updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise<string>
updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise<string>
mintTo: (senderAddress: string, recipient: string) => Promise<string>
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
}
export interface OpenEditionMinterMessages {
mint: () => MintMessage
purge: () => PurgeMessage
updateMintPrice: (price: string) => UpdateMintPriceMessage
updateStartTime: (time: Timestamp) => UpdateStartTimeMessage
updateEndTime: (time: Timestamp) => UpdateEndTimeMessage
updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
mintTo: (recipient: string) => MintToMessage
batchMint: (recipient: string, batchNumber: number) => CustomMessage
airdrop: (recipients: string[]) => CustomMessage
}
export interface MintMessage {
sender: string
contract: string
msg: {
mint: Record<string, never>
}
funds: Coin[]
}
export interface PurgeMessage {
sender: string
contract: string
msg: {
purge: Record<string, never>
}
funds: Coin[]
}
export interface UpdateStartTradingTimeMessage {
sender: string
contract: string
msg: {
update_start_trading_time: string
}
funds: Coin[]
}
export interface UpdateStartTimeMessage {
sender: string
contract: string
msg: {
update_start_time: string
}
funds: Coin[]
}
export interface UpdateEndTimeMessage {
sender: string
contract: string
msg: {
update_end_time: string
}
funds: Coin[]
}
export interface UpdateMintPriceMessage {
sender: string
contract: string
msg: {
update_mint_price: {
price: string
}
}
funds: Coin[]
}
export interface UpdatePerAddressLimitMessage {
sender: string
contract: string
msg: {
update_per_address_limit: {
per_address_limit: number
}
}
funds: Coin[]
}
export interface MintToMessage {
sender: string
contract: string
msg: {
mint_to: {
recipient: string
}
}
funds: Coin[]
}
export interface CustomMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
funds: Coin[]
}
export interface MintPriceMessage {
public_price: {
denom: string
amount: string
}
airdrop_price: {
denom: string
amount: string
}
current_price: {
denom: string
amount: string
}
}
export interface OpenEditionMinterContract {
instantiate: (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
) => Promise<MigrateResponse>
use: (contractAddress: string) => OpenEditionMinterInstance
messages: (contractAddress: string) => OpenEditionMinterMessages
}
export const openEditionMinter = (client: SigningCosmWasmClient, txSigner: string): OpenEditionMinterContract => {
const use = (contractAddress: string): OpenEditionMinterInstance => {
//Query
const getFactoryParameters = async (factoryAddress: string): Promise<any> => {
const res = await client.queryContractSmart(factoryAddress, { params: {} })
return res
}
const getConfig = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
config: {},
})
return res
}
const getStartTime = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
start_time: {},
})
return res
}
const getEndTime = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
end_time: {},
})
return res
}
const getMintPrice = async (): Promise<MintPriceMessage> => {
const res = await client.queryContractSmart(contractAddress, {
mint_price: {},
})
return res
}
const getMintCount = async (address: string): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
mint_count: { address },
})
return res
}
const getTotalMintCount = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
total_mint_count: {},
})
return res
}
const getStatus = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
status: {},
})
return res
}
//Execute
const mint = async (senderAddress: string): Promise<string> => {
const price = (await getMintPrice()).public_price.amount
const res = await client.execute(
senderAddress,
contractAddress,
{
mint: {},
},
'auto',
'',
[coin(price, 'ustars')],
)
return res.transactionHash
}
const purge = async (senderAddress: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
purge: {},
},
'auto',
'',
)
return res.transactionHash
}
const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_start_trading_time: time || null,
},
'auto',
'',
)
return res.transactionHash
}
const updateStartTime = async (senderAddress: string, time: Timestamp): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_start_time: time,
},
'auto',
'',
)
return res.transactionHash
}
const updateEndTime = async (senderAddress: string, time: Timestamp): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_end_time: time,
},
'auto',
'',
)
return res.transactionHash
}
const updateMintPrice = async (senderAddress: string, price: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_mint_price: {
price: (Number(price) * 1000000).toString(),
},
},
'auto',
'',
)
return res.transactionHash
}
const updatePerAddressLimit = async (senderAddress: string, perAddressLimit: number): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_per_address_limit: { per_address_limit: perAddressLimit },
},
'auto',
'',
)
return res.transactionHash
}
const mintTo = async (senderAddress: string, recipient: string): Promise<string> => {
const txHash = await getConfig().then(async (response) => {
const factoryParameters = await toast.promise(getFactoryParameters(response.factory), {
loading: 'Querying Factory Parameters...',
error: 'Querying Factory Parameters failed!',
success: 'Query successful! Minting...',
})
console.log(factoryParameters?.params?.extension?.airdrop_mint_fee_bps)
const price = factoryParameters?.params?.extension?.airdrop_mint_price.amount
const denom = factoryParameters?.params?.extension?.airdrop_mint_price.denom || 'ustars'
if (!price) {
throw new Error(
'Unable to retrieve a valid airdrop mint price. It may be that the given contract address does not belong to an Open Edition Factory.',
)
}
const airdropFee = Number(price) * Number(factoryParameters.params.extension?.airdrop_mint_fee_bps)
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_to: { recipient },
},
'auto',
'',
airdropFee > 0 ? [coin(airdropFee / 100 / 100, denom as string)] : [],
)
return res.transactionHash
})
return txHash
}
const batchMint = async (senderAddress: string, recipient: string, batchNumber: number): Promise<string> => {
const txHash = await getConfig().then(async (response) => {
const factoryParameters = await toast.promise(getFactoryParameters(response?.factory), {
loading: 'Querying Factory Parameters...',
error: 'Querying Factory Parameters failed!',
success: 'Query successful! Minting...',
})
const price = factoryParameters?.params?.extension?.airdrop_mint_price.amount
const denom = factoryParameters?.params?.extension?.airdrop_mint_price.denom || 'ustars'
if (!price) {
throw new Error(
'Unable to retrieve a valid airdrop mint price. It may be that the given contract address does not belong to a Open Edition Factory.',
)
}
const airdropFee = Number(price) * Number(factoryParameters.params.extension?.airdrop_mint_fee_bps)
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < batchNumber; i++) {
const msg = {
mint_to: { recipient },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
funds: airdropFee > 0 ? [coin(airdropFee / 100 / 100, denom as string)] : [],
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'batch mint')
return res.transactionHash
})
return txHash
}
const airdrop = async (senderAddress: string, recipients: string[]): Promise<string> => {
const txHash = await getConfig().then(async (response) => {
const factoryParameters = await toast.promise(getFactoryParameters(response?.factory), {
loading: 'Querying Factory Parameters...',
error: 'Querying Factory Parameters failed!',
success: 'Query successful! Minting...',
})
const price = factoryParameters?.params?.extension?.airdrop_mint_price.amount
const denom = factoryParameters?.params?.extension?.airdrop_mint_price.denom || 'ustars'
if (!price) {
throw new Error(
'Unable to retrieve a valid airdrop mint price. It may be that the given contract address does not belong to a Open Edition Factory.',
)
}
const airdropFee = Number(price) * Number(factoryParameters.params.extension?.airdrop_mint_fee_bps)
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < recipients.length; i++) {
const msg = {
mint_to: { recipient: recipients[i] },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
funds: airdropFee > 0 ? [coin(airdropFee / 100 / 100, denom as string)] : [],
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop')
return res.transactionHash
})
return txHash
}
return {
contractAddress,
getConfig,
getStartTime,
getEndTime,
getMintPrice,
getMintCount,
getTotalMintCount,
getStatus,
mint,
purge,
updateStartTradingTime,
updateStartTime,
updateEndTime,
updateMintPrice,
updatePerAddressLimit,
mintTo,
batchMint,
airdrop,
}
}
const migrate = async (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
): Promise<MigrateResponse> => {
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
return {
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const instantiate = async (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
): Promise<InstantiateResponse> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
funds: [coin('1000000000', 'ustars')],
})
return {
contractAddress: result.contractAddress,
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const messages = (contractAddress: string) => {
const mint = (): MintMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint: {},
},
funds: [],
}
}
const purge = (): PurgeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
purge: {},
},
funds: [],
}
}
const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_start_trading_time: startTime,
},
funds: [],
}
}
const updateStartTime = (startTime: string): UpdateStartTimeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_start_time: startTime,
},
funds: [],
}
}
const updateEndTime = (endTime: string): UpdateEndTimeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_end_time: endTime,
},
funds: [],
}
}
const updateMintPrice = (price: string): UpdateMintPriceMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_mint_price: {
price: (Number(price) * 1000000).toString(),
},
},
funds: [],
}
}
const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_per_address_limit: {
per_address_limit: limit,
},
},
funds: [],
}
}
const mintTo = (recipient: string): MintToMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_to: {
recipient,
},
},
funds: [],
}
}
const batchMint = (recipient: string, batchNumber: number): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < batchNumber; i++) {
msg.push({ mint_to: { recipient } })
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const airdrop = (recipients: string[]): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < recipients.length; i++) {
msg.push({ mint_to: { recipient: recipients[i] } })
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
return {
mint,
purge,
updateStartTradingTime,
updateStartTime,
updateEndTime,
updateMintPrice,
updatePerAddressLimit,
mintTo,
batchMint,
airdrop,
}
}
return { use, instantiate, migrate, messages }
}
+2
View File
@@ -0,0 +1,2 @@
export * from './contract'
export * from './useContract'
@@ -0,0 +1,163 @@
import type { OpenEditionMinterInstance } from '../index'
import { useOpenEditionMinterContract } from '../index'
export type ExecuteType = typeof EXECUTE_TYPES[number]
export const EXECUTE_TYPES = [
'mint',
'update_start_time',
'update_end_time',
'update_mint_price',
'update_start_trading_time',
'update_per_address_limit',
'mint_to',
'purge',
] as const
export interface ExecuteListItem {
id: ExecuteType
name: string
description?: string
}
export const EXECUTE_LIST: ExecuteListItem[] = [
{
id: 'mint',
name: 'Mint',
description: `Mint a new token`,
},
{
id: 'update_mint_price',
name: 'Update Mint Price',
description: `Update the mint price per token`,
},
{
id: 'update_start_time',
name: 'Update Start Time',
description: `Update the start time for minting`,
},
{
id: 'update_end_time',
name: 'Update End Time',
description: `Update the end time for minting`,
},
{
id: 'update_start_trading_time',
name: 'Update Start Trading Time',
description: `Update start trading time for minting`,
},
{
id: 'update_per_address_limit',
name: 'Update Per Address Limit',
description: `Update token per address limit`,
},
{
id: 'mint_to',
name: 'Mint To',
description: `Mint tokens to a given address`,
},
{
id: 'purge',
name: 'Purge',
description: `Purge`,
},
]
export interface DispatchExecuteProps {
type: ExecuteType
[k: string]: unknown
}
type Select<T extends ExecuteType> = T
/** @see {@link OpenEditionMinterInstance} */
export type DispatchExecuteArgs = {
contract: string
messages?: OpenEditionMinterInstance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'mint'> }
| { type: Select<'purge'> }
| { type: Select<'update_start_time'>; startTime: string }
| { type: Select<'update_end_time'>; endTime: string }
| { type: Select<'update_mint_price'>; price: string }
| { type: Select<'update_start_trading_time'>; startTime?: string }
| { type: Select<'update_per_address_limit'>; limit: number }
| { type: Select<'mint_to'>; recipient: string }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages, txSigner } = args
if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined')
}
switch (args.type) {
case 'mint': {
return messages.mint(txSigner)
}
case 'purge': {
return messages.purge(txSigner)
}
case 'update_start_time': {
return messages.updateStartTime(txSigner, args.startTime)
}
case 'update_end_time': {
return messages.updateEndTime(txSigner, args.endTime)
}
case 'update_mint_price': {
return messages.updateMintPrice(txSigner, args.price)
}
case 'update_start_trading_time': {
return messages.updateStartTradingTime(txSigner, args.startTime)
}
case 'update_per_address_limit': {
return messages.updatePerAddressLimit(txSigner, args.limit)
}
case 'mint_to': {
return messages.mintTo(txSigner, args.recipient)
}
default: {
throw new Error('unknown execute type')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useOpenEditionMinterContract()
const { contract } = args
switch (args.type) {
case 'mint': {
return messages(contract)?.mint()
}
case 'purge': {
return messages(contract)?.purge()
}
case 'update_start_time': {
return messages(contract)?.updateStartTime(args.startTime)
}
case 'update_end_time': {
return messages(contract)?.updateEndTime(args.endTime)
}
case 'update_mint_price': {
return messages(contract)?.updateMintPrice(args.price)
}
case 'update_start_trading_time': {
return messages(contract)?.updateStartTradingTime(args.startTime as string)
}
case 'update_per_address_limit': {
return messages(contract)?.updatePerAddressLimit(args.limit)
}
case 'mint_to': {
return messages(contract)?.mintTo(args.recipient)
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}
@@ -0,0 +1,73 @@
import type { OpenEditionMinterInstance } from '../contract'
export type QueryType = typeof QUERY_TYPES[number]
export const QUERY_TYPES = [
'config',
'start_time',
'end_time',
'mint_price',
'mint_count',
'total_mint_count',
'status',
] as const
export interface QueryListItem {
id: QueryType
name: string
description?: string
}
export const QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'start_time', name: 'Start Time', description: 'View the start time for minting' },
{ id: 'end_time', name: 'End Time', description: 'View the end time for minting' },
{ id: 'mint_price', name: 'Mint Price', description: 'View the mint price' },
{
id: 'mint_count',
name: 'Mint Count for Address',
description: 'View the total amount of minted tokens for an address',
},
{
id: 'total_mint_count',
name: 'Total Mint Count',
description: 'View the total amount of minted tokens',
},
{ id: 'status', name: 'Status', description: 'View contract status' },
]
export interface DispatchQueryProps {
address: string
messages: OpenEditionMinterInstance | undefined
type: QueryType
}
export const dispatchQuery = (props: DispatchQueryProps) => {
const { address, messages, type } = props
switch (type) {
case 'config': {
return messages?.getConfig()
}
case 'status': {
return messages?.getStatus()
}
case 'start_time': {
return messages?.getStartTime()
}
case 'end_time': {
return messages?.getEndTime()
}
case 'mint_price': {
return messages?.getMintPrice()
}
case 'mint_count': {
return messages?.getMintCount(address)
}
case 'total_mint_count': {
return messages?.getTotalMintCount()
}
default: {
throw new Error('unknown query type')
}
}
}
+119
View File
@@ -0,0 +1,119 @@
import type { Coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import { useWallet } from 'contexts/wallet'
import { useCallback, useEffect, useState } from 'react'
import type {
MigrateResponse,
OpenEditionMinterContract,
OpenEditionMinterInstance,
OpenEditionMinterMessages,
} from './contract'
import { openEditionMinter as initContract } from './contract'
/*export interface InstantiateResponse {
/** The address of the newly instantiated contract *-/
readonly contractAddress: string
readonly logs: readonly logs.Log[]
/** Block height in which the transaction is included *-/
readonly height: number
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/
readonly transactionHash: string
readonly gasWanted: number
readonly gasUsed: number
}*/
interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface UseOpenEditionMinterContractProps {
instantiate: (
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
use: (customAddress: string) => OpenEditionMinterInstance | undefined
updateContractAddress: (contractAddress: string) => void
getContractAddress: () => string | undefined
messages: (contractAddress: string) => OpenEditionMinterMessages | undefined
}
export function useOpenEditionMinterContract(): UseOpenEditionMinterContractProps {
const wallet = useWallet()
const [address, setAddress] = useState<string>('')
const [openEditionMinter, setOpenEditionMinter] = useState<OpenEditionMinterContract>()
useEffect(() => {
setAddress(localStorage.getItem('contract_address') || '')
}, [])
useEffect(() => {
const OpenEditionMinterBaseContract = initContract(wallet.getClient(), wallet.address)
setOpenEditionMinter(OpenEditionMinterBaseContract)
}, [wallet])
const updateContractAddress = (contractAddress: string) => {
setAddress(contractAddress)
}
const instantiate = useCallback(
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
return new Promise((resolve, reject) => {
if (!openEditionMinter) {
reject(new Error('Contract is not initialized.'))
return
}
openEditionMinter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
})
},
[openEditionMinter, wallet],
)
const migrate = useCallback(
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
return new Promise((resolve, reject) => {
if (!openEditionMinter) {
reject(new Error('Contract is not initialized.'))
return
}
console.log(wallet.address, contractAddress, codeId)
openEditionMinter.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
})
},
[openEditionMinter, wallet],
)
const use = useCallback(
(customAddress = ''): OpenEditionMinterInstance | undefined => {
return openEditionMinter?.use(address || customAddress)
},
[openEditionMinter, address],
)
const getContractAddress = (): string | undefined => {
return address
}
const messages = useCallback(
(customAddress = ''): OpenEditionMinterMessages | undefined => {
return openEditionMinter?.messages(address || customAddress)
},
[openEditionMinter, address],
)
return {
instantiate,
use,
updateContractAddress,
getContractAddress,
messages,
migrate,
}
}
+334 -4
View File
@@ -1,16 +1,31 @@
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { toBase64, toUtf8 } from '@cosmjs/encoding'
import type { Coin } from '@cosmjs/stargate'
import type { Coin, logs } from '@cosmjs/stargate'
import { coin } from '@cosmjs/stargate'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import type { RoyaltyInfo } from '../vendingMinter/contract'
export interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
}
export interface MigrateResponse {
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export type Expiration = { at_height: number } | { at_time: string } | { never: Record<string, never> }
export interface CollectionInfo {
description?: string
image?: string
external_link?: string
explicit_content?: boolean
royalty_info?: RoyaltyInfo | undefined
}
export interface SG721Instance {
readonly contractAddress: string
@@ -65,11 +80,16 @@ export interface SG721Instance {
revokeAll: (operator: string) => Promise<string>
/// Mint a new NFT, can only be called by the contract minter
mint: (tokenId: string, owner: string, tokenURI?: string) => Promise<string> //MintMsg<T>
updateCollectionInfo: (collectionInfo: CollectionInfo) => Promise<string>
freezeCollectionInfo: () => Promise<string>
/// Burn an NFT the sender has access to
burn: (tokenId: string) => Promise<string>
batchBurn: (tokenIds: string) => Promise<string>
batchTransfer: (recipient: string, tokenIds: string) => Promise<string>
updateTokenMetadata: (tokenId: string, tokenURI: string) => Promise<string>
batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string, jsonExtensions: boolean) => Promise<string>
freezeTokenMetadata: () => Promise<string>
enableUpdatable: () => Promise<string>
}
export interface Sg721Messages {
@@ -83,6 +103,16 @@ export interface Sg721Messages {
burn: (tokenId: string) => BurnMessage
batchBurn: (tokenIds: string) => BatchBurnMessage
batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage
updateCollectionInfo: (collectionInfo: CollectionInfo) => UpdateCollectionInfoMessage
freezeCollectionInfo: () => FreezeCollectionInfoMessage
updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage
batchUpdateTokenMetadata: (
tokenIds: string,
tokenURI: string,
jsonExtensions: boolean,
) => BatchUpdateTokenMetadataMessage
freezeTokenMetadata: () => FreezeTokenMetadataMessage
enableUpdatable: () => EnableUpdatableMessage
}
export interface TransferNFTMessage {
@@ -197,6 +227,57 @@ export interface BatchTransferMessage {
funds: Coin[]
}
export interface UpdateTokenMetadataMessage {
sender: string
contract: string
msg: {
update_token_metadata: {
token_id: string
token_uri: string
}
}
funds: Coin[]
}
export interface BatchUpdateTokenMetadataMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
funds: Coin[]
}
export interface FreezeTokenMetadataMessage {
sender: string
contract: string
msg: { freeze_token_metadata: Record<string, never> }
funds: Coin[]
}
export interface EnableUpdatableMessage {
sender: string
contract: string
msg: { enable_updatable: Record<string, never> }
funds: Coin[]
}
export interface UpdateCollectionInfoMessage {
sender: string
contract: string
msg: {
update_collection_info: {
collection_info: CollectionInfo
}
}
funds: Coin[]
}
export interface FreezeCollectionInfoMessage {
sender: string
contract: string
msg: { freeze_collection_info: Record<string, never> }
funds: Coin[]
}
export interface SG721Contract {
instantiate: (
senderAddress: string,
@@ -206,6 +287,13 @@ export interface SG721Contract {
admin?: string,
) => Promise<InstantiateResponse>
migrate: (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
) => Promise<MigrateResponse>
use: (contractAddress: string) => SG721Instance
messages: (contractAddress: string) => Sg721Messages
@@ -513,6 +601,129 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
return res.transactionHash
}
// eslint-disable-next-line @typescript-eslint/no-shadow
const updateCollectionInfo = async (collectionInfo: CollectionInfo): Promise<string> => {
const res = await client.execute(
txSigner,
contractAddress,
{
update_collection_info: { collection_info: collectionInfo },
},
'auto',
'',
)
return res.transactionHash
}
const batchUpdateTokenMetadata = async (
tokenIds: string,
baseURI: string,
jsonExtensions: boolean,
): Promise<string> => {
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
if (tokenIds.includes(':')) {
const [start, end] = tokenIds.split(':').map(Number)
for (let i = start; i <= end; i++) {
const msg = {
update_token_metadata: {
token_id: i.toString(),
token_uri: `${baseURI}/${i}${jsonExtensions ? '.json' : ''}`,
},
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: txSigner,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
} else {
const tokenNumbers = tokenIds.split(',').map(Number)
for (let i = 0; i < tokenNumbers.length; i++) {
const msg = {
update_token_metadata: {
token_id: tokenNumbers[i].toString(),
token_uri: `${baseURI}/${tokenNumbers[i]}${jsonExtensions ? '.json' : ''}`,
},
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: txSigner,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
}
const res = await client.signAndBroadcast(txSigner, executeContractMsgs, 'auto', 'batch update metadata')
return res.transactionHash
}
const updateTokenMetadata = async (tokenId: string, tokenURI: string): Promise<string> => {
const res = await client.execute(
txSigner,
contractAddress,
{
update_token_metadata: {
token_id: tokenId,
token_uri: tokenURI,
},
},
'auto',
'',
)
return res.transactionHash
}
const freezeCollectionInfo = async (): Promise<string> => {
const res = await client.execute(
txSigner,
contractAddress,
{
freeze_collection_info: {},
},
'auto',
'',
)
return res.transactionHash
}
const freezeTokenMetadata = async (): Promise<string> => {
const res = await client.execute(
txSigner,
contractAddress,
{
freeze_token_metadata: {},
},
'auto',
'',
)
return res.transactionHash
}
const enableUpdatable = async (): Promise<string> => {
const res = await client.execute(
txSigner,
contractAddress,
{
enable_updatable: {},
},
'auto',
'',
[coin('500000000', 'ustars')],
)
return res.transactionHash
}
return {
contractAddress,
ownerOf,
@@ -537,6 +748,12 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
burn,
batchBurn,
batchTransfer,
updateCollectionInfo,
freezeCollectionInfo,
updateTokenMetadata,
batchUpdateTokenMetadata,
freezeTokenMetadata,
enableUpdatable,
}
}
@@ -548,7 +765,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
admin?: string,
): Promise<InstantiateResponse> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
funds: [coin('1000000000', 'ustars')],
funds: [coin('3000000000', 'ustars')],
memo: '',
admin,
})
@@ -558,6 +775,19 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
}
}
const migrate = async (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
): Promise<MigrateResponse> => {
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
return {
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const messages = (contractAddress: string) => {
const transferNft = (recipient: string, tokenId: string) => {
return {
@@ -720,6 +950,100 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
}
}
const batchUpdateTokenMetadata = (
tokenIds: string,
baseURI: string,
jsonExtensions: boolean,
): BatchUpdateTokenMetadataMessage => {
const msg: Record<string, unknown>[] = []
if (tokenIds.includes(':')) {
const [start, end] = tokenIds.split(':').map(Number)
for (let i = start; i <= end; i++) {
msg.push({
update_token_metadata: {
token_id: i.toString(),
token_uri: `${baseURI}/${i}${jsonExtensions ? '.json' : ''}`,
},
})
}
} else {
const tokenNumbers = tokenIds.split(',').map(Number)
for (let i = 0; i < tokenNumbers.length; i++) {
msg.push({
update_token_metadata: {
token_id: tokenNumbers[i].toString(),
token_uri: `${baseURI}/${tokenNumbers[i]}${jsonExtensions ? '.json' : ''}`,
},
})
}
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const updateTokenMetadata = (tokenId: string, tokenURI: string) => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_token_metadata: {
token_id: tokenId,
token_uri: tokenURI,
},
},
funds: [],
}
}
const freezeTokenMetadata = () => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
freeze_token_metadata: {},
},
funds: [],
}
}
const enableUpdatable = () => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
enable_updatable: {},
},
funds: [coin('500000000', 'ustars')],
}
}
const updateCollectionInfo = (collectionInfo: CollectionInfo) => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_collection_info: { collection_info: collectionInfo },
},
funds: [],
}
}
const freezeCollectionInfo = () => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
freeze_collection_info: {},
},
funds: [],
}
}
return {
transferNft,
sendNft,
@@ -731,8 +1055,14 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
burn,
batchBurn,
batchTransfer,
updateCollectionInfo,
freezeCollectionInfo,
updateTokenMetadata,
batchUpdateTokenMetadata,
freezeTokenMetadata,
enableUpdatable,
}
}
return { use, instantiate, messages }
return { use, instantiate, migrate, messages }
}

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