├── .gitignore ├── .husky └── pre-commit ├── README.md ├── contracts └── mint_nft │ ├── .gitignore │ ├── Move.toml │ └── sources │ ├── big_vector.move │ ├── bucket_table.move │ └── minting.move └── minting-tool ├── cli ├── .eslintignore ├── .eslintrc.js ├── .gitignroe ├── .prettierrc ├── docs │ ├── .nojekyll │ ├── assets │ │ ├── highlight.css │ │ ├── main.js │ │ ├── search.js │ │ └── style.css │ ├── classes │ │ └── TokenSDK.html │ ├── index.html │ └── modules.html ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src │ ├── asset-uploader.ts │ ├── cli.ts │ ├── nft-mint.ts │ ├── templates │ │ ├── collection.json │ │ ├── config.json │ │ └── token.json │ ├── tests │ │ ├── assets │ │ │ ├── images │ │ │ │ ├── 1.png │ │ │ │ ├── 2.png │ │ │ │ ├── 3.png │ │ │ │ ├── 4.png │ │ │ │ └── 5.png │ │ │ └── json │ │ │ │ ├── 1.json │ │ │ │ ├── 2.json │ │ │ │ ├── 3.json │ │ │ │ ├── 4.json │ │ │ │ └── 5.json │ │ └── run_test.md │ └── utils.ts ├── tsconfig.json └── typedoc.json └── minting-site ├── .gitignore ├── README.md ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── assets │ └── aptos-zero.png ├── components │ └── Navbar │ │ ├── index.tsx │ │ ├── navbar.module.css │ │ └── wallet-adapter.css ├── index.css ├── index.tsx ├── logo.svg ├── pages │ └── home │ │ ├── home.module.css │ │ └── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts └── setupTests.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | **/node_modules 3 | package-lock.json 4 | 5 | # build 6 | dist 7 | /types 8 | 9 | # eslint 10 | .eslintcache 11 | 12 | # jest 13 | coverage 14 | 15 | src/test/assets 16 | 17 | #.idea 18 | .idea 19 | 20 | .DS_Store 21 | 22 | .aptos 23 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | cd minting-tool/cli 5 | yarn fmt && yarn lint 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Token Tooling 2 | 3 | ## Aptos NFT mint tool 4 | 5 | The Aptos Non-Fungible Token (NFT) Minting Tool includes a CLI, a mint contract, and a template minting site that aim to lower the barriers for NFT creators to launch NFTs on Aptos. Currently, supported features include: 6 | 7 | - Paying storage services with APTs. The Aptos NFT mint tool uploads NFT assets to Arweave through the [Bundlr](https://bundlr.network/) service. Bundlr makes it easy for creators to pay for the storage with APTs. 8 | - Presale support through whitelisted addresses. 9 | - Randomizing NFT mint order to reduce the impact of the rarity sniping tool. 10 | - Supporting large-scale collections. The tool uploads assets in parallel and prepares the NFTs data in batches. 11 | - Tracking progresses in a local DB 12 | 13 | For details about using the tool see: [https://aptos.dev/concepts/coin-and-token/nft-minting-tool/](https://aptos.dev/concepts/coin-and-token/nft-minting-tool/) 14 | -------------------------------------------------------------------------------- /contracts/mint_nft/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /contracts/mint_nft/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "NFTMint" 3 | version = "0.0.0" 4 | 5 | [dependencies.AptosFramework] 6 | git = 'https://github.com/aptos-labs/aptos-core.git' 7 | rev = 'testnet' 8 | subdir = 'aptos-move/framework/aptos-framework' 9 | 10 | [dependencies.AptosToken] 11 | git = 'https://github.com/aptos-labs/aptos-core.git' 12 | rev = 'testnet' 13 | subdir = 'aptos-move/framework/aptos-token' 14 | 15 | [addresses] 16 | aptos_framework = "0x1" 17 | source_addr = "_" 18 | mint_nft = "_" 19 | 20 | [dev-addresses] 21 | source_addr = "0xcafe" 22 | mint_nft = "0xc3bb8488ab1a5815a9d543d7e41b0e0df46a7396f89b22821f07a4362f75ddc5" 23 | -------------------------------------------------------------------------------- /contracts/mint_nft/sources/big_vector.move: -------------------------------------------------------------------------------- 1 | module mint_nft::big_vector { 2 | use std::error; 3 | use std::vector; 4 | use aptos_std::table_with_length::{Self, TableWithLength}; 5 | 6 | /// Vector index is out of bounds 7 | const EINDEX_OUT_OF_BOUNDS: u64 = 1; 8 | /// Vector is full 9 | const EOUT_OF_CAPACITY: u64 = 2; 10 | /// Cannot destroy a non-empty vector 11 | const EVECTOR_NOT_EMPTY: u64 = 3; 12 | 13 | /// Index of the value in the buckets. 14 | struct BigVectorIndex has copy, drop, store { 15 | bucket_index: u64, 16 | vec_index: u64, 17 | } 18 | 19 | /// A Scalable vector implementation based on tables, elements are grouped into buckets with `bucket_size`. 20 | struct BigVector has store { 21 | buckets: TableWithLength>, 22 | end_index: BigVectorIndex, 23 | num_buckets: u64, 24 | bucket_size: u64 25 | } 26 | 27 | /// Regular Vector API 28 | 29 | /// Create an empty vector. 30 | public fun new(bucket_size: u64): BigVector { 31 | assert!(bucket_size > 0, 0); 32 | BigVector { 33 | buckets: table_with_length::new(), 34 | end_index: BigVectorIndex { 35 | bucket_index: 0, 36 | vec_index: 0, 37 | }, 38 | num_buckets: 0, 39 | bucket_size, 40 | } 41 | } 42 | 43 | /// Create an empty vector with `num_buckets` reserved. 44 | public fun new_with_capacity(bucket_size: u64, num_buckets: u64): BigVector { 45 | let v = new(bucket_size); 46 | reserve(&mut v, num_buckets); 47 | v 48 | } 49 | 50 | /// Destroy the vector `v`. 51 | /// Aborts if `v` is not empty. 52 | public fun destroy_empty(v: BigVector) { 53 | assert!(is_empty(&v), error::invalid_argument(EVECTOR_NOT_EMPTY)); 54 | shrink_to_fit(&mut v); 55 | let BigVector { buckets, end_index: _, num_buckets: _, bucket_size: _ } = v; 56 | table_with_length::destroy_empty(buckets); 57 | } 58 | 59 | /// Add element `val` to the end of the vector `v`. It grows the buckets when the current buckets are full. 60 | /// This operation will cost more gas when it adds new bucket. 61 | public fun push_back(v: &mut BigVector, val: T) { 62 | if (v.end_index.bucket_index == v.num_buckets) { 63 | table_with_length::add(&mut v.buckets, v.num_buckets, vector::empty()); 64 | v.num_buckets = v.num_buckets + 1; 65 | }; 66 | vector::push_back(table_with_length::borrow_mut(&mut v.buckets, v.end_index.bucket_index), val); 67 | increment_index(&mut v.end_index, v.bucket_size); 68 | } 69 | 70 | /// Add element `val` to the end of the vector `v`. 71 | /// Aborts if all buckets are full. 72 | /// It can split the gas responsibility between user of the vector and owner of the vector. 73 | /// Call `reserve` to explicit add more buckets. 74 | public fun push_back_no_grow(v: &mut BigVector, val: T) { 75 | assert!(v.end_index.bucket_index < v.num_buckets, error::invalid_argument(EOUT_OF_CAPACITY)); 76 | vector::push_back(table_with_length::borrow_mut(&mut v.buckets, v.end_index.bucket_index), val); 77 | increment_index(&mut v.end_index, v.bucket_size); 78 | } 79 | 80 | /// Pop an element from the end of vector `v`. It doesn't shrink the buckets even if they're empty. 81 | /// Call `shrink_to_fit` explicity to deallocate empty buckets. 82 | /// Aborts if `v` is empty. 83 | public fun pop_back(v: &mut BigVector): T { 84 | assert!(!is_empty(v), error::invalid_argument(EINDEX_OUT_OF_BOUNDS)); 85 | decrement_index(&mut v.end_index, v.bucket_size); 86 | let val = vector::pop_back(table_with_length::borrow_mut(&mut v.buckets, v.end_index.bucket_index)); 87 | val 88 | } 89 | 90 | /// Acquire an immutable reference to the `i`th element of the vector `v`. 91 | /// Aborts if `i` is out of bounds. 92 | public fun borrow(v: &BigVector, index: &BigVectorIndex): &T { 93 | vector::borrow(table_with_length::borrow(&v.buckets, index.bucket_index), index.vec_index) 94 | } 95 | 96 | /// Return a mutable reference to the `i`th element in the vector `v`. 97 | /// Aborts if `i` is out of bounds. 98 | public fun borrow_mut(v: &mut BigVector, index: &BigVectorIndex): &mut T { 99 | vector::borrow_mut(table_with_length::borrow_mut(&mut v.buckets, index.bucket_index), index.vec_index) 100 | } 101 | 102 | /// Return the length of the vector. 103 | public fun length(v: &BigVector): u64 { 104 | v.end_index.bucket_index * v.bucket_size + v.end_index.vec_index 105 | } 106 | 107 | /// Return `true` if the vector `v` has no elements and `false` otherwise. 108 | public fun is_empty(v: &BigVector): bool { 109 | length(v) == 0 110 | } 111 | 112 | /// Swap the `i`th element of the vector `v` with the last element and then pop the vector. 113 | /// This is O(1), but does not preserve ordering of elements in the vector. 114 | /// Aborts if `i` is out of bounds. 115 | public fun swap_remove(v: &mut BigVector, index: &BigVectorIndex): T { 116 | let last_val = pop_back(v); 117 | // if the requested value is the last one, return it 118 | if (v.end_index.bucket_index == index.bucket_index && v.end_index.vec_index == index.vec_index) { 119 | return last_val 120 | }; 121 | // because the lack of mem::swap, here we swap remove the requested value from the bucket 122 | // and append the last_val to the bucket then swap the last bucket val back 123 | let bucket = table_with_length::borrow_mut(&mut v.buckets, index.bucket_index); 124 | let bucket_len = vector::length(bucket); 125 | let val = vector::swap_remove(bucket, index.vec_index); 126 | vector::push_back(bucket, last_val); 127 | vector::swap(bucket, index.vec_index, bucket_len - 1); 128 | val 129 | } 130 | 131 | /// Return true if `val` is in the vector `v`. 132 | public fun contains(v: &BigVector, val: &T): bool { 133 | if (is_empty(v)) return false; 134 | let (exist, _) = index_of(v, val); 135 | exist 136 | } 137 | 138 | /// Return `(true, i)` if `val` is in the vector `v` at index `i`. 139 | /// Otherwise, returns `(false, 0)`. 140 | public fun index_of(v: &BigVector, val: &T): (bool, u64) { 141 | let i = 0; 142 | let len = length(v); 143 | let index = bucket_index(v, 0); 144 | while (i < len) { 145 | if (borrow(v, &index) == val) { 146 | return (true, i) 147 | }; 148 | i = i + 1; 149 | increment_index(&mut index, v.bucket_size); 150 | }; 151 | (false, 0) 152 | } 153 | 154 | /// Buckets related API 155 | 156 | /// Return corresponding BigVectorIndex for `i`, we can avoid this once table supports lookup by value instead of by reference. 157 | /// Aborts if `i` is out of bounds. 158 | public fun bucket_index(v: &BigVector, i: u64): BigVectorIndex { 159 | assert!(i < length(v), EINDEX_OUT_OF_BOUNDS); 160 | BigVectorIndex { 161 | bucket_index: i / v.bucket_size, 162 | vec_index: i % v.bucket_size, 163 | } 164 | } 165 | 166 | /// Return the bucket size of the vector. 167 | public fun bucket_size(v: &BigVector): u64 { 168 | v.bucket_size 169 | } 170 | 171 | /// Equivalent to i = i + 1 for BigVectorIndex with `bucket_size`. 172 | public fun increment_index(index: &mut BigVectorIndex, bucket_size: u64) { 173 | if (index.vec_index + 1 == bucket_size) { 174 | index.bucket_index = index.bucket_index + 1; 175 | index.vec_index = 0; 176 | } else { 177 | index.vec_index = index.vec_index + 1; 178 | } 179 | } 180 | 181 | /// Equivalent to i = i - 1 for BigVectorIndex with `bucket_size`. 182 | /// Aborts if `i` becomes out of bounds. 183 | public fun decrement_index(index: &mut BigVectorIndex, bucket_size: u64) { 184 | if (index.vec_index == 0) { 185 | assert!(index.bucket_index > 0, EINDEX_OUT_OF_BOUNDS); 186 | index.bucket_index = index.bucket_index - 1; 187 | index.vec_index = bucket_size - 1; 188 | } else { 189 | index.vec_index = index.vec_index - 1; 190 | } 191 | } 192 | 193 | /// Reserve `additional_buckets` more buckets. 194 | public fun reserve(v: &mut BigVector, additional_buckets: u64) { 195 | while (additional_buckets > 0) { 196 | table_with_length::add(&mut v.buckets, v.num_buckets, vector::empty()); 197 | v.num_buckets = v.num_buckets + 1; 198 | additional_buckets = additional_buckets - 1; 199 | } 200 | } 201 | 202 | /// Shrink the buckets to fit the current length. 203 | public fun shrink_to_fit(v: &mut BigVector) { 204 | while (v.num_buckets > buckets_required(&v.end_index)) { 205 | v.num_buckets = v.num_buckets - 1; 206 | let v = table_with_length::remove(&mut v.buckets, v.num_buckets); 207 | vector::destroy_empty(v); 208 | } 209 | } 210 | 211 | fun buckets_required(end_index: &BigVectorIndex): u64 { 212 | let additional = if (end_index.vec_index == 0) { 0 } else { 1 }; 213 | end_index.bucket_index + additional 214 | } 215 | 216 | #[test] 217 | fun big_vector_test() { 218 | let v = new(5); 219 | let i = 0; 220 | while (i < 100) { 221 | push_back(&mut v, i); 222 | i = i + 1; 223 | }; 224 | let j = 0; 225 | while (j < 100) { 226 | let index = bucket_index(&v, j); 227 | let val = borrow(&v, &index); 228 | assert!(*val == j, 0); 229 | j = j + 1; 230 | }; 231 | while (i > 0) { 232 | i = i - 1; 233 | let (exist, index) = index_of(&v, &i); 234 | let j = pop_back(&mut v); 235 | assert!(exist, 0); 236 | assert!(index == i, 0); 237 | assert!(j == i, 0); 238 | }; 239 | while (i < 100) { 240 | push_back(&mut v, i); 241 | i = i + 1; 242 | }; 243 | let last_index = bucket_index(&v, length(&v) - 1); 244 | assert!(swap_remove(&mut v, &last_index) == 99, 0); 245 | let first_index = bucket_index(&v, 0); 246 | assert!(swap_remove(&mut v, &first_index) == 0, 0); 247 | while (length(&v) > 0) { 248 | // the vector is always [N, 1, 2, ... N-1] with repetitive swap_remove(&mut v, 0) 249 | let expected = length(&v); 250 | let index = bucket_index(&v, 0); 251 | let val = swap_remove(&mut v, &index); 252 | assert!(val == expected, 0); 253 | }; 254 | shrink_to_fit(&mut v); 255 | destroy_empty(v); 256 | } 257 | 258 | #[test] 259 | #[expected_failure] 260 | fun big_vector_need_grow() { 261 | let v = new_with_capacity(5, 1); 262 | let i = 0; 263 | while (i < 6) { 264 | push_back_no_grow(&mut v, i); 265 | i = i + 1; 266 | }; 267 | destroy_empty(v); 268 | } 269 | 270 | #[test] 271 | fun big_vector_reserve_and_shrink() { 272 | let v = new (10); 273 | reserve(&mut v, 10); 274 | assert!(v.num_buckets == 10, 0); 275 | let i = 0; 276 | while (i < 100) { 277 | push_back_no_grow(&mut v, i); 278 | i = i + 1; 279 | }; 280 | while (i < 120) { 281 | push_back(&mut v, i); 282 | i = i + 1; 283 | }; 284 | while (i > 90) { 285 | pop_back(&mut v); 286 | i = i - 1; 287 | }; 288 | assert!(v.num_buckets == 12, 0); 289 | shrink_to_fit(&mut v); 290 | assert!(v.num_buckets == 9, 0); 291 | while (i > 55) { 292 | pop_back(&mut v); 293 | i = i - 1; 294 | }; 295 | shrink_to_fit(&mut v); 296 | assert!(v.num_buckets == 6, 0); 297 | while (i > 0) { 298 | pop_back(&mut v); 299 | i = i - 1; 300 | }; 301 | shrink_to_fit(&mut v); 302 | destroy_empty(v); 303 | } 304 | 305 | #[test] 306 | fun big_vector_empty_contains() { 307 | let v = new (10); 308 | assert!(!contains(&v, &(1 as u64)), 0); 309 | destroy_empty(v); 310 | } 311 | } -------------------------------------------------------------------------------- /contracts/mint_nft/sources/bucket_table.move: -------------------------------------------------------------------------------- 1 | /// A bucket table implementation based on linear hashing. (https://en.wikipedia.org/wiki/Linear_hashing) 2 | /// Compare to Table, it uses less storage slots but has higher chance of collision, it's a trade-off between space and time. 3 | /// Compare to other implementation, linear hashing splits one bucket a time instead of doubling buckets when expanding to avoid unexpected gas cost. 4 | /// BucketTable uses faster hash function SipHash instead of cryptographically secure hash functions like sha3-256 since it tolerates collisions. 5 | module mint_nft::bucket_table { 6 | use std::error; 7 | use std::vector; 8 | use aptos_std::aptos_hash::sip_hash_from_value; 9 | use aptos_std::table_with_length::{Self, TableWithLength}; 10 | 11 | const TARGET_LOAD_PER_BUCKET: u64 = 10; 12 | const SPLIT_THRESHOLD: u64 = 75; 13 | 14 | /// Key not found in the bucket table 15 | const ENOT_FOUND: u64 = 1; 16 | /// Bucket table capacity must be larger than 0 17 | const EZERO_CAPACITY: u64 = 2; 18 | /// Cannot destroy non-empty hashmap 19 | const ENOT_EMPTY: u64 = 3; 20 | /// Key already exists 21 | const EALREADY_EXIST: u64 = 4; 22 | 23 | /// BucketTable entry contains both the key and value. 24 | struct Entry has store { 25 | hash: u64, 26 | key: K, 27 | value: V, 28 | } 29 | 30 | struct BucketTable has store { 31 | buckets: TableWithLength>>, 32 | num_buckets: u64, 33 | // number of bits to represent num_buckets 34 | level: u8, 35 | // total number of items 36 | len: u64, 37 | } 38 | 39 | /// Create an empty BucketTable with `initial_buckets` buckets. 40 | public fun new(initial_buckets: u64): BucketTable { 41 | assert!(initial_buckets > 0, error::invalid_argument(EZERO_CAPACITY)); 42 | let buckets = table_with_length::new(); 43 | table_with_length::add(&mut buckets, 0, vector::empty()); 44 | let map = BucketTable { 45 | buckets, 46 | num_buckets: 1, 47 | level: 0, 48 | len: 0, 49 | }; 50 | split(&mut map, initial_buckets - 1); 51 | map 52 | } 53 | 54 | /// Destroy empty map. 55 | /// Aborts if it's not empty. 56 | public fun destroy_empty(map: BucketTable) { 57 | assert!(map.len == 0, error::invalid_argument(ENOT_EMPTY)); 58 | let i = 0; 59 | while (i < map.num_buckets) { 60 | vector::destroy_empty(table_with_length::remove(&mut map.buckets, i)); 61 | i = i + 1; 62 | }; 63 | let BucketTable {buckets, num_buckets: _, level: _, len: _} = map; 64 | table_with_length::destroy_empty(buckets); 65 | } 66 | 67 | /// Add (key, value) pair in the hash map, it may grow one bucket if current load factor exceeds the threshold. 68 | /// Note it may not split the actual overflowed bucket. 69 | /// Abort if `key` already exists. 70 | public fun add(map: &mut BucketTable, key: K, value: V) { 71 | let hash = sip_hash_from_value(&key); 72 | let index = bucket_index(map.level, map.num_buckets, hash); 73 | let bucket = table_with_length::borrow_mut(&mut map.buckets, index); 74 | let i = 0; 75 | let len = vector::length(bucket); 76 | while (i < len) { 77 | let entry = vector::borrow(bucket, i); 78 | assert!(&entry.key != &key, error::invalid_argument(EALREADY_EXIST)); 79 | i = i + 1; 80 | }; 81 | vector::push_back(bucket, Entry {hash, key, value}); 82 | map.len = map.len + 1; 83 | 84 | if (load_factor(map) > SPLIT_THRESHOLD) { 85 | split_one_bucket(map); 86 | } 87 | } 88 | 89 | fun xor(a: u64, b: u64): u64 { 90 | a ^ b 91 | } 92 | spec xor { // TODO: temporary mockup until Prover supports the operator `^`. 93 | pragma opaque; 94 | pragma verify = false; 95 | } 96 | 97 | /// Split the next bucket into two and re-insert existing items. 98 | fun split_one_bucket(map: &mut BucketTable) { 99 | let new_bucket_index = map.num_buckets; 100 | // the next bucket to split is num_bucket without the most significant bit. 101 | let to_split = xor(new_bucket_index, (1 << map.level)); 102 | let new_bucket = vector::empty(); 103 | map.num_buckets = new_bucket_index + 1; 104 | // if the whole level is splitted once, bump the level. 105 | if (to_split + 1 == 1 << map.level) { 106 | map.level = map.level + 1; 107 | }; 108 | let old_bucket = table_with_length::borrow_mut(&mut map.buckets, to_split); 109 | // partition the bucket. after the loop, i == j and [0..i) stays in old bucket, [j..len) goes to new bucket 110 | let i = 0; 111 | let j = vector::length(old_bucket); 112 | let len = j; 113 | while (i < j) { 114 | let entry = vector::borrow(old_bucket, i); 115 | let index = bucket_index(map.level, map.num_buckets, entry.hash); 116 | if (index == new_bucket_index) { 117 | j = j - 1; 118 | vector::swap(old_bucket, i, j); 119 | } else { 120 | i = i + 1; 121 | }; 122 | }; 123 | while (j < len) { 124 | let entry = vector::pop_back(old_bucket); 125 | vector::push_back(&mut new_bucket, entry); 126 | len = len - 1; 127 | }; 128 | table_with_length::add(&mut map.buckets, new_bucket_index, new_bucket); 129 | } 130 | 131 | /// Return the expected bucket index to find the hash. 132 | fun bucket_index(level: u8, num_buckets: u64, hash: u64): u64 { 133 | let index = hash % (1 << (level + 1)); 134 | if (index < num_buckets) { 135 | // in existing bucket 136 | index 137 | } else { 138 | // in unsplitted bucket 139 | index % (1 << level) 140 | } 141 | } 142 | 143 | /// Acquire an immutable reference to the value which `key` maps to. 144 | /// Aborts if there is no entry for `key`. 145 | /// The requirement of &mut BucketTable is to bypass the borrow checker issue described in https://github.com/move-language/move/issues/95 146 | /// Once Table supports borrow by K, we can remove the &mut 147 | public fun borrow(map: &mut BucketTable, key: K): &V { 148 | let index = bucket_index(map.level, map.num_buckets, sip_hash_from_value(&key)); 149 | let bucket = table_with_length::borrow_mut(&mut map.buckets, index); 150 | let i = 0; 151 | let len = vector::length(bucket); 152 | while (i < len) { 153 | let entry = vector::borrow(bucket, i); 154 | if (&entry.key == &key) { 155 | return &entry.value 156 | }; 157 | i = i + 1; 158 | }; 159 | abort error::invalid_argument(ENOT_FOUND) 160 | } 161 | 162 | /// Acquire a mutable reference to the value which `key` maps to. 163 | /// Aborts if there is no entry for `key`. 164 | public fun borrow_mut(map: &mut BucketTable, key: K): &mut V { 165 | let index = bucket_index(map.level, map.num_buckets, sip_hash_from_value(&key)); 166 | let bucket = table_with_length::borrow_mut(&mut map.buckets, index); 167 | let i = 0; 168 | let len = vector::length(bucket); 169 | while (i < len) { 170 | let entry = vector::borrow_mut(bucket, i); 171 | if (&entry.key == &key) { 172 | return &mut entry.value 173 | }; 174 | i = i + 1; 175 | }; 176 | abort error::invalid_argument(ENOT_FOUND) 177 | } 178 | 179 | /// Returns true iff `table` contains an entry for `key`. 180 | public fun contains(map: &BucketTable, key: &K): bool { 181 | let index = bucket_index(map.level, map.num_buckets, sip_hash_from_value(key)); 182 | let bucket = table_with_length::borrow(&map.buckets, index); 183 | let i = 0; 184 | let len = vector::length(bucket); 185 | while (i < len) { 186 | let entry = vector::borrow(bucket, i); 187 | if (&entry.key == key) { 188 | return true 189 | }; 190 | i = i + 1; 191 | }; 192 | false 193 | } 194 | 195 | /// Remove from `table` and return the value which `key` maps to. 196 | /// Aborts if there is no entry for `key`. 197 | public fun remove(map: &mut BucketTable, key: &K): V { 198 | let index = bucket_index(map.level, map.num_buckets, sip_hash_from_value(key)); 199 | let bucket = table_with_length::borrow_mut(&mut map.buckets, index); 200 | let i = 0; 201 | let len = vector::length(bucket); 202 | while (i < len) { 203 | let entry = vector::borrow(bucket, i); 204 | if (&entry.key == key) { 205 | let Entry {hash:_, key:_, value} = vector::swap_remove(bucket, i); 206 | map.len = map.len - 1; 207 | return value 208 | }; 209 | i = i + 1; 210 | }; 211 | abort error::invalid_argument(ENOT_FOUND) 212 | } 213 | 214 | /// Returns the length of the table, i.e. the number of entries. 215 | public fun length(map: &BucketTable): u64 { 216 | map.len 217 | } 218 | 219 | /// Return the load factor of the hashmap. 220 | public fun load_factor(map: &BucketTable): u64 { 221 | map.len * 100 / (map.num_buckets * TARGET_LOAD_PER_BUCKET) 222 | } 223 | 224 | /// Reserve `additional_buckets` more buckets. 225 | public fun split(map: &mut BucketTable, additional_buckets: u64) { 226 | while (additional_buckets > 0) { 227 | additional_buckets = additional_buckets - 1; 228 | split_one_bucket(map); 229 | } 230 | } 231 | 232 | #[test] 233 | fun hash_map_test() { 234 | let map = new(1); 235 | let i = 0; 236 | while (i < 200) { 237 | add(&mut map, i, i); 238 | i = i + 1; 239 | }; 240 | assert!(length(&map) == 200, 0); 241 | i = 0; 242 | while (i < 200) { 243 | *borrow_mut(&mut map, i) = i * 2; 244 | assert!(*borrow(&mut map, i) == i * 2, 0); 245 | i = i + 1; 246 | }; 247 | i = 0; 248 | assert!(map.num_buckets > 20, map.num_buckets); 249 | while (i < 200) { 250 | assert!(contains(&map, &i), 0); 251 | assert!(remove(&mut map, &i) == i * 2, 0); 252 | i = i + 1; 253 | }; 254 | destroy_empty(map); 255 | } 256 | 257 | #[test] 258 | fun hash_map_split_test() { 259 | let map: BucketTable = new(1); 260 | let i = 1; 261 | let level = 0; 262 | while (i <= 256) { 263 | assert!(map.num_buckets == i, 0); 264 | assert!(map.level == level, i); 265 | split_one_bucket(&mut map); 266 | i = i + 1; 267 | if (i == 1 << (level + 1)) { 268 | level = level + 1; 269 | }; 270 | }; 271 | destroy_empty(map); 272 | } 273 | 274 | #[test] 275 | fun hash_map_bucket_index_test() { 276 | let map: BucketTable = new(8); 277 | assert!(map.level == 3, 0); 278 | let i = 0; 279 | while (i < 4) { 280 | split_one_bucket(&mut map); 281 | i = i + 1; 282 | }; 283 | assert!(map.level == 3, 0); 284 | assert!(map.num_buckets == 12, 0); 285 | i = 0; 286 | while (i < 256) { 287 | let j = i & 15; // i % 16 288 | if (j >= map.num_buckets) { 289 | j = xor(j, 8); // i % 8 290 | }; 291 | let index = bucket_index(map.level, map.num_buckets, i); 292 | assert!(index == j, 0); 293 | i = i + 1; 294 | }; 295 | destroy_empty(map); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /minting-tool/cli/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages/**/node_modules/ 3 | dist/** 4 | **/*.test.ts 5 | -------------------------------------------------------------------------------- /minting-tool/cli/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | ignorePatterns: ["*.js", "examples/*"], 8 | extends: ["airbnb-base", "airbnb-typescript/base", "prettier"], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | tsconfigRootDir: __dirname, 12 | project: ["tsconfig.json"], 13 | ecmaVersion: "latest", 14 | sourceType: "module", 15 | }, 16 | plugins: ["@typescript-eslint"], 17 | rules: { 18 | quotes: ["error", "double"], 19 | "max-len": ["error", 120], 20 | "import/extensions": ["error", "never"], 21 | "max-classes-per-file": ["error", 10], 22 | "import/prefer-default-export": "off", 23 | "object-curly-newline": "off", 24 | "no-use-before-define": "off", 25 | "no-unused-vars": "off", 26 | "@typescript-eslint/no-use-before-define": [ 27 | "error", 28 | { functions: false, classes: false }, 29 | ], 30 | "@typescript-eslint/no-unused-vars": ["error"], 31 | "class-methods-use-this": "off", 32 | "no-console": "off", 33 | }, 34 | settings: { 35 | "import/resolver": { 36 | node: { 37 | extensions: [".js", ".jsx", ".ts", ".tsx"], 38 | }, 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /minting-tool/cli/.gitignroe: -------------------------------------------------------------------------------- 1 | /coverage -------------------------------------------------------------------------------- /minting-tool/cli/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /minting-tool/cli/docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /minting-tool/cli/docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #008000; 3 | --dark-hl-0: #6A9955; 4 | --light-hl-1: #0000FF; 5 | --dark-hl-1: #569CD6; 6 | --light-hl-2: #000000; 7 | --dark-hl-2: #D4D4D4; 8 | --light-hl-3: #0070C1; 9 | --dark-hl-3: #4FC1FF; 10 | --light-hl-4: #795E26; 11 | --dark-hl-4: #DCDCAA; 12 | --light-code-background: #FFFFFF; 13 | --dark-code-background: #1E1E1E; 14 | } 15 | 16 | @media (prefers-color-scheme: light) { :root { 17 | --hl-0: var(--light-hl-0); 18 | --hl-1: var(--light-hl-1); 19 | --hl-2: var(--light-hl-2); 20 | --hl-3: var(--light-hl-3); 21 | --hl-4: var(--light-hl-4); 22 | --code-background: var(--light-code-background); 23 | } } 24 | 25 | @media (prefers-color-scheme: dark) { :root { 26 | --hl-0: var(--dark-hl-0); 27 | --hl-1: var(--dark-hl-1); 28 | --hl-2: var(--dark-hl-2); 29 | --hl-3: var(--dark-hl-3); 30 | --hl-4: var(--dark-hl-4); 31 | --code-background: var(--dark-code-background); 32 | } } 33 | 34 | :root[data-theme='light'] { 35 | --hl-0: var(--light-hl-0); 36 | --hl-1: var(--light-hl-1); 37 | --hl-2: var(--light-hl-2); 38 | --hl-3: var(--light-hl-3); 39 | --hl-4: var(--light-hl-4); 40 | --code-background: var(--light-code-background); 41 | } 42 | 43 | :root[data-theme='dark'] { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --code-background: var(--dark-code-background); 50 | } 51 | 52 | .hl-0 { color: var(--hl-0); } 53 | .hl-1 { color: var(--hl-1); } 54 | .hl-2 { color: var(--hl-2); } 55 | .hl-3 { color: var(--hl-3); } 56 | .hl-4 { color: var(--hl-4); } 57 | pre, code { background: var(--code-background); } 58 | -------------------------------------------------------------------------------- /minting-tool/cli/docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"kinds\":{\"128\":\"Class\",\"512\":\"Constructor\",\"2048\":\"Method\"},\"rows\":[{\"kind\":128,\"name\":\"TokenSDK\",\"url\":\"classes/TokenSDK.html\",\"classes\":\"tsd-kind-class\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/TokenSDK.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"TokenSDK\"},{\"kind\":2048,\"name\":\"initConfig\",\"url\":\"classes/TokenSDK.html#initConfig\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\"TokenSDK\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,9.808]],[\"comment/0\",[]],[\"name/1\",[1,9.808]],[\"comment/1\",[]],[\"name/2\",[2,9.808]],[\"comment/2\",[]]],\"invertedIndex\":[[\"constructor\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"initconfig\",{\"_index\":2,\"name\":{\"2\":{}},\"comment\":{}}],[\"tokensdk\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /minting-tool/cli/docs/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Light */ 3 | --light-color-background: #f2f4f8; 4 | --light-color-background-secondary: #eff0f1; 5 | --light-color-icon-background: var(--light-color-background); 6 | --light-color-accent: #c5c7c9; 7 | --light-color-text: #222; 8 | --light-color-text-aside: #707070; 9 | --light-color-link: #4da6ff; 10 | --light-color-ts: #db1373; 11 | --light-color-ts-interface: #139d2c; 12 | --light-color-ts-enum: #9c891a; 13 | --light-color-ts-class: #2484e5; 14 | --light-color-ts-function: #572be7; 15 | --light-color-ts-namespace: #b111c9; 16 | --light-color-ts-private: #707070; 17 | --light-color-ts-variable: #4d68ff; 18 | --light-external-icon: url("data:image/svg+xml;utf8,"); 19 | --light-color-scheme: light; 20 | 21 | /* Dark */ 22 | --dark-color-background: #2b2e33; 23 | --dark-color-background-secondary: #1e2024; 24 | --dark-color-icon-background: var(--dark-color-background-secondary); 25 | --dark-color-accent: #9096a2; 26 | --dark-color-text: #f5f5f5; 27 | --dark-color-text-aside: #dddddd; 28 | --dark-color-link: #00aff4; 29 | --dark-color-ts: #ff6492; 30 | --dark-color-ts-interface: #6cff87; 31 | --dark-color-ts-enum: #f4d93e; 32 | --dark-color-ts-class: #61b0ff; 33 | --dark-color-ts-function: #9772ff; 34 | --dark-color-ts-namespace: #e14dff; 35 | --dark-color-ts-private: #e2e2e2; 36 | --dark-color-ts-variable: #4d68ff; 37 | --dark-external-icon: url("data:image/svg+xml;utf8,"); 38 | --dark-color-scheme: dark; 39 | } 40 | 41 | @media (prefers-color-scheme: light) { 42 | :root { 43 | --color-background: var(--light-color-background); 44 | --color-background-secondary: var(--light-color-background-secondary); 45 | --color-icon-background: var(--light-color-icon-background); 46 | --color-accent: var(--light-color-accent); 47 | --color-text: var(--light-color-text); 48 | --color-text-aside: var(--light-color-text-aside); 49 | --color-link: var(--light-color-link); 50 | --color-ts: var(--light-color-ts); 51 | --color-ts-interface: var(--light-color-ts-interface); 52 | --color-ts-enum: var(--light-color-ts-enum); 53 | --color-ts-class: var(--light-color-ts-class); 54 | --color-ts-function: var(--light-color-ts-function); 55 | --color-ts-namespace: var(--light-color-ts-namespace); 56 | --color-ts-private: var(--light-color-ts-private); 57 | --color-ts-variable: var(--light-color-ts-variable); 58 | --external-icon: var(--light-external-icon); 59 | --color-scheme: var(--light-color-scheme); 60 | } 61 | } 62 | 63 | @media (prefers-color-scheme: dark) { 64 | :root { 65 | --color-background: var(--dark-color-background); 66 | --color-background-secondary: var(--dark-color-background-secondary); 67 | --color-icon-background: var(--dark-color-icon-background); 68 | --color-accent: var(--dark-color-accent); 69 | --color-text: var(--dark-color-text); 70 | --color-text-aside: var(--dark-color-text-aside); 71 | --color-link: var(--dark-color-link); 72 | --color-ts: var(--dark-color-ts); 73 | --color-ts-interface: var(--dark-color-ts-interface); 74 | --color-ts-enum: var(--dark-color-ts-enum); 75 | --color-ts-class: var(--dark-color-ts-class); 76 | --color-ts-function: var(--dark-color-ts-function); 77 | --color-ts-namespace: var(--dark-color-ts-namespace); 78 | --color-ts-private: var(--dark-color-ts-private); 79 | --color-ts-variable: var(--dark-color-ts-variable); 80 | --external-icon: var(--dark-external-icon); 81 | --color-scheme: var(--dark-color-scheme); 82 | } 83 | } 84 | 85 | html { 86 | color-scheme: var(--color-scheme); 87 | } 88 | 89 | body { 90 | margin: 0; 91 | } 92 | 93 | :root[data-theme="light"] { 94 | --color-background: var(--light-color-background); 95 | --color-background-secondary: var(--light-color-background-secondary); 96 | --color-icon-background: var(--light-color-icon-background); 97 | --color-accent: var(--light-color-accent); 98 | --color-text: var(--light-color-text); 99 | --color-text-aside: var(--light-color-text-aside); 100 | --color-link: var(--light-color-link); 101 | --color-ts: var(--light-color-ts); 102 | --color-ts-interface: var(--light-color-ts-interface); 103 | --color-ts-enum: var(--light-color-ts-enum); 104 | --color-ts-class: var(--light-color-ts-class); 105 | --color-ts-function: var(--light-color-ts-function); 106 | --color-ts-namespace: var(--light-color-ts-namespace); 107 | --color-ts-private: var(--light-color-ts-private); 108 | --color-ts-variable: var(--light-color-ts-variable); 109 | --external-icon: var(--light-external-icon); 110 | --color-scheme: var(--light-color-scheme); 111 | } 112 | 113 | :root[data-theme="dark"] { 114 | --color-background: var(--dark-color-background); 115 | --color-background-secondary: var(--dark-color-background-secondary); 116 | --color-icon-background: var(--dark-color-icon-background); 117 | --color-accent: var(--dark-color-accent); 118 | --color-text: var(--dark-color-text); 119 | --color-text-aside: var(--dark-color-text-aside); 120 | --color-link: var(--dark-color-link); 121 | --color-ts: var(--dark-color-ts); 122 | --color-ts-interface: var(--dark-color-ts-interface); 123 | --color-ts-enum: var(--dark-color-ts-enum); 124 | --color-ts-class: var(--dark-color-ts-class); 125 | --color-ts-function: var(--dark-color-ts-function); 126 | --color-ts-namespace: var(--dark-color-ts-namespace); 127 | --color-ts-private: var(--dark-color-ts-private); 128 | --color-ts-variable: var(--dark-color-ts-variable); 129 | --external-icon: var(--dark-external-icon); 130 | --color-scheme: var(--dark-color-scheme); 131 | } 132 | 133 | h1, 134 | h2, 135 | h3, 136 | h4, 137 | h5, 138 | h6 { 139 | line-height: 1.2; 140 | } 141 | 142 | h1 { 143 | font-size: 1.875rem; 144 | margin: 0.67rem 0; 145 | } 146 | 147 | h2 { 148 | font-size: 1.5rem; 149 | margin: 0.83rem 0; 150 | } 151 | 152 | h3 { 153 | font-size: 1.25rem; 154 | margin: 1rem 0; 155 | } 156 | 157 | h4 { 158 | font-size: 1.05rem; 159 | margin: 1.33rem 0; 160 | } 161 | 162 | h5 { 163 | font-size: 1rem; 164 | margin: 1.5rem 0; 165 | } 166 | 167 | h6 { 168 | font-size: 0.875rem; 169 | margin: 2.33rem 0; 170 | } 171 | 172 | .uppercase { 173 | text-transform: uppercase; 174 | } 175 | 176 | pre { 177 | white-space: pre; 178 | white-space: pre-wrap; 179 | word-wrap: break-word; 180 | } 181 | 182 | dl, 183 | menu, 184 | ol, 185 | ul { 186 | margin: 1em 0; 187 | } 188 | 189 | dd { 190 | margin: 0 0 0 40px; 191 | } 192 | 193 | .container { 194 | max-width: 1600px; 195 | padding: 0 2rem; 196 | } 197 | 198 | @media (min-width: 640px) { 199 | .container { 200 | padding: 0 4rem; 201 | } 202 | } 203 | @media (min-width: 1200px) { 204 | .container { 205 | padding: 0 8rem; 206 | } 207 | } 208 | @media (min-width: 1600px) { 209 | .container { 210 | padding: 0 12rem; 211 | } 212 | } 213 | 214 | /* Footer */ 215 | .tsd-generator { 216 | border-top: 1px solid var(--color-accent); 217 | padding-top: 1rem; 218 | padding-bottom: 1rem; 219 | max-height: 3.5rem; 220 | } 221 | 222 | .tsd-generator > p { 223 | margin-top: 0; 224 | margin-bottom: 0; 225 | padding: 0 1rem; 226 | } 227 | 228 | .container-main { 229 | display: flex; 230 | justify-content: space-between; 231 | position: relative; 232 | margin: 0 auto; 233 | } 234 | 235 | .col-4, 236 | .col-8 { 237 | box-sizing: border-box; 238 | float: left; 239 | padding: 2rem 1rem; 240 | } 241 | 242 | .col-4 { 243 | flex: 0 0 25%; 244 | } 245 | .col-8 { 246 | flex: 1 0; 247 | flex-wrap: wrap; 248 | padding-left: 0; 249 | } 250 | 251 | @keyframes fade-in { 252 | from { 253 | opacity: 0; 254 | } 255 | to { 256 | opacity: 1; 257 | } 258 | } 259 | @keyframes fade-out { 260 | from { 261 | opacity: 1; 262 | visibility: visible; 263 | } 264 | to { 265 | opacity: 0; 266 | } 267 | } 268 | @keyframes fade-in-delayed { 269 | 0% { 270 | opacity: 0; 271 | } 272 | 33% { 273 | opacity: 0; 274 | } 275 | 100% { 276 | opacity: 1; 277 | } 278 | } 279 | @keyframes fade-out-delayed { 280 | 0% { 281 | opacity: 1; 282 | visibility: visible; 283 | } 284 | 66% { 285 | opacity: 0; 286 | } 287 | 100% { 288 | opacity: 0; 289 | } 290 | } 291 | @keyframes shift-to-left { 292 | from { 293 | transform: translate(0, 0); 294 | } 295 | to { 296 | transform: translate(-25%, 0); 297 | } 298 | } 299 | @keyframes unshift-to-left { 300 | from { 301 | transform: translate(-25%, 0); 302 | } 303 | to { 304 | transform: translate(0, 0); 305 | } 306 | } 307 | @keyframes pop-in-from-right { 308 | from { 309 | transform: translate(100%, 0); 310 | } 311 | to { 312 | transform: translate(0, 0); 313 | } 314 | } 315 | @keyframes pop-out-to-right { 316 | from { 317 | transform: translate(0, 0); 318 | visibility: visible; 319 | } 320 | to { 321 | transform: translate(100%, 0); 322 | } 323 | } 324 | body { 325 | background: var(--color-background); 326 | font-family: "Segoe UI", sans-serif; 327 | font-size: 16px; 328 | color: var(--color-text); 329 | } 330 | 331 | a { 332 | color: var(--color-link); 333 | text-decoration: none; 334 | } 335 | a:hover { 336 | text-decoration: underline; 337 | } 338 | a.external[target="_blank"] { 339 | background-image: var(--external-icon); 340 | background-position: top 3px right; 341 | background-repeat: no-repeat; 342 | padding-right: 13px; 343 | } 344 | 345 | code, 346 | pre { 347 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 348 | padding: 0.2em; 349 | margin: 0; 350 | font-size: 0.875rem; 351 | border-radius: 0.8em; 352 | } 353 | 354 | pre { 355 | padding: 10px; 356 | border: 0.1em solid var(--color-accent); 357 | } 358 | pre code { 359 | padding: 0; 360 | font-size: 100%; 361 | } 362 | 363 | blockquote { 364 | margin: 1em 0; 365 | padding-left: 1em; 366 | border-left: 4px solid gray; 367 | } 368 | 369 | .tsd-typography { 370 | line-height: 1.333em; 371 | } 372 | .tsd-typography ul { 373 | list-style: square; 374 | padding: 0 0 0 20px; 375 | margin: 0; 376 | } 377 | .tsd-typography h4, 378 | .tsd-typography .tsd-index-panel h3, 379 | .tsd-index-panel .tsd-typography h3, 380 | .tsd-typography h5, 381 | .tsd-typography h6 { 382 | font-size: 1em; 383 | margin: 0; 384 | } 385 | .tsd-typography h5, 386 | .tsd-typography h6 { 387 | font-weight: normal; 388 | } 389 | .tsd-typography p, 390 | .tsd-typography ul, 391 | .tsd-typography ol { 392 | margin: 1em 0; 393 | } 394 | 395 | @media (max-width: 1024px) { 396 | html .col-content { 397 | float: none; 398 | max-width: 100%; 399 | width: 100%; 400 | padding-top: 3rem; 401 | } 402 | html .col-menu { 403 | position: fixed !important; 404 | overflow-y: auto; 405 | -webkit-overflow-scrolling: touch; 406 | z-index: 1024; 407 | top: 0 !important; 408 | bottom: 0 !important; 409 | left: auto !important; 410 | right: 0 !important; 411 | padding: 1.5rem 1.5rem 0 0; 412 | max-width: 25rem; 413 | visibility: hidden; 414 | background-color: var(--color-background); 415 | transform: translate(100%, 0); 416 | } 417 | html .col-menu > *:last-child { 418 | padding-bottom: 20px; 419 | } 420 | html .overlay { 421 | content: ""; 422 | display: block; 423 | position: fixed; 424 | z-index: 1023; 425 | top: 0; 426 | left: 0; 427 | right: 0; 428 | bottom: 0; 429 | background-color: rgba(0, 0, 0, 0.75); 430 | visibility: hidden; 431 | } 432 | 433 | .to-has-menu .overlay { 434 | animation: fade-in 0.4s; 435 | } 436 | 437 | .to-has-menu :is(header, footer, .col-content) { 438 | animation: shift-to-left 0.4s; 439 | } 440 | 441 | .to-has-menu .col-menu { 442 | animation: pop-in-from-right 0.4s; 443 | } 444 | 445 | .from-has-menu .overlay { 446 | animation: fade-out 0.4s; 447 | } 448 | 449 | .from-has-menu :is(header, footer, .col-content) { 450 | animation: unshift-to-left 0.4s; 451 | } 452 | 453 | .from-has-menu .col-menu { 454 | animation: pop-out-to-right 0.4s; 455 | } 456 | 457 | .has-menu body { 458 | overflow: hidden; 459 | } 460 | .has-menu .overlay { 461 | visibility: visible; 462 | } 463 | .has-menu :is(header, footer, .col-content) { 464 | transform: translate(-25%, 0); 465 | } 466 | .has-menu .col-menu { 467 | visibility: visible; 468 | transform: translate(0, 0); 469 | display: grid; 470 | align-items: center; 471 | grid-template-rows: auto 1fr; 472 | grid-gap: 1.5rem; 473 | max-height: 100vh; 474 | padding: 1rem 2rem; 475 | } 476 | .has-menu .tsd-navigation { 477 | max-height: 100%; 478 | } 479 | } 480 | 481 | .tsd-breadcrumb { 482 | margin: 0; 483 | padding: 0; 484 | color: var(--color-text-aside); 485 | } 486 | .tsd-breadcrumb a { 487 | color: var(--color-text-aside); 488 | text-decoration: none; 489 | } 490 | .tsd-breadcrumb a:hover { 491 | text-decoration: underline; 492 | } 493 | .tsd-breadcrumb li { 494 | display: inline; 495 | } 496 | .tsd-breadcrumb li:after { 497 | content: " / "; 498 | } 499 | 500 | .tsd-comment-tags { 501 | display: flex; 502 | flex-direction: column; 503 | } 504 | dl.tsd-comment-tag-group { 505 | display: flex; 506 | align-items: center; 507 | overflow: hidden; 508 | margin: 0.5em 0; 509 | } 510 | dl.tsd-comment-tag-group dt { 511 | display: flex; 512 | margin-right: 0.5em; 513 | font-size: 0.875em; 514 | font-weight: normal; 515 | } 516 | dl.tsd-comment-tag-group dd { 517 | margin: 0; 518 | } 519 | code.tsd-tag { 520 | padding: 0.25em 0.4em; 521 | border: 0.1em solid var(--color-accent); 522 | margin-right: 0.25em; 523 | font-size: 70%; 524 | } 525 | h1 code.tsd-tag:first-of-type { 526 | margin-left: 0.25em; 527 | } 528 | 529 | dl.tsd-comment-tag-group dd:before, 530 | dl.tsd-comment-tag-group dd:after { 531 | content: " "; 532 | } 533 | dl.tsd-comment-tag-group dd pre, 534 | dl.tsd-comment-tag-group dd:after { 535 | clear: both; 536 | } 537 | dl.tsd-comment-tag-group p { 538 | margin: 0; 539 | } 540 | 541 | .tsd-panel.tsd-comment .lead { 542 | font-size: 1.1em; 543 | line-height: 1.333em; 544 | margin-bottom: 2em; 545 | } 546 | .tsd-panel.tsd-comment .lead:last-child { 547 | margin-bottom: 0; 548 | } 549 | 550 | .tsd-filter-visibility h4 { 551 | font-size: 1rem; 552 | padding-top: 0.75rem; 553 | padding-bottom: 0.5rem; 554 | margin: 0; 555 | } 556 | .tsd-filter-item:not(:last-child) { 557 | margin-bottom: 0.5rem; 558 | } 559 | .tsd-filter-input { 560 | display: flex; 561 | width: fit-content; 562 | width: -moz-fit-content; 563 | align-items: center; 564 | user-select: none; 565 | -webkit-user-select: none; 566 | -moz-user-select: none; 567 | -ms-user-select: none; 568 | cursor: pointer; 569 | } 570 | .tsd-filter-input input[type="checkbox"] { 571 | cursor: pointer; 572 | position: absolute; 573 | width: 1.5em; 574 | height: 1.5em; 575 | opacity: 0; 576 | } 577 | .tsd-filter-input input[type="checkbox"]:disabled { 578 | pointer-events: none; 579 | } 580 | .tsd-filter-input svg { 581 | cursor: pointer; 582 | width: 1.5em; 583 | height: 1.5em; 584 | margin-right: 0.5em; 585 | border-radius: 0.33em; 586 | /* Leaving this at full opacity breaks event listeners on Firefox. 587 | Don't remove unless you know what you're doing. */ 588 | opacity: 0.99; 589 | } 590 | .tsd-filter-input input[type="checkbox"]:focus + svg { 591 | transform: scale(0.95); 592 | } 593 | .tsd-filter-input input[type="checkbox"]:focus:not(:focus-visible) + svg { 594 | transform: scale(1); 595 | } 596 | .tsd-checkbox-background { 597 | fill: var(--color-accent); 598 | } 599 | input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { 600 | stroke: var(--color-text); 601 | } 602 | .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { 603 | fill: var(--color-background); 604 | stroke: var(--color-accent); 605 | stroke-width: 0.25rem; 606 | } 607 | .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { 608 | stroke: var(--color-accent); 609 | } 610 | 611 | .tsd-theme-toggle { 612 | padding-top: 0.75rem; 613 | } 614 | .tsd-theme-toggle > h4 { 615 | display: inline; 616 | vertical-align: middle; 617 | margin-right: 0.75rem; 618 | } 619 | 620 | .tsd-hierarchy { 621 | list-style: square; 622 | margin: 0; 623 | } 624 | .tsd-hierarchy .target { 625 | font-weight: bold; 626 | } 627 | 628 | .tsd-panel-group.tsd-index-group { 629 | margin-bottom: 0; 630 | } 631 | .tsd-index-panel .tsd-index-list { 632 | list-style: none; 633 | line-height: 1.333em; 634 | margin: 0; 635 | padding: 0.25rem 0 0 0; 636 | overflow: hidden; 637 | display: grid; 638 | grid-template-columns: repeat(3, 1fr); 639 | column-gap: 1rem; 640 | grid-template-rows: auto; 641 | } 642 | @media (max-width: 1024px) { 643 | .tsd-index-panel .tsd-index-list { 644 | grid-template-columns: repeat(2, 1fr); 645 | } 646 | } 647 | @media (max-width: 768px) { 648 | .tsd-index-panel .tsd-index-list { 649 | grid-template-columns: repeat(1, 1fr); 650 | } 651 | } 652 | .tsd-index-panel .tsd-index-list li { 653 | -webkit-page-break-inside: avoid; 654 | -moz-page-break-inside: avoid; 655 | -ms-page-break-inside: avoid; 656 | -o-page-break-inside: avoid; 657 | page-break-inside: avoid; 658 | } 659 | .tsd-index-panel a, 660 | .tsd-index-panel a.tsd-parent-kind-module { 661 | color: var(--color-ts); 662 | } 663 | .tsd-index-panel a.tsd-parent-kind-interface { 664 | color: var(--color-ts-interface); 665 | } 666 | .tsd-index-panel a.tsd-parent-kind-enum { 667 | color: var(--color-ts-enum); 668 | } 669 | .tsd-index-panel a.tsd-parent-kind-class { 670 | color: var(--color-ts-class); 671 | } 672 | .tsd-index-panel a.tsd-kind-module { 673 | color: var(--color-ts-namespace); 674 | } 675 | .tsd-index-panel a.tsd-kind-interface { 676 | color: var(--color-ts-interface); 677 | } 678 | .tsd-index-panel a.tsd-kind-enum { 679 | color: var(--color-ts-enum); 680 | } 681 | .tsd-index-panel a.tsd-kind-class { 682 | color: var(--color-ts-class); 683 | } 684 | .tsd-index-panel a.tsd-kind-function { 685 | color: var(--color-ts-function); 686 | } 687 | .tsd-index-panel a.tsd-kind-namespace { 688 | color: var(--color-ts-namespace); 689 | } 690 | .tsd-index-panel a.tsd-kind-variable { 691 | color: var(--color-ts-variable); 692 | } 693 | .tsd-index-panel a.tsd-is-private { 694 | color: var(--color-ts-private); 695 | } 696 | 697 | .tsd-flag { 698 | display: inline-block; 699 | padding: 0.25em 0.4em; 700 | border-radius: 4px; 701 | color: var(--color-comment-tag-text); 702 | background-color: var(--color-comment-tag); 703 | text-indent: 0; 704 | font-size: 75%; 705 | line-height: 1; 706 | font-weight: normal; 707 | } 708 | 709 | .tsd-anchor { 710 | position: absolute; 711 | top: -100px; 712 | } 713 | 714 | .tsd-member { 715 | position: relative; 716 | } 717 | .tsd-member .tsd-anchor + h3 { 718 | display: flex; 719 | align-items: center; 720 | margin-top: 0; 721 | margin-bottom: 0; 722 | border-bottom: none; 723 | } 724 | .tsd-member [data-tsd-kind] { 725 | color: var(--color-ts); 726 | } 727 | .tsd-member [data-tsd-kind="Interface"] { 728 | color: var(--color-ts-interface); 729 | } 730 | .tsd-member [data-tsd-kind="Enum"] { 731 | color: var(--color-ts-enum); 732 | } 733 | .tsd-member [data-tsd-kind="Class"] { 734 | color: var(--color-ts-class); 735 | } 736 | .tsd-member [data-tsd-kind="Private"] { 737 | color: var(--color-ts-private); 738 | } 739 | 740 | .tsd-navigation a { 741 | display: block; 742 | margin: 0.4rem 0; 743 | border-left: 2px solid transparent; 744 | color: var(--color-text); 745 | text-decoration: none; 746 | transition: border-left-color 0.1s; 747 | } 748 | .tsd-navigation a:hover { 749 | text-decoration: underline; 750 | } 751 | .tsd-navigation ul { 752 | margin: 0; 753 | padding: 0; 754 | list-style: none; 755 | } 756 | .tsd-navigation li { 757 | padding: 0; 758 | } 759 | 760 | .tsd-navigation.primary .tsd-accordion-details > ul { 761 | margin-top: 0.75rem; 762 | } 763 | .tsd-navigation.primary a { 764 | padding: 0.75rem 0.5rem; 765 | margin: 0; 766 | } 767 | .tsd-navigation.primary ul li a { 768 | margin-left: 0.5rem; 769 | } 770 | .tsd-navigation.primary ul li li a { 771 | margin-left: 1.5rem; 772 | } 773 | .tsd-navigation.primary ul li li li a { 774 | margin-left: 2.5rem; 775 | } 776 | .tsd-navigation.primary ul li li li li a { 777 | margin-left: 3.5rem; 778 | } 779 | .tsd-navigation.primary ul li li li li li a { 780 | margin-left: 4.5rem; 781 | } 782 | .tsd-navigation.primary ul li li li li li li a { 783 | margin-left: 5.5rem; 784 | } 785 | .tsd-navigation.primary li.current > a { 786 | border-left: 0.15rem var(--color-text) solid; 787 | } 788 | .tsd-navigation.primary li.selected > a { 789 | font-weight: bold; 790 | border-left: 0.2rem var(--color-text) solid; 791 | } 792 | .tsd-navigation.primary ul li a:hover { 793 | border-left: 0.2rem var(--color-text-aside) solid; 794 | } 795 | .tsd-navigation.primary li.globals + li > span, 796 | .tsd-navigation.primary li.globals + li > a { 797 | padding-top: 20px; 798 | } 799 | 800 | .tsd-navigation.secondary.tsd-navigation--toolbar-hide { 801 | max-height: calc(100vh - 1rem); 802 | top: 0.5rem; 803 | } 804 | .tsd-navigation.secondary > ul { 805 | display: inline; 806 | padding-right: 0.5rem; 807 | transition: opacity 0.2s; 808 | } 809 | .tsd-navigation.secondary ul li a { 810 | padding-left: 0; 811 | } 812 | .tsd-navigation.secondary ul li li a { 813 | padding-left: 1.1rem; 814 | } 815 | .tsd-navigation.secondary ul li li li a { 816 | padding-left: 2.2rem; 817 | } 818 | .tsd-navigation.secondary ul li li li li a { 819 | padding-left: 3.3rem; 820 | } 821 | .tsd-navigation.secondary ul li li li li li a { 822 | padding-left: 4.4rem; 823 | } 824 | .tsd-navigation.secondary ul li li li li li li a { 825 | padding-left: 5.5rem; 826 | } 827 | 828 | a.tsd-index-link { 829 | margin: 0.25rem 0; 830 | font-size: 1rem; 831 | line-height: 1.25rem; 832 | display: inline-flex; 833 | align-items: center; 834 | } 835 | .tsd-accordion-summary > h1, 836 | .tsd-accordion-summary > h2, 837 | .tsd-accordion-summary > h3, 838 | .tsd-accordion-summary > h4, 839 | .tsd-accordion-summary > h5 { 840 | display: inline-flex; 841 | align-items: center; 842 | vertical-align: middle; 843 | margin-bottom: 0; 844 | user-select: none; 845 | -moz-user-select: none; 846 | -webkit-user-select: none; 847 | -ms-user-select: none; 848 | } 849 | .tsd-accordion-summary { 850 | display: block; 851 | cursor: pointer; 852 | } 853 | .tsd-accordion-summary > * { 854 | margin-top: 0; 855 | margin-bottom: 0; 856 | padding-top: 0; 857 | padding-bottom: 0; 858 | } 859 | .tsd-accordion-summary::-webkit-details-marker { 860 | display: none; 861 | } 862 | .tsd-index-accordion .tsd-accordion-summary svg { 863 | margin-right: 0.25rem; 864 | } 865 | .tsd-index-content > :not(:first-child) { 866 | margin-top: 0.75rem; 867 | } 868 | .tsd-index-heading { 869 | margin-top: 1.5rem; 870 | margin-bottom: 0.75rem; 871 | } 872 | 873 | .tsd-kind-icon { 874 | margin-right: 0.5rem; 875 | width: 1.25rem; 876 | height: 1.25rem; 877 | min-width: 1.25rem; 878 | min-height: 1.25rem; 879 | } 880 | .tsd-kind-icon path { 881 | transform-origin: center; 882 | transform: scale(1.1); 883 | } 884 | .tsd-signature > .tsd-kind-icon { 885 | margin-right: 0.8rem; 886 | } 887 | 888 | @media (min-width: 1024px) { 889 | .col-content { 890 | margin: 2rem auto; 891 | } 892 | 893 | .menu-sticky-wrap { 894 | position: sticky; 895 | height: calc(100vh - 2rem); 896 | top: 4rem; 897 | right: 0; 898 | padding: 0 1.5rem; 899 | padding-top: 1rem; 900 | margin-top: 3rem; 901 | transition: 0.3s ease-in-out; 902 | transition-property: top, padding-top, padding, height; 903 | overflow-y: auto; 904 | } 905 | .col-menu { 906 | border-left: 1px solid var(--color-accent); 907 | } 908 | .col-menu--hide { 909 | top: 1rem; 910 | } 911 | .col-menu .tsd-navigation:not(:last-child) { 912 | padding-bottom: 1.75rem; 913 | } 914 | } 915 | 916 | .tsd-panel { 917 | margin-bottom: 2.5rem; 918 | } 919 | .tsd-panel.tsd-member { 920 | margin-bottom: 4rem; 921 | } 922 | .tsd-panel:empty { 923 | display: none; 924 | } 925 | .tsd-panel > h1, 926 | .tsd-panel > h2, 927 | .tsd-panel > h3 { 928 | margin: 1.5rem -1.5rem 0.75rem -1.5rem; 929 | padding: 0 1.5rem 0.75rem 1.5rem; 930 | } 931 | .tsd-panel > h1.tsd-before-signature, 932 | .tsd-panel > h2.tsd-before-signature, 933 | .tsd-panel > h3.tsd-before-signature { 934 | margin-bottom: 0; 935 | border-bottom: none; 936 | } 937 | 938 | .tsd-panel-group { 939 | margin: 4rem 0; 940 | } 941 | .tsd-panel-group.tsd-index-group { 942 | margin: 2rem 0; 943 | } 944 | .tsd-panel-group.tsd-index-group details { 945 | margin: 2rem 0; 946 | } 947 | 948 | #tsd-search { 949 | transition: background-color 0.2s; 950 | } 951 | #tsd-search .title { 952 | position: relative; 953 | z-index: 2; 954 | } 955 | #tsd-search .field { 956 | position: absolute; 957 | left: 0; 958 | top: 0; 959 | right: 2.5rem; 960 | height: 100%; 961 | } 962 | #tsd-search .field input { 963 | box-sizing: border-box; 964 | position: relative; 965 | top: -50px; 966 | z-index: 1; 967 | width: 100%; 968 | padding: 0 10px; 969 | opacity: 0; 970 | outline: 0; 971 | border: 0; 972 | background: transparent; 973 | color: var(--color-text); 974 | } 975 | #tsd-search .field label { 976 | position: absolute; 977 | overflow: hidden; 978 | right: -40px; 979 | } 980 | #tsd-search .field input, 981 | #tsd-search .title { 982 | transition: opacity 0.2s; 983 | } 984 | #tsd-search .results { 985 | position: absolute; 986 | visibility: hidden; 987 | top: 40px; 988 | width: 100%; 989 | margin: 0; 990 | padding: 0; 991 | list-style: none; 992 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); 993 | } 994 | #tsd-search .results li { 995 | padding: 0 10px; 996 | background-color: var(--color-background); 997 | } 998 | #tsd-search .results li:nth-child(even) { 999 | background-color: var(--color-background-secondary); 1000 | } 1001 | #tsd-search .results li.state { 1002 | display: none; 1003 | } 1004 | #tsd-search .results li.current, 1005 | #tsd-search .results li:hover { 1006 | background-color: var(--color-accent); 1007 | } 1008 | #tsd-search .results a { 1009 | display: block; 1010 | } 1011 | #tsd-search .results a:before { 1012 | top: 10px; 1013 | } 1014 | #tsd-search .results span.parent { 1015 | color: var(--color-text-aside); 1016 | font-weight: normal; 1017 | } 1018 | #tsd-search.has-focus { 1019 | background-color: var(--color-accent); 1020 | } 1021 | #tsd-search.has-focus .field input { 1022 | top: 0; 1023 | opacity: 1; 1024 | } 1025 | #tsd-search.has-focus .title { 1026 | z-index: 0; 1027 | opacity: 0; 1028 | } 1029 | #tsd-search.has-focus .results { 1030 | visibility: visible; 1031 | } 1032 | #tsd-search.loading .results li.state.loading { 1033 | display: block; 1034 | } 1035 | #tsd-search.failure .results li.state.failure { 1036 | display: block; 1037 | } 1038 | 1039 | .tsd-signature { 1040 | margin: 0 0 1rem 0; 1041 | padding: 1rem 0.5rem; 1042 | border: 1px solid var(--color-accent); 1043 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 1044 | font-size: 14px; 1045 | overflow-x: auto; 1046 | } 1047 | 1048 | .tsd-signature-symbol { 1049 | color: var(--color-text-aside); 1050 | font-weight: normal; 1051 | } 1052 | 1053 | .tsd-signature-type { 1054 | font-style: italic; 1055 | font-weight: normal; 1056 | } 1057 | 1058 | .tsd-signatures { 1059 | padding: 0; 1060 | margin: 0 0 1em 0; 1061 | list-style-type: none; 1062 | } 1063 | .tsd-signatures .tsd-signature { 1064 | margin: 0; 1065 | border-color: var(--color-accent); 1066 | border-width: 1px 0; 1067 | transition: background-color 0.1s; 1068 | } 1069 | .tsd-description .tsd-signatures .tsd-signature { 1070 | border-width: 1px; 1071 | } 1072 | 1073 | ul.tsd-parameter-list, 1074 | ul.tsd-type-parameter-list { 1075 | list-style: square; 1076 | margin: 0; 1077 | padding-left: 20px; 1078 | } 1079 | ul.tsd-parameter-list > li.tsd-parameter-signature, 1080 | ul.tsd-type-parameter-list > li.tsd-parameter-signature { 1081 | list-style: none; 1082 | margin-left: -20px; 1083 | } 1084 | ul.tsd-parameter-list h5, 1085 | ul.tsd-type-parameter-list h5 { 1086 | font-size: 16px; 1087 | margin: 1em 0 0.5em 0; 1088 | } 1089 | .tsd-sources { 1090 | margin-top: 1rem; 1091 | font-size: 0.875em; 1092 | } 1093 | .tsd-sources a { 1094 | color: var(--color-text-aside); 1095 | text-decoration: underline; 1096 | } 1097 | .tsd-sources ul { 1098 | list-style: none; 1099 | padding: 0; 1100 | } 1101 | 1102 | .tsd-page-toolbar { 1103 | position: fixed; 1104 | z-index: 1; 1105 | top: 0; 1106 | left: 0; 1107 | width: 100%; 1108 | color: var(--color-text); 1109 | background: var(--color-background-secondary); 1110 | border-bottom: 1px var(--color-accent) solid; 1111 | transition: transform 0.3s ease-in-out; 1112 | } 1113 | .tsd-page-toolbar a { 1114 | color: var(--color-text); 1115 | text-decoration: none; 1116 | } 1117 | .tsd-page-toolbar a.title { 1118 | font-weight: bold; 1119 | } 1120 | .tsd-page-toolbar a.title:hover { 1121 | text-decoration: underline; 1122 | } 1123 | .tsd-page-toolbar .tsd-toolbar-contents { 1124 | display: flex; 1125 | justify-content: space-between; 1126 | height: 2.5rem; 1127 | margin: 0 auto; 1128 | } 1129 | .tsd-page-toolbar .table-cell { 1130 | position: relative; 1131 | white-space: nowrap; 1132 | line-height: 40px; 1133 | } 1134 | .tsd-page-toolbar .table-cell:first-child { 1135 | width: 100%; 1136 | } 1137 | 1138 | .tsd-page-toolbar--hide { 1139 | transform: translateY(-100%); 1140 | } 1141 | 1142 | .tsd-widget { 1143 | display: inline-block; 1144 | overflow: hidden; 1145 | opacity: 0.8; 1146 | height: 40px; 1147 | transition: opacity 0.1s, background-color 0.2s; 1148 | vertical-align: bottom; 1149 | cursor: pointer; 1150 | } 1151 | .tsd-widget:hover { 1152 | opacity: 0.9; 1153 | } 1154 | .tsd-widget.active { 1155 | opacity: 1; 1156 | background-color: var(--color-accent); 1157 | } 1158 | .tsd-widget.no-caption { 1159 | width: 40px; 1160 | } 1161 | .tsd-widget.no-caption:before { 1162 | margin: 0; 1163 | } 1164 | 1165 | .tsd-widget.options, 1166 | .tsd-widget.menu { 1167 | display: none; 1168 | } 1169 | @media (max-width: 1024px) { 1170 | .tsd-widget.options, 1171 | .tsd-widget.menu { 1172 | display: inline-block; 1173 | } 1174 | } 1175 | input[type="checkbox"] + .tsd-widget:before { 1176 | background-position: -120px 0; 1177 | } 1178 | input[type="checkbox"]:checked + .tsd-widget:before { 1179 | background-position: -160px 0; 1180 | } 1181 | 1182 | img { 1183 | max-width: 100%; 1184 | } 1185 | 1186 | .tsd-anchor-icon { 1187 | display: inline-flex; 1188 | align-items: center; 1189 | margin-left: 0.5rem; 1190 | vertical-align: middle; 1191 | color: var(--color-text); 1192 | } 1193 | 1194 | .tsd-anchor-icon svg { 1195 | width: 1em; 1196 | height: 1em; 1197 | visibility: hidden; 1198 | } 1199 | 1200 | .tsd-anchor-link:hover > .tsd-anchor-icon svg { 1201 | visibility: visible; 1202 | } 1203 | 1204 | .deprecated { 1205 | text-decoration: line-through; 1206 | } 1207 | 1208 | * { 1209 | scrollbar-width: thin; 1210 | scrollbar-color: var(--color-accent) var(--color-icon-background); 1211 | } 1212 | 1213 | *::-webkit-scrollbar { 1214 | width: 0.75rem; 1215 | } 1216 | 1217 | *::-webkit-scrollbar-track { 1218 | background: var(--color-icon-background); 1219 | } 1220 | 1221 | *::-webkit-scrollbar-thumb { 1222 | background-color: var(--color-accent); 1223 | border-radius: 999rem; 1224 | border: 0.25rem solid var(--color-icon-background); 1225 | } 1226 | -------------------------------------------------------------------------------- /minting-tool/cli/docs/classes/TokenSDK.html: -------------------------------------------------------------------------------- 1 | TokenSDK | aptos_token
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Class TokenSDK

16 |
17 |

this is a test class

18 |
// We can initialize like this
const sdk = new TokenSDK(); 19 |
20 |
21 |
22 |

Hierarchy

23 |
    24 |
  • TokenSDK
27 |
28 |
29 |
30 | 31 |
32 |
33 |

Constructors

34 |
36 |
37 |

Methods

38 |
40 |
41 |

Constructors

42 |
43 | 44 |
48 |
49 |

Methods

50 |
51 | 52 |
    53 | 54 |
  • 55 |

    @description:

    56 | 57 |

    Returns

    58 |
    59 |

    Parameters

    60 |
      61 |
    • 62 |
      id: string
    • 63 |
    • 64 |
      url: string
    65 |

    Returns void

68 |
95 |
96 |

Generated using TypeDoc

97 |
-------------------------------------------------------------------------------- /minting-tool/cli/docs/index.html: -------------------------------------------------------------------------------- 1 | aptos_token
2 |
3 | 8 |
9 |
10 |
11 |
12 |

aptos_token

13 |
18 |
42 |
43 |

Generated using TypeDoc

44 |
-------------------------------------------------------------------------------- /minting-tool/cli/docs/modules.html: -------------------------------------------------------------------------------- 1 | aptos_token
2 |
3 | 8 |
9 |
10 |
11 |
12 |

aptos_token

13 |
14 |
15 |

Index

16 |
17 |

Classes

18 |
TokenSDK 19 |
20 |
44 |
45 |

Generated using TypeDoc

46 |
-------------------------------------------------------------------------------- /minting-tool/cli/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | moduleNameMapper: { 5 | '^(\\.{1,2}/.*)\\.js$': '$1' 6 | }, 7 | testEnvironment: 'node', 8 | testPathIgnorePatterns: ['dist/*'], 9 | collectCoverage: true 10 | }; 11 | -------------------------------------------------------------------------------- /minting-tool/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aptos-mint", 3 | "description": "Aptos token minting tool", 4 | "license": "Apache-2.0", 5 | "engines": { 6 | "node": ">=11.0.0" 7 | }, 8 | "main": "./dist/src/cli.js", 9 | "bin": "./dist/src/cli.js", 10 | "scripts": { 11 | "build": "rm -rf dist && tsc -p . && cp -r src/templates dist/src && cp -r ../../contracts/mint_nft dist/src/contracts && rm -rf dist/src/contracts/build", 12 | "test": "jest", 13 | "fmt": "prettier --config .prettierrc 'src/**/*.ts' --write", 14 | "lint": "eslint \"**/*.ts\"", 15 | "prepare": "cd ../.. && husky install .husky", 16 | "cli": "ts-node ./src/cli.ts" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://git@github.com:aptos-labs/token.git" 21 | }, 22 | "author": "aptoslabs.com", 23 | "keywords": [ 24 | "Aptos", 25 | "Aptos Labs" 26 | ], 27 | "dependencies": { 28 | "@bundlr-network/client": "^0.9.2", 29 | "@noble/hashes": "1.1.2", 30 | "aptos": "^1.5.0", 31 | "bignumber.js": "^9.1.0", 32 | "canonicalize": "^1.0.8", 33 | "chalk": "4.1.2", 34 | "commander": "^9.4.1", 35 | "glob": "^8.1.0", 36 | "prompts": "^2.4.2", 37 | "short-uuid": "^4.2.2", 38 | "sqlite3": "^5.1.2", 39 | "untildify": "^4.0.0", 40 | "yaml": "^2.1.3" 41 | }, 42 | "devDependencies": { 43 | "@types/docopt": "^0.6.33", 44 | "@types/glob": "^8.0.1", 45 | "@types/jest": "28.1.8", 46 | "@types/node": "18.6.2", 47 | "@types/node-fetch": "^2.6.2", 48 | "@types/prompts": "^2.4.1", 49 | "@typescript-eslint/eslint-plugin": "5.36.2", 50 | "@typescript-eslint/parser": "5.36.2", 51 | "eslint": "8.23.0", 52 | "eslint-config-airbnb-base": "15.0.0", 53 | "eslint-config-airbnb-typescript": "17.0.0", 54 | "eslint-config-prettier": "8.5.0", 55 | "eslint-plugin-import": "2.26.0", 56 | "husky": "^8.0.2", 57 | "jest": "^28.1.3", 58 | "prettier": "^2.6.2", 59 | "ts-jest": "^28.0.8", 60 | "ts-node": "10.9.1", 61 | "typedoc": "^0.23.16", 62 | "typescript": "^4.8.4" 63 | }, 64 | "version": "0.0.1" 65 | } 66 | -------------------------------------------------------------------------------- /minting-tool/cli/src/asset-uploader.ts: -------------------------------------------------------------------------------- 1 | import Bundlr from "@bundlr-network/client"; 2 | import NodeBundlr from "@bundlr-network/client/build/node/bundlr"; 3 | import { AptosAccount } from "aptos"; 4 | import BigNumber from "bignumber.js"; 5 | import { 6 | MAINNET_APTOS_URL, 7 | MAINNET_BUNDLR_URL, 8 | TESTNET_APTOS_URL, 9 | TESTNET_BUNDLR_URL, 10 | } from "./utils"; 11 | 12 | export interface AssetUploader { 13 | provider: string; 14 | 15 | // Upload content to off-chain storage and return a link 16 | uploadFile(filePath: string): Promise; 17 | } 18 | 19 | export class BundlrUploader implements AssetUploader { 20 | provider: string; 21 | 22 | bundlrPromise: Promise; 23 | 24 | account: AptosAccount; 25 | 26 | constructor(account: AptosAccount, network: "mainnet" | "testnet") { 27 | this.provider = "bundlr"; 28 | this.account = account; 29 | const signingFunction = async (msg: Uint8Array) => 30 | this.account.signBuffer(msg).toUint8Array(); 31 | 32 | this.bundlrPromise = Bundlr.init({ 33 | url: network === "mainnet" ? MAINNET_BUNDLR_URL : TESTNET_BUNDLR_URL, 34 | providerUrl: 35 | network === "mainnet" ? MAINNET_APTOS_URL : TESTNET_APTOS_URL, 36 | currency: "aptos", 37 | publicKey: this.account.pubKey().toString(), 38 | signingFunction, 39 | }); 40 | } 41 | 42 | async uploadFile(filePath: string): Promise { 43 | const bundlr = await this.bundlrPromise; 44 | 45 | const res = await bundlr.uploadFile(filePath); 46 | return res.id; 47 | } 48 | 49 | async fund(amount: string) { 50 | const bundlr = await this.bundlrPromise; 51 | await bundlr.fund(BigNumber(amount)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /minting-tool/cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable max-len */ 3 | 4 | import fs from "fs"; 5 | import { exit } from "process"; 6 | import chalk from "chalk"; 7 | import glob from "glob"; 8 | import path from "path"; 9 | import prompts from "prompts"; 10 | import Bundlr from "@bundlr-network/client"; 11 | import { sha3_256 as sha3Hash } from "@noble/hashes/sha3"; 12 | import { program } from "commander"; 13 | import cluster from "node:cluster"; 14 | import short from "short-uuid"; 15 | import { Database } from "sqlite3"; 16 | import util from "util"; 17 | 18 | import { AptosAccount, BCS, HexString, MaybeHexString } from "aptos"; 19 | import { version } from "../package.json"; 20 | import { BundlrUploader } from "./asset-uploader"; 21 | import { NFTMint } from "./nft-mint"; 22 | import { 23 | MAINNET_BUNDLR_URL, 24 | TESTNET_BUNDLR_URL, 25 | MAINNET, 26 | NetworkType, 27 | resolvePath, 28 | resolveProfile, 29 | octasToApt, 30 | readProjectConfig, 31 | exitWithError, 32 | runWithAptosCLI, 33 | OCTAS_PER_APT, 34 | } from "./utils"; 35 | 36 | program 37 | .name("aptos-mint") 38 | .description("CLI to create NFT collections") 39 | .option( 40 | "-v, --verbose", 41 | "Print more information. This is useful for debugging purpose.", 42 | false, 43 | ) 44 | .version(version); 45 | 46 | program 47 | .command("init") 48 | .description("Creates a NFT project with a config file.") 49 | .requiredOption("--name ", "Name of the project.") 50 | .requiredOption( 51 | "--asset-path ", 52 | "The path to the hashlips asset directory.", 53 | ) 54 | .action(async ({ name: projectName, assetPath }) => { 55 | await checkHashLipsAsset(assetPath); 56 | await initProject(projectName, assetPath); 57 | }); 58 | 59 | program 60 | .command("set-minting-contract") 61 | .description("Set the minting contract address for a project.") 62 | .requiredOption( 63 | "--contract-address ", 64 | "The address of the minting contract.", 65 | ) 66 | .option("--project-path ", "The path to the NFT project", ".") 67 | .action(async ({ projectPath, contractAddress }) => { 68 | await setMintingContractAddress(projectPath, contractAddress); 69 | console.log(`Set contract address to ${contractAddress}`); 70 | }); 71 | 72 | program 73 | .command("validate") 74 | .description("Validate if the config of a project is valid.") 75 | .option("--project-path ", "The path to the NFT project", ".") 76 | .option("--check-asset-hashes") 77 | .action(async ({ projectPath, checkAssetHashes }) => { 78 | await assertProjectValid(projectPath, true, checkAssetHashes || false); 79 | }); 80 | 81 | program 82 | .command("fund") 83 | .description( 84 | "Fund Bundlr Aptos. This is required before you can upload asset files to Arweave.", 85 | ) 86 | .requiredOption( 87 | "--profile ", 88 | "The profile name of the Aptos CLI. This account needs to have the APTs to fund Bundlr.", 89 | ) 90 | .requiredOption( 91 | "--octas ", 92 | "The amount of Octas to fund the Bundlr service.", 93 | ) 94 | .action(async ({ profile, octas }) => { 95 | const [account, network] = await resolveProfile(profile); 96 | await fundBundlr(account, octas, network); 97 | }); 98 | 99 | program 100 | .command("balance") 101 | .description("Get the balance available in the Bundlr service.") 102 | .requiredOption( 103 | "--profile ", 104 | "The profile name of the Aptos CLI.", 105 | ) 106 | .action(async ({ profile }) => { 107 | const [account, network] = await resolveProfile(profile); 108 | await getBundlrBalance(account.address(), network); 109 | }); 110 | 111 | program 112 | .command("update-minting-time-and-price") 113 | .description("Update the minting time and price.") 114 | .requiredOption( 115 | "--profile ", 116 | "The profile name of the Aptos CLI.", 117 | ) 118 | .option( 119 | "--minting-contract ", 120 | "The on-chain address of the minting contract.", 121 | ) 122 | .option("--project-path ", "The path to the NFT project", ".") 123 | .action(async ({ projectPath, profile, mintingContract }) => { 124 | await assertProjectValid(projectPath, true); 125 | const mintingEngine = await createNFTMintingEngine({ 126 | projectPath, 127 | profile, 128 | mintingContract, 129 | }); 130 | 131 | await mintingEngine.setMintingTimeAndPrice(); 132 | console.log("Minting time and price are updated successfully"); 133 | }); 134 | 135 | program 136 | .command("add-to-whitelist") 137 | .description( 138 | "Whitelist the addresses that can mint NFTs during whitelist period.", 139 | ) 140 | .requiredOption( 141 | "--addresses ", 142 | "A list of addresses separated by commas.", 143 | ) 144 | .requiredOption( 145 | "--limit ", 146 | "The limit of NFTs that each account is allowed to mint.", 147 | ) 148 | .requiredOption( 149 | "--profile ", 150 | "The profile name of the Aptos CLI.", 151 | ) 152 | .option( 153 | "--minting-contract ", 154 | "The on-chain address of the minting contract.", 155 | ) 156 | .option("--project-path ", "The path to the NFT project", ".") 157 | .action( 158 | async ({ addresses, limit, profile, mintingContract, projectPath }) => { 159 | const mintingEngine = await createNFTMintingEngine({ 160 | projectPath, 161 | profile, 162 | mintingContract, 163 | }); 164 | await mintingEngine.addToWhiteList(addresses.split(","), limit); 165 | console.log("Addresses are whitelisted successfully"); 166 | }, 167 | ); 168 | 169 | program 170 | .command("upload") 171 | .description("Upload assets to Arweave.") 172 | .requiredOption( 173 | "--profile ", 174 | "The profile name of the Aptos CLI.", 175 | ) 176 | .option( 177 | "--minting-contract ", 178 | "The on-chain address of the minting contract.", 179 | ) 180 | .option("--project-path ", "The path to the NFT project", ".") 181 | .action(async ({ profile, mintingContract, projectPath }) => { 182 | // Only primary process needs to validate the project. 183 | if (cluster.isPrimary) { 184 | await assertProjectValid(projectPath, true); 185 | } 186 | 187 | const mintingEngine = await createNFTMintingEngine({ 188 | projectPath, 189 | profile, 190 | mintingContract, 191 | }); 192 | await mintingEngine.run(); 193 | }); 194 | 195 | program 196 | .command("publish-contract") 197 | .description( 198 | "Build the smart contract with the Aptos CLI and publish the smart contract to a resource account", 199 | ) 200 | .requiredOption( 201 | "--profile ", 202 | "The profile name of the Aptos CLI.", 203 | ) 204 | .option( 205 | "--resource-account-seed", 206 | "The seed that is used to the resource account.", 207 | ) 208 | .option("--project-path ", "The path to the NFT project", ".") 209 | .action(async ({ profile, resourceAccountSeed, projectPath }) => { 210 | const [account] = await resolveProfile(profile); 211 | const seed = resourceAccountSeed || short.generate(); 212 | 213 | const fullProjectPath = resolvePath(projectPath); 214 | 215 | const resourceAccountAddr = AptosAccount.getResourceAccountAddress( 216 | account.address(), 217 | BCS.bcsSerializeStr(seed), 218 | ); 219 | 220 | await runWithAptosCLI( 221 | `aptos move create-resource-account-and-publish-package --seed ${seed} --package-dir ${path.join( 222 | fullProjectPath, 223 | "contracts", 224 | )} --address-name mint_nft --named-addresses source_addr=${profile} --profile ${profile}`, 225 | ); 226 | 227 | await setMintingContractAddress(projectPath, resourceAccountAddr.hex()); 228 | }); 229 | 230 | async function initProject(name: string, assetPath: string) { 231 | const fullPath = resolvePath(".", name); 232 | if (fs.existsSync(fullPath)) { 233 | exitWithError(`${fullPath} already exists.`); 234 | } 235 | fs.mkdirSync(fullPath, { recursive: true }); 236 | 237 | const configPath = path.join(fullPath, "config.json"); 238 | if (fs.existsSync(configPath)) { 239 | exitWithError(`${configPath} already exists.`); 240 | } 241 | 242 | fs.mkdirSync(fullPath, { recursive: true }); 243 | 244 | // We would like to pull in the smart contract to the project folder 245 | fs.cpSync( 246 | `${path.join(__dirname, "contracts")}`, 247 | path.join(fullPath, "contracts"), 248 | { 249 | recursive: true, 250 | }, 251 | ); 252 | 253 | let enableWL = true; 254 | 255 | const questions = [ 256 | { 257 | type: "text", 258 | name: "collectionName", 259 | message: "What is the collection name?", 260 | validate: (value: string) => 261 | value.length === 0 ? "Collection name cannot be empty" : true, 262 | }, 263 | { 264 | type: "text", 265 | name: "collectionDescription", 266 | message: "What is the collection description?", 267 | }, 268 | { 269 | type: "text", 270 | name: "collectionCover", 271 | message: "Enter the collection cover image path", 272 | }, 273 | { 274 | type: "text", 275 | name: "tokenNameBase", 276 | message: 277 | // eslint-disable-next-line max-len 278 | "Enter the base name of tokens. Final token names will be derived from the base name by appending sequence numbers", 279 | }, 280 | { 281 | type: "number", 282 | float: true, 283 | name: "royaltyPercent", 284 | message: "Enter royalty percentage. e.g. 5 represents 5%", 285 | }, 286 | { 287 | type: "text", 288 | name: "royaltyPayeeAcct", 289 | message: "Enter royalty payee account address", 290 | }, 291 | { 292 | type: "date", 293 | name: "mintStart", 294 | message: "Enter the public minting start time", 295 | }, 296 | { 297 | type: "date", 298 | name: "mintEnd", 299 | message: "Enter the public minting end time", 300 | }, 301 | { 302 | type: "number", 303 | name: "maxMintsPerAddress", 304 | message: 305 | "Enter the maximum allowed mints per address. 0 means no limits.", 306 | }, 307 | { 308 | type: "number", 309 | float: true, 310 | name: "mintPrice", 311 | message: "Enter the public minting price in APTs", 312 | }, 313 | { 314 | type: "confirm", 315 | name: "enableWL", 316 | message: "Do you want to support whitelist minting?", 317 | }, 318 | { 319 | type: (prev: any) => { 320 | enableWL = prev; 321 | return null; 322 | }, 323 | }, 324 | { 325 | type: () => (enableWL ? "date" : null), 326 | name: "wlMintStart", 327 | message: "Enter the whitelist minting start time", 328 | }, 329 | { 330 | type: () => (enableWL ? "date" : null), 331 | name: "wlMintEnd", 332 | message: "Enter the whitelist minting end time", 333 | }, 334 | { 335 | type: () => (enableWL ? "number" : null), 336 | name: "wlPrice", 337 | float: true, 338 | message: "Enter the whitelist minting price in APTs", 339 | }, 340 | ]; 341 | 342 | const response = await prompts(questions as any); 343 | const [configBuf, collectionBuf, tokenBuf] = await Promise.all([ 344 | fs.promises.readFile(path.join(__dirname, "templates", "config.json")), 345 | fs.promises.readFile(path.join(__dirname, "templates", "collection.json")), 346 | fs.promises.readFile(path.join(__dirname, "templates", "token.json")), 347 | ]); 348 | 349 | const configJson = JSON.parse(configBuf.toString("utf8")); 350 | const collectionJson = JSON.parse(collectionBuf.toString("utf8")); 351 | const tokenJson = JSON.parse(tokenBuf.toString("utf8")); 352 | 353 | const outJson = { 354 | assetPath, 355 | ...configJson, 356 | collection: { 357 | ...collectionJson, 358 | }, 359 | tokens: [], 360 | }; 361 | 362 | outJson.collection.name = response.collectionName; 363 | outJson.collection.description = response.collectionDescription; 364 | outJson.collection.file_path = resolvePath(response.collectionCover); 365 | outJson.collection.token_name_base = response.tokenNameBase; 366 | outJson.collection.token_description = response.tokenDescription || ""; 367 | 368 | outJson.mint_start = response.mintStart; 369 | outJson.mint_end = response.mintEnd; 370 | outJson.mint_price = response.mintPrice * OCTAS_PER_APT; 371 | outJson.max_mints_per_address = response.maxMintsPerAddress || 0; 372 | 373 | // Here we need to do number scaling since the smart contract only accepts integers. We only allow creators to provide 374 | // a number with a maximum of two digits precision. 375 | outJson.royalty_points_numerator = Math.floor( 376 | Number.parseFloat(response.royaltyPercent) * 100, 377 | ); 378 | outJson.royalty_points_denominator = 10000; 379 | outJson.royalty_payee_account = response.royaltyPayeeAcct; 380 | 381 | if (enableWL) { 382 | outJson.whitelist_mint_start = response.wlMintStart; 383 | outJson.whitelist_mint_end = response.wlMintEnd; 384 | outJson.whitelist_mint_price = response.wlPrice * OCTAS_PER_APT; 385 | } 386 | 387 | const jsonFiles: string[] = glob.sync( 388 | resolvePath(assetPath, "json", "*.json"), 389 | { windowsPathsNoEscape: true }, 390 | ); 391 | 392 | jsonFiles.forEach((p) => { 393 | if (path.basename(p) === "_metadata.json") return; 394 | 395 | const buf = fs.readFileSync(p); 396 | const json = JSON.parse(buf.toString("utf8")); 397 | 398 | const token = { 399 | ...tokenJson, 400 | }; 401 | 402 | token.file_path = resolvePath( 403 | assetPath, 404 | "images", 405 | `${path.basename(p, ".json")}.png`, 406 | ); 407 | token.metadata.attributes = json.attributes ?? []; 408 | token.supply = 1; 409 | token.royalty_points_denominator = outJson.royalty_points_denominator; 410 | token.royalty_points_numerator = outJson.royalty_points_numerator; 411 | 412 | outJson.tokens.push(token); 413 | }); 414 | 415 | await fs.promises.writeFile( 416 | path.join(fullPath, "config.json"), 417 | JSON.stringify(outJson, null, 4), 418 | "utf8", 419 | ); 420 | } 421 | 422 | /** 423 | * Verify that a path contains the required asset and metadata files 424 | * @param assetPath the build output path of HashLips 425 | */ 426 | async function checkHashLipsAsset(assetPath: string) { 427 | if (!fs.existsSync(assetPath)) { 428 | exitWithError(`"${assetPath}" is not a valid path.`); 429 | } 430 | 431 | // We first check "images" and "json" directories exist 432 | 433 | if (!fs.existsSync(resolvePath(assetPath, "images"))) { 434 | exitWithError( 435 | `Directory "${resolvePath(assetPath, "images")}" doesn't exist.`, 436 | ); 437 | } 438 | 439 | if (!fs.existsSync(resolvePath(assetPath, "json"))) { 440 | exitWithError( 441 | `Directory "${resolvePath(assetPath, "json")}" doesn't exist.`, 442 | ); 443 | } 444 | 445 | // Check that if every image file has a corresponding json file 446 | const images: string[] = glob.sync( 447 | resolvePath(assetPath, "images", "*.png"), 448 | { windowsPathsNoEscape: true }, 449 | ); // only png files are supported 450 | const jsonFiles: string[] = glob.sync( 451 | resolvePath(assetPath, "json", "*.json"), 452 | { windowsPathsNoEscape: true }, 453 | ); 454 | const jsonSet = new Set(); 455 | jsonFiles.forEach((p) => jsonSet.add(path.basename(p, ".json"))); 456 | images.forEach((p) => { 457 | if (!jsonSet.has(path.basename(p, ".png"))) { 458 | // eslint-disable-next-line quotes 459 | exitWithError('"images" and "json" files don\'t match.'); 460 | } 461 | }); 462 | 463 | // Check the json file format 464 | jsonFiles.forEach(async (p) => { 465 | if (path.basename(p) === "_metadata.json") return; 466 | 467 | const buf = await fs.promises.readFile(p); 468 | const json = JSON.parse(buf.toString("utf8")); 469 | if (!json.name?.length) { 470 | exitWithError(`"name" cannot be empty in ${p}`); 471 | } 472 | if (!json.description?.length) { 473 | exitWithError(`"description" cannot be empty in ${p}`); 474 | } 475 | }); 476 | } 477 | 478 | async function fundBundlr( 479 | account: AptosAccount, 480 | amount: string, 481 | network: NetworkType, 482 | ) { 483 | const questions = [ 484 | { 485 | type: "confirm", 486 | name: "continue", 487 | message: `Do you want to fund the storage service ${octasToApt( 488 | amount, 489 | )} APT from account address ${account.address()}`, 490 | }, 491 | ]; 492 | const response = await prompts(questions as any); 493 | if (!response.continue) return; 494 | 495 | const bundlr = new BundlrUploader(account, network); 496 | await bundlr.fund(amount); 497 | console.log("The storage service is funded."); 498 | } 499 | 500 | async function getBundlrBalance( 501 | accountAddress: MaybeHexString, 502 | network: NetworkType, 503 | ) { 504 | const bundlrUrl = 505 | network === MAINNET ? MAINNET_BUNDLR_URL : TESTNET_BUNDLR_URL; 506 | const bundlr = await Bundlr.init({ 507 | url: bundlrUrl, 508 | currency: "aptos", 509 | }); 510 | 511 | const balance = await bundlr.getBalance( 512 | HexString.ensure(accountAddress).hex(), 513 | ); 514 | console.log(`${balance} OCTAS (${octasToApt(balance.toString())} APTs)`); 515 | } 516 | 517 | async function isMintingTimeAndPriceAlreadySet( 518 | projectPath: string, 519 | ): Promise { 520 | try { 521 | const db = new Database(path.join(projectPath, "minting.sqlite")); 522 | const dbGet = util.promisify(db.get.bind(db)); 523 | 524 | const row: any = await dbGet( 525 | "SELECT finished FROM tasks where type = 'set_minting_time_and_price' and name = 'set_minting_time_and_price'", 526 | ); 527 | 528 | if (row?.finished) { 529 | return true; 530 | } 531 | // eslint-disable-next-line no-empty 532 | } catch (e) {} 533 | 534 | return false; 535 | } 536 | 537 | async function assertProjectValid( 538 | projectPath: string, 539 | print: boolean, 540 | checkAssetHashes: boolean = false, 541 | ) { 542 | const config = readProjectConfig(projectPath); 543 | const { collection } = config; 544 | 545 | const errors: string[] = []; 546 | const warnings: string[] = []; 547 | 548 | if (config?.royalty_points_numerator <= 0) { 549 | // eslint-disable-next-line quotes 550 | warnings.push('Did you forget to set "royalty_points_numerator".'); 551 | } 552 | 553 | if (!config?.royalty_payee_account) { 554 | // eslint-disable-next-line quotes 555 | warnings.push('Did you forget to set "royalty_payee_account".'); 556 | } 557 | 558 | const skipMintingTimeCheck = await isMintingTimeAndPriceAlreadySet( 559 | projectPath, 560 | ); 561 | 562 | if (!skipMintingTimeCheck) { 563 | if ( 564 | config.mint_start && 565 | config.mint_end && 566 | new Date(config.mint_end) <= new Date(config.mint_start) 567 | ) { 568 | // eslint-disable-next-line quotes 569 | errors.push('"mint_end" should not be earlier than "mint_start".'); 570 | } 571 | 572 | if ( 573 | config.whitelist_mint_start && 574 | config.whitelist_mint_end && 575 | new Date(config.whitelist_mint_end) <= 576 | new Date(config.whitelist_mint_start) 577 | ) { 578 | errors.push( 579 | // eslint-disable-next-line quotes 580 | '"whitelist_mint_end" should not be earlier than "whitelist_mint_start".', 581 | ); 582 | } 583 | 584 | if ( 585 | config.mint_start && 586 | config.whitelist_mint_end && 587 | new Date(config.mint_start) < new Date(config.whitelist_mint_end) 588 | ) { 589 | errors.push( 590 | // eslint-disable-next-line quotes 591 | '"whitelist_mint_end" should be earlier than "mint_start".', 592 | ); 593 | } 594 | 595 | if ( 596 | config.whitelist_mint_start && 597 | new Date(config.whitelist_mint_start) < new Date() 598 | ) { 599 | errors.push( 600 | // eslint-disable-next-line quotes 601 | '"whitelist_mint_start" should be a future time.', 602 | ); 603 | } 604 | 605 | if ( 606 | config.whitelist_mint_price !== 0 && 607 | config.mint_price < config.whitelist_mint_price 608 | ) { 609 | errors.push( 610 | // eslint-disable-next-line quotes 611 | '"mint_price" should be not less than "whitelist_mint_price".', 612 | ); 613 | } 614 | } 615 | 616 | if (!collection?.name) { 617 | errors.push("Collection name cannot be empty."); 618 | } 619 | 620 | if (!collection?.file_path) { 621 | errors.push("Collection has no cover image."); 622 | } else if (!fs.existsSync(collection.file_path)) { 623 | errors.push(`Collection cover file ${collection.file_path} doesn't exist.`); 624 | } 625 | 626 | if (!config.tokens || config.tokens.length === 0) { 627 | errors.push("No tokens available for minting."); 628 | } 629 | 630 | const tokenImages = new Set(); 631 | 632 | const assetHashMap = new Map(); 633 | 634 | config.tokens.forEach((token: any, i: number) => { 635 | if (!token.file_path) { 636 | errors.push(`Token at index ${i} has no image.`); 637 | } else if (!fs.existsSync(token.file_path)) { 638 | errors.push(`Token image ${token.file_path} doesn't exist.`); 639 | } else if (tokenImages.has(token.file_path)) { 640 | errors.push(`Duplicated token image file ${token.file_path}.`); 641 | } else { 642 | tokenImages.add(token.file_path); 643 | } 644 | 645 | if (token.supply <= 0) { 646 | errors.push(`${token.name} "supply" is <= 0`); 647 | } 648 | 649 | // Make sure token attributes are unique 650 | const attributeKeys = new Set(); 651 | token.metadata?.attributes?.forEach((attr: any) => { 652 | if (attr?.trait_type) { 653 | if (attributeKeys.has(attr.trait_type)) { 654 | throw new Error( 655 | `Found duplicate trait type "${attr.trait_type}" for token ${i}`, 656 | ); 657 | } 658 | attributeKeys.add(attr.trait_type); 659 | } 660 | }); 661 | 662 | // Warning! This is going to be really slow. 663 | if (checkAssetHashes) { 664 | const fbuf = fs.readFileSync(token.file_path); 665 | const hash = sha3Hash.create(); 666 | hash.update(fbuf); 667 | 668 | const hashHex = HexString.fromUint8Array(hash.digest()).hex(); 669 | if (assetHashMap.has(hashHex)) { 670 | exitWithError( 671 | `${token.name} and ${assetHashMap.get( 672 | hashHex, 673 | )} have the same asset files!`, 674 | ); 675 | } else { 676 | assetHashMap.set(hashHex, token.name); 677 | } 678 | } 679 | }); 680 | 681 | if (print) { 682 | errors.forEach((err: string) => console.error(chalk.red(err))); 683 | warnings.forEach((warn: string) => console.error(chalk.yellow(warn))); 684 | 685 | if (errors.length === 0 && warnings.length === 0) { 686 | console.log( 687 | `${path.join(projectPath, "config.json")} passed validation check.`, 688 | ); 689 | } 690 | } 691 | 692 | if (errors.length > 0) { 693 | exit(1); 694 | } 695 | } 696 | 697 | async function createNFTMintingEngine({ 698 | projectPath, 699 | profile, 700 | mintingContract, 701 | }: { 702 | projectPath: string; 703 | profile: string; 704 | mintingContract: string; 705 | }): Promise { 706 | const [account, network] = await resolveProfile(profile); 707 | const nftContract = 708 | mintingContract || readProjectConfig(projectPath)?.contractAddress; 709 | if (!nftContract) { 710 | throw new Error("Minting contract address is unknown."); 711 | } 712 | return new NFTMint(projectPath, account, nftContract, network); 713 | } 714 | 715 | async function setMintingContractAddress( 716 | projectPath: string, 717 | contractAddress: string, 718 | ) { 719 | const config = readProjectConfig(projectPath); 720 | config.contractAddress = contractAddress; 721 | 722 | await fs.promises.writeFile( 723 | `${resolvePath(projectPath, "config.json")}`, 724 | JSON.stringify(config, null, 4), 725 | "utf8", 726 | ); 727 | } 728 | 729 | async function run() { 730 | program.parse(); 731 | } 732 | 733 | process.on("uncaughtException", (err: Error) => { 734 | if (program.opts().verbose) { 735 | console.error(err); 736 | } 737 | 738 | exitWithError(err.message); 739 | }); 740 | 741 | run(); 742 | -------------------------------------------------------------------------------- /minting-tool/cli/src/nft-mint.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | /* eslint-disable max-len */ 3 | import { 4 | AptosAccount, 5 | AptosClient, 6 | HexString, 7 | MaybeHexString, 8 | getPropertyValueRaw, 9 | TxnBuilderTypes, 10 | } from "aptos"; 11 | import { Database } from "sqlite3"; 12 | import path from "path"; 13 | import util from "util"; 14 | import { sha3_256 as sha3Hash } from "@noble/hashes/sha3"; 15 | import canonicalize from "canonicalize"; 16 | import cluster from "node:cluster"; 17 | import { cpus } from "node:os"; 18 | import { program } from "commander"; 19 | 20 | import fs from "fs"; 21 | 22 | import chalk from "chalk"; 23 | import { exit } from "process"; 24 | import { AssetUploader, BundlrUploader } from "./asset-uploader"; 25 | import { 26 | dateTimeStrToUnixSecs, 27 | MAINNET, 28 | MAINNET_APTOS_URL, 29 | MapWithDefault, 30 | NetworkType, 31 | readProjectConfig, 32 | sleep, 33 | TESTNET_APTOS_URL, 34 | } from "./utils"; 35 | 36 | const numCPUs = Math.max(20, cpus().length); 37 | 38 | const MAX_TXN_BATCH_SIZE = 40; 39 | 40 | // This class gets the minting contract ready for lazy minting. 41 | export class NFTMint { 42 | private readonly client: AptosClient; 43 | 44 | private readonly uploader: AssetUploader; 45 | 46 | private readonly db: Database; 47 | 48 | private readonly config: Record; 49 | 50 | private dbGet: (sql: string) => Promise; 51 | 52 | private dbRun: (sql: string) => Promise; 53 | 54 | private dbAll: (sql: string) => Promise; 55 | 56 | private txnBatchSize = MAX_TXN_BATCH_SIZE; 57 | 58 | exitWorkers: number; 59 | 60 | constructor( 61 | public readonly projectPath: string, 62 | private readonly account: AptosAccount, 63 | public readonly mintingContractAddress: MaybeHexString, 64 | public readonly network: NetworkType, 65 | ) { 66 | const uploader = new BundlrUploader(account, network); 67 | const nodeURL = network === MAINNET ? MAINNET_APTOS_URL : TESTNET_APTOS_URL; 68 | this.db = new Database(path.join(projectPath, "minting.sqlite")); 69 | // Wait for up to two minutes when others are holding the lock 70 | this.db.configure("busyTimeout", 1200000); 71 | this.projectPath = projectPath ?? "."; 72 | this.config = readProjectConfig(projectPath); 73 | this.uploader = uploader; 74 | this.client = new AptosClient(nodeURL); 75 | this.dbGet = util.promisify(this.db.get.bind(this.db)); 76 | this.dbRun = util.promisify(this.db.run.bind(this.db)); 77 | this.dbAll = util.promisify(this.db.all.bind(this.db)); 78 | this.mintingContractAddress = HexString.ensure( 79 | mintingContractAddress, 80 | ).hex(); 81 | this.exitWorkers = 0; 82 | 83 | if (this.config.txnBatchSize) { 84 | this.txnBatchSize = this.config.txnBatchSize; 85 | } 86 | } 87 | 88 | getExplorerLink(txnHash: string): string { 89 | return `https://explorer.aptoslabs.com/txn/${txnHash}?network=${this.network}`; 90 | } 91 | 92 | async checkTxnSuccessWithMessage(txnHash: string, message: string) { 93 | const txn = await this.client.waitForTransactionWithResult(txnHash, { 94 | timeoutSecs: 600, 95 | }); 96 | 97 | if (!(txn as any)?.success) { 98 | throw new Error( 99 | `${message}\nTransaction link ${this.getExplorerLink(txnHash)}`, 100 | ); 101 | } 102 | } 103 | 104 | hash(jsonObj: any): string { 105 | const canonicalStr = canonicalize(jsonObj)!; 106 | 107 | const hash = sha3Hash.create(); 108 | hash.update(canonicalStr); 109 | 110 | return HexString.fromUint8Array(hash.digest()).hex(); 111 | } 112 | 113 | async insertTask( 114 | taskType: "collection_img_upload" | "token" | "set_minting_time_and_price", 115 | name: string, 116 | ) { 117 | const row = await this.dbGet( 118 | `SELECT id FROM tasks where type = '${taskType}' and name = '${name}'`, 119 | ); 120 | 121 | if (!row) { 122 | await this.dbRun( 123 | `INSERT INTO tasks(name, type, extra_data, finished) VALUES('${name}', '${taskType}', '', 0)`, 124 | ); 125 | } 126 | } 127 | 128 | // Theses tasks can be ran on multiple cpu cores 129 | async loadTasks(config: Record) { 130 | await this.insertTask( 131 | "set_minting_time_and_price", 132 | "set_minting_time_and_price", 133 | ); 134 | 135 | await this.insertTask("collection_img_upload", config.collection.name); 136 | 137 | config.tokens.forEach(async (token: any, i: number) => { 138 | await this.insertTask("token", i.toString()); 139 | }); 140 | } 141 | 142 | async ensureTablesExist() { 143 | // Minting has not started in the past. Let's create the minting tracking db 144 | await this.dbRun(` 145 | CREATE TABLE IF NOT EXISTS tasks( 146 | id INTEGER PRIMARY KEY AUTOINCREMENT, 147 | name TEXT, 148 | type TEXT, 149 | extra_data TEXT, 150 | finished INTEGER 151 | ) 152 | `); 153 | } 154 | 155 | async uploadTokenImageTask(token: any, i: number) { 156 | const row: any = await this.dbGet( 157 | `SELECT finished FROM tasks where type = 'token' and name = '${i}'`, 158 | ); 159 | 160 | if (row?.finished) { 161 | console.log( 162 | `The asset of the token with index "${i}" was uploaded. Skip.`, 163 | ); 164 | return; 165 | } 166 | 167 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 168 | const [assetUri, hash] = await this.uploadOffChainMetaData( 169 | token.file_path, 170 | token, 171 | ); 172 | 173 | await this.dbRun( 174 | `UPDATE tasks set finished = 1, extra_data = '${assetUri}' where type = 'token' and name = '${i}'`, 175 | ); 176 | 177 | console.log( 178 | `The asset of the token with index "${i}" is uploaded to ${assetUri}`, 179 | ); 180 | } 181 | 182 | async setCollectionConfigTask() { 183 | try { 184 | // If the mint config actually exists, return early. 185 | await this.client.getAccountResource( 186 | this.mintingContractAddress, 187 | `${this.mintingContractAddress}::minting::CollectionConfig`, 188 | ); 189 | 190 | return; 191 | // eslint-disable-next-line no-empty 192 | } catch (e) {} 193 | 194 | const row: any = await this.dbGet( 195 | `SELECT extra_data FROM tasks where type = 'collection_img_upload' and name = '${this.config.collection.name}'`, 196 | ); 197 | 198 | if (!row?.extra_data) { 199 | throw new Error("Collection asset url is not available."); 200 | } 201 | 202 | const collectionUri = row.extra_data; 203 | 204 | const { collection } = this.config; 205 | 206 | const rawTxn = await this.client.generateTransaction( 207 | this.account.address(), 208 | { 209 | function: `${this.mintingContractAddress}::minting::set_collection_config_and_create_collection`, 210 | type_arguments: [], 211 | arguments: [ 212 | collection.name, 213 | collection.description, 214 | collection.maximum, 215 | collectionUri, 216 | collection.mutability_config, 217 | collection.token_name_base, 218 | this.config.royalty_payee_account, 219 | collection.token_description, 220 | 1, // TODO: remove the hard coded value for token_maximum 221 | collection.token_mutate_config, 222 | this.config.royalty_points_denominator, 223 | this.config.royalty_points_numerator, 224 | this.config.max_mints_per_address, 225 | ], 226 | }, 227 | ); 228 | 229 | const bcsTxn = await this.client.signTransaction(this.account, rawTxn); 230 | const pendingTxn = await this.client.submitTransaction(bcsTxn); 231 | 232 | await this.checkTxnSuccessWithMessage( 233 | pendingTxn.hash, 234 | "Failed to set collection config and create collection.", 235 | ); 236 | } 237 | 238 | async setMintingTimeAndPrice() { 239 | const rawTxn = await this.client.generateTransaction( 240 | this.account.address(), 241 | { 242 | function: `${this.mintingContractAddress}::minting::set_minting_time_and_price`, 243 | type_arguments: [], 244 | arguments: [ 245 | this.config.whitelist_mint_start 246 | ? dateTimeStrToUnixSecs(this.config.whitelist_mint_start) 247 | : 0, 248 | this.config.whitelist_mint_end 249 | ? dateTimeStrToUnixSecs(this.config.whitelist_mint_end) 250 | : 0, 251 | this.config.whitelist_mint_price || 0, 252 | dateTimeStrToUnixSecs(this.config.mint_start), 253 | dateTimeStrToUnixSecs(this.config.mint_end), 254 | this.config.mint_price, 255 | ], 256 | }, 257 | ); 258 | 259 | const bcsTxn = await this.client.signTransaction(this.account, rawTxn); 260 | const pendingTxn = await this.client.submitTransaction(bcsTxn); 261 | 262 | await this.checkTxnSuccessWithMessage( 263 | pendingTxn.hash, 264 | "Failed to set minting time and price.", 265 | ); 266 | } 267 | 268 | async addToWhiteList(addresses: string[], mintLimitPerAddress: number) { 269 | const rawTxn = await this.client.generateTransaction( 270 | this.account.address(), 271 | { 272 | function: `${this.mintingContractAddress}::minting::add_to_whitelist`, 273 | type_arguments: [], 274 | arguments: [addresses, mintLimitPerAddress], 275 | }, 276 | ); 277 | 278 | const bcsTxn = await this.client.signTransaction(this.account, rawTxn); 279 | const pendingTxn = await this.client.submitTransaction(bcsTxn); 280 | 281 | await this.checkTxnSuccessWithMessage( 282 | pendingTxn.hash, 283 | "Failed to to add adresses to whitelist.", 284 | ); 285 | } 286 | 287 | async setMintingTimeAndPriceTask() { 288 | const dbGet = util.promisify(this.db.get.bind(this.db)); 289 | const row: any = await dbGet( 290 | "SELECT finished FROM tasks where type = 'set_minting_time_and_price' and name = 'set_minting_time_and_price'", 291 | ); 292 | 293 | if (row?.finished) { 294 | return; 295 | } 296 | 297 | await this.setMintingTimeAndPrice(); 298 | 299 | await this.dbRun( 300 | "UPDATE tasks set finished = 1 where type = 'set_minting_time_and_price' and name = 'set_minting_time_and_price'", 301 | ); 302 | } 303 | 304 | private async genAddTokensTxn( 305 | tokens: [any, number][], 306 | ): Promise { 307 | const tokenNames = tokens.map(([, i]) => `'${i}'`); 308 | 309 | const query = `SELECT extra_data FROM tasks where type = 'token' and name in (${tokenNames.join( 310 | ",", 311 | )})`; 312 | 313 | // Fetch the asset urls from local sqlite. 314 | const rows: any = await this.dbAll(query); 315 | 316 | if (!rows) { 317 | throw new Error(`Failed to run query "${query}"`); 318 | } 319 | 320 | const urls = rows.map((row: any) => row.extra_data); 321 | 322 | const propertyKeys: any[] = []; 323 | const propertyValues: any[] = []; 324 | const propertyTypes: any[] = []; 325 | 326 | tokens.forEach(([token]) => { 327 | const keys = [...token.property_map.property_keys]; 328 | const values = getPropertyValueRaw( 329 | token.property_map.property_values, 330 | token.property_map.property_types, 331 | ); 332 | const types = [...token.property_map.property_types]; 333 | 334 | // We would store token attributes on chain too 335 | token?.metadata?.attributes?.forEach((attr: any) => { 336 | if (attr?.trait_type && attr?.value) { 337 | keys?.unshift(attr?.trait_type); 338 | values?.unshift( 339 | ...getPropertyValueRaw([attr?.value], ["0x1::string::String"]), 340 | ); 341 | types?.unshift("0x1::string::String"); 342 | } 343 | }); 344 | 345 | propertyKeys.push(keys); 346 | propertyValues.push(values); 347 | propertyTypes.push(types); 348 | }); 349 | 350 | return this.client.generateTransaction(this.account.address(), { 351 | function: `${this.mintingContractAddress}::minting::add_tokens`, 352 | type_arguments: [], 353 | arguments: [urls, propertyKeys, propertyValues, propertyTypes], 354 | }); 355 | } 356 | 357 | // WARNING: we are adding tokens one by one. This costs more gas. However, this will avoid the exception that 358 | // transaction size exceeds limits. For simplicity, we only support adding token one by one at the moment. 359 | async addTokensTask(token: any, i: number) { 360 | const row: any = await this.dbGet( 361 | `SELECT * FROM tasks where type = 'token' and name = '${i}'`, 362 | ); 363 | 364 | // 2 means the token has been added to the smart contract. 365 | if (row.finished === 2) { 366 | console.log(`Token at index ${i} was added to the smart contract. Skip.`); 367 | return; 368 | } 369 | 370 | const rawTxn = await this.genAddTokensTxn([[token, i]]); 371 | 372 | const bcsTxn = await this.client.signTransaction(this.account, rawTxn); 373 | const pendingTxn = await this.client.submitTransaction(bcsTxn); 374 | 375 | const txnResult = await this.client.waitForTransactionWithResult( 376 | pendingTxn.hash, 377 | { 378 | timeoutSecs: 600, 379 | }, 380 | ); 381 | 382 | if ( 383 | !(txnResult as any)?.success && 384 | !(txnResult as any).vm_status.includes("EDUPLICATED_TOKENS") 385 | ) { 386 | throw new Error( 387 | `Failed to add the token at index ${i} to smart contract.\nTransaction link ${this.getExplorerLink( 388 | pendingTxn.hash, 389 | )}`, 390 | ); 391 | } 392 | 393 | await this.dbRun( 394 | `UPDATE tasks set finished = 2 where type = 'token' and name = '${row.name}'`, 395 | ); 396 | console.log(`Token at index ${i} is added to the smart contract.`); 397 | } 398 | 399 | async addTokensBatchTask(tokens: [any, number][]) { 400 | try { 401 | const rawTxn = await this.genAddTokensTxn(tokens); 402 | 403 | const bcsTxn = await this.client.signTransaction(this.account, rawTxn); 404 | const pendingTxn = await this.client.submitTransaction(bcsTxn); 405 | 406 | const tokenNames = tokens.map(([, i]) => `'${i}'`); 407 | 408 | await this.checkTxnSuccessWithMessage( 409 | pendingTxn.hash, 410 | `Failed to add the tokens at indices ${tokenNames.join( 411 | ", ", 412 | )} to smart contract.`, 413 | ); 414 | 415 | await this.dbRun( 416 | `UPDATE tasks set finished = 2 where type = 'token' and name in (${tokenNames.join( 417 | ",", 418 | )})`, 419 | ); 420 | 421 | console.log( 422 | `Tokens at indices ${tokenNames.join( 423 | ", ", 424 | )} are added to the smart contract.`, 425 | ); 426 | } catch (e) { 427 | if (program.opts().verbose) { 428 | console.error(e); 429 | } 430 | 431 | // Falls back to single txn mode 432 | for (let i = 0; i < tokens.length; i += 1) { 433 | const [token, index] = tokens[i]; 434 | try { 435 | // In single txn mode, we allow individual txn fail and continue with the rest. 436 | // The reason is that some txns of the batch might have already been uploaded. 437 | await this.addTokensTask(token, index); 438 | } catch (err) { 439 | if (program.opts().verbose) { 440 | console.error(err); 441 | } 442 | } 443 | } 444 | } 445 | } 446 | 447 | async decideBatchSize() { 448 | while (this.txnBatchSize > 1) { 449 | try { 450 | // Simulate token submittion, halve txnBatchSize if simulation failed 451 | const batchTokens = this.config.tokens.slice(-1 * this.txnBatchSize); 452 | 453 | const tokens = batchTokens.map((t: any, i: number) => [ 454 | t, 455 | this.config.tokens.length - 1 - i, 456 | ]); 457 | 458 | const rawTxn = await this.genAddTokensTxn(tokens); 459 | 460 | const result = await this.client.simulateTransaction( 461 | this.account, 462 | rawTxn, 463 | ); 464 | 465 | if (result?.[0].success) { 466 | return; 467 | } 468 | 469 | this.txnBatchSize = Math.ceil(this.txnBatchSize / 2); 470 | } catch (e) { 471 | this.txnBatchSize = Math.ceil(this.txnBatchSize / 2); 472 | } 473 | } 474 | } 475 | 476 | async uploadCollectionImageTask(collection: any) { 477 | const row: any = await this.dbGet( 478 | `SELECT finished FROM tasks where type = 'collection_img_upload' and name = '${collection.name}'`, 479 | ); 480 | 481 | if (row?.finished) { 482 | console.log( 483 | `The asset of the collection "${collection.name}" was uploaded. Skip.`, 484 | ); 485 | return; 486 | } 487 | 488 | if (!collection.file_path) return; 489 | 490 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 491 | const [coverUri, hash] = await this.uploadOffChainMetaData( 492 | collection.file_path, 493 | collection, 494 | ); 495 | 496 | await this.dbRun( 497 | `UPDATE tasks set finished = 1, extra_data = '${coverUri}' where type = 'collection_img_upload' and name = '${collection.name}'`, 498 | ); 499 | 500 | console.log(`Collection cover image is uploaded to "${coverUri}"`); 501 | } 502 | 503 | async verifyAllTasksDone() { 504 | console.log(chalk.greenBright("Verifying if all tasks are done...")); 505 | 506 | const rows: any = await this.dbAll( 507 | "SELECT finished FROM tasks where type = 'token'", 508 | ); 509 | 510 | if (!rows) { 511 | console.error(chalk.red("Unable to read task statuses.")); 512 | return; 513 | } 514 | 515 | rows.forEach((row: any) => { 516 | if (row.finished !== 2) { 517 | throw new Error( 518 | "Some tasks did not finish. You can rerun the upload command.", 519 | ); 520 | } 521 | }); 522 | console.log(chalk.greenBright("All tasks are done")); 523 | } 524 | 525 | private generateTaskIds(): MapWithDefault { 526 | const cpuTaskQeue = new MapWithDefault(() => []); 527 | 528 | const taskIds = this.config.tokens.map((_: any, i: number) => i); 529 | let i = 0; 530 | while (taskIds.length > 0) { 531 | const task = taskIds[0]; 532 | cpuTaskQeue.get(i % numCPUs)!.push(task); 533 | taskIds.shift(); 534 | i += 1; 535 | } 536 | 537 | return cpuTaskQeue; 538 | } 539 | 540 | private forkWorkers(env: { [key: string]: any }) { 541 | const taskIds = this.generateTaskIds(); 542 | // Fork workers. 543 | for (let i = 0; i < numCPUs; i += 1) { 544 | if (taskIds.has(i)) { 545 | cluster.fork({ TASKS: JSON.stringify(taskIds.get(i)), ...env }); 546 | } 547 | } 548 | } 549 | 550 | private async joinWorkers() { 551 | this.exitWorkers = 0; 552 | const numberTasks = this.generateTaskIds().size; 553 | 554 | while (this.exitWorkers < numberTasks) { 555 | await sleep(2000); 556 | } 557 | } 558 | 559 | // Run in parallel for a large number of assets 560 | async run() { 561 | if (cluster.isPrimary) { 562 | cluster.on("exit", () => { 563 | this.exitWorkers += 1; 564 | }); 565 | const config = await this.validateProjectFolder(); 566 | 567 | await this.ensureTablesExist(); 568 | await this.loadTasks(config); 569 | 570 | // Upload the collection asset 571 | await this.uploadCollectionImageTask(config.collection); 572 | 573 | // Fork workers 574 | this.forkWorkers({ STEP: "upload_token_assets" }); 575 | } else if (process.env.STEP === "upload_token_assets") { 576 | // In worker 577 | const tasks = JSON.parse(process.env.TASKS || "[]"); 578 | // Upload the token assets 579 | for (let i = 0; i < tasks.length; i += 1) { 580 | const tokenIndex = tasks[i]; 581 | const token = this.config.tokens[tokenIndex]; 582 | await this.uploadTokenImageTask(token, tokenIndex); 583 | } 584 | // Make sure workers exit here 585 | exit(0); 586 | } 587 | 588 | // Now, we are back to the primary process 589 | await this.joinWorkers(); 590 | 591 | // Create the collection 592 | await this.setCollectionConfigTask(); 593 | 594 | // Set minting time 595 | await this.setMintingTimeAndPriceTask(); 596 | 597 | await this.decideBatchSize(); 598 | 599 | // Add tokens 600 | const rows: any = await this.dbAll( 601 | "SELECT name FROM tasks where type = 'token' and finished = 1", 602 | ); 603 | const tokensToBeAdded = rows 604 | .map((r: any) => r.name) 605 | .sort((a: string, b: string) => parseInt(a, 10) - parseInt(b, 10)); 606 | 607 | const batches: [any, number][][] = []; 608 | 609 | let currentBatch: [any, number][] = []; 610 | while (tokensToBeAdded.length > 0) { 611 | const i = tokensToBeAdded.shift(); 612 | currentBatch.push([this.config.tokens[i], i]); 613 | 614 | if (currentBatch.length === this.txnBatchSize) { 615 | batches.push(currentBatch); 616 | currentBatch = []; 617 | } 618 | } 619 | 620 | if (currentBatch.length > 0) { 621 | batches.push(currentBatch); 622 | } 623 | 624 | for (let j = 0; j < batches.length; j += 1) { 625 | await this.addTokensBatchTask(batches[j]); 626 | } 627 | 628 | await this.verifyAllTasksDone(); 629 | } 630 | 631 | async validateProjectFolder(): Promise> { 632 | if (!fs.existsSync(path.join(this.projectPath, "config.json"))) { 633 | throw new Error(`config.json doesn't exist in ${this.projectPath}`); 634 | } 635 | 636 | const { config } = this; 637 | 638 | if (!config?.collection?.name) { 639 | throw new Error("collection name cannot be empty"); 640 | } 641 | 642 | config?.collection?.tokens?.forEach((token: any) => { 643 | if (!token?.name) { 644 | throw new Error("token name cannot be empty"); 645 | } 646 | }); 647 | 648 | return config; 649 | } 650 | 651 | // create shared account for royalty 652 | // TODO: this should dedup if the creators and weights are same 653 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 654 | async createRoyaltyAccount(config: any): Promise { 655 | throw new Error("Unimplemented"); 656 | } 657 | 658 | // construct the final json with URL and input config 659 | async uploadOffChainMetaData( 660 | assetPath: string, 661 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 662 | metaData: { [key: string]: any }, 663 | ): Promise<[string, string]> { 664 | const dataId = await this.uploader.uploadFile(assetPath); 665 | const url = this.createArweaveURLfromId(dataId); 666 | 667 | return [url, ""]; 668 | } 669 | 670 | createArweaveURLfromId(dataId: string): string { 671 | return `https://arweave.net/${dataId}`; 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /minting-tool/cli/src/templates/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "", 4 | "file_path": "", 5 | "token_name_base": "", 6 | "token_description": "", 7 | "token_mutate_config": [false, false, false, false, false], 8 | "metadata": { 9 | "attributes": [], 10 | "properties": {} 11 | }, 12 | "maximum": 0, 13 | "mutability_config": [false, false, false] 14 | } 15 | -------------------------------------------------------------------------------- /minting-tool/cli/src/templates/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "royalty_payee_account": "", 3 | "royalty_points_denominator": 100, 4 | "royalty_points_numerator": -1, 5 | "mint_start": null, 6 | "mint_end": null, 7 | "mint_price": null, 8 | "max_mints_per_address": 0, 9 | "whitelist_mint_start": 0, 10 | "whitelist_mint_end": 0, 11 | "whitelist_mint_price": 0 12 | } 13 | -------------------------------------------------------------------------------- /minting-tool/cli/src/templates/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_path": "", 3 | "metadata": { 4 | "attributes": [], 5 | "properties": {} 6 | }, 7 | "supply": 0, 8 | "royalty_creator_weights": {}, 9 | "royalty_points_denominator": 100, 10 | "royalty_points_numerator": -1, 11 | "royalty_payee_account": "", 12 | "token_mutate_setting": [false, false, false], 13 | "property_map": { 14 | "property_keys": [], 15 | "property_values": [], 16 | "property_types": [] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/cli/src/tests/assets/images/1.png -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/cli/src/tests/assets/images/2.png -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/cli/src/tests/assets/images/3.png -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/cli/src/tests/assets/images/4.png -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/cli/src/tests/assets/images/5.png -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/json/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Your Collection #1", 3 | "description": "Remember to replace this description", 4 | "image": "ipfs://NewUriToReplace/1.png", 5 | "dna": "01f773ac1b5a67929370a6ff1ed5cef02cf13625", 6 | "edition": 1, 7 | "date": 1668640132863, 8 | "attributes": [ 9 | { 10 | "trait_type": "Background", 11 | "value": "Black" 12 | }, 13 | { 14 | "trait_type": "Eyeball", 15 | "value": "White" 16 | }, 17 | { 18 | "trait_type": "Eye color", 19 | "value": "Yellow" 20 | }, 21 | { 22 | "trait_type": "Iris", 23 | "value": "Medium" 24 | }, 25 | { 26 | "trait_type": "Shine", 27 | "value": "Shapes" 28 | }, 29 | { 30 | "trait_type": "Bottom lid", 31 | "value": "Low" 32 | }, 33 | { 34 | "trait_type": "Top lid", 35 | "value": "Middle" 36 | } 37 | ], 38 | "compiler": "HashLips Art Engine" 39 | } -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/json/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Your Collection #2", 3 | "description": "Remember to replace this description", 4 | "image": "ipfs://NewUriToReplace/2.png", 5 | "dna": "53102f97aa4809a74e155b31ca63c9ccd71e14d0", 6 | "edition": 2, 7 | "date": 1668640133034, 8 | "attributes": [ 9 | { 10 | "trait_type": "Background", 11 | "value": "Black" 12 | }, 13 | { 14 | "trait_type": "Eyeball", 15 | "value": "White" 16 | }, 17 | { 18 | "trait_type": "Eye color", 19 | "value": "Yellow" 20 | }, 21 | { 22 | "trait_type": "Iris", 23 | "value": "Large" 24 | }, 25 | { 26 | "trait_type": "Shine", 27 | "value": "Shapes" 28 | }, 29 | { 30 | "trait_type": "Bottom lid", 31 | "value": "High" 32 | }, 33 | { 34 | "trait_type": "Top lid", 35 | "value": "Middle" 36 | } 37 | ], 38 | "compiler": "HashLips Art Engine" 39 | } -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/json/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Your Collection #3", 3 | "description": "Remember to replace this description", 4 | "image": "ipfs://NewUriToReplace/3.png", 5 | "dna": "8894598ad4e2c53c243c67f84975e85c477f48ca", 6 | "edition": 3, 7 | "date": 1668640133203, 8 | "attributes": [ 9 | { 10 | "trait_type": "Background", 11 | "value": "Black" 12 | }, 13 | { 14 | "trait_type": "Eyeball", 15 | "value": "White" 16 | }, 17 | { 18 | "trait_type": "Eye color", 19 | "value": "Yellow" 20 | }, 21 | { 22 | "trait_type": "Iris", 23 | "value": "Medium" 24 | }, 25 | { 26 | "trait_type": "Shine", 27 | "value": "Shapes" 28 | }, 29 | { 30 | "trait_type": "Bottom lid", 31 | "value": "Middle" 32 | }, 33 | { 34 | "trait_type": "Top lid", 35 | "value": "High" 36 | } 37 | ], 38 | "compiler": "HashLips Art Engine" 39 | } -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/json/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Your Collection #4", 3 | "description": "Remember to replace this description", 4 | "image": "ipfs://NewUriToReplace/4.png", 5 | "dna": "10e90a23c9d3af21b6d08f7b156544f3fc055db7", 6 | "edition": 4, 7 | "date": 1668640133389, 8 | "attributes": [ 9 | { 10 | "trait_type": "Background", 11 | "value": "Black" 12 | }, 13 | { 14 | "trait_type": "Eyeball", 15 | "value": "White" 16 | }, 17 | { 18 | "trait_type": "Eye color", 19 | "value": "Green" 20 | }, 21 | { 22 | "trait_type": "Iris", 23 | "value": "Small" 24 | }, 25 | { 26 | "trait_type": "Shine", 27 | "value": "Shapes" 28 | }, 29 | { 30 | "trait_type": "Bottom lid", 31 | "value": "High" 32 | }, 33 | { 34 | "trait_type": "Top lid", 35 | "value": "Middle" 36 | } 37 | ], 38 | "compiler": "HashLips Art Engine" 39 | } -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/assets/json/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Your Collection #5", 3 | "description": "Remember to replace this description", 4 | "image": "ipfs://NewUriToReplace/5.png", 5 | "dna": "f6decd3758fec287ebba5cda110e0f50afacd3d1", 6 | "edition": 5, 7 | "date": 1668640133541, 8 | "attributes": [ 9 | { 10 | "trait_type": "Background", 11 | "value": "Black" 12 | }, 13 | { 14 | "trait_type": "Eyeball", 15 | "value": "Red" 16 | }, 17 | { 18 | "trait_type": "Eye color", 19 | "value": "Yellow" 20 | }, 21 | { 22 | "trait_type": "Iris", 23 | "value": "Medium" 24 | }, 25 | { 26 | "trait_type": "Shine", 27 | "value": "Shapes" 28 | }, 29 | { 30 | "trait_type": "Bottom lid", 31 | "value": "Middle" 32 | }, 33 | { 34 | "trait_type": "Top lid", 35 | "value": "Middle" 36 | } 37 | ], 38 | "compiler": "HashLips Art Engine" 39 | } -------------------------------------------------------------------------------- /minting-tool/cli/src/tests/run_test.md: -------------------------------------------------------------------------------- 1 | Make sure you have created an account in Aptos testnet. You also need to fund the test account some coins with the testnet faucet. 2 | 3 | Go to the `minting-tool/typescript` folder 4 | 5 | Fund the storage service 0.1 APT 6 | `yarn cli fund --private-key xxxxx --amount 10000000` 7 | 8 | Create a project 9 | `yarn cli init --asset-path ./src/tests/assets --name awesome-nft` 10 | 11 | Mint the project 12 | `yarn cli mint --private-key xxxxx --project-path ./awesome-nft` 13 | -------------------------------------------------------------------------------- /minting-tool/cli/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { AptosAccount, HexString } from "aptos"; 3 | import chalk from "chalk"; 4 | import fs from "fs"; 5 | import os from "os"; 6 | import path from "path"; 7 | import { exit } from "process"; 8 | import untildify from "untildify"; 9 | import YAML from "yaml"; 10 | import { exec, spawn } from "child_process"; 11 | 12 | export const OCTAS_PER_APT = 100000000; 13 | 14 | export const MAINNET_BUNDLR_URL = "https://node1.bundlr.network"; 15 | export const TESTNET_BUNDLR_URL = "https://devnet.bundlr.network"; 16 | export const MAINNET_APTOS_URL = "https://mainnet.aptoslabs.com/v1"; 17 | export const TESTNET_APTOS_URL = "https://fullnode.testnet.aptoslabs.com/v1"; 18 | export const MAINNET = "mainnet"; 19 | export const TESTNET = "testnet"; 20 | export type NetworkType = "mainnet" | "testnet"; 21 | 22 | export function dateTimeStrToUnixSecs(dateTimeStr: string): number { 23 | const date = new Date(dateTimeStr); 24 | const timestampInMs = date.getTime(); 25 | return Math.floor(timestampInMs / 1000); 26 | } 27 | 28 | export function readProjectConfig(project: string): any { 29 | const projectPath = project || "."; 30 | 31 | const configBuf = fs.readFileSync(resolvePath(projectPath, "config.json")); 32 | 33 | return JSON.parse(configBuf.toString("utf8")); 34 | } 35 | 36 | export function resolvePath(p: string, ...rest: string[]): string { 37 | if (!p) return ""; 38 | return path.resolve(untildify(p), ...rest); 39 | } 40 | 41 | export async function resolveProfile( 42 | profileName: string, 43 | ): Promise<[AptosAccount, NetworkType]> { 44 | // Check if Aptos CLI config file exists 45 | const cliConfigFile = resolvePath(os.homedir(), ".aptos", "config.yaml"); 46 | if (!fs.existsSync(cliConfigFile)) { 47 | throw new Error( 48 | "Cannot find the global config for Aptos CLI. Did you forget to run command 'aptos config set-global-config --config-type global && aptos init --profile '?", 49 | ); 50 | } 51 | 52 | const configBuf = await fs.promises.readFile(cliConfigFile); 53 | const config = YAML.parse(configBuf.toString("utf8")); 54 | if (!config?.profiles?.[profileName]) { 55 | throw new Error( 56 | `Profile '${profileName}' is not found. Run command 'aptos config show-global-config' to make sure the config type is "Global". Run command 'aptos config show-profiles' to see available profiles.`, 57 | ); 58 | } 59 | 60 | const profile = config.profiles[profileName]; 61 | 62 | if (!profile.private_key || !profile.rest_url) { 63 | throw new Error(`Profile '${profileName}' format is invalid.`); 64 | } 65 | 66 | let network = ""; 67 | 68 | if (profile.rest_url.includes(TESTNET)) { 69 | network = TESTNET; 70 | } 71 | 72 | if (profile.rest_url.includes(MAINNET)) { 73 | network = MAINNET; 74 | } 75 | 76 | if (network !== TESTNET && network !== MAINNET) { 77 | throw new Error( 78 | `Make sure profile '${profileName}' points to '${TESTNET}' or '${MAINNET}'. Run command 'aptos config show-profiles --profile ${profileName}' to see profile details.`, 79 | ); 80 | } 81 | 82 | return [ 83 | new AptosAccount(new HexString(profile.private_key).toUint8Array()), 84 | network, 85 | ]; 86 | } 87 | 88 | export function octasToApt(amount: string): string { 89 | return (Number.parseInt(amount, 10) / 100000000).toFixed(2); 90 | } 91 | 92 | export function exitWithError(message: string) { 93 | console.error(chalk.red(message)); 94 | exit(1); 95 | } 96 | 97 | export class MapWithDefault extends Map { 98 | private readonly default: () => V; 99 | 100 | get(key: K) { 101 | if (!this.has(key)) { 102 | this.set(key, this.default()); 103 | } 104 | return super.get(key); 105 | } 106 | 107 | constructor(defaultFunction: () => V) { 108 | super(); 109 | this.default = defaultFunction; 110 | } 111 | } 112 | 113 | export async function sleep(timeMs: number): Promise { 114 | return new Promise((resolve) => { 115 | setTimeout(resolve, timeMs); 116 | }); 117 | } 118 | 119 | async function ensureAptosCLIExists() { 120 | return new Promise((resolve, reject) => { 121 | exec("aptos --version", (error) => { 122 | if (error) { 123 | reject(new Error("The 'aptos' cli is not found.")); 124 | return; 125 | } 126 | 127 | resolve(undefined); 128 | }); 129 | }); 130 | } 131 | 132 | export async function runWithAptosCLI(cmd: string) { 133 | await ensureAptosCLIExists(); 134 | 135 | return new Promise((resolve, reject) => { 136 | const parts = cmd.split(" "); 137 | const child = spawn(parts[0], parts.slice(1), { 138 | stdio: [process.stdin, process.stdout, process.stderr], 139 | }); 140 | 141 | child.on("exit", (code) => { 142 | if (code !== 0) { 143 | reject(new Error(`Failed to run ${cmd}`)); 144 | } 145 | resolve(undefined); 146 | }); 147 | }); 148 | } 149 | -------------------------------------------------------------------------------- /minting-tool/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowJs": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "outDir": "./dist", 13 | "sourceMap": true, 14 | "strict": true, 15 | "target": "es2020", 16 | "pretty": true, 17 | "resolveJsonModule": true, 18 | }, 19 | "include": ["src"], 20 | } -------------------------------------------------------------------------------- /minting-tool/cli/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints" : ["src/index.ts"], 3 | "out": "docs" 4 | } 5 | -------------------------------------------------------------------------------- /minting-tool/minting-site/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /minting-tool/minting-site/README.md: -------------------------------------------------------------------------------- 1 | # Mint site template 2 | 3 | A template NFT mint website. Creators can host the template as is or customize it before hosting. 4 | 5 | ## Run the website 6 | 7 | The website can be served through a node server by running the below command: 8 | 9 | ``` 10 | REACT_APP_MINTING_CONTRACT="" REACT_APP_NODE_URL="" npm run start. 11 | ``` 12 | 13 | See [doc](https://aptos.dev/concepts/coin-and-token/nft-minting-tool/) for how to create a collection and launch the mint site template. 14 | -------------------------------------------------------------------------------- /minting-tool/minting-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minting-site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aptos-labs/wallet-adapter-ant-design": "^0.1.1", 7 | "@aptos-labs/wallet-adapter-react": "^0.2.3", 8 | "@blocto/aptos-wallet-adapter-plugin": "^0.1.1", 9 | "@martianwallet/aptos-wallet-adapter": "^0.0.2", 10 | "@pontem/wallet-adapter-plugin": "^0.1.4", 11 | "@rise-wallet/wallet-adapter": "^0.1.2", 12 | "@spika/aptos-plugin": "^0.1.2", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "@trustwallet/aptos-wallet-adapter": "^0.1.6", 16 | "@types/jest": "^27.5.2", 17 | "@types/node": "^16.18.4", 18 | "@types/react": "^18.0.25", 19 | "@types/react-dom": "^18.0.9", 20 | "antd": "^5.0.3", 21 | "aptos": "^1.4.0", 22 | "classnames": "^2.3.2", 23 | "fewcha-plugin-wallet-adapter": "^0.1.1", 24 | "moment": "^2.29.4", 25 | "msafe-plugin-wallet-adapter": "^0.1.0", 26 | "petra-plugin-wallet-adapter": "^0.1.3", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-helmet": "^6.1.0", 30 | "react-scripts": "5.0.1", 31 | "typescript": "^4.9.3", 32 | "web-vitals": "^2.1.4" 33 | }, 34 | "scripts": { 35 | "start": "GENERATE_SOURCEMAP=false react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@testing-library/dom": "^8.20.0", 60 | "@testing-library/jest-dom": "^5.16.5", 61 | "@types/react-helmet": "^6.1.6", 62 | "@types/testing-library__jest-dom": "^5.14.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /minting-tool/minting-site/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/minting-site/public/favicon.ico -------------------------------------------------------------------------------- /minting-tool/minting-site/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /minting-tool/minting-site/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/minting-site/public/logo192.png -------------------------------------------------------------------------------- /minting-tool/minting-site/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/minting-site/public/logo512.png -------------------------------------------------------------------------------- /minting-tool/minting-site/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /minting-tool/minting-site/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | width: 100%; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { Home } from './pages/home'; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/assets/aptos-zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/token/aeef75178c76f6f50f919914d45f348bd55cbb7c/minting-tool/minting-site/src/assets/aptos-zero.png -------------------------------------------------------------------------------- /minting-tool/minting-site/src/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import cx from 'classnames'; 3 | import styles from './navbar.module.css'; 4 | import { WalletSelector } from '@aptos-labs/wallet-adapter-ant-design'; 5 | import { Avatar, Typography } from 'antd'; 6 | import './wallet-adapter.css'; 7 | 8 | const { Text } = Typography; 9 | 10 | export function Navbar({ pic, title }: { pic: string; title: string }) { 11 | const [sticky, setSticky] = useState(false); 12 | 13 | useEffect(() => { 14 | const handleScroll = () => { 15 | if (!sticky && window.scrollY > 70) { 16 | setSticky(true); 17 | } else if (sticky && window.scrollY === 0) { 18 | setSticky(false); 19 | } 20 | }; 21 | 22 | window.addEventListener('scroll', handleScroll); 23 | return () => { 24 | window.removeEventListener('scroll', handleScroll); 25 | }; 26 | }, [sticky]); 27 | 28 | return ( 29 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/components/Navbar/navbar.module.css: -------------------------------------------------------------------------------- 1 | .menuBarFlat { 2 | overflow: hidden; 3 | display: flex; 4 | align-items: center; 5 | z-index: 1; 6 | width: 100%; 7 | height: 64px; 8 | background-color: #fff; 9 | justify-content: space-between; 10 | } 11 | 12 | .menuBarFlat .logo { 13 | padding: 0 10px; 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | .menuBarFlat .logoTitle { 19 | font-weight: 500; 20 | font-size: 1.2em; 21 | flex: 1; 22 | width: 100% 23 | } 24 | 25 | 26 | :global(.sticky) { 27 | @-webkit-keyframes fadeInDown { 28 | 0% { 29 | opacity: 0; 30 | -webkit-transform: translateY(-20px); 31 | } 32 | 100% { 33 | opacity: 1; 34 | -webkit-transform: translateY(0); 35 | } 36 | } 37 | 38 | @keyframes fadeInDown { 39 | 0% { 40 | opacity: 0; 41 | transform: translateY(-20px); 42 | } 43 | 100% { 44 | opacity: 1; 45 | transform: translateY(0); 46 | } 47 | } 48 | 49 | position: fixed; 50 | top: 0px; 51 | left: 0px; 52 | z-index: 1000; 53 | width: 100%; 54 | animation: 500ms ease-in-out 0s 1 normal none running fadeInDown; 55 | box-shadow: rgb(0 0 0 / 10%) 0px 8px 20px 0px; 56 | background-color: #fff; 57 | } -------------------------------------------------------------------------------- /minting-tool/minting-site/src/components/Navbar/wallet-adapter.css: -------------------------------------------------------------------------------- 1 | .ant-menu { 2 | border: none !important; 3 | } 4 | .ant-menu-item { 5 | background-color: rgba(0, 0, 0, 0.1); 6 | padding: 15px; 7 | height: auto !important; 8 | margin-bottom: 10px !important; 9 | } 10 | .ant-menu-item-selected { 11 | background-color: rgba(0, 0, 0, 0.06) !important; 12 | color: black !important; 13 | } 14 | .wallet-selector-text { 15 | font-size: 14px; 16 | } 17 | .wallet-connect-button-text { 18 | color: white; 19 | } 20 | .wallet-menu-wrapper { 21 | display: flex; 22 | justify-content: space-between; 23 | font-size: 20px; 24 | } 25 | .wallet-name-wrapper { 26 | display: flex; 27 | align-items: center; 28 | } 29 | .wallet-connect-button { 30 | align-self: center; 31 | background-color: #1D4ED8; 32 | height: auto; 33 | border: none !important; 34 | } 35 | .wallet-connect-install { 36 | align-self: center; 37 | color: #1D4ED8; 38 | padding-right: 15px; 39 | font-size: 16px; 40 | padding-top: 3px; 41 | padding-bottom: 3px; 42 | } 43 | .wallet-button { 44 | padding: 10px 20px; 45 | height: auto; 46 | font-size: 16px; 47 | background-color: #1D4ED8; 48 | color: white; 49 | border: none !important; 50 | } 51 | .wallet-button:hover { 52 | color: white !important; 53 | transition: color 0.3s; 54 | opacity: 0.8; 55 | } 56 | .wallet-modal-title { 57 | text-align: center; 58 | font-size: 2rem; 59 | } -------------------------------------------------------------------------------- /minting-tool/minting-site/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ConfigProvider } from 'antd'; 3 | import ReactDOM from 'react-dom/client'; 4 | import './index.css'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | import { AptosWalletAdapterProvider } from '@aptos-labs/wallet-adapter-react'; 8 | 9 | import { PetraWallet } from 'petra-plugin-wallet-adapter'; 10 | import { TrustWallet } from '@trustwallet/aptos-wallet-adapter'; 11 | import { PontemWallet } from '@pontem/wallet-adapter-plugin'; 12 | import { MartianWallet } from '@martianwallet/aptos-wallet-adapter'; 13 | import { RiseWallet } from '@rise-wallet/wallet-adapter'; 14 | import { SpikaWallet } from '@spika/aptos-plugin'; 15 | import { FewchaWallet } from 'fewcha-plugin-wallet-adapter'; 16 | import { MSafeWalletAdapter } from 'msafe-plugin-wallet-adapter'; 17 | 18 | const root = ReactDOM.createRoot( 19 | document.getElementById('root') as HTMLElement 20 | ); 21 | root.render( 22 | 23 | 36 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | // If you want to start measuring performance in your app, pass a function 51 | // to log results (for example: reportWebVitals(console.log)) 52 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 53 | reportWebVitals(); 54 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/pages/home/home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | width: 100%; 6 | } 7 | 8 | .innerContainer { 9 | max-width: 450px; 10 | width: 100%; 11 | padding-bottom: 30px; 12 | padding-top: 5%; 13 | } 14 | 15 | .coverImage { 16 | width: 100%; 17 | } 18 | 19 | .actionsContainer { 20 | padding: 10px 0; 21 | } 22 | 23 | @media screen and (max-width: 768px) { 24 | .innerContainer { 25 | padding-top: 0; 26 | } 27 | 28 | .actionsContainer { 29 | padding-left: 10px; 30 | padding-right: 10px; 31 | } 32 | } 33 | 34 | .mintAmountInput { 35 | width: 100%; 36 | } 37 | 38 | .mintTimeText { 39 | font-size: 1.1em; 40 | } -------------------------------------------------------------------------------- /minting-tool/minting-site/src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { 3 | Button, 4 | InputNumber, 5 | Card, 6 | Form, 7 | Space, 8 | Typography, 9 | message, 10 | } from 'antd'; 11 | import styles from './home.module.css'; 12 | import { AptosClient } from 'aptos'; 13 | import moment from 'moment'; 14 | import { Helmet } from 'react-helmet'; 15 | import { useWallet } from '@aptos-labs/wallet-adapter-react'; 16 | import { Navbar } from '../../components/Navbar'; 17 | 18 | const MINTING_CONTRACT = process.env.REACT_APP_MINTING_CONTRACT; 19 | 20 | const { useForm } = Form; 21 | const { Text } = Typography; 22 | 23 | function formatDate(dt: string | number): string { 24 | const mt = moment.unix(Number.parseInt(dt.toString(), 10)); 25 | console.log(mt.toDate()); 26 | return mt.format('LLLL'); 27 | } 28 | 29 | function formatDateRange(start: string | number, end: string | number): string { 30 | const startMt = moment.unix(Number.parseInt(start.toString(), 10)); 31 | const endMt = moment.unix(Number.parseInt(end.toString(), 10)); 32 | if (new Date() < startMt.toDate()) { 33 | return `starts ${startMt.fromNow()}`; 34 | } else if (new Date() >= startMt.toDate() && new Date() <= endMt.toDate()) { 35 | return `ends ${endMt.fromNow()}`; 36 | } else { 37 | return 'already ended'; 38 | } 39 | } 40 | 41 | function capitalize(s: string): string { 42 | return s.charAt(0).toUpperCase() + s.slice(1); 43 | } 44 | 45 | function isStarted(start: string | number): boolean { 46 | const startMt = moment.unix(Number.parseInt(start.toString(), 10)); 47 | return new Date() > startMt.toDate(); 48 | } 49 | 50 | const client = new AptosClient(process.env.REACT_APP_NODE_URL!); 51 | 52 | export function Home() { 53 | const [publicMintConf, setPublicMintConf] = useState<{ 54 | start: string; 55 | end: string; 56 | price?: string; 57 | }>(); 58 | const [wlMintConf, setWlMintConf] = useState<{ 59 | start: string; 60 | end: string; 61 | price?: string; 62 | }>(); 63 | 64 | const [refreshCounter, setRefreshCounter] = useState(1); 65 | 66 | const [wlTableHandle, setWlTableHandle] = useState(''); 67 | 68 | const [loadingMintingTime, setLoadingMintingTime] = useState(false); 69 | const [minting, setMinting] = useState(false); 70 | 71 | const [collectionData, setCollectionData] = useState<{ 72 | collection_uri: string; 73 | collection_name: string; 74 | }>({ collection_uri: '', collection_name: '' }); 75 | 76 | const [form] = useForm(); 77 | const { account, signAndSubmitTransaction } = useWallet(); 78 | 79 | useEffect(() => { 80 | form.setFieldValue('amount', 1); 81 | }, [form]); 82 | 83 | useEffect(() => { 84 | const handle = setInterval(() => { 85 | setRefreshCounter((prev) => prev + 1); 86 | }, 1000); 87 | 88 | return function cleanup() { 89 | if (handle) { 90 | clearInterval(handle); 91 | } 92 | }; 93 | }, []); 94 | 95 | useEffect(() => { 96 | if (!wlTableHandle || !account?.address) return; 97 | (async () => { 98 | try { 99 | await client.getTableItem(wlTableHandle, { 100 | key_type: 'address', 101 | value_type: 'u64', 102 | key: account.address!, 103 | }); 104 | } catch (e) { 105 | console.error(e); 106 | } 107 | })(); 108 | }, [account, account?.address, wlTableHandle]); 109 | 110 | useEffect(() => { 111 | (async () => { 112 | try { 113 | setLoadingMintingTime(true); 114 | 115 | try { 116 | const wlConfig = await client.getAccountResource( 117 | MINTING_CONTRACT!, 118 | `${MINTING_CONTRACT!}::minting::WhitelistMintConfig` 119 | ); 120 | 121 | const wlConfigData = wlConfig.data as any; 122 | setWlMintConf({ 123 | start: wlConfigData.whitelist_minting_start_time, 124 | end: wlConfigData.whitelist_minting_end_time, 125 | price: wlConfigData.whitelist_mint_price, 126 | }); 127 | 128 | setWlTableHandle( 129 | (wlConfig.data as any)?.whitelisted_address?.buckets?.inner 130 | ?.handle || '' 131 | ); 132 | } catch (e: any) { 133 | // ignore the errors 134 | } 135 | 136 | const [pubMintConfig, collectionConfig] = await Promise.all([ 137 | client.getAccountResource( 138 | MINTING_CONTRACT!, 139 | `${MINTING_CONTRACT!}::minting::PublicMintConfig` 140 | ), 141 | client.getAccountResource( 142 | MINTING_CONTRACT!, 143 | `${MINTING_CONTRACT!}::minting::CollectionConfig` 144 | ), 145 | ]); 146 | 147 | const collectionData = collectionConfig.data as any; 148 | setCollectionData(collectionData); 149 | 150 | const pubMintConfigData = pubMintConfig.data as any; 151 | setPublicMintConf({ 152 | start: pubMintConfigData.public_minting_start_time, 153 | end: pubMintConfigData.public_minting_end_time, 154 | price: pubMintConfigData.public_mint_price, 155 | }); 156 | } catch (e: any) { 157 | console.error(e); 158 | message.error(e?.message || 'Failed to load the minting information.'); 159 | } finally { 160 | setLoadingMintingTime(false); 161 | } 162 | })(); 163 | }, []); 164 | 165 | const onFinish = async (values: any) => { 166 | try { 167 | setMinting(true); 168 | const { hash } = await signAndSubmitTransaction({ 169 | type: 'entry_function_payload', 170 | function: `${MINTING_CONTRACT}::minting::mint_nft`, 171 | type_arguments: [], 172 | arguments: [values.amount], 173 | }); 174 | 175 | await client.waitForTransaction(hash, { 176 | timeoutSecs: 120, 177 | checkSuccess: true, 178 | }); 179 | message.success(`Successfully minted ${values.amount} NFTs`); 180 | } catch (e: any) { 181 | console.error(e); 182 | message.error(e?.message || 'Failed to mint.'); 183 | } finally { 184 | setMinting(false); 185 | } 186 | }; 187 | 188 | return ( 189 |
190 | 191 | {collectionData.collection_name} 192 | 193 | 197 |
198 | cover 203 |
210 | ({ 219 | validator(_, value: number) { 220 | if (value > 0) { 221 | return Promise.resolve(); 222 | } 223 | return Promise.reject( 224 | new Error('Must be a positive number!') 225 | ); 226 | }, 227 | }), 228 | ]} 229 | > 230 | 231 | 232 | 233 | 246 | 247 | 248 | {refreshCounter > 0 && publicMintConf && publicMintConf.start && ( 249 |
250 | 251 | 252 | Public sale 253 | {!isStarted(publicMintConf.start) && ( 254 | 255 | {formatDateRange( 256 | publicMintConf.start, 257 | publicMintConf.end 258 | )} 259 | 260 | )} 261 | 262 | {!isStarted(publicMintConf.start) ? ( 263 | 264 | {formatDate(publicMintConf.start)} 265 | 266 | ) : ( 267 | 268 | {capitalize( 269 | formatDateRange( 270 | publicMintConf.start, 271 | publicMintConf.end 272 | ) 273 | )} 274 | 275 | )} 276 | 277 |
278 | )} 279 |
280 | {refreshCounter > 0 && wlMintConf && wlMintConf.start && ( 281 |
282 | 283 | 284 | Presale 285 | {!isStarted(wlMintConf.start) && ( 286 | 287 | {formatDateRange(wlMintConf.start, wlMintConf.end)} 288 | 289 | )} 290 | 291 | {!isStarted(wlMintConf.start) ? ( 292 | 293 | {formatDate(wlMintConf.start)} 294 | 295 | ) : ( 296 | 297 | {capitalize( 298 | formatDateRange(wlMintConf.start, wlMintConf.end) 299 | )} 300 | 301 | )} 302 | 303 |
304 | )} 305 |
306 |
307 |
308 |
309 | ); 310 | } 311 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /minting-tool/minting-site/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /minting-tool/minting-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------