├── backend ├── .gitignore ├── README.md ├── backend │ ├── candid │ │ ├── exchange.mo │ │ ├── icp.mo │ │ └── icrc2.mo │ ├── database │ │ ├── create.mo │ │ ├── read.mo │ │ └── update.mo │ ├── main.mo │ ├── services │ │ ├── fileStorage.mo │ │ ├── swap.mo │ │ ├── transfers.mo │ │ └── xrc.mo │ ├── tables │ │ ├── tokens.mo │ │ └── transfers.mo │ ├── token │ │ ├── index_wasm_version.mo │ │ ├── token.mo │ │ └── wasm_version.mo │ ├── types │ │ ├── input.mo │ │ └── output.mo │ └── utils │ │ └── constants.mo ├── canister_ids.json ├── dfx.json ├── mops.toml └── package-lock.json ├── frontend ├── .env ├── .eslintrc.cjs ├── .gitignore ├── .nvmrc ├── README.md ├── canister_ids.json ├── dfx.json ├── index.css ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── reset.css ├── src │ ├── App.tsx │ ├── Assets │ │ ├── Images │ │ │ ├── BG │ │ │ │ ├── bus.png │ │ │ │ ├── cloud_big.svg │ │ │ │ ├── cloud_sky.png │ │ │ │ ├── cloud_sm.svg │ │ │ │ ├── flask.svg │ │ │ │ ├── forest.png │ │ │ │ ├── house.png │ │ │ │ ├── house.svg │ │ │ │ ├── night.png │ │ │ │ ├── p_grid.svg │ │ │ │ ├── road_black.svg │ │ │ │ ├── road_color.svg │ │ │ │ ├── savannah.png │ │ │ │ └── win_dog.png │ │ │ ├── Character │ │ │ │ ├── jesse_neutral.svg │ │ │ │ ├── jesse_smile.svg │ │ │ │ └── white_neutral.svg │ │ │ └── Icons │ │ │ │ ├── bit-finity.png │ │ │ │ ├── check.png │ │ │ │ ├── flask.png │ │ │ │ ├── flask.svg │ │ │ │ ├── info.png │ │ │ │ ├── paint-file.png │ │ │ │ ├── pick-img.png │ │ │ │ ├── plug.png │ │ │ │ ├── plug.svg │ │ │ │ ├── red-close.png │ │ │ │ ├── warning.png │ │ │ │ └── warning.svg │ │ └── Sound │ │ │ └── bg_noise.mp3 │ ├── Candid │ │ ├── js │ │ │ ├── backend.did.js │ │ │ └── icp.did.js │ │ └── ts │ │ │ ├── backend.did.d.ts │ │ │ └── icp.did.d.ts │ ├── Components │ │ ├── Bubble │ │ │ ├── Bubble.tsx │ │ │ ├── bubbles.module.scss │ │ │ └── index.tsx │ │ ├── Character │ │ │ ├── Jesse │ │ │ │ ├── Jesse.tsx │ │ │ │ └── jesse.module.scss │ │ │ ├── White │ │ │ │ ├── White.tsx │ │ │ │ └── white.module.scss │ │ │ └── index.tsx │ │ ├── Chip │ │ │ ├── Chip.tsx │ │ │ ├── chip.module.scss │ │ │ └── index.tsx │ │ ├── Icons │ │ │ ├── ArrowCircleLeft.tsx │ │ │ ├── ArrowCircleRight.tsx │ │ │ ├── Discord.tsx │ │ │ ├── Facebook.tsx │ │ │ ├── Instagram.tsx │ │ │ ├── Twitter.tsx │ │ │ ├── WebMoney.tsx │ │ │ ├── XLogo.tsx │ │ │ └── index.tsx │ │ ├── Modal │ │ │ ├── index.tsx │ │ │ └── modal.module.scss │ │ ├── ProgressLoader │ │ │ ├── ProgressLoader.tsx │ │ │ └── progressLoader.module.scss │ │ ├── Stepper │ │ │ ├── Stepper.tsx │ │ │ ├── index.tsx │ │ │ └── stepper.module.scss │ │ ├── StoryBoardScene │ │ │ ├── DashboardEmptyScene │ │ │ │ ├── DashboardEmptyScene.tsx │ │ │ │ └── dashboardEmptyScene.module.scss │ │ │ ├── TokenCookingScene │ │ │ │ ├── TokenCookingScene.tsx │ │ │ │ └── tokenCookingScene.module.scss │ │ │ ├── TokenFormStep1Scene │ │ │ │ ├── TokenFormStep1Scene.tsx │ │ │ │ └── tokenFormStep1Scene.module.scss │ │ │ ├── TokenFormStep2Scene │ │ │ │ ├── TokenFormStep2Scene.tsx │ │ │ │ └── tokenFormStep2Scene.module.scss │ │ │ ├── TokenFormStep3Scene │ │ │ │ ├── TokenFormStep3Scene.tsx │ │ │ │ └── tokenFormStep3Scene.module.scss │ │ │ ├── WelcomeInfoScene │ │ │ │ ├── WelcomeInfoScene.tsx │ │ │ │ └── welcomeInfoScene.module.scss │ │ │ ├── WelcomeScene │ │ │ │ ├── WelcomeScene.tsx │ │ │ │ └── welcomeScene.module.scss │ │ │ └── index.tsx │ │ ├── Textbox │ │ │ ├── Textbox.module.scss │ │ │ └── Textbox.tsx │ │ ├── TokenTraits │ │ │ ├── TokenTraits.scss │ │ │ └── TokenTraits.tsx │ │ └── Tooltip │ │ │ ├── Tooltip.module.scss │ │ │ └── Tooltip.tsx │ ├── Hooks │ │ └── useImagePreloader.ts │ ├── Pages │ │ ├── Dashboard │ │ │ ├── Dashboard.tsx │ │ │ ├── UserTokens.tsx │ │ │ ├── dashboard.module.scss │ │ │ ├── index.tsx │ │ │ └── userToken.module.scss │ │ ├── TokenForm │ │ │ ├── TokenForm.tsx │ │ │ ├── index.tsx │ │ │ └── tokenForm.module.scss │ │ ├── TokenPreview │ │ │ ├── TokenPreview.tsx │ │ │ ├── index.tsx │ │ │ └── tokenPreview.module.scss │ │ ├── Welcome │ │ │ ├── Welcome.tsx │ │ │ ├── index.tsx │ │ │ └── welcome.module.scss │ │ └── WelcomeInfo │ │ │ ├── ConnectWalletDialog.tsx │ │ │ ├── WelcomeInfo.tsx │ │ │ ├── connectWallet.module.scss │ │ │ ├── index.tsx │ │ │ └── welcomeInfo.module.scss │ ├── Services │ │ ├── backendServices.ts │ │ ├── bitfinityServices.ts │ │ └── plugServices.ts │ ├── Store │ │ ├── appStore.ts │ │ ├── authStore.ts │ │ ├── createTokenStore.ts │ │ ├── routeStore.ts │ │ ├── storyStore.ts │ │ └── userStore.ts │ ├── Utils │ │ ├── constants.ts │ │ └── functions.ts │ ├── app.module.scss │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── package-lock.json /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | 13 | # generated files 14 | **/declarations/ 15 | 16 | # rust 17 | target/ 18 | 19 | # frontend code 20 | node_modules/ 21 | dist/ 22 | .svelte-kit/ 23 | 24 | # environment variables 25 | .env 26 | 27 | .mops 28 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # `backend` 2 | 3 | Welcome to your new `backend` project and to the Internet Computer development community. By default, creating a new project adds this README and some template files to your project directory. You can edit these template files to customize your project and to include your own code to speed up the development cycle. 4 | 5 | To get started, you might want to explore the project directory structure and the default configuration file. Working with this project in your development environment will not affect any production deployment or identity tokens. 6 | 7 | To learn more before you start working with `backend`, see the following documentation available online: 8 | 9 | - [Quick Start](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-locally) 10 | - [SDK Developer Tools](https://internetcomputer.org/docs/current/developer-docs/setup/install) 11 | - [Motoko Programming Language Guide](https://internetcomputer.org/docs/current/motoko/main/motoko) 12 | - [Motoko Language Quick Reference](https://internetcomputer.org/docs/current/motoko/main/language-manual) 13 | 14 | If you want to start working on your project right away, you might want to try the following commands: 15 | 16 | ```bash 17 | cd backend/ 18 | dfx help 19 | dfx canister --help 20 | ``` 21 | 22 | ## Running the project locally 23 | 24 | If you want to test your project locally, you can use the following commands: 25 | 26 | ```bash 27 | # Starts the replica, running in the background 28 | dfx start --background 29 | 30 | # Deploys your canisters to the replica and generates your candid interface 31 | dfx deploy 32 | ``` 33 | 34 | Once the job completes, your application will be available at `http://localhost:4943?canisterId={asset_canister_id}`. 35 | 36 | If you have made changes to your backend canister, you can generate a new candid interface with 37 | 38 | ```bash 39 | npm run generate 40 | ``` 41 | 42 | at any time. This is recommended before starting the frontend development server, and will be run automatically any time you run `dfx deploy`. 43 | 44 | If you are making frontend changes, you can start a development server with 45 | 46 | ```bash 47 | npm start 48 | ``` 49 | 50 | Which will start a server at `http://localhost:8080`, proxying API requests to the replica at port 4943. 51 | 52 | ### Note on frontend environment variables 53 | 54 | If you are hosting frontend code somewhere without using DFX, you may need to make one of the following adjustments to ensure your project does not fetch the root key in production: 55 | 56 | - set`DFX_NETWORK` to `ic` if you are using Webpack 57 | - use your own preferred method to replace `process.env.DFX_NETWORK` in the autogenerated declarations 58 | - Setting `canisters -> {asset_canister_id} -> declarations -> env_override to a string` in `dfx.json` will replace `process.env.DFX_NETWORK` with the string in the autogenerated declarations 59 | - Write your own `createActor` constructor 60 | -------------------------------------------------------------------------------- /backend/backend/candid/icrc2.mo: -------------------------------------------------------------------------------- 1 | // This is a generated Motoko binding. 2 | // Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. 3 | 4 | module Icrc2{ 5 | public type Account = { owner : Principal; subaccount : ?Blob }; 6 | public type Allowance = { allowance : Nat; expires_at : ?Nat64 }; 7 | public type AllowanceArgs = { account : Account; spender : Account }; 8 | public type Approve = { 9 | fee : ?Nat; 10 | from : Account; 11 | memo : ?Blob; 12 | created_at_time : ?Nat64; 13 | amount : Nat; 14 | expected_allowance : ?Nat; 15 | expires_at : ?Nat64; 16 | spender : Account; 17 | }; 18 | public type ApproveArgs = { 19 | fee : ?Nat; 20 | memo : ?Blob; 21 | from_subaccount : ?Blob; 22 | created_at_time : ?Nat64; 23 | amount : Nat; 24 | expected_allowance : ?Nat; 25 | expires_at : ?Nat64; 26 | spender : Account; 27 | }; 28 | public type ApproveError = { 29 | #GenericError : { message : Text; error_code : Nat }; 30 | #TemporarilyUnavailable; 31 | #Duplicate : { duplicate_of : Nat }; 32 | #BadFee : { expected_fee : Nat }; 33 | #AllowanceChanged : { current_allowance : Nat }; 34 | #CreatedInFuture : { ledger_time : Nat64 }; 35 | #TooOld; 36 | #Expired : { ledger_time : Nat64 }; 37 | #InsufficientFunds : { balance : Nat }; 38 | }; 39 | public type ArchiveOptions = { 40 | num_blocks_to_archive : Nat64; 41 | max_transactions_per_response : ?Nat64; 42 | trigger_threshold : Nat64; 43 | max_message_size_bytes : ?Nat64; 44 | cycles_for_archive_creation : ?Nat64; 45 | node_max_memory_size_bytes : ?Nat64; 46 | controller_id : Principal; 47 | }; 48 | public type ArchivedRange = { 49 | callback : shared query GetBlocksRequest -> async BlockRange; 50 | start : Nat; 51 | length : Nat; 52 | }; 53 | public type ArchivedRange_1 = { 54 | callback : shared query GetBlocksRequest -> async TransactionRange; 55 | start : Nat; 56 | length : Nat; 57 | }; 58 | public type BlockRange = { blocks : [Value] }; 59 | public type Burn = { 60 | from : Account; 61 | memo : ?Blob; 62 | created_at_time : ?Nat64; 63 | amount : Nat; 64 | spender : ?Account; 65 | }; 66 | public type ChangeFeeCollector = { #SetTo : Account; #Unset }; 67 | public type DataCertificate = { certificate : ?Blob; hash_tree : Blob }; 68 | public type FeatureFlags = { icrc2 : Bool }; 69 | public type GetBlocksRequest = { start : Nat; length : Nat }; 70 | public type GetBlocksResponse = { 71 | certificate : ?Blob; 72 | first_index : Nat; 73 | blocks : [Value]; 74 | chain_length : Nat64; 75 | archived_blocks : [ArchivedRange]; 76 | }; 77 | public type GetTransactionsResponse = { 78 | first_index : Nat; 79 | log_length : Nat; 80 | transactions : [Transaction]; 81 | archived_transactions : [ArchivedRange_1]; 82 | }; 83 | public type InitArgs = { 84 | decimals : ?Nat8; 85 | token_symbol : Text; 86 | transfer_fee : Nat; 87 | metadata : [(Text, MetadataValue)]; 88 | minting_account : Account; 89 | initial_balances : [(Account, Nat)]; 90 | maximum_number_of_accounts : ?Nat64; 91 | accounts_overflow_trim_quantity : ?Nat64; 92 | fee_collector_account : ?Account; 93 | archive_options : ArchiveOptions; 94 | max_memo_length : ?Nat16; 95 | token_name : Text; 96 | feature_flags : ?FeatureFlags; 97 | }; 98 | public type LedgerArgument = { #Upgrade : ?UpgradeArgs; #Init : InitArgs }; 99 | public type MetadataValue = { 100 | #Int : Int; 101 | #Nat : Nat; 102 | #Blob : Blob; 103 | #Text : Text; 104 | }; 105 | public type Mint = { 106 | to : Account; 107 | memo : ?Blob; 108 | created_at_time : ?Nat64; 109 | amount : Nat; 110 | }; 111 | public type Result = { #Ok : Nat; #Err : TransferError }; 112 | public type Result_1 = { #Ok : Nat; #Err : ApproveError }; 113 | public type Result_2 = { #Ok : Nat; #Err : TransferFromError }; 114 | public type StandardRecord = { url : Text; name : Text }; 115 | public type Transaction = { 116 | burn : ?Burn; 117 | kind : Text; 118 | mint : ?Mint; 119 | approve : ?Approve; 120 | timestamp : Nat64; 121 | transfer : ?Transfer; 122 | }; 123 | public type TransactionRange = { transactions : [Transaction] }; 124 | public type Transfer = { 125 | to : Account; 126 | fee : ?Nat; 127 | from : Account; 128 | memo : ?Blob; 129 | created_at_time : ?Nat64; 130 | amount : Nat; 131 | spender : ?Account; 132 | }; 133 | public type TransferArg = { 134 | to : Account; 135 | fee : ?Nat; 136 | memo : ?Blob; 137 | from_subaccount : ?Blob; 138 | created_at_time : ?Nat64; 139 | amount : Nat; 140 | }; 141 | public type TransferError = { 142 | #GenericError : { message : Text; error_code : Nat }; 143 | #TemporarilyUnavailable; 144 | #BadBurn : { min_burn_amount : Nat }; 145 | #Duplicate : { duplicate_of : Nat }; 146 | #BadFee : { expected_fee : Nat }; 147 | #CreatedInFuture : { ledger_time : Nat64 }; 148 | #TooOld; 149 | #InsufficientFunds : { balance : Nat }; 150 | }; 151 | public type TransferFromArgs = { 152 | to : Account; 153 | fee : ?Nat; 154 | spender_subaccount : ?Blob; 155 | from : Account; 156 | memo : ?Blob; 157 | created_at_time : ?Nat64; 158 | amount : Nat; 159 | }; 160 | public type TransferFromError = { 161 | #GenericError : { message : Text; error_code : Nat }; 162 | #TemporarilyUnavailable; 163 | #InsufficientAllowance : { allowance : Nat }; 164 | #BadBurn : { min_burn_amount : Nat }; 165 | #Duplicate : { duplicate_of : Nat }; 166 | #BadFee : { expected_fee : Nat }; 167 | #CreatedInFuture : { ledger_time : Nat64 }; 168 | #TooOld; 169 | #InsufficientFunds : { balance : Nat }; 170 | }; 171 | public type UpgradeArgs = { 172 | token_symbol : ?Text; 173 | transfer_fee : ?Nat; 174 | metadata : ?[(Text, MetadataValue)]; 175 | maximum_number_of_accounts : ?Nat64; 176 | accounts_overflow_trim_quantity : ?Nat64; 177 | change_fee_collector : ?ChangeFeeCollector; 178 | max_memo_length : ?Nat16; 179 | token_name : ?Text; 180 | feature_flags : ?FeatureFlags; 181 | }; 182 | public type Value = { 183 | #Int : Int; 184 | #Map : [(Text, Value)]; 185 | #Nat : Nat; 186 | #Nat64 : Nat64; 187 | #Blob : Blob; 188 | #Text : Text; 189 | #Array : Vec; 190 | }; 191 | public type Vec = [ 192 | { 193 | #Int : Int; 194 | #Map : [(Text, Value)]; 195 | #Nat : Nat; 196 | #Nat64 : Nat64; 197 | #Blob : Blob; 198 | #Text : Text; 199 | #Array : Vec; 200 | } 201 | ]; 202 | public type Self = actor { 203 | get_blocks : shared query GetBlocksRequest -> async GetBlocksResponse; 204 | get_data_certificate : shared query () -> async DataCertificate; 205 | get_transactions : shared query GetBlocksRequest -> async GetTransactionsResponse; 206 | icrc1_balance_of : shared query Account -> async Nat; 207 | icrc1_decimals : shared query () -> async Nat8; 208 | icrc1_fee : shared query () -> async Nat; 209 | icrc1_metadata : shared query () -> async [(Text, MetadataValue)]; 210 | icrc1_minting_account : shared query () -> async ?Account; 211 | icrc1_name : shared query () -> async Text; 212 | icrc1_supported_standards : shared query () -> async [StandardRecord]; 213 | icrc1_symbol : shared query () -> async Text; 214 | icrc1_total_supply : shared query () -> async Nat; 215 | icrc1_transfer : shared TransferArg -> async Result; 216 | icrc2_allowance : shared query AllowanceArgs -> async Allowance; 217 | icrc2_approve : shared ApproveArgs -> async Result_1; 218 | icrc2_transfer_from : shared TransferFromArgs -> async Result_2; 219 | } 220 | } -------------------------------------------------------------------------------- /backend/backend/database/create.mo: -------------------------------------------------------------------------------- 1 | import AlfangoDB "mo:alfangodb/AlfangoDB"; 2 | 3 | import TokensTable "../tables/tokens"; 4 | import TransferTable "../tables/transfers"; 5 | import Constants "../utils/constants"; 6 | 7 | module DbCreate { 8 | public class Create(alfangoDB : AlfangoDB.AlfangoDB) { 9 | public func initialize() : async Text { 10 | 11 | let _res = await AlfangoDB.updateOperation({ 12 | updateOpsInput = #CreateDatabaseInput({ 13 | name = Constants.BreakingBitsDatabase; 14 | }); 15 | alfangoDB; 16 | }); 17 | 18 | let _res1 = await AlfangoDB.updateOperation({ 19 | updateOpsInput = #CreateTableInput({ 20 | databaseName = Constants.BreakingBitsDatabase; 21 | name = Constants.TokenTable; 22 | attributes = TokensTable.TokenTableAttributes; 23 | indexes = TokensTable.TokenTableIndexes; 24 | }); 25 | alfangoDB; 26 | }); 27 | 28 | let _res2 = await AlfangoDB.updateOperation({ 29 | updateOpsInput = #CreateTableInput({ 30 | databaseName = Constants.BreakingBitsDatabase; 31 | name = Constants.TransfersTable; 32 | attributes = TransferTable.TransferTableAttributes; 33 | indexes = TransferTable.TransferTableIndexes; 34 | }); 35 | alfangoDB; 36 | }); 37 | 38 | let _addAttributesToTokenTable = addAttributesToTokenTable(alfangoDB); 39 | 40 | return "All tables created successfully"; 41 | }; 42 | }; 43 | 44 | public func addAttributesToTokenTable(databases : AlfangoDB.AlfangoDB) : Text { 45 | 46 | let attributes = [ 47 | { 48 | name = "core_traits"; 49 | datatype = #text; 50 | unique = false; 51 | required = true; 52 | }, 53 | ]; 54 | 55 | for (attribute in attributes.vals()) { 56 | let _item = AlfangoDB.addAttribute({ 57 | addAttributeInput = { 58 | databaseName = Constants.BreakingBitsDatabase; 59 | tableName = Constants.TokenTable; 60 | attribute = { 61 | name = attribute.name; 62 | dataType = attribute.datatype; 63 | unique = attribute.unique; 64 | required = attribute.required; 65 | defaultValue = #default; 66 | }; 67 | }; 68 | alfangoDB = databases; 69 | }); 70 | 71 | }; 72 | return "All attributes added successfully"; 73 | }; 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /backend/backend/database/read.mo: -------------------------------------------------------------------------------- 1 | import AlfangoDB "mo:alfangodb/AlfangoDB"; 2 | import Bool "mo:base/Bool"; 3 | import Buffer "mo:base/Buffer"; 4 | import Nat64 "mo:base/Nat64"; 5 | import Principal "mo:base/Principal"; 6 | 7 | import Input "../types/input"; 8 | import Output "../types/output"; 9 | import Constants "../utils/constants"; 10 | 11 | module DbRead { 12 | public class Read(alfangoDB : AlfangoDB.AlfangoDB) { 13 | public func get_tokens_by_pid(args : Input.GetTokensByPid) : Output.GetTokensByPidOutput { 14 | let res = AlfangoDB.queryOperation({ 15 | queryOpsInput = #ScanInput({ 16 | databaseName = Constants.BreakingBitsDatabase; 17 | tableName = Constants.TokenTable; 18 | filterExpressions = [{ 19 | attributeName = "creator_pid"; 20 | filterExpressionCondition = #EQ(#principal(args.pid)); 21 | }]; 22 | }); 23 | alfangoDB; 24 | }); 25 | 26 | switch (res) { 27 | case (#ScanOutput(#ok(items))) { 28 | let tokenBuffer = Buffer.Buffer(0); 29 | for (item in items.vals()) { 30 | let token = { 31 | creator_pid = switch (item.item[0]) { 32 | case ("creator_pid", #principal(pid)) pid; 33 | case _ Principal.fromText(""); 34 | }; 35 | name = switch (item.item[1]) { 36 | case ("name", #text(name)) name; 37 | case _ ""; 38 | }; 39 | symbol = switch (item.item[2]) { 40 | case ("symbol", #text(symbol)) symbol; 41 | case _ ""; 42 | }; 43 | core_traits = switch (item.item[2]) { 44 | case ("core_traits", #text(core_traits)) core_traits; 45 | case _ ""; 46 | }; 47 | logo_url = switch (item.item[3]) { 48 | case ("logo_url", #text(url)) url; 49 | case _ ""; 50 | }; 51 | initial_supply = switch (item.item[4]) { 52 | case ("initial_supply", #nat(supply)) supply; 53 | case _ 0; 54 | }; 55 | initial_fee = switch (item.item[5]) { 56 | case ("initial_fee", #nat(fee)) fee; 57 | case _ 0; 58 | }; 59 | canister_id = switch (item.item[6]) { 60 | case ("canister_id", #text(id)) id; 61 | case _ ""; 62 | }; 63 | index_canister_id = switch (item.item[7]) { 64 | case ("index_canister_id", #text(id)) id; 65 | case _ ""; 66 | }; 67 | bits_evaporated = switch (item.item[8]) { 68 | case ("bits_evaporated", #float(bits)) bits; 69 | case _ 0.0; 70 | }; 71 | token_created_at = switch (item.item[9]) { 72 | case ("token_created_at", #nat64(created)) created; 73 | case _ Nat64.fromNat(0); 74 | }; 75 | }; 76 | tokenBuffer.add(token); 77 | }; 78 | let tokens = Buffer.toArray(tokenBuffer); 79 | #ok(tokens); 80 | }; 81 | case (#ScanOutput(#err(errors))) #err(errors); 82 | case _ #err(["Failed to retrieve user info"]); 83 | }; 84 | }; 85 | 86 | public func unique_name(name : Text) : Bool { 87 | let res = AlfangoDB.queryOperation({ 88 | queryOpsInput = #ScanInput({ 89 | databaseName = Constants.BreakingBitsDatabase; 90 | tableName = Constants.TokenTable; 91 | filterExpressions = [{ 92 | attributeName = "name"; 93 | filterExpressionCondition = #EQ(#text(name)); 94 | }]; 95 | }); 96 | alfangoDB; 97 | }); 98 | 99 | switch (res) { 100 | case (#ScanOutput(#ok(item))) (item.size() == 0); 101 | case (#ScanOutput(#err(_errors))) false; 102 | case _ false; 103 | }; 104 | }; 105 | 106 | public func unique_symbol(symbol : Text) : Bool { 107 | let res = AlfangoDB.queryOperation({ 108 | queryOpsInput = #ScanInput({ 109 | databaseName = Constants.BreakingBitsDatabase; 110 | tableName = Constants.TokenTable; 111 | filterExpressions = [{ 112 | attributeName = "symbol"; 113 | filterExpressionCondition = #EQ(#text(symbol)); 114 | }]; 115 | }); 116 | alfangoDB; 117 | }); 118 | 119 | switch (res) { 120 | case (#ScanOutput(#ok(item))) (item.size() == 0); 121 | case (#ScanOutput(#err(_errors))) false; 122 | case _ false; 123 | }; 124 | }; 125 | }; 126 | }; 127 | -------------------------------------------------------------------------------- /backend/backend/database/update.mo: -------------------------------------------------------------------------------- 1 | import AlfangoDB "mo:alfangodb/AlfangoDB"; 2 | import Debug "mo:base/Debug"; 3 | import Nat64 "mo:base/Nat64"; 4 | import Time "mo:base/Time"; 5 | 6 | import Input "../types/input"; 7 | import Output "../types/output"; 8 | import Constants "../utils/constants"; 9 | 10 | module DbUpdate { 11 | public class Update(alfangoDB : AlfangoDB.AlfangoDB) { 12 | 13 | public func add_new_token(args : Input.AddNewToken) : async Output.OperationResult { 14 | Debug.print("add_new_token args : " #debug_show (args)); 15 | let res = await AlfangoDB.updateOperation({ 16 | updateOpsInput = #CreateItemInput({ 17 | databaseName = Constants.BreakingBitsDatabase; 18 | tableName = Constants.TokenTable; 19 | attributeDataValues = [ 20 | ("creator_pid", #principal(args.user)), 21 | ("name", #text(args.token_name)), 22 | ("symbol", #text(args.token_symbol)), 23 | ("core_traits", #text(args.token_core_traits)), 24 | ("logo_url", #text(args.logo_url)), 25 | ("initial_supply", #nat(args.initial_supply)), 26 | ("initial_fee", #nat(args.initial_fee)), 27 | ("canister_id", #text(args.canister_id)), 28 | ("index_canister_id", #text(args.index_canister_id)), 29 | ("bits_evaporated", #float(args.bits_evaporated)), 30 | ("token_created_at", #nat64(Nat64.fromIntWrap(Time.now()))), 31 | ]; 32 | }); 33 | alfangoDB; 34 | }); 35 | 36 | switch res { 37 | case (#CreateItemOutput(#ok({ id }))) #ok(id); 38 | case (#CreateItemOutput(#err(errors))) #err(errors); 39 | case (_) #err(["Failed to register user"]); 40 | }; 41 | }; 42 | 43 | public func update_bits_evaporated({ 44 | token_id : Text; 45 | bits_evaporated : Float; 46 | }) : async Output.OperationResult { 47 | let res = await AlfangoDB.updateOperation({ 48 | updateOpsInput = #UpdateItemInput({ 49 | databaseName = Constants.BreakingBitsDatabase; 50 | tableName = Constants.TokenTable; 51 | id = token_id; 52 | attributeDataValues = [ 53 | ("bits_evaporated", #float(bits_evaporated)), 54 | ]; 55 | }); 56 | alfangoDB; 57 | }); 58 | switch res { 59 | case (#UpdateItemOutput(#ok(_))) #ok("Bits evaporated updated successfully"); 60 | case (#UpdateItemOutput(#err(errors))) #err(errors); 61 | case (_) #err(["Failed to update bits evaporated"]); 62 | }; 63 | }; 64 | 65 | public func add_new_transfer(args : Input.AddNewTransfer) : async Output.OperationResult { 66 | let res = await AlfangoDB.updateOperation({ 67 | updateOpsInput = #CreateItemInput({ 68 | databaseName = Constants.BreakingBitsDatabase; 69 | tableName = Constants.TransfersTable; 70 | attributeDataValues = [ 71 | ("sender_pid", #principal(args.sender_pid)), 72 | ("token_id", #text(args.token_id)), 73 | ("burned_bits", #float(args.burned_bits)), 74 | ("to_sagar", #float(args.to_sagar)), 75 | ("to_breaking_bits", #float(args.to_breaking_bits)), 76 | ("to_exe", #float(args.to_exe)), 77 | ]; 78 | }); 79 | alfangoDB; 80 | }); 81 | 82 | switch res { 83 | case (#CreateItemOutput(#ok({ id }))) #ok(id); 84 | case (#CreateItemOutput(#err(errors))) #err(errors); 85 | case (_) #err(["Failed to register user"]); 86 | }; 87 | }; 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /backend/backend/main.mo: -------------------------------------------------------------------------------- 1 | import AlfangoDB "mo:alfangodb/AlfangoDB"; 2 | import Array "mo:base/Array"; 3 | import Float "mo:base/Float"; 4 | import Nat64 "mo:base/Nat64"; 5 | import Prelude "mo:base/Prelude"; 6 | import Principal "mo:base/Principal"; 7 | import Result "mo:base/Result"; 8 | import D3 "mo:d3storage/D3"; 9 | 10 | import DbCreate "database/create"; 11 | import DbRead "database/read"; 12 | import DbUpdate "database/update"; 13 | import Storage "services/fileStorage"; 14 | import Transfers "services/transfers"; 15 | import Xrc "services/xrc"; 16 | import Token "token/token"; 17 | import Input "types/input"; 18 | import Output "types/output"; 19 | import Constants "utils/constants"; 20 | 21 | shared ({ caller = initializer }) actor class Backend() = this { 22 | stable let alfangoDB = AlfangoDB.AlfangoDB(); 23 | stable let d3 = D3.D3(); 24 | stable var totalBurnedBits : Float = 0; 25 | stable var totalBurnedExe : Float = 0; 26 | let storageService = Storage.D3Service(d3); 27 | let xrcService = Xrc.XRCService(); 28 | let dbCreate = DbCreate.Create(alfangoDB); 29 | let dbRead = DbRead.Read(alfangoDB); 30 | let dbUpdate = DbUpdate.Update(alfangoDB); 31 | let transferService = Transfers.transfers(dbUpdate); 32 | let tokenService = Token.Token(dbUpdate, transferService); 33 | 34 | //initialize database 35 | private func _initialize_db() : async Text { 36 | await dbCreate.initialize(); 37 | }; 38 | 39 | // returns equivalent amount of USD in ICP 40 | public shared (msg) func usd_to_icp(usd : Float) : async Nat { 41 | isCallerAuthenticated(msg.caller); 42 | let exchangeRate = await xrcService.getExchangeRate("ICP", "USD"); 43 | let icp_amt : Nat = Nat64.toNat(Nat64.fromIntWrap(Float.toInt((usd / exchangeRate) * (Constants.e8s)))); 44 | return icp_amt; 45 | }; 46 | 47 | // to get the total bits evaporated 48 | public query (msg) func get_total_bits_evaporated() : async Float { 49 | isCallerAuthenticated(msg.caller); 50 | return totalBurnedBits; 51 | }; 52 | 53 | public query (msg) func get_burned_tokens() : async Output.BurnedTokenOutput { 54 | isCallerAuthenticated(msg.caller); 55 | { 56 | burned_bits = totalBurnedBits; 57 | burned_exe = totalBurnedExe; 58 | }; 59 | }; 60 | 61 | public query func get_burned_tokens_test(pid : Text) : async Output.BurnedTokenOutput { 62 | isCallerAuthenticated(Principal.fromText(pid)); 63 | { 64 | burned_bits = totalBurnedBits; 65 | burned_exe = totalBurnedExe; 66 | }; 67 | }; 68 | 69 | public query func get_total_bits_evaporated_test(pid : Text) : async Float { 70 | isCallerAuthenticated(Principal.fromText(pid)); 71 | return totalBurnedBits; 72 | }; 73 | 74 | // File upload 75 | public shared (msg) func uploadImage(args : Input.UploadFile) : async Output.uploadFile { 76 | isCallerAuthenticated(msg.caller); 77 | let fileUrl = await storageService.uploadFile(args); 78 | return fileUrl; 79 | }; 80 | 81 | public shared ({ caller }) func create_token(args : Input.CreateToken) : async Result.Result { 82 | if (Principal.isAnonymous(caller)) { 83 | return #err(["Anonymous user cannot create token"]); 84 | }; 85 | let res = await tokenService.initialize_token_canister(caller, args); 86 | //update total burned bits 87 | let burned_bits = transferService.get_burned_bits(); 88 | totalBurnedBits += burned_bits; 89 | 90 | let burned_exe = transferService.get_burned_exe(); 91 | totalBurnedExe += burned_exe; 92 | return res; 93 | }; 94 | 95 | public query (msg) func get_user_tokens() : async Output.GetTokensByPidOutput { 96 | isCallerAuthenticated(msg.caller); 97 | let args : Input.GetTokensByPid = { pid = msg.caller }; 98 | return dbRead.get_tokens_by_pid(args); 99 | }; 100 | 101 | public query func get_user_tokens_test(pid : Text) : async Output.GetTokensByPidOutput { 102 | isCallerAuthenticated(Principal.fromText(pid)); 103 | let args : Input.GetTokensByPid = { pid = Principal.fromText(pid) }; 104 | return dbRead.get_tokens_by_pid(args); 105 | }; 106 | 107 | public query (msg) func is_unique_token_name(name : Text) : async Bool { 108 | isCallerAuthenticated(msg.caller); 109 | return dbRead.unique_name(name); 110 | }; 111 | 112 | public query (msg) func is_unique_token_symbol(symbol : Text) : async Bool { 113 | isCallerAuthenticated(msg.caller); 114 | return dbRead.unique_symbol(symbol); 115 | }; 116 | 117 | // D3 methods 118 | public query func http_request(httpRequest : D3.HttpRequest) : async D3.HttpResponse { 119 | D3.getFileHTTP({ d3; httpRequest; httpStreamingCallbackActor = this }); 120 | }; 121 | 122 | public query func http_request_streaming_callback(streamingCallbackToken : D3.StreamingCallbackToken) : async D3.StreamingCallbackHttpResponse { 123 | D3.httpStreamingCallback({ d3; streamingCallbackToken }); 124 | }; 125 | 126 | public shared (msg) func updateOperation({ 127 | updateOpsInput : AlfangoDB.UpdateOpsInputType; 128 | }) : async AlfangoDB.UpdateOpsOutputType { 129 | if (not isCallerWhitelisted(msg.caller)) Prelude.unreachable(); 130 | return await AlfangoDB.updateOperation({ 131 | updateOpsInput; 132 | alfangoDB; 133 | }); 134 | }; 135 | 136 | public query (msg) func queryOperation({ 137 | queryOpsInput : AlfangoDB.QueryOpsInputType; 138 | }) : async AlfangoDB.QueryOpsOutputType { 139 | if (not isCallerWhitelisted(msg.caller)) Prelude.unreachable(); 140 | return AlfangoDB.queryOperation({ queryOpsInput; alfangoDB }); 141 | }; 142 | 143 | private func isCallerAuthenticated(caller : Principal) { 144 | if (Principal.isAnonymous(caller)) Prelude.unreachable(); 145 | }; 146 | 147 | private func isCallerWhitelisted(caller : Principal) : Bool { 148 | let found = Array.find(Constants.WhitelistedPrincipals, func(p : Text) : Bool { p == Principal.toText(caller) }); 149 | switch (found) { 150 | case null { return false }; 151 | case _ { return true }; 152 | }; 153 | }; 154 | 155 | }; 156 | -------------------------------------------------------------------------------- /backend/backend/services/fileStorage.mo: -------------------------------------------------------------------------------- 1 | import D3 "mo:d3storage/D3"; 2 | 3 | import Constants "../utils/constants"; 4 | 5 | module Storage { 6 | public class D3Service(d3 : D3.D3) { 7 | public func uploadFile({ 8 | fileBlob : Blob; 9 | fileName : Text; 10 | fileType : Text; 11 | }) : async Text { 12 | let imageId = await D3.storeFile({ 13 | d3; 14 | storeFileInput = { 15 | fileDataObject = fileBlob; 16 | fileName; 17 | fileType; 18 | }; 19 | }); 20 | 21 | let canisterId = Constants.BackendCanisterId; 22 | let isLocal = Constants.IsLocal; 23 | if (isLocal) { 24 | return "http://127.0.0.1:4943/d3?canisterId=" # canisterId # "&file_id=" # imageId.fileId; 25 | } else { 26 | return "https://" # canisterId # ".raw.icp0.io/d3?file_id=" # imageId.fileId; 27 | }; 28 | }; 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /backend/backend/services/swap.mo: -------------------------------------------------------------------------------- 1 | import Nat "mo:base/Nat"; 2 | import Nat64 "mo:base/Nat64"; 3 | import Principal "mo:base/Principal"; 4 | import Result "mo:base/Result"; 5 | import Time "mo:base/Time"; 6 | 7 | import Exchange "../candid/exchange"; 8 | import Icp "../candid/icp"; 9 | import Constants "../utils/constants"; 10 | 11 | module Swap { 12 | public class swap(icpLedger : Icp.Self) { 13 | 14 | let icpCanister = Constants.IcpLedgerCanister; 15 | let bitsCanister = Constants.BitsCanister; 16 | let exchangeCanitser = "jrlks-saaaa-aaaag-qj7kq-cai"; // Icp/Bits 17 | let exchange = actor (exchangeCanitser) : Exchange.Self; // Icp/Bits 18 | let icpFee = Constants.IcpFee; 19 | let bitsFee = Constants.BitsFee; 20 | 21 | public func approve_swappool(amount : Nat) : async Icp.Result_2 { 22 | let res = await icpLedger.icrc2_approve({ 23 | fee = null; 24 | memo = null; 25 | from_subaccount = null; 26 | created_at_time = ?Nat64.fromIntWrap(Time.now()); 27 | amount = amount; 28 | expected_allowance = null; 29 | expires_at = null; 30 | spender = { 31 | owner = Principal.fromText(exchangeCanitser); 32 | subaccount = null; 33 | }; 34 | }); 35 | return res; 36 | }; 37 | 38 | // return amount of bits withdrawn 39 | public func swap_icp_to_bits(amount : Nat) : async Result.Result { 40 | // bcz here there are fees for approve and deposit so reduce the amount 41 | 42 | // make approve call for swap pool 43 | let approve_amount = Nat.sub(amount, icpFee); 44 | let approve_res = await approve_swappool(approve_amount); 45 | 46 | switch (approve_res) { 47 | case (#Ok(_block_index)) { 48 | //depositFrom method to deposit token in exchange 49 | let deposit_amount = Nat.sub(approve_amount, icpFee); 50 | let deposit_res = await exchange.depositFrom({ 51 | fee = icpFee; 52 | token = icpCanister; 53 | amount = deposit_amount; 54 | }); 55 | 56 | switch (deposit_res) { 57 | case (#ok(deposit_amt)) { 58 | //swap tokens on exchange 59 | let swap_res = await exchange.swap({ 60 | amountIn = Nat.toText(deposit_amt); 61 | amountOutMinimum = Nat.toText(0); 62 | zeroForOne = false; 63 | }); 64 | 65 | switch (swap_res) { 66 | case (#ok(received_amt)) { 67 | //withdraw tokens from exchange 68 | let withdraw_res = await exchange.withdraw({ 69 | fee = bitsFee; 70 | amount = received_amt; 71 | token = bitsCanister; 72 | }); 73 | switch (withdraw_res) { 74 | case (#ok(withdraw_amount)) { 75 | return #ok(withdraw_amount); 76 | }; 77 | case (#err(err)) { 78 | return #err(err); 79 | }; 80 | }; 81 | }; 82 | case (#err(err)) { 83 | return #err(err); 84 | }; 85 | }; 86 | }; 87 | case (#err(err)) { 88 | return #err(err); 89 | }; 90 | }; 91 | }; 92 | case (#Err(_err)) { 93 | return #err(#InternalError("Approve failed for swap pool")); 94 | }; 95 | }; 96 | }; 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /backend/backend/services/transfers.mo: -------------------------------------------------------------------------------- 1 | import Float "mo:base/Float"; 2 | import Nat "mo:base/Nat"; 3 | import Nat64 "mo:base/Nat64"; 4 | import Principal "mo:base/Principal"; 5 | import Time "mo:base/Time"; 6 | 7 | import Icp "../candid/icp"; 8 | import Icrc2 "../candid/icrc2"; 9 | import DbUpdate "../database/update"; 10 | import Output "../types/output"; 11 | import Constants "../utils/constants"; 12 | import Swap "swap"; 13 | 14 | module Transfers { 15 | public class transfers(dbUpdate : DbUpdate.Update) { 16 | let icpLedger = actor (Constants.IcpLedgerCanister) : Icp.Self; 17 | let bitsLedger = actor (Constants.BitsCanister) : Icrc2.Self; 18 | let icpFee = Constants.IcpFee; 19 | let swap = Swap.swap(icpLedger); 20 | var burned_bits : Float = 0; 21 | var burned_exe : Float = 0; 22 | 23 | public func transfer_icp_to_backend({ 24 | user : Principal; 25 | amount : Nat; 26 | token_id : Text; 27 | }) : async Output.OperationResult { 28 | let transfer_amt = Nat.sub(amount, icpFee); 29 | let res = await icpLedger.icrc2_transfer_from({ 30 | fee = null; 31 | memo = null; 32 | spender_subaccount = null; 33 | from = { 34 | owner = user; 35 | subaccount = null; 36 | }; 37 | to = { 38 | owner = Principal.fromText(Constants.BackendCanisterId); 39 | subaccount = null; 40 | }; 41 | created_at_time = ?Nat64.fromIntWrap(Time.now()); 42 | amount = transfer_amt; 43 | }); 44 | 45 | switch (res) { 46 | case (#Ok(_block_index)) { 47 | // distribute icp of backend canister 48 | let _res1 = await distribute_icp({ 49 | amount = transfer_amt; 50 | token_id; 51 | user; 52 | }); 53 | return #ok("ICP transferred successfully"); 54 | }; 55 | case (#Err(_err)) { 56 | return #err(["Failed to transfer ICP"]); 57 | }; 58 | }; 59 | }; 60 | 61 | // returns amount of icp transferred 62 | public func transfer_icp({ amount : Nat; user : Text }) : async Nat { 63 | let transfer_amt = Nat.sub(amount, icpFee); 64 | let res = await icpLedger.icrc1_transfer({ 65 | to = { 66 | owner = Principal.fromText(user); 67 | subaccount = null; 68 | }; 69 | fee = null; 70 | memo = null; 71 | from_subaccount = null; 72 | created_at_time = ?Nat64.fromIntWrap(Time.now()); 73 | amount = transfer_amt; 74 | }); 75 | switch (res) { 76 | case (#Ok(_transaction_idx)) { 77 | return transfer_amt; 78 | }; 79 | case (#Err(_err)) { 80 | return 0; 81 | }; 82 | }; 83 | }; 84 | 85 | // returns amount of bits burned 86 | public func burn_bits(amount : Nat) : async Nat { 87 | let swap_res = await swap.swap_icp_to_bits(amount); 88 | switch (swap_res) { 89 | case (#ok(bits_amt)) { 90 | let burn_res = await bitsLedger.icrc1_transfer({ 91 | to = { 92 | owner = Principal.fromText(Constants.BitsLedgerPrincipal); 93 | subaccount = null; 94 | }; 95 | fee = null; 96 | memo = null; 97 | from_subaccount = null; 98 | created_at_time = ?Nat64.fromIntWrap(Time.now()); 99 | amount = bits_amt; 100 | }); 101 | switch (burn_res) { 102 | case (#Ok(_transaction_idx)) { 103 | return bits_amt; 104 | }; 105 | case (#Err(_err)) { 106 | return 0; 107 | }; 108 | }; 109 | }; 110 | case (#err(_err)) { 111 | return 0; 112 | }; 113 | }; 114 | }; 115 | 116 | public func distribute_icp({ 117 | amount : Nat; 118 | token_id : Text; 119 | user : Principal; 120 | }) : async Nat { 121 | let bits_to_burn : Nat = Nat.div(Nat.mul(amount, 15), 100); 122 | let to_sagar : Nat = Nat.div(Nat.mul(amount, 25), 100); 123 | let to_breakingbits : Nat = Nat.div(Nat.mul(amount, 45), 100); 124 | let to_exe : Nat = amount - (bits_to_burn + to_sagar + to_breakingbits); 125 | 126 | // step 1 : transfer to sagar 127 | let res1 = await transfer_icp({ 128 | amount = to_sagar; 129 | user = Constants.SagarICPTransferPrincipal; 130 | }); 131 | 132 | // step 2 : transfer to breaking bits 133 | let res2 = await transfer_icp({ 134 | amount = to_breakingbits; 135 | user = Constants.BreakingBitsICPTransferPrincipal; 136 | }); 137 | 138 | // step 3 : transfer to exe 139 | let exe_transferred = await transfer_icp({ 140 | amount = to_exe; 141 | user = Constants.ExeICPTransferPrincipal; 142 | }); 143 | 144 | // step 4 : burn bits 145 | let bits_burned = await burn_bits(bits_to_burn); 146 | 147 | // update tokens table with bits evaporated 148 | let _res4 = await dbUpdate.update_bits_evaporated({ 149 | token_id; 150 | bits_evaporated = (Float.fromInt(bits_burned) / Constants.e8s); 151 | }); 152 | 153 | var burned_bits_icp = 0; 154 | if (bits_burned > 0) { 155 | burned_bits_icp := bits_to_burn; 156 | burned_bits := Float.fromInt(burned_bits_icp) / Constants.e8s; 157 | }; 158 | 159 | var burned_exe_icp = 0; 160 | if (exe_transferred > 0) { 161 | burned_exe_icp := to_exe; 162 | burned_exe := Float.fromInt(burned_exe_icp) / Constants.e8s; 163 | }; 164 | 165 | // add new transfer details in table of transfers 166 | let _res5 = await dbUpdate.add_new_transfer({ 167 | sender_pid = user; 168 | token_id; 169 | burned_bits = (Float.fromInt(burned_bits_icp) / Constants.e8s); 170 | to_sagar = (Float.fromInt(res1) / Constants.e8s); 171 | to_breaking_bits = (Float.fromInt(res2) / Constants.e8s); 172 | to_exe = (Float.fromInt(exe_transferred) / Constants.e8s); 173 | }); 174 | 175 | return 1; 176 | }; 177 | 178 | public func get_burned_bits() : Float { 179 | return burned_bits; 180 | }; 181 | 182 | public func get_burned_exe() : Float { 183 | return burned_exe; 184 | }; 185 | 186 | public func get_burned_tokens() : Output.BurnedTokenOutput { 187 | { 188 | burned_bits; 189 | burned_exe; 190 | }; 191 | }; 192 | }; 193 | }; 194 | -------------------------------------------------------------------------------- /backend/backend/services/xrc.mo: -------------------------------------------------------------------------------- 1 | import XRC "mo:xrc-types"; 2 | import ExperimentalCycles "mo:base/ExperimentalCycles"; 3 | import Error "mo:base/Error"; 4 | import Nat64 "mo:base/Nat64"; 5 | import Float "mo:base/Float"; 6 | import Nat "mo:base/Nat"; 7 | import Nat32 "mo:base/Nat32"; 8 | import Int64 "mo:base/Int64"; 9 | import Time "mo:base/Time"; 10 | 11 | module Xrc { 12 | public class XRCService() { 13 | public func getExchangeRate(base : Text, quote : Text) : async Float { 14 | let xrc = XRC.xrc; 15 | 16 | ExperimentalCycles.add(1_000_000_000); 17 | 18 | let res = await xrc.get_exchange_rate({ 19 | base_asset = { 20 | class_ = #Cryptocurrency; 21 | symbol = base; 22 | }; 23 | quote_asset = { 24 | class_ = #FiatCurrency; 25 | symbol = quote; 26 | }; 27 | timestamp = ?Nat64.fromIntWrap(Time.now() / 1000000000); 28 | }); 29 | 30 | switch (res) { 31 | case (#Ok(exchangeRate)) { 32 | let divisor : Nat64 = Nat64.fromNat(Nat.pow(10, Nat32.toNat(exchangeRate.metadata.decimals))); 33 | let finalValue : Float = Float.fromInt64(Int64.fromNat64(exchangeRate.rate)) / Float.fromInt64(Int64.fromNat64(divisor)); 34 | return finalValue; 35 | }; 36 | case (#Err(err)) { 37 | throw Error.reject(debug_show (err)); 38 | return 0; 39 | }; 40 | }; 41 | }; 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /backend/backend/tables/tokens.mo: -------------------------------------------------------------------------------- 1 | module TokensTable { 2 | public let TokenTableAttributes = [ 3 | { 4 | name = "creator_pid"; 5 | dataType = #principal; 6 | unique = false; 7 | required = true; 8 | defaultValue = #default; 9 | }, 10 | { 11 | name = "name"; 12 | dataType = #text; 13 | unique = true; 14 | required = true; 15 | defaultValue = #default; 16 | }, 17 | { 18 | name = "symbol"; 19 | dataType = #text; 20 | unique = true; 21 | required = true; 22 | defaultValue = #default; 23 | }, 24 | { 25 | name = "core_traits"; 26 | dataType = #text; 27 | unique = false; 28 | required = true; 29 | defaultValue = #default; 30 | }, 31 | { 32 | name = "logo_url"; 33 | dataType = #text; 34 | unique = true; 35 | required = true; 36 | defaultValue = #default; 37 | }, 38 | { 39 | name = "initial_supply"; 40 | dataType = #nat; 41 | unique = false; 42 | required = true; 43 | defaultValue = #default; 44 | }, 45 | { 46 | name = "initial_fee"; 47 | dataType = #nat; 48 | unique = false; 49 | required = true; 50 | defaultValue = #default; 51 | }, 52 | { 53 | name = "canister_id"; 54 | dataType = #text; 55 | unique = true; 56 | required = true; 57 | defaultValue = #default; 58 | }, 59 | { 60 | name = "index_canister_id"; 61 | dataType = #text; 62 | unique = true; 63 | required = true; 64 | defaultValue = #default; 65 | }, 66 | { 67 | name = "bits_evaporated"; 68 | dataType = #float; 69 | unique = false; 70 | required = true; 71 | defaultValue = #default; 72 | }, 73 | { 74 | name = "token_created_at"; 75 | dataType = #nat64; 76 | unique = false; 77 | required = true; 78 | defaultValue = #default; 79 | }, 80 | ]; 81 | public let TokenTableIndexes = [{ 82 | name = "principal_id_index"; 83 | nonUnique = true; 84 | attributeName = "creator_pid"; 85 | }]; 86 | }; 87 | -------------------------------------------------------------------------------- /backend/backend/tables/transfers.mo: -------------------------------------------------------------------------------- 1 | module TransferTable { 2 | public let TransferTableAttributes = [ 3 | { 4 | name = "sender_pid"; 5 | dataType = #principal; 6 | unique = false; 7 | required = true; 8 | defaultValue = #default; 9 | }, 10 | { 11 | name = "token_id"; 12 | dataType = #text; 13 | unique = true; 14 | required = true; 15 | defaultValue = #default; 16 | }, 17 | { 18 | name = "burned_bits"; 19 | dataType = #float; 20 | unique = false; 21 | required = true; 22 | defaultValue = #default; 23 | }, 24 | { 25 | name = "to_sagar"; 26 | dataType = #float; 27 | unique = false; 28 | required = true; 29 | defaultValue = #default; 30 | }, 31 | { 32 | name = "to_breaking_bits"; 33 | dataType = #float; 34 | unique = false; 35 | required = true; 36 | defaultValue = #default; 37 | }, 38 | { 39 | name = "to_exe"; 40 | dataType = #float; 41 | unique = false; 42 | required = true; 43 | defaultValue = #default; 44 | }, 45 | ]; 46 | public let TransferTableIndexes = [ 47 | { 48 | name = "principal_id_index"; 49 | nonUnique = true; 50 | attributeName = "sender_pid"; 51 | }, 52 | ]; 53 | }; 54 | -------------------------------------------------------------------------------- /backend/backend/token/token.mo: -------------------------------------------------------------------------------- 1 | import Cycles "mo:base/ExperimentalCycles"; 2 | import Principal "mo:base/Principal"; 3 | import Result "mo:base/Result"; 4 | import Text "mo:base/Text"; 5 | import { ic } "mo:ic"; 6 | 7 | import DbUpdate "../database/update"; 8 | import Transfers "../services/transfers"; 9 | import Input "../types/input"; 10 | import Constants "../utils/constants"; 11 | import indexWasm "./index_wasm_version"; 12 | import Wasm "./wasm_version"; 13 | 14 | module Token { 15 | public class Token(dbUpdate : DbUpdate.Update, transfer : Transfers.transfers) { 16 | /* 17 | * Create a new icrc3 token canister. 18 | * The creator of the canister is added as the minting account by default. 19 | * @params token - init arguments of token. 20 | */ 21 | public func initialize_token_canister( 22 | user : Principal, 23 | { 24 | token_name : Text; 25 | token_symbol : Text; 26 | token_core_traits : Text; 27 | initial_balances : [(Input.Account, Nat)]; 28 | metadata : [(Text, Input.MetadataValue)]; 29 | transfer_fee : Nat; 30 | icp_amount : Nat; 31 | is_blackholed : Bool; 32 | }, 33 | ) : async Result.Result { 34 | 35 | let mint_acc : Input.Account = { 36 | owner = Principal.fromText(Constants.MintingAccountController); 37 | subaccount = null; 38 | }; 39 | // check if canister has enough cycles. 40 | let response = await ic.canister_status({ 41 | canister_id = Principal.fromText(Constants.BackendCanisterId); 42 | }); 43 | 44 | let cycles = response.cycles; 45 | if (cycles < Constants.CyclesLimit) { 46 | return #err(["Backend canister do not have enough cycles contact developers"]); 47 | }; 48 | 49 | // add cycles to create canister. 50 | Cycles.add(Constants.CyclesLimit); 51 | 52 | var isBlackholed = is_blackholed; 53 | var controllers : [Principal] = [ 54 | Principal.fromText(Constants.BackendCanisterId), 55 | Principal.fromText(Constants.SagarControllerPrincipal), 56 | ]; 57 | 58 | if (isBlackholed) { 59 | controllers := [Principal.fromText(Constants.BlackholeCanister)]; 60 | }; 61 | 62 | // create canister. 63 | let canister = await ic.create_canister({ 64 | settings = ?{ 65 | controllers = ?controllers; 66 | freezing_threshold = null; 67 | reserved_cycles_limit = null; 68 | memory_allocation = null; 69 | compute_allocation = null; 70 | }; 71 | sender_canister_version = null; 72 | }); 73 | 74 | let initArgs : Input.InitArgs = { 75 | decimals = ?8; 76 | token_symbol; 77 | transfer_fee; 78 | metadata; 79 | minting_account = mint_acc; 80 | initial_balances; 81 | maximum_number_of_accounts = null; 82 | accounts_overflow_trim_quantity = null; 83 | fee_collector_account = null; 84 | archive_options = { 85 | num_blocks_to_archive = 1000; 86 | max_transactions_per_response = null; 87 | trigger_threshold = 2000; 88 | max_message_size_bytes = null; 89 | cycles_for_archive_creation = null; 90 | node_max_memory_size_bytes = null; 91 | controller_id = user; 92 | }; 93 | max_memo_length = null; 94 | token_name; 95 | feature_flags = ?{ icrc2 = true }; 96 | }; 97 | 98 | let ledgerArg : Input.LedgerArg = #Init(initArgs); 99 | 100 | //get wasm version 101 | let wasmVersion = Wasm.wasm_version(); 102 | 103 | // install the token code in empty canister. 104 | await ic.install_code({ 105 | arg = to_candid (ledgerArg); 106 | wasm_module = wasmVersion.wasm_blob_icrc3; 107 | mode = #install; 108 | canister_id = canister.canister_id; 109 | sender_canister_version = null; 110 | }); 111 | 112 | // add cycles to create canister. 113 | Cycles.add(Constants.CyclesLimit); 114 | 115 | // create canister. 116 | let indexCanister = await ic.create_canister({ 117 | settings = ?{ 118 | controllers = ?controllers; 119 | freezing_threshold = null; 120 | reserved_cycles_limit = null; 121 | memory_allocation = null; 122 | compute_allocation = null; 123 | }; 124 | sender_canister_version = null; 125 | }); 126 | 127 | let indexArg : Input.IndexCanisterInitArg = #Init({ 128 | ledger_id = canister.canister_id; 129 | }); 130 | 131 | //get wasm version 132 | let indexWasmVersion = indexWasm.wasm_version(); 133 | 134 | // install the token code in empty canister. 135 | await ic.install_code({ 136 | arg = to_candid (indexArg); 137 | wasm_module = indexWasmVersion.wasm_blob_v1; 138 | mode = #install; 139 | canister_id = indexCanister.canister_id; 140 | sender_canister_version = null; 141 | }); 142 | 143 | var logo_url = ""; 144 | switch (metadata[0].1) { 145 | case (#Text(logo)) { 146 | logo_url := logo; 147 | }; 148 | case _ { 149 | logo_url := ""; 150 | }; 151 | }; 152 | 153 | // add new token details in table of tokens 154 | let update_res = await dbUpdate.add_new_token({ 155 | token_name; 156 | token_symbol; 157 | token_core_traits; 158 | logo_url; 159 | initial_supply = initial_balances[0].1; 160 | initial_fee = transfer_fee; 161 | canister_id = Principal.toText(canister.canister_id); 162 | index_canister_id = Principal.toText(indexCanister.canister_id); 163 | bits_evaporated = 0; 164 | user; 165 | }); 166 | 167 | switch (update_res) { 168 | case (#ok(token_id)) { 169 | // transfer icp from user to backend 170 | let transfer_res = await transfer.transfer_icp_to_backend({ 171 | user; 172 | amount = icp_amount; 173 | token_id; 174 | }); 175 | switch (transfer_res) { 176 | case (#ok(_text)) { 177 | return #ok("Token created successfully"); 178 | }; 179 | case (#err(errors)) { 180 | return #err(errors); 181 | }; 182 | }; 183 | }; 184 | case (#err(errors)) { 185 | return #err(errors); 186 | }; 187 | }; 188 | }; 189 | }; 190 | }; 191 | -------------------------------------------------------------------------------- /backend/backend/types/input.mo: -------------------------------------------------------------------------------- 1 | import Bool "mo:base/Bool"; 2 | import Float "mo:base/Float"; 3 | import Principal "mo:base/Principal"; 4 | module Input { 5 | public type UploadFile = { 6 | fileBlob : Blob; 7 | fileName : Text; 8 | fileType : Text; 9 | }; 10 | public type Account = { owner : Principal; subaccount : ?Subaccount }; 11 | 12 | public type Subaccount = Blob; 13 | public type MetadataValue = { 14 | #Int : Int; 15 | #Nat : Nat; 16 | #Blob : Blob; 17 | #Text : Text; 18 | }; 19 | ///////////////////////////////////////////////// read db ////////////////////////////////////////////////// 20 | 21 | public type GetTokensByPid = { 22 | pid : Principal; 23 | }; 24 | 25 | ///////////////////////////////////////////////// update db ////////////////////////////////////////////////// 26 | 27 | public type AddNewToken = { 28 | token_name : Text; 29 | token_symbol : Text; 30 | token_core_traits : Text; 31 | user : Principal; 32 | logo_url : Text; 33 | initial_supply : Nat; 34 | initial_fee : Nat; 35 | canister_id : Text; 36 | index_canister_id : Text; 37 | bits_evaporated : Float; 38 | }; 39 | 40 | public type AddNewTransfer = { 41 | sender_pid : Principal; 42 | token_id : Text; 43 | burned_bits : Float; 44 | to_sagar : Float; 45 | to_breaking_bits : Float; 46 | to_exe : Float; 47 | }; 48 | 49 | ///////////////////////////////////// token //////////////////////////////////////////////////////////// 50 | 51 | public type CreateToken = { 52 | is_blackholed : Bool; 53 | token_name : Text; 54 | token_symbol : Text; 55 | token_core_traits : Text; 56 | initial_balances : [(Account, Nat)]; 57 | metadata : [(Text, MetadataValue)]; 58 | transfer_fee : Nat; 59 | icp_amount : Nat; 60 | }; 61 | 62 | public type FeatureFlags = { icrc2 : Bool }; 63 | 64 | public type ChangeFeeCollector = { #SetTo : Account; #Unset }; 65 | 66 | public type UpgradeArgs = { 67 | token_symbol : ?Text; 68 | transfer_fee : ?Nat; 69 | metadata : ?[(Text, MetadataValue)]; 70 | maximum_number_of_accounts : ?Nat64; 71 | accounts_overflow_trim_quantity : ?Nat64; 72 | change_fee_collector : ?ChangeFeeCollector; 73 | max_memo_length : ?Nat16; 74 | token_name : ?Text; 75 | feature_flags : ?FeatureFlags; 76 | }; 77 | 78 | public type InitArgs = { 79 | decimals : ?Nat8; 80 | token_symbol : Text; 81 | transfer_fee : Nat; 82 | metadata : [(Text, MetadataValue)]; 83 | minting_account : Account; 84 | initial_balances : [(Account, Nat)]; 85 | maximum_number_of_accounts : ?Nat64; 86 | accounts_overflow_trim_quantity : ?Nat64; 87 | fee_collector_account : ?Account; 88 | archive_options : { 89 | num_blocks_to_archive : Nat64; 90 | max_transactions_per_response : ?Nat64; 91 | trigger_threshold : Nat64; 92 | max_message_size_bytes : ?Nat64; 93 | cycles_for_archive_creation : ?Nat64; 94 | node_max_memory_size_bytes : ?Nat64; 95 | controller_id : Principal; 96 | }; 97 | max_memo_length : ?Nat16; 98 | token_name : Text; 99 | feature_flags : ?FeatureFlags; 100 | }; 101 | 102 | public type LedgerArg = { #Upgrade : ?UpgradeArgs; #Init : InitArgs }; 103 | 104 | public type InitArg = { 105 | ledger_id : Principal; 106 | }; 107 | 108 | public type UpgradeArg = { 109 | ledger_id : ?Principal; 110 | }; 111 | 112 | public type IndexCanisterInitArg = { 113 | #Init : InitArg; 114 | #Upgrade : UpgradeArg; 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /backend/backend/types/output.mo: -------------------------------------------------------------------------------- 1 | import Principal "mo:base/Principal"; 2 | import Result "mo:base/Result"; 3 | import Text "mo:base/Text"; 4 | 5 | module Output { 6 | public type uploadFile = Text; 7 | public type OperationResult = Result.Result; 8 | 9 | /////////////////////////////////////////// 10 | public type GetTokensByPid = { 11 | creator_pid : Principal; 12 | name : Text; 13 | symbol : Text; 14 | core_traits : Text; 15 | logo_url : Text; 16 | initial_supply : Nat; 17 | initial_fee : Nat; 18 | canister_id : Text; 19 | index_canister_id : Text; 20 | bits_evaporated : Float; 21 | token_created_at : Nat64; 22 | }; 23 | public type GetTokensByPidOutput = Result.Result<[GetTokensByPid], [Text]>; 24 | 25 | public type BurnedTokenOutput = { burned_bits : Float; burned_exe : Float } 26 | /////////////////////////////////////////////////////////////// 27 | }; 28 | -------------------------------------------------------------------------------- /backend/backend/utils/constants.mo: -------------------------------------------------------------------------------- 1 | import Float "mo:base/Float"; 2 | 3 | module Constants { 4 | 5 | public let IsLocal = false; 6 | 7 | public let e8s : Float = 100_000_000; 8 | public let IcpFee = 10000; 9 | public let BitsFee = 1483370; 10 | public let CyclesLimit = 3_000_000_000_000; 11 | 12 | public let BackendCanisterId = "prdxk-aqaaa-aaaai-qpfta-cai"; 13 | public let IcpLedgerCanister = "ryjl3-tyaaa-aaaaa-aaaba-cai"; 14 | public let BitsCanister = "j5lhj-xyaaa-aaaai-qpfeq-cai"; 15 | 16 | public let BreakingBitsDatabase = "bitwizard_db"; 17 | public let TokenTable = "tokens"; 18 | public let TransfersTable = "transfers"; 19 | 20 | public let SagarICPTransferPrincipal = "eutvf-g6bsi-qh2wf-fqb36-jf5cp-cogzu-clao4-l6anb-govub-e4uza-lae"; 21 | 22 | public let SagarControllerPrincipal = "ui2e3-bjedh-zjjlk-tr242-ia6te-4ir66-akdvx-fha66-gbcuj-a6q62-kqe"; 23 | 24 | // public let BlackholeCanister = "e3mmv-5qaaa-aaaah-aadma-cai"; 25 | public let BlackholeCanister = SagarControllerPrincipal; 26 | 27 | public let NisargPrincipal = "chw5b-zpfm7-v2twr-f7kp3-ezp3b-icoh3-x22fp-ajsdf-vkfxo-quboc-3qe"; 28 | 29 | public let DaivikPrincipal = "35746-jxry4-ec5mx-fcpgj-h7ysb-gmzeq-ttm4d-aq6td-ozyvu-3owrr-gae"; 30 | 31 | public let BreakingBitsICPTransferPrincipal = SagarICPTransferPrincipal; 32 | 33 | public let ExeICPTransferPrincipal = SagarICPTransferPrincipal; 34 | 35 | public let MintingAccountController = "x75xh-fwg4u-ggq2v-g6oit-k6uif-eqb2e-d7xgb-lfnlz-z5jxb-rlmb4-zae"; 36 | 37 | public let BitsLedgerPrincipal = "rc4zm-gdz44-x55iz-w2nkq-ugrl2-ltvxs-yetvq-7xbyk-ur4dv-inept-tqe"; 38 | 39 | public let AnonymousPrincipal = "2vxsx-fae"; 40 | 41 | public let WhitelistedPrincipals = [ 42 | NisargPrincipal, 43 | DaivikPrincipal, 44 | SagarICPTransferPrincipal, 45 | SagarControllerPrincipal, 46 | AnonymousPrincipal, 47 | ]; 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /backend/canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "backend": { 3 | "ic": "prdxk-aqaaa-aaaai-qpfta-cai" 4 | } 5 | } -------------------------------------------------------------------------------- /backend/dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "backend": { 4 | "main": "backend/main.mo", 5 | "type": "motoko" 6 | } 7 | }, 8 | "defaults": { 9 | "build": { 10 | "args": "", 11 | "packtool": "mops sources" 12 | } 13 | }, 14 | "output_env_file": ".env", 15 | "version": 1 16 | } -------------------------------------------------------------------------------- /backend/mops.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | base = "0.11.1" 3 | xrc-types = "1.0.0" 4 | map = "9.0.1" 5 | alfangodb = "https://github.com/sagcryptoicp/alfangodb#master@2127b62cd0cb69f49881bb2a61cb2685ce214947" 6 | rand = "https://github.com/aviate-labs/rand.mo#v0.2.3" 7 | ulid = "https://github.com/aviate-labs/ulid.mo#v0.1.3" 8 | d3storage = "https://github.com/ragpatel8742/d3storage#main@d618819e85fe741569d9b54c54cb5e4a489be3fa" 9 | httpParser = "https://github.com/NatLabs/http-parser.mo#v0.1.2@27cba8ed0d39387e0fb660f65909ffe2a7d54413" 10 | ic = "1.0.1" 11 | -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_DFX_NETWORK=production 2 | VITE_LOCAL_CANISTERID=bkyz2-fmaaa-aaaaa-qaaaq-cai 3 | VITE_SUBNET_CANISTERID=prdxk-aqaaa-aaaai-qpfta-cai 4 | VITE_ICPLEDGERCANISTER=ryjl3-tyaaa-aaaaa-aaaba-cai 5 | # DFX CANISTER ENVIRONMENT VARIABLES 6 | DFX_VERSION='0.20.0' 7 | DFX_NETWORK='local' 8 | CANISTER_ID_FRONTEND='be2us-64aaa-aaaaa-qaabq-cai' 9 | CANISTER_ID='be2us-64aaa-aaaaa-qaabq-cai' 10 | CANISTER_CANDID_PATH='/home/nisarg/Projects/ICP Projects/breaking-bits/frontend/.dfx/local/canisters/frontend/assetstorage.did' 11 | # END DFX CANISTER ENVIRONMENT VARIABLES -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | "@typescript-eslint/no-unused-vars": ["warn"], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # dfx temporary files 27 | .dfx/ 28 | 29 | # generated files 30 | **/declarations/ 31 | 32 | # # environment variables 33 | # .env -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | v18.19.0 -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /frontend/canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "frontend": { 3 | "ic": "pwcr6-niaaa-aaaai-qpftq-cai" 4 | } 5 | } -------------------------------------------------------------------------------- /frontend/dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "frontend": { 4 | "source": [ 5 | "dist" 6 | ], 7 | "type": "assets" 8 | } 9 | }, 10 | "defaults": { 11 | "build": { 12 | "args": "", 13 | "packtool": "" 14 | } 15 | }, 16 | "output_env_file": ".env", 17 | "version": 1 18 | } -------------------------------------------------------------------------------- /frontend/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-gray: rgba(128, 128, 128, 1); 3 | --c-light-gray: rgba(192, 192, 192, 1); 4 | } 5 | 6 | * { 7 | -webkit-font-smoothing: antialiased !important; 8 | } 9 | 10 | .button { 11 | font-size: 16px !important; 12 | padding-block: 7.5px !important; 13 | padding-inline: 24px !important; 14 | background-color: var(--c-light-gray) !important; 15 | } 16 | 17 | .ty-h1, 18 | h1 { 19 | font-size: 64px !important; 20 | font-weight: bold !important; 21 | } 22 | 23 | .ty-h2, 24 | h2 { 25 | font-size: 50px !important; 26 | font-weight: bold !important; 27 | } 28 | 29 | .ty-h3, 30 | h3 { 31 | font-size: 44px !important; 32 | line-height: 44px; 33 | font-weight: bold !important; 34 | } 35 | 36 | .ty-h4, 37 | h4 { 38 | font-size: 32px !important; 39 | line-height: 48px; 40 | font-weight: bold !important; 41 | } 42 | 43 | .ty-h6, 44 | h6 { 45 | font-size: 20px !important; 46 | font-weight: bold !important; 47 | } 48 | 49 | .ty-p1 { 50 | font-size: 16px; 51 | font-weight: normal; 52 | } 53 | .ty-p2 { 54 | font-size: 14px; 55 | font-weight: normal; 56 | } 57 | .ty-p3 { 58 | font-size: 12px; 59 | line-height: 18px; 60 | font-weight: normal; 61 | } 62 | 63 | .ty-b1 { 64 | font-size: 16px; 65 | font-weight: bold; 66 | } 67 | 68 | abbr { 69 | text-decoration: none; 70 | } 71 | 72 | .truncate { 73 | text-overflow: ellipsis; 74 | overflow: hidden; 75 | white-space: nowrap; 76 | flex-shrink: 1; 77 | max-width: 100px; 78 | } 79 | 80 | .visually-hidden { 81 | clip: rect(0 0 0 0); 82 | clip-path: inset(50%); 83 | height: 1px; 84 | overflow: hidden; 85 | position: absolute; 86 | white-space: nowrap; 87 | width: 1px; 88 | } 89 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Breaking Bits Yo! 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breaking-bits", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@dfinity/agent": "^2.0.0", 14 | "@emotion/react": "^11.13.0", 15 | "@emotion/styled": "^11.13.0", 16 | "@mui/icons-material": "^5.16.7", 17 | "@mui/material": "^5.16.7", 18 | "@mui/styled-engine-sc": "^6.0.0-alpha.18", 19 | "@types/node": "^20.14.12", 20 | "98.css": "^0.1.20", 21 | "classnames": "^2.5.1", 22 | "dotenv": "^16.4.5", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-qr-code": "^2.0.15", 26 | "styled-components": "^6.1.12", 27 | "use-debounce": "^10.0.2", 28 | "vite-plugin-environment": "^1.1.3", 29 | "zustand": "^4.5.4" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^18.3.3", 33 | "@types/react-dom": "^18.3.0", 34 | "@typescript-eslint/eslint-plugin": "^7.15.0", 35 | "@typescript-eslint/parser": "^7.15.0", 36 | "@vitejs/plugin-react": "^4.3.1", 37 | "eslint": "^8.57.0", 38 | "eslint-plugin-react-hooks": "^4.6.2", 39 | "eslint-plugin-react-refresh": "^0.4.7", 40 | "sass": "^1.77.8", 41 | "typescript": "^5.2.2", 42 | "vite": "^5.3.4", 43 | "vite-tsconfig-paths": "^4.3.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | Josh's Custom CSS Reset 3 | https://www.joshwcomeau.com/css/custom-css-reset/ 4 | */ 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: border-box; 9 | } 10 | * { 11 | margin: 0; 12 | } 13 | body { 14 | line-height: 1.5; 15 | -webkit-font-smoothing: antialiased; 16 | } 17 | img, 18 | picture, 19 | video, 20 | canvas, 21 | svg { 22 | display: block; 23 | max-width: 100%; 24 | } 25 | input, 26 | button, 27 | textarea, 28 | select { 29 | font: inherit; 30 | } 31 | p, 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | overflow-wrap: break-word; 39 | } 40 | #root, 41 | #__next { 42 | isolation: isolate; 43 | } 44 | 45 | #app-root { 46 | width: 100%; 47 | height: 100%; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import useRouteStore from "Store/routeStore"; 2 | import type { RouteName } from "Store/routeStore"; 3 | 4 | import Dashboard from "Pages/Dashboard"; 5 | import Welcome from "Pages/Welcome/"; 6 | import WelcomeInfo from "Pages/WelcomeInfo"; 7 | import TokenPreview from "Pages/TokenPreview"; 8 | import TokenForm from "Pages/TokenForm"; 9 | 10 | import styles from "./app.module.scss"; 11 | 12 | import useImagePreloader from "Hooks/useImagePreloader"; 13 | import bgNoise from "Assets/Sound/bg_noise.mp3"; 14 | import useAppStore from "Store/appStore"; 15 | 16 | const RoutePageComponent: Record = { 17 | welcome: Welcome, 18 | "welcome-info": WelcomeInfo, 19 | dashboard: Dashboard, 20 | "token-preview": TokenPreview, 21 | "token-form": TokenForm, 22 | }; 23 | 24 | function App() { 25 | useImagePreloader(); 26 | 27 | const currentRoute = useRouteStore((state) => state.currentRoute); 28 | const isLoading = useAppStore((state) => state.isLoading); 29 | 30 | const PageComponent = RoutePageComponent[currentRoute]; 31 | 32 | return ( 33 |
34 |
35 | {isLoading ? ( 36 |
37 | 38 |
39 | ) : null} 40 | 41 |
42 |
44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/bus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/bus.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/cloud_sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/cloud_sky.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/forest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/forest.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/house.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/house.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/night.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/road_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/road_color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/savannah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/savannah.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/BG/win_dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/BG/win_dog.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/bit-finity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/bit-finity.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/check.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/flask.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/flask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/info.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/paint-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/paint-file.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/pick-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/pick-img.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/plug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/plug.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/red-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/red-close.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Images/Icons/warning.png -------------------------------------------------------------------------------- /frontend/src/Assets/Images/Icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/src/Assets/Sound/bg_noise.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skytech2510/Token_Generator_AI/7d12accd4ff9557b994718e9a3c8bc13bbef3171/frontend/src/Assets/Sound/bg_noise.mp3 -------------------------------------------------------------------------------- /frontend/src/Components/Bubble/Bubble.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./bubbles.module.scss"; 2 | import cn from "classnames"; 3 | import { useEffect, useState } from "react"; 4 | 5 | interface BubbleProps { 6 | content: string; 7 | contentType?: "img" | "text"; 8 | arrowPosition?: "top" | "bottom" | "right" | "left"; 9 | inverted?: boolean; 10 | isTooltip?: boolean; 11 | disableAnimation?: boolean; 12 | } 13 | 14 | const Bubble = (props: BubbleProps) => { 15 | const { 16 | content, 17 | contentType = "text", 18 | arrowPosition, 19 | inverted = false, 20 | isTooltip = false, 21 | disableAnimation = false, 22 | } = props; 23 | const [visibleContentIndex, setVisibleContentIndex] = useState( 24 | disableAnimation ? content.length : 0 25 | ); 26 | 27 | const audioElem = document.getElementById( 28 | "bg_noise" 29 | ) as HTMLAudioElement | null; 30 | 31 | const className = cn({ 32 | [styles.bubble]: true, 33 | [styles.top]: arrowPosition === "top", 34 | [styles.bottom]: arrowPosition === "bottom", 35 | [styles.left]: arrowPosition === "left", 36 | [styles.right]: arrowPosition === "right", 37 | [styles.inverted]: inverted, 38 | [styles.tooltip]: isTooltip, 39 | }); 40 | 41 | useEffect(() => { 42 | let interval: number; 43 | const playAnimation = () => { 44 | if (audioElem) { 45 | audioElem.volume = 0.2; 46 | } 47 | setVisibleContentIndex(0); 48 | if (contentType === "text" && content.length > 0) { 49 | audioElem?.play(); 50 | interval = window.setInterval(() => { 51 | setVisibleContentIndex((prevIndex) => { 52 | if (prevIndex === content.length) { 53 | clearInterval(interval); 54 | audioElem?.pause(); 55 | } 56 | if (prevIndex < content.length) { 57 | return prevIndex + 1; 58 | } else { 59 | return prevIndex; 60 | } 61 | }); 62 | }, 50); 63 | } 64 | }; 65 | 66 | if (!disableAnimation) { 67 | playAnimation(); 68 | } 69 | 70 | return () => { 71 | clearInterval(interval); 72 | audioElem?.pause(); 73 | }; 74 | }, [content, contentType, audioElem, disableAnimation]); 75 | 76 | useEffect(() => { 77 | const stopPlayer = () => { 78 | if (audioElem) { 79 | audioElem.pause(); 80 | } 81 | }; 82 | 83 | window.addEventListener("blur", stopPlayer); 84 | 85 | return () => { 86 | window.removeEventListener("blur", stopPlayer); 87 | }; 88 | }, [audioElem]); 89 | 90 | if (content.length === 0) { 91 | return null; 92 | } 93 | 94 | return ( 95 |
96 | {contentType === "text" ? ( 97 | content.slice(0, visibleContentIndex) 98 | ) : ( 99 | 100 | )} 101 |
102 | ); 103 | }; 104 | 105 | export default Bubble; 106 | -------------------------------------------------------------------------------- /frontend/src/Components/Bubble/bubbles.module.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | $white: #dfdfdf; 3 | $shadow: rgba(0, 0, 0, 0.1); 4 | 5 | $px: 4px; 6 | 7 | $bubble-border: 0 -1 * $px $white, 0 -2 * $px $black, $px 0 $white, 8 | $px -1 * $px $black, 2 * $px 0 $black, 0 $px $white, 0 2 * $px $black, 9 | -1 * $px 0 $white, -1 * $px $px $black, -2 * $px 0 $black, 10 | -1 * $px -1 * $px $black, $px $px $black; 11 | 12 | .bubble { 13 | position: relative; 14 | display: inline-block; 15 | margin: 5 * $px; 16 | text-align: center; 17 | font-size: 16px; 18 | line-height: 1.3em; 19 | font-weight: bold; 20 | background-color: $white; 21 | color: $black; 22 | padding: 2 * $px; 23 | box-shadow: $bubble-border; 24 | white-space: break-spaces; 25 | 26 | box-sizing: border-box; 27 | 28 | img { 29 | aspect-ratio: 1; 30 | height: 80px; 31 | border-radius: 50%; 32 | object-fit: cover; 33 | } 34 | 35 | &::after { 36 | content: ""; 37 | display: block; 38 | position: absolute; 39 | box-sizing: border-box; 40 | } 41 | 42 | &.shadow { 43 | box-shadow: $bubble-border, $px 3 * $px $shadow, 3 * $px $px $shadow, 44 | 2 * $px 2 * $px $shadow; 45 | } 46 | 47 | &.tooltip { 48 | font-size: 12px; 49 | font-weight: 400; 50 | line-height: 18px; 51 | text-align: left; 52 | max-width: 212px; 53 | width: max-content; 54 | } 55 | 56 | &.mini { 57 | width: 110px; 58 | font-size: 16px; 59 | padding: 4px; 60 | font-family: monospace; 61 | } 62 | &.medium { 63 | width: 350px; 64 | } 65 | &.large { 66 | width: 560px; 67 | font-size: 24px; 68 | text-align: left; 69 | text-transform: uppercase; 70 | } 71 | &.grow { 72 | width: initial; 73 | } 74 | 75 | &.top::after { 76 | height: $px; 77 | width: $px; 78 | top: -2 * $px; 79 | left: 8 * $px; 80 | box-shadow: 0 -1 * $px $black, 0 -2 * $px $black, 0 -3 * $px $black, 81 | 0 -4 * $px $black, -1 * $px -3 * $px $black, -2 * $px -2 * $px $black, 82 | -3 * $px -1 * $px $black, -1 * $px -1 * $px $white, 83 | -2 * $px -1 * $px $white, -1 * $px -2 * $px $white, -1 * $px 0 $white, 84 | -2 * $px 0 $white, -3 * $px 0 $white; 85 | } 86 | 87 | &.right::after { 88 | height: $px; 89 | width: $px; 90 | top: 5 * $px; 91 | right: -2 * $px; 92 | background: white; 93 | box-shadow: 1 * $px -1 * $px $white, 1 * $px 0 $white, 2 * $px 0 $white, 94 | 0 -2 * $px $white, 1 * $px 1 * $px $black, 2 * $px 1 * $px $black, 95 | 3 * $px 1 * $px $black, 4 * $px 1 * $px $black, 3 * $px 0 $black, 96 | 2 * $px -1 * $px $black, 1 * $px -2 * $px $black, 0 -1 * $px $white; 97 | } 98 | 99 | &.bottom::after { 100 | height: $px; 101 | width: $px; 102 | bottom: -2 * $px; 103 | left: 5 * $px; 104 | box-shadow: 0 $px $black, 0 2 * $px $black, 0 3 * $px $black, 105 | 0 4 * $px $black, -1 * $px 3 * $px $black, -2 * $px 2 * $px $black, 106 | -3 * $px 1 * $px $black, -1 * $px $px $white, -2 * $px $px $white, 107 | -1 * $px 2 * $px $white, -1 * $px 0 $white, -2 * $px 0 $white, 108 | -3 * $px 0 $white; 109 | transform: scaleX(-1); 110 | } 111 | &.inverted::after { 112 | transform: scaleX(1); 113 | left: initial; 114 | right: 5 * $px; 115 | } 116 | 117 | &.left::after { 118 | height: $px; 119 | width: $px; 120 | top: 5 * $px; 121 | left: -2 * $px; 122 | background: white; 123 | box-shadow: -1 * $px -1 * $px $white, -1 * $px 0 $white, -2 * $px 0 $white, 124 | 0 -2 * $px $white, -1 * $px 1 * $px $black, -2 * $px 1 * $px $black, 125 | -3 * $px 1 * $px $black, -4 * $px 1 * $px $black, -3 * $px 0 $black, 126 | -2 * $px -1 * $px $black, -1 * $px -2 * $px $black, 0 -1 * $px $white; 127 | } 128 | } 129 | 130 | @keyframes bubble-anim { 131 | 0% { 132 | opacity: 0; 133 | } 134 | 100% { 135 | opacity: 1; 136 | } 137 | } 138 | 139 | .animate { 140 | animation: bubble-anim 700ms ease-in-out forwards; 141 | } 142 | -------------------------------------------------------------------------------- /frontend/src/Components/Bubble/index.tsx: -------------------------------------------------------------------------------- 1 | import Bubble from "./Bubble"; 2 | export default Bubble; 3 | -------------------------------------------------------------------------------- /frontend/src/Components/Character/Jesse/Jesse.tsx: -------------------------------------------------------------------------------- 1 | import jesseNeutral from "Assets/Images/Character/jesse_neutral.svg"; 2 | import jesseSmile from "Assets/Images/Character/jesse_smile.svg"; 3 | import Bubble from "Components/Bubble"; 4 | 5 | import styles from "./jesse.module.scss"; 6 | import useStoryStore from "Store/storyStore"; 7 | 8 | const CharacterMap = { 9 | neutral: jesseNeutral, 10 | smile: jesseSmile, 11 | }; 12 | 13 | interface JesseProps { 14 | height?: number; 15 | width?: number; 16 | inverted?: boolean; 17 | } 18 | 19 | const Jesse = (props: JesseProps) => { 20 | const { height = 188, width = 140, inverted } = props; 21 | const { 22 | visible, 23 | expression, 24 | styleOverride, 25 | bubbleContent, 26 | bubbleStyleOverride, 27 | } = useStoryStore((state) => state.jesse ?? {}); 28 | 29 | if (!visible) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 | {`Jesse 43 |
44 | 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default Jesse; 56 | -------------------------------------------------------------------------------- /frontend/src/Components/Character/Jesse/jesse.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | 4 | img { 5 | &[data-inverted="true"] { 6 | transform: scaleX(-1); 7 | } 8 | } 9 | } 10 | 11 | .bubbleContainer { 12 | position: absolute; 13 | top: -40%; 14 | right: -85%; 15 | z-index: 10; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/Components/Character/White/White.tsx: -------------------------------------------------------------------------------- 1 | import whiteNeutral from "Assets/Images/Character/white_neutral.svg"; 2 | import Bubble from "Components/Bubble"; 3 | 4 | import styles from "./white.module.scss"; 5 | import useStoryStore from "Store/storyStore"; 6 | 7 | const CharacterMap = { 8 | neutral: whiteNeutral, 9 | smile: whiteNeutral, 10 | }; 11 | 12 | interface WhiteProps { 13 | height?: number; 14 | width?: number; 15 | inverted?: boolean; 16 | } 17 | 18 | const White = (props: WhiteProps) => { 19 | const { height = 188, width = 150 } = props; 20 | const { 21 | visible, 22 | inverted, 23 | expression, 24 | styleOverride, 25 | bubbleContent, 26 | bubbleStyleOverride, 27 | } = useStoryStore((state) => state.white ?? {}); 28 | 29 | if (!visible) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 | {`Mr. 43 |
48 | 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default White; 59 | -------------------------------------------------------------------------------- /frontend/src/Components/Character/White/white.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | img { 4 | &[data-inverted="true"] { 5 | transform: scaleX(-1); 6 | } 7 | } 8 | } 9 | 10 | .bubbleContainer { 11 | position: absolute; 12 | top: -40%; 13 | left: -70%; 14 | z-index: 10; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/Components/Character/index.tsx: -------------------------------------------------------------------------------- 1 | import Jesse from "./Jesse/Jesse"; 2 | import White from "./White/White"; 3 | 4 | export { Jesse, White }; 5 | -------------------------------------------------------------------------------- /frontend/src/Components/Chip/Chip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./chip.module.scss"; 3 | 4 | interface ChipProps { 5 | text: string; 6 | onClose?: () => void; 7 | } 8 | 9 | const Chip: React.FC = ({ text, onClose }) => { 10 | return ( 11 |
12 | {text} 13 | {onClose && ( 14 | 17 | )} 18 |
19 | ); 20 | }; 21 | 22 | export default Chip; 23 | -------------------------------------------------------------------------------- /frontend/src/Components/Chip/chip.module.scss: -------------------------------------------------------------------------------- 1 | .chipContainer { 2 | display: inline-flex; 3 | align-items: center; 4 | padding: 4px 8px; 5 | margin: 4px; 6 | background-color: #c3c3c3; 7 | border: 2px solid #000; 8 | border-right: 2px solid #fff; 9 | border-bottom: 2px solid #fff; 10 | font-family: "MS Sans Serif", sans-serif; 11 | font-size: 14px; 12 | color: #000; 13 | box-shadow: 14 | 2px 2px 0 #fff, 15 | 4px 4px 0 #000; 16 | cursor: pointer; 17 | 18 | &:hover { 19 | background-color: #e4e4e4; 20 | } 21 | } 22 | 23 | .deleteButton { 24 | margin-left: 8px; 25 | padding: 2px 6px; 26 | background-color: #c3c3c3; 27 | border: 2px solid #000; 28 | border-right: 2px solid #fff; 29 | border-bottom: 2px solid #fff; 30 | cursor: pointer; 31 | 32 | &:hover { 33 | background-color: #e4e4e4; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/Components/Chip/index.tsx: -------------------------------------------------------------------------------- 1 | import Chip from "./Chip"; 2 | export default Chip; 3 | -------------------------------------------------------------------------------- /frontend/src/Components/Icons/ArrowCircleLeft.tsx: -------------------------------------------------------------------------------- 1 | interface ArrowCircleLeftProps { 2 | width?: number; 3 | height?: number; 4 | fill?: string; 5 | } 6 | 7 | const ArrowCircleLeft = (props: ArrowCircleLeftProps) => { 8 | const { width = 24, height = 24, fill = "black" } = props; 9 | return ( 10 | 17 | 25 | 32 | 33 | ); 34 | }; 35 | 36 | export default ArrowCircleLeft; 37 | -------------------------------------------------------------------------------- /frontend/src/Components/Icons/ArrowCircleRight.tsx: -------------------------------------------------------------------------------- 1 | interface ArrowCircleRightProps { 2 | width?: number; 3 | height?: number; 4 | fill?: string; 5 | } 6 | 7 | const ArrowCircleRight = (props: ArrowCircleRightProps) => { 8 | const { width = 24, height = 24, fill = "black" } = props; 9 | return ( 10 | 17 | 25 | 32 | 33 | ); 34 | }; 35 | 36 | export default ArrowCircleRight; 37 | -------------------------------------------------------------------------------- /frontend/src/Components/Icons/WebMoney.tsx: -------------------------------------------------------------------------------- 1 | interface WebMoneyProps { 2 | width?: number; 3 | height?: number; 4 | } 5 | const WebMoney = (props: WebMoneyProps) => ( 6 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 41 | ); 42 | export default WebMoney; 43 | -------------------------------------------------------------------------------- /frontend/src/Components/Icons/index.tsx: -------------------------------------------------------------------------------- 1 | import ArrowCircleRight from "./ArrowCircleRight"; 2 | import ArrowCircleLeft from "./ArrowCircleLeft"; 3 | import Discord from "./Discord"; 4 | import Facebook from "./Facebook"; 5 | import Instagram from "./Instagram"; 6 | import XLogo from "./XLogo"; 7 | import Twitter from "./Twitter"; 8 | import WebMoney from "./WebMoney"; 9 | 10 | export { 11 | ArrowCircleLeft, 12 | ArrowCircleRight, 13 | Discord, 14 | Facebook, 15 | Instagram, 16 | Twitter, 17 | XLogo, 18 | WebMoney, 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/Components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./modal.module.scss"; 2 | import warning from "Assets/Images/Icons/warning.svg"; 3 | import flask from "Assets/Images/Icons/flask.svg"; 4 | 5 | interface ModalProps { 6 | onClose: () => void; 7 | title: string; 8 | icon: string; 9 | message: string; 10 | buttonMessage: string; 11 | } 12 | 13 | const Modal = (props: ModalProps) => { 14 | const { onClose, title, message, icon, buttonMessage } = props; 15 | 16 | const handleSubmit = async () => { 17 | onClose(); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 |
Confirmation
25 |
26 | 27 |
28 |
29 | 30 |
31 | {icon == "1" ? ( 32 | 33 | ) : ( 34 | 35 | )} 36 |
{title}
37 |
38 |
39 | {message} 40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | export default Modal; 48 | -------------------------------------------------------------------------------- /frontend/src/Components/Modal/modal.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade { 2 | from { 3 | opacity: 0; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | .backdrop { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | inset: 0; 15 | left: 50%; 16 | top: 50%; 17 | transform: translate(-50%, -50%); 18 | background-color: rgba(0, 0, 0, 0.3); 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | 23 | animation: fade 200ms ease-in-out forwards; 24 | z-index: 100; 25 | } 26 | 27 | .container { 28 | width: 320px; 29 | } 30 | 31 | .header { 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | justify-content: flex-start; 36 | gap: 8px; 37 | padding-inline: 16px; 38 | padding-block: 8px; 39 | } 40 | 41 | .body { 42 | display: flex; 43 | flex-direction: column; 44 | gap: 8px; 45 | padding-block-end: 25px; 46 | padding-block-start: 8px; 47 | padding-inline: 16px; 48 | font-size: 16px; 49 | font-weight: normal; 50 | 51 | button { 52 | display: flex !important; 53 | flex-direction: row !important; 54 | justify-content: center !important; 55 | align-items: center; 56 | font-size: 16px !important; 57 | line-height: 24px !important; 58 | padding-block: 8px !important; 59 | padding-inline: 7.5px !important; 60 | 61 | img { 62 | aspect-ratio: 1; 63 | height: 32px; 64 | border-radius: 50%; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/Components/ProgressLoader/ProgressLoader.tsx: -------------------------------------------------------------------------------- 1 | import style from "./progressLoader.module.scss"; 2 | 3 | const ProgressLoader = () => { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | }; 10 | 11 | export default ProgressLoader; 12 | -------------------------------------------------------------------------------- /frontend/src/Components/ProgressLoader/progressLoader.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 8px; 3 | position: relative; 4 | overflow: hidden; 5 | } 6 | 7 | @keyframes loading { 8 | 0% { 9 | transform: scaleX(0); 10 | } 11 | 100% { 12 | transform: scaleX(1); 13 | } 14 | } 15 | 16 | .loader { 17 | height: 24px; 18 | width: 100%; 19 | background: linear-gradient(to right, #1084d0, #000080); 20 | transform-origin: left center; 21 | transform: scaleX(0); 22 | 23 | animation: loading 2s infinite linear alternate; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/Components/Stepper/Stepper.tsx: -------------------------------------------------------------------------------- 1 | import style from "./stepper.module.scss"; 2 | 3 | interface StepperProps { 4 | currentStep: number; 5 | totalStep: number; 6 | } 7 | 8 | const Stepper = (props: StepperProps) => { 9 | const { currentStep, totalStep } = props; 10 | 11 | return ( 12 |
13 | {Array(totalStep) 14 | .fill(1) 15 | .map((_v, i) => { 16 | return ( 17 |
i} /> 18 | ); 19 | })} 20 |
21 | Step {currentStep} / {totalStep} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Stepper; 28 | -------------------------------------------------------------------------------- /frontend/src/Components/Stepper/index.tsx: -------------------------------------------------------------------------------- 1 | import Stepper from "./Stepper"; 2 | export default Stepper; 3 | -------------------------------------------------------------------------------- /frontend/src/Components/Stepper/stepper.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: flex-start; 5 | align-items: center; 6 | gap: 8px; 7 | width: 100%; 8 | padding: 8px !important; 9 | position: relative; 10 | } 11 | 12 | .step { 13 | display: flex; 14 | position: relative; 15 | flex: 1; 16 | background: linear-gradient(to right, #808080, #b5b5b5); 17 | height: 24px; 18 | } 19 | 20 | @keyframes expand { 21 | from { 22 | width: 0px; 23 | } 24 | to { 25 | width: calc-size(auto); 26 | } 27 | } 28 | 29 | [data-active="true"].step::after { 30 | content: ""; 31 | position: absolute; 32 | background: linear-gradient(to right, #000080, #1084d0); 33 | height: 100%; 34 | width: 100%; 35 | animation: expand 300ms ease-in-out forwards; 36 | } 37 | 38 | .stepInfo { 39 | position: absolute; 40 | top: -24px; 41 | right: 0px; 42 | font-size: 12px; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/DashboardEmptyScene/DashboardEmptyScene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse, White } from "Components/Character"; 2 | import style from "./dashboardEmptyScene.module.scss"; 3 | 4 | const DashboardEmptyScene = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | ); 16 | }; 17 | 18 | export default DashboardEmptyScene; 19 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/DashboardEmptyScene/dashboardEmptyScene.module.scss: -------------------------------------------------------------------------------- 1 | $roadheight: 48px; 2 | 3 | .background { 4 | background: url("/src/Assets/Images/BG/bus.png"); 5 | background-size: 100% calc(100% - $roadheight + 12px); 6 | height: 100%; 7 | width: 100%; 8 | object-fit: cover; 9 | position: relative; 10 | } 11 | 12 | .jesseContainer { 13 | position: absolute; 14 | bottom: calc($roadheight - 16px); 15 | left: 8%; 16 | z-index: 10; 17 | } 18 | 19 | .whiteContainer { 20 | position: absolute; 21 | bottom: $roadheight; 22 | bottom: calc($roadheight - 16px); 23 | right: 8%; 24 | z-index: 10; 25 | } 26 | 27 | .road { 28 | background: url("/src/Assets/Images/BG/road_black.svg"); 29 | background-repeat: repeat-x repeat-y; 30 | width: 100%; 31 | height: $roadheight; 32 | object-fit: cover; 33 | position: absolute; 34 | bottom: 0; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenCookingScene/TokenCookingScene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse, White } from "Components/Character"; 2 | import style from "./tokenCookingScene.module.scss"; 3 | import cloudBig from "Assets/Images/BG/cloud_big.svg"; 4 | import cloudSmall from "Assets/Images/BG/cloud_sm.svg"; 5 | import flask from "Assets/Images/BG/flask.svg"; 6 | 7 | const TokenCookingScene = () => { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default TokenCookingScene; 24 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenCookingScene/tokenCookingScene.module.scss: -------------------------------------------------------------------------------- 1 | $charTiming: cubic-bezier(1, -0.15, 0.74, 0.1); 2 | $cloudTiming: cubic-bezier(1, -0.01, 0.57, 0.54); 3 | $flaskTiming: cubic-bezier(1, 0.03, 0, 1.03); 4 | $charInset: 2; 5 | 6 | $flaskSteps: 4; 7 | $flaskHalfSteps: 2; 8 | $flaskVerticalDisplacement: 10; 9 | 10 | $delay: 200ms; 11 | 12 | .container { 13 | width: 100%; 14 | background: url("/src/Assets/Images/BG/road_color.svg"); 15 | height: 200px; 16 | background-position: bottom; 17 | background-repeat: no-repeat; 18 | position: relative; 19 | container-type: size; 20 | } 21 | 22 | .jesseContainer, 23 | .whiteContainer { 24 | position: absolute; 25 | bottom: 0px; 26 | z-index: 10; 27 | } 28 | 29 | .bigCloud, 30 | .smallCloud { 31 | position: absolute; 32 | z-index: 5; 33 | } 34 | 35 | @keyframes jesse-wave { 36 | 0% { 37 | transform: translateX(0); 38 | } 39 | 45% { 40 | transform: translateX(#{$charInset}cqw); 41 | } 42 | 50% { 43 | transform: translateX(#{$charInset}cqw); 44 | } 45 | 100% { 46 | transform: translateX(0px); 47 | } 48 | } 49 | 50 | @keyframes white-wave { 51 | 0% { 52 | transform: translateX(0); 53 | } 54 | 45% { 55 | transform: translateX(-#{$charInset}cqw); 56 | } 57 | 50% { 58 | transform: translateX(-#{$charInset}cqw); 59 | } 60 | 100% { 61 | transform: translateX(0px); 62 | } 63 | } 64 | 65 | .jesseContainer { 66 | right: #{$charInset}cqw; 67 | animation: jesse-wave 2s $charTiming infinite reverse; 68 | animation-delay: $delay; 69 | } 70 | 71 | .whiteContainer { 72 | left: #{$charInset}cqw; 73 | animation: white-wave 2s $charTiming infinite reverse; 74 | animation-delay: $delay; 75 | } 76 | 77 | @keyframes big-cloud-wave { 78 | 0% { 79 | transform: translateX(0); 80 | } 81 | 45% { 82 | transform: translateX(-50cqw); 83 | } 84 | 55% { 85 | transform: translateX(-50cqw); 86 | } 87 | 100% { 88 | transform: translateX(0); 89 | } 90 | } 91 | 92 | @keyframes small-cloud-wave { 93 | 0% { 94 | transform: translateX(0); 95 | } 96 | 45% { 97 | transform: translateX(50cqw); 98 | } 99 | 50% { 100 | transform: translateX(50cqw); 101 | } 102 | 100% { 103 | transform: translateX(0); 104 | } 105 | } 106 | 107 | .bigCloud { 108 | top: 25%; 109 | right: 20%; 110 | animation: big-cloud-wave 2s $cloudTiming infinite reverse; 111 | animation-delay: $delay; 112 | } 113 | .smallCloud { 114 | top: 50%; 115 | left: 20%; 116 | animation: small-cloud-wave 2s $cloudTiming infinite reverse; 117 | animation-delay: $delay; 118 | } 119 | 120 | @keyframes flask-move { 121 | @for $i from 1 through $flaskSteps { 122 | #{calc($i * 80% / $flaskSteps)} { 123 | transform: translateX(#{$i * calc(64 / $flaskSteps)}cqw) 124 | translateY( 125 | if( 126 | $i<$flaskHalfSteps, 127 | -#{($i % $flaskHalfSteps) * $flaskVerticalDisplacement}cqh, 128 | -#{( 129 | ($i % $flaskHalfSteps) + 130 | ($flaskSteps - $i) * 131 | $flaskVerticalDisplacement 132 | )}cqh 133 | ) 134 | ); 135 | } 136 | } 137 | 100% { 138 | transform: translateX(0) translateY(0); 139 | } 140 | } 141 | 142 | .flask { 143 | position: absolute; 144 | z-index: 11; 145 | bottom: 30%; 146 | left: 16%; 147 | animation: flask-move 4s $flaskTiming infinite forwards; 148 | animation-delay: $delay; 149 | } 150 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenFormStep1Scene/TokenFormStep1Scene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse } from "Components/Character"; 2 | import style from "./tokenFormStep1Scene.module.scss"; 3 | 4 | const TokenFormStep1Scene = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default TokenFormStep1Scene; 16 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenFormStep1Scene/tokenFormStep1Scene.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | background: url("/src/Assets/Images/BG/cloud_sky.png"); 3 | background-size: 100% 100%; 4 | height: 100%; 5 | width: 100%; 6 | object-fit: cover; 7 | position: relative; 8 | } 9 | 10 | .jesseContainer { 11 | position: absolute; 12 | bottom: 20px; 13 | left: 8%; 14 | } 15 | 16 | .road { 17 | background: url("/src/Assets/Images/BG/road_black.svg"); 18 | background-repeat: repeat-x repeat-y; 19 | width: 100%; 20 | height: 20px; 21 | object-fit: cover; 22 | position: absolute; 23 | bottom: 0; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenFormStep2Scene/TokenFormStep2Scene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse } from "Components/Character"; 2 | import style from "./tokenFormStep2Scene.module.scss"; 3 | 4 | const TokenFormStep2Scene = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default TokenFormStep2Scene; 16 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenFormStep2Scene/tokenFormStep2Scene.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | background: url("/src/Assets/Images/BG/house.png"); 3 | background-size: 100% 100%; 4 | height: 100%; 5 | width: 100%; 6 | object-fit: cover; 7 | position: relative; 8 | } 9 | 10 | .jesseContainer { 11 | position: absolute; 12 | bottom: 20px; 13 | left: 8%; 14 | } 15 | 16 | .road { 17 | background: url("/src/Assets/Images/BG/road_black.svg"); 18 | background-repeat: repeat-x repeat-y; 19 | width: 100%; 20 | height: 20px; 21 | object-fit: cover; 22 | position: absolute; 23 | bottom: 0; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenFormStep3Scene/TokenFormStep3Scene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse } from "Components/Character"; 2 | import style from "./tokenFormStep3Scene.module.scss"; 3 | 4 | const TokenFormStep3Scene = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default TokenFormStep3Scene; 16 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/TokenFormStep3Scene/tokenFormStep3Scene.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | background: url("/src/Assets/Images/BG/forest.png"); 3 | background-size: 100% 100%; 4 | height: 100%; 5 | width: 100%; 6 | object-fit: cover; 7 | position: relative; 8 | } 9 | 10 | .jesseContainer { 11 | position: absolute; 12 | bottom: 20px; 13 | left: 8%; 14 | } 15 | 16 | .road { 17 | background: url("/src/Assets/Images/BG/road_black.svg"); 18 | background-repeat: repeat-x repeat-y; 19 | width: 100%; 20 | height: 20px; 21 | object-fit: cover; 22 | position: absolute; 23 | bottom: 0; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/WelcomeInfoScene/WelcomeInfoScene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse } from "Components/Character"; 2 | import styles from "./welcomeInfoScene.module.scss"; 3 | 4 | const WelcomeInfoScene = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default WelcomeInfoScene; 15 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/WelcomeInfoScene/welcomeInfoScene.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | background: url("/src/Assets/Images/BG/night.png"); 3 | background-size: 100% 100%; 4 | height: 100%; 5 | width: 100%; 6 | object-fit: cover; 7 | position: relative; 8 | } 9 | 10 | .jesseContainer { 11 | position: absolute; 12 | bottom: 8%; 13 | left: 8%; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/WelcomeScene/WelcomeScene.tsx: -------------------------------------------------------------------------------- 1 | import { Jesse } from "Components/Character"; 2 | import styles from "./welcomeScene.module.scss"; 3 | 4 | const WelcomeScene = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default WelcomeScene; 15 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/WelcomeScene/welcomeScene.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | background: url("/src/Assets/Images/BG/savannah.png"); 3 | background-size: 100% 100%; 4 | height: 100%; 5 | width: 100%; 6 | object-fit: cover; 7 | position: relative; 8 | } 9 | 10 | .jesseContainer { 11 | position: absolute; 12 | bottom: 8%; 13 | left: 8%; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/Components/StoryBoardScene/index.tsx: -------------------------------------------------------------------------------- 1 | import WelcomeScene from "./WelcomeScene/WelcomeScene"; 2 | import WelcomeInfoScene from "./WelcomeInfoScene/WelcomeInfoScene"; 3 | import DashboardEmptyScene from "./DashboardEmptyScene/DashboardEmptyScene"; 4 | import TokenFormStep1Scene from "./TokenFormStep1Scene/TokenFormStep1Scene"; 5 | import TokenFormStep2Scene from "./TokenFormStep2Scene/TokenFormStep2Scene"; 6 | import TokenFormStep3Scene from "./TokenFormStep3Scene/TokenFormStep3Scene"; 7 | import TokenCookingScene from "./TokenCookingScene/TokenCookingScene"; 8 | 9 | export { 10 | WelcomeScene, 11 | WelcomeInfoScene, 12 | DashboardEmptyScene, 13 | TokenFormStep1Scene, 14 | TokenFormStep2Scene, 15 | TokenFormStep3Scene, 16 | TokenCookingScene, 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/Components/Textbox/Textbox.module.scss: -------------------------------------------------------------------------------- 1 | .textboxContainer { 2 | display: flex; 3 | align-items: center; 4 | flex-wrap: wrap; 5 | padding: 4px; 6 | margin: 8px 0; 7 | background-color: #c3c3c3; 8 | border: 2px solid #000; 9 | border-right: 2px solid #fff; 10 | border-bottom: 2px solid #fff; 11 | box-shadow: 12 | 2px 2px 0 #fff, 13 | 4px 4px 0 #000; 14 | } 15 | 16 | .chipContainer { 17 | display: inline-flex; 18 | align-items: center; 19 | padding: 4px 8px; 20 | margin: 4px; 21 | background-color: #c3c3c3; 22 | border: 2px solid #000; 23 | border-right: 2px solid #fff; 24 | border-bottom: 2px solid #fff; 25 | font-family: "MS Sans Serif", sans-serif; 26 | font-size: 14px; 27 | color: #000; 28 | box-shadow: 29 | 2px 2px 0 #fff, 30 | 4px 4px 0 #000; 31 | cursor: pointer; 32 | 33 | &:hover { 34 | background-color: #e4e4e4; 35 | } 36 | } 37 | 38 | .deleteButton { 39 | margin-left: 8px; 40 | padding: 2px 6px; 41 | background-color: #c3c3c3; 42 | border: 2px solid #000; 43 | border-right: 2px solid #fff; 44 | border-bottom: 2px solid #fff; 45 | cursor: pointer; 46 | 47 | &:hover { 48 | background-color: #e4e4e4; 49 | } 50 | } 51 | 52 | .styledInput { 53 | flex-grow: 1; 54 | min-width: 100px; 55 | padding: 4px; 56 | border: none; 57 | font-family: "MS Sans Serif", sans-serif; 58 | font-size: 14px; 59 | color: #000; 60 | background-color: #e4e4e4; 61 | box-shadow: inset 2px 2px 0 #fff; 62 | 63 | &:focus { 64 | outline: none; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/Components/Textbox/Textbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styles from "./Textbox.module.scss"; 3 | 4 | type Chip = { 5 | id: number; 6 | label: string; 7 | }; 8 | 9 | type TextboxProps = { 10 | onChipsChange?: (chips: Chip[]) => void; 11 | }; 12 | 13 | const Textbox: React.FC = ({ onChipsChange }) => { 14 | const [chips, setChips] = useState([]); 15 | const [inputValue, setInputValue] = useState(""); 16 | 17 | const handleKeyPress = (e: React.KeyboardEvent) => { 18 | if (e.key === "Enter" && inputValue.trim()) { 19 | const newChip: Chip = { id: Date.now(), label: inputValue.trim() }; 20 | const updatedChips = [...chips, newChip]; 21 | setChips(updatedChips); 22 | setInputValue(""); 23 | onChipsChange?.(updatedChips); 24 | } 25 | }; 26 | 27 | const handleDeleteChip = (id: number) => { 28 | const updatedChips = chips.filter((chip) => chip.id !== id); 29 | setChips(updatedChips); 30 | onChipsChange?.(updatedChips); 31 | }; 32 | 33 | return ( 34 |
35 | {chips.map((chip) => ( 36 |
37 | {chip.label} 38 | handleDeleteChip(chip.id)} 41 | > 42 | x 43 | 44 |
45 | ))} 46 | setInputValue(e.target.value)} 51 | onKeyPress={handleKeyPress} 52 | placeholder="Type and press Enter" 53 | /> 54 |
55 | ); 56 | }; 57 | 58 | export default Textbox; 59 | -------------------------------------------------------------------------------- /frontend/src/Components/TokenTraits/TokenTraits.scss: -------------------------------------------------------------------------------- 1 | .MuiOutlinedInput-root .MuiAutocomplete-input, 2 | .MuiChip-label { 3 | font-family: "Pixelated MS Sans Serif", Arial; 4 | box-shadow: none; 5 | } 6 | 7 | .MuiOutlinedInput-root .MuiAutocomplete-endAdornment, 8 | .MuiAutocomplete-root:hover .MuiAutocomplete-clearIndicator { 9 | right: 0px !important; 10 | visibility: hidden !important; 11 | } 12 | 13 | .MuiAutocomplete-root { 14 | .MuiOutlinedInput-root { 15 | .MuiAutocomplete-input { 16 | padding: 8px !important; 17 | margin: 0; 18 | margin-right: 24px; 19 | } 20 | } 21 | .MuiAutocomplete-input { 22 | opacity: 1; 23 | } 24 | .MuiAutocomplete-inputRoot { 25 | display: flex; 26 | align-items: center; 27 | background: white; 28 | height: 100%; 29 | box-shadow: 30 | inset -1px -1px #fff, 31 | inset 1px 1px grey, 32 | inset -2px -2px #dfdfdf, 33 | inset 2px 2px #0a0a0a; 34 | border-radius: 0; 35 | padding: 0.5rem; 36 | } 37 | .MuiAutocomplete-tag { 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | border: 1px dashed; 42 | height: 40px; 43 | line-height: normal; 44 | } 45 | } 46 | 47 | .MuiChip-label { 48 | font-size: 16px; 49 | } 50 | 51 | .MuiChip-deleteIcon { 52 | color: rgba(0, 0, 0, 0.87) !important; 53 | } 54 | 55 | .MuiFormControl-root-MuiTextField-root { 56 | align-items: center; 57 | } 58 | 59 | .MuiInputBase-root { 60 | &.MuiOutlinedInput-root { 61 | justify-content: flex-start; 62 | } 63 | } 64 | 65 | .token-core-traits [data-isvalid="false"]::after, 66 | .token-core-traits [data-isvalid="true"]::after { 67 | position: absolute !important; 68 | height: 100% !important; 69 | background-repeat: no-repeat !important; 70 | background-position: center !important; 71 | top: 0px !important; 72 | } 73 | 74 | fieldset { 75 | visibility: hidden; 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/Components/TokenTraits/TokenTraits.tsx: -------------------------------------------------------------------------------- 1 | import { CancelOutlined } from "@mui/icons-material"; 2 | import { Autocomplete, Chip, TextField } from "@mui/material"; 3 | import React from "react"; 4 | import "./TokenTraits.scss"; 5 | 6 | interface TokenTraitsProps { 7 | coreTraits: string[]; 8 | handleInputChange: (value: string[]) => void; 9 | } 10 | 11 | const TokenTraits: React.FC = ({ 12 | coreTraits, 13 | handleInputChange, 14 | }: TokenTraitsProps) => { 15 | return ( 16 | { 22 | handleInputChange(newval); 23 | }} 24 | limitTags={4} 25 | freeSolo 26 | multiple 27 | disableClearable={true} 28 | renderTags={(value, props) => 29 | value.map((option, index) => { 30 | const { key, ...tagProps } = props({ index }); 31 | return ( 32 | } 38 | /> 39 | ); 40 | }) 41 | } 42 | renderInput={(params) => ( 43 | { 46 | if (e.code === "enter" && e.target.value) { 47 | handleInputChange(coreTraits.concat(e.target.value)); 48 | } 49 | }} 50 | /> 51 | )} 52 | /> 53 | ); 54 | }; 55 | 56 | export default TokenTraits; 57 | -------------------------------------------------------------------------------- /frontend/src/Components/Tooltip/Tooltip.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | aspect-ratio: 1; 3 | width: 24px; 4 | position: relative; 5 | display: inline; 6 | background: url("/src/Assets/Images/Icons/info.png"); 7 | background-size: 100%; 8 | background-repeat: no-repeat; 9 | background-position: center; 10 | } 11 | 12 | .bubbleContent { 13 | position: absolute; 14 | z-index: 10; 15 | max-width: 212px; 16 | transform: translate(-100%, -100%); 17 | opacity: 0; 18 | animation: fade-in 300ms ease-in-out forwards; 19 | } 20 | 21 | @keyframes fade-in { 22 | from { 23 | opacity: 0; 24 | } 25 | to { 26 | opacity: 1; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/Components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styles from "./Tooltip.module.scss"; 3 | import Bubble from "Components/Bubble"; 4 | import classNames from "classnames"; 5 | 6 | interface TooltipProps { 7 | label: string; 8 | className?: string; 9 | bubbleClassName?: string; 10 | } 11 | 12 | const Tooltip = (props: TooltipProps) => { 13 | const [isVisible, setIsVisible] = useState(false); 14 | const { label, className, bubbleClassName } = props; 15 | 16 | const show = () => { 17 | setIsVisible(true); 18 | }; 19 | const hide = () => { 20 | setIsVisible(false); 21 | }; 22 | 23 | return ( 24 |
35 | {isVisible ? ( 36 |
42 | 43 |
44 | ) : null} 45 |
46 | ); 47 | }; 48 | 49 | export default Tooltip; 50 | -------------------------------------------------------------------------------- /frontend/src/Hooks/useImagePreloader.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function preloadImage(src: string) { 4 | return new Promise((resolve, reject) => { 5 | const img = new Image(); 6 | img.onload = function () { 7 | resolve(img); 8 | }; 9 | img.onerror = img.onabort = function () { 10 | reject(src); 11 | }; 12 | img.src = src; 13 | }); 14 | } 15 | 16 | export default function useImagePreloader() { 17 | const [imagesPreloaded, setImagesPreloaded] = useState(false); 18 | 19 | useEffect(() => { 20 | let isCancelled = false; 21 | 22 | async function effect() { 23 | if (isCancelled) { 24 | return; 25 | } 26 | 27 | const imagesPaths: string[] = await Promise.all( 28 | Object.values( 29 | import.meta.glob( 30 | [ 31 | "/src/Assets/Images/BG/*", 32 | "/src/Assets/Images/Icons/*", 33 | "/src/Assets/Images/Character/*", 34 | ], 35 | { eager: true } 36 | ) 37 | ).map((v) => (v as { default: string }).default) 38 | ); 39 | 40 | const imagesPromiseList = []; 41 | for (const i of imagesPaths) { 42 | imagesPromiseList.push(preloadImage(i)); 43 | } 44 | 45 | await Promise.all(imagesPromiseList); 46 | 47 | if (isCancelled) { 48 | return; 49 | } 50 | 51 | setImagesPreloaded(true); 52 | } 53 | 54 | effect(); 55 | 56 | return () => { 57 | isCancelled = true; 58 | }; 59 | }, []); 60 | 61 | return { imagesPreloaded }; 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/Pages/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip from "Components/Tooltip/Tooltip"; 2 | import styles from "./dashboard.module.scss"; 3 | import UserTokens from "./UserTokens"; 4 | 5 | import paintImg from "Assets/Images/Icons/paint-file.png"; 6 | import useRouteStore from "Store/routeStore"; 7 | 8 | const Dashboard = () => { 9 | const goTo = useRouteStore((s) => s.goTo); 10 | 11 | const handleCreateNewTokenPress = () => { 12 | goTo("token-form"); 13 | }; 14 | 15 | return ( 16 |
17 |
18 |

Yo, what up, biatch! 👋🏻

19 |
20 |
21 | 38 | 54 |
55 | 67 |
68 |
69 | 70 |
71 | ); 72 | }; 73 | 74 | export default Dashboard; 75 | -------------------------------------------------------------------------------- /frontend/src/Pages/Dashboard/UserTokens.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { ArrowCircleLeft, ArrowCircleRight } from "Components/Icons"; 3 | import { DashboardEmptyScene } from "Components/StoryBoardScene"; 4 | import { useEffect, useLayoutEffect, useRef, useState } from "react"; 5 | import useStoryStore from "Store/storyStore"; 6 | import useUserStore, { Token } from "Store/userStore"; 7 | import dashboardStyles from "./dashboard.module.scss"; 8 | import styles from "./userToken.module.scss"; 9 | import useRouteStore from "Store/routeStore"; 10 | 11 | const NoTokensCreated = () => { 12 | const setStage = useStoryStore((state) => state.setStage); 13 | 14 | useLayoutEffect(() => { 15 | setStage("no_tokens"); 16 | }, [setStage]); 17 | 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }; 24 | 25 | interface TokenButtonProps { 26 | token: Token; 27 | onClick: () => void; 28 | } 29 | 30 | const TokenButton = (props: TokenButtonProps) => { 31 | const { token, onClick } = props; 32 | const { name, symbol, image } = token; 33 | 34 | return ( 35 | 47 |
48 | 49 | ); 50 | }; 51 | 52 | const UserTokens = () => { 53 | const scrollRef = useRef(null); 54 | const tokens = useUserStore((state) => state.tokens); 55 | const openTokenPreview = useRouteStore((s) => s.openTokenPreview); 56 | const [showScrollButton, setShowScrollButton] = useState(false); 57 | 58 | useEffect(() => { 59 | const isScrolling = scrollRef.current 60 | ? scrollRef.current.clientWidth < scrollRef.current.scrollWidth 61 | : false; 62 | setShowScrollButton(isScrolling); 63 | }, [tokens.length]); 64 | 65 | const onPrevPress = () => { 66 | if (scrollRef.current) { 67 | scrollRef.current.scrollLeft = scrollRef.current.scrollLeft - 300; 68 | } 69 | }; 70 | const onNextPress = () => { 71 | if (scrollRef.current) { 72 | scrollRef.current.scrollLeft = scrollRef.current.scrollLeft + 300; 73 | } 74 | }; 75 | 76 | return ( 77 |
78 |
79 |

Breaking Badass Creations

80 | {showScrollButton ? ( 81 |
82 | 85 | 88 |
89 | ) : null} 90 |
91 | {tokens.length === 0 ? ( 92 | 93 | ) : ( 94 |
95 | {tokens.map((t, i) => ( 96 | { 99 | openTokenPreview(t); 100 | }} 101 | key={i} 102 | /> 103 | ))} 104 |
105 | )} 106 |
107 | ); 108 | }; 109 | 110 | export default UserTokens; 111 | -------------------------------------------------------------------------------- /frontend/src/Pages/Dashboard/dashboard.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | justify-content: center; 6 | gap: 32px; 7 | } 8 | 9 | .sectionContainer { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: stretch; 13 | justify-content: flex-start; 14 | gap: 16px; 15 | 16 | .sectionHeader { 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: space-between; 21 | } 22 | 23 | .buttonsContainer { 24 | display: flex; 25 | flex-direction: column; 26 | align-items: stretch; 27 | justify-content: flex-start; 28 | gap: 24px; 29 | 30 | .buttonsFirstRowContainer { 31 | display: grid; 32 | grid-template-columns: repeat(2, 1fr); 33 | gap: 24px; 34 | .button { 35 | flex-direction: column; 36 | align-items: center; 37 | justify-content: center; 38 | gap: 24px; 39 | p { 40 | text-align: center; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | .tooltip { 48 | display: inline-block; 49 | margin-right: 8px; 50 | } 51 | .tooltipBubble { 52 | transform: translate(24px, -40%); 53 | } 54 | 55 | .button { 56 | display: flex; 57 | flex-direction: row; 58 | justify-content: space-between; 59 | align-items: flex-end; 60 | font-size: 16px !important; 61 | line-height: 24px !important; 62 | padding-block: 19px; 63 | padding-inline: 24px; 64 | gap: 64px; 65 | 66 | p { 67 | display: flex; 68 | 69 | &:first-child { 70 | text-align: left; 71 | } 72 | &:last-child { 73 | text-align: right; 74 | span { 75 | align-self: flex-end; 76 | padding-bottom: 4px; 77 | } 78 | } 79 | } 80 | } 81 | 82 | .buttonLabelContainer { 83 | display: flex; 84 | align-items: stretch; 85 | justify-content: space-between; 86 | gap: 9px; 87 | 88 | .leftImage { 89 | aspect-ratio: 1; 90 | height: 100%; 91 | } 92 | 93 | .nameContainer { 94 | display: flex; 95 | flex-direction: column; 96 | align-items: flex-start; 97 | gap: 3px; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /frontend/src/Pages/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from "./Dashboard"; 2 | export default Dashboard; 3 | -------------------------------------------------------------------------------- /frontend/src/Pages/Dashboard/userToken.module.scss: -------------------------------------------------------------------------------- 1 | .emptyStateContainer { 2 | width: 100%; 3 | height: 320px; 4 | border: 2px solid var(--c-gray); 5 | } 6 | 7 | .tokenListContainer { 8 | display: flex; 9 | flex-direction: row; 10 | gap: 16px; 11 | align-items: center; 12 | justify-content: flex-start; 13 | width: 100%; 14 | overflow-x: auto; 15 | scroll-behavior: smooth; 16 | scroll-snap-type: x mandatory; 17 | 18 | button { 19 | min-width: unset; 20 | scroll-snap-align: start; 21 | } 22 | } 23 | .tokenListContainer::-webkit-scrollbar { 24 | display: none; 25 | } 26 | 27 | .buttonContainer { 28 | display: flex; 29 | flex-direction: row; 30 | align-items: center; 31 | justify-content: flex-end; 32 | gap: 8px; 33 | } 34 | .iconButton { 35 | border-radius: 50%; 36 | padding-inline: 10px !important; 37 | padding-block: 10px !important; 38 | min-width: unset; 39 | min-height: unset; 40 | } 41 | 42 | .tokenButton { 43 | padding-inline: 8px !important; 44 | padding-block: 8px !important; 45 | display: flex; 46 | flex-direction: column; 47 | align-items: center; 48 | justify-content: flex-start; 49 | gap: 6px; 50 | 51 | .tokenImageContainer { 52 | background: url("/src/Assets/Images/BG/p_grid.svg"), var(--c-light-gray); 53 | aspect-ratio: 237/131; 54 | height: 131px; 55 | max-width: 100%; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | 60 | img { 61 | aspect-ratio: 1; 62 | height: 80px; 63 | border-radius: 50%; 64 | } 65 | } 66 | 67 | .tokenDetailsContainer { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: stretch; 71 | justify-content: flex-start; 72 | gap: 8px; 73 | width: 100%; 74 | 75 | div { 76 | display: flex; 77 | flex-direction: row; 78 | align-items: center; 79 | justify-content: space-between; 80 | } 81 | } 82 | 83 | .tokenViewMoreButton { 84 | border: 2px solid rgba(128, 128, 128, 1); 85 | box-shadow: unset; 86 | border-radius: 40px; 87 | font-size: 12px; 88 | line-height: 18px; 89 | padding-block: 8px; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /frontend/src/Pages/TokenForm/index.tsx: -------------------------------------------------------------------------------- 1 | import TokenForm from "./TokenForm"; 2 | export default TokenForm; 3 | -------------------------------------------------------------------------------- /frontend/src/Pages/TokenForm/tokenForm.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | justify-content: flex-start; 6 | gap: 24px; 7 | } 8 | 9 | .bodyContainer { 10 | display: flex; 11 | justify-content: flex-start; 12 | flex-direction: row; 13 | gap: 24px; 14 | } 15 | 16 | .body { 17 | display: flex; 18 | align-items: stretch; 19 | justify-content: space-between; 20 | flex-direction: column; 21 | gap: 16px; 22 | flex: 1; 23 | } 24 | .centerBody { 25 | align-self: center; 26 | justify-content: flex-start; 27 | } 28 | 29 | .tooltip { 30 | // display: inline-block; 31 | // margin-right: 8px; 32 | content: ""; 33 | position: absolute; 34 | height: 24px; 35 | width: 24px; 36 | background: url("/src/Assets/Images/Icons/info.png"); 37 | background-size: 24px 24px; 38 | background-position: center; 39 | object-fit: cover; 40 | border-radius: 50%; 41 | right: 85px; 42 | top: 285px; 43 | } 44 | .tooltipBubble { 45 | transform: translate(24px, -90%, -40%); 46 | } 47 | 48 | .inputGroup { 49 | display: flex; 50 | flex-direction: column; 51 | align-items: stretch; 52 | justify-content: flex-start; 53 | gap: 8px; 54 | 55 | label { 56 | font-size: 16px; 57 | font-weight: bold; 58 | } 59 | 60 | .inputContainer { 61 | position: relative; 62 | input { 63 | padding: 8px !important; 64 | font-size: 16px; 65 | position: relative; 66 | width: 100%; 67 | height: unset !important; 68 | line-height: 24px; 69 | } 70 | 71 | &[data-isvalid="true"]::after { 72 | content: ""; 73 | position: absolute; 74 | height: 24px; 75 | width: 24px; 76 | background: url("/src/Assets/Images/Icons/check.png"); 77 | background-size: 24px 24px; 78 | background-position: center; 79 | object-fit: cover; 80 | border-radius: 50%; 81 | right: 8px; 82 | top: 8px; 83 | } 84 | 85 | &[data-isvalid="false"]::after { 86 | content: ""; 87 | position: absolute; 88 | height: 24px; 89 | width: 24px; 90 | background: url("/src/Assets/Images/Icons/red-close.png"); 91 | background-size: 24px 24px; 92 | background-position: center; 93 | object-fit: cover; 94 | border-radius: 50%; 95 | right: 8px; 96 | top: 8px; 97 | } 98 | } 99 | 100 | div { 101 | display: flex; 102 | flex-direction: row; 103 | justify-content: space-between; 104 | align-items: flex-start; 105 | } 106 | } 107 | 108 | .sliderInputGroup { 109 | display: flex; 110 | flex-direction: column; 111 | align-items: stretch; 112 | justify-content: flex-start; 113 | gap: 8px; 114 | 115 | label { 116 | font-size: 16px; 117 | font-weight: bold; 118 | } 119 | 120 | .sliderLabelRow { 121 | display: flex; 122 | flex-direction: row; 123 | align-items: center; 124 | justify-content: space-between; 125 | label { 126 | font-size: 12px; 127 | font-weight: normal; 128 | } 129 | } 130 | 131 | input[type="range"] { 132 | margin-bottom: 8px; 133 | } 134 | } 135 | 136 | .sceneContainer { 137 | height: 351px; 138 | width: 294px; 139 | border: 2px solid var(--c-gray); 140 | } 141 | 142 | .imgDropZone { 143 | flex: 1; 144 | display: flex; 145 | align-items: center; 146 | justify-content: center; 147 | border: 1px dashed var(--c-gray); 148 | position: relative; 149 | } 150 | .activeImageDropZone::after { 151 | content: ""; 152 | inset: 0; 153 | position: absolute; 154 | height: 100%; 155 | width: 100%; 156 | z-index: 10; 157 | background: rgb(0, 0, 0, 0.3); 158 | } 159 | 160 | .imgPickerControl { 161 | display: flex; 162 | flex-direction: row; 163 | align-items: center; 164 | justify-content: center; 165 | gap: 24px; 166 | 167 | img { 168 | height: 80px; 169 | aspect-ratio: 1; 170 | } 171 | 172 | .imgPickerLabelGroup { 173 | display: flex; 174 | flex-direction: column; 175 | align-items: stretch; 176 | justify-content: flex-start; 177 | gap: 8px; 178 | 179 | [type="file"]input { 180 | opacity: 0; 181 | height: 0; 182 | width: 0; 183 | } 184 | 185 | button { 186 | padding-block: 8px; 187 | padding-inline: 24px; 188 | font-size: 16px; 189 | width: 100%; 190 | } 191 | } 192 | } 193 | 194 | .stepContainer { 195 | display: flex; 196 | align-items: stretch; 197 | justify-content: flex-start; 198 | flex-direction: column; 199 | gap: 24px; 200 | } 201 | 202 | .stepFooterWrapper { 203 | display: flex; 204 | align-items: stretch; 205 | justify-content: flex-start; 206 | flex-direction: column; 207 | gap: 16px; 208 | } 209 | 210 | .stepFooter { 211 | display: flex; 212 | flex-direction: row; 213 | align-items: center; 214 | justify-content: space-between; 215 | gap: 8px; 216 | 217 | button { 218 | display: flex; 219 | flex-direction: row; 220 | align-items: center; 221 | justify-content: center; 222 | flex: 1; 223 | padding-block: 8px; 224 | padding-inline: 24px; 225 | font-size: 16px; 226 | 227 | &[data-secondary="true"] { 228 | flex: 0.5; 229 | } 230 | } 231 | } 232 | 233 | .stepRadioFooter { 234 | display: flex; 235 | flex-direction: row; 236 | align-items: center; 237 | justify-content: center; 238 | gap: 8px; 239 | 240 | div { 241 | display: flex; 242 | flex-direction: row; 243 | align-items: center; 244 | justify-content: center; 245 | flex: 1; 246 | padding-block: 8px; 247 | padding-inline: 24px; 248 | font-size: 16px; 249 | 250 | border: 2px solid var(--c-gray); 251 | border-radius: 100px; 252 | } 253 | } 254 | 255 | .imageContainer { 256 | width: 100%; 257 | aspect-ratio: 777/191; 258 | display: flex; 259 | align-items: center; 260 | justify-content: center; 261 | background: url("/src/Assets/Images/BG/p_grid.svg"); 262 | background-size: cover; 263 | background-position: center; 264 | border: 2px solid var(--c-gray); 265 | 266 | img { 267 | aspect-ratio: 1; 268 | height: 150px; 269 | border-radius: 50%; 270 | } 271 | } 272 | 273 | .stepDisclaimer { 274 | display: flex; 275 | flex-direction: row; 276 | align-items: center; 277 | justify-content: center; 278 | padding-inline: 24px; 279 | padding-block: 8px; 280 | 281 | border: 2px solid var(--c-gray); 282 | border-radius: 100px; 283 | 284 | font-size: 12px; 285 | line-height: 18px; 286 | } 287 | 288 | .infoGroupContainer { 289 | display: flex; 290 | flex-direction: column; 291 | align-items: stretch; 292 | justify-content: flex-start; 293 | gap: 24px; 294 | } 295 | 296 | .infoRow { 297 | display: flex; 298 | flex-direction: row; 299 | align-items: center; 300 | justify-content: space-between; 301 | gap: 100px; 302 | } 303 | 304 | .infoContainer { 305 | display: flex; 306 | flex-direction: row; 307 | align-items: center; 308 | justify-content: space-between; 309 | flex: 1; 310 | 311 | p { 312 | &:last-child { 313 | font-weight: bold; 314 | font-size: 20px; 315 | } 316 | } 317 | } 318 | 319 | .header { 320 | display: flex; 321 | flex-direction: row; 322 | justify-content: space-between; 323 | align-items: center; 324 | } 325 | 326 | .loader { 327 | margin-top: calc(40px - 24px); 328 | } 329 | 330 | .traitsContainer { 331 | display: flex; 332 | align-items: center; 333 | padding: 8px; 334 | background-color: #fff; 335 | border: 1px solid #000; 336 | font-family: "MS Sans Serif", Tahoma, Geneva, sans-serif; 337 | font-size: 12px; 338 | color: #000; 339 | } 340 | -------------------------------------------------------------------------------- /frontend/src/Pages/TokenPreview/TokenPreview.tsx: -------------------------------------------------------------------------------- 1 | import useRouteStore from "Store/routeStore"; 2 | import style from "./tokenPreview.module.scss"; 3 | import QRCode from "react-qr-code"; 4 | 5 | import { Discord, Twitter, Facebook, Instagram, XLogo } from "Components/Icons"; 6 | 7 | const TokenPreview = () => { 8 | const routeParams = useRouteStore((state) => state.params); 9 | const goTo = useRouteStore((state) => state.goTo); 10 | const token = routeParams?.type === "token" ? routeParams.value : undefined; 11 | 12 | if (!token) { 13 | return null; 14 | } 15 | 16 | const { symbol, name, evaporation, supply, mintedAt, canisterId, image } = 17 | token; 18 | 19 | const goToDashboard = () => { 20 | goTo("dashboard"); 21 | }; 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |

Name:

32 |

{name}

33 |
34 |
35 |

Symbol:

36 |

{symbol}

37 |
38 |
39 | 45 |
46 |
47 |
48 |
49 |

Canister ID

50 | 51 | {canisterId} 52 | 53 |
54 |
55 |

Fee:

56 |

{evaporation}

57 |
58 |
59 |
60 |
61 |
62 |

Supply

63 |

{Intl.NumberFormat("en-US").format(supply)}

64 |
65 |
66 |

Date

67 |

{new Date(mintedAt).toDateString()}

68 |
69 |
70 |
71 |
72 | 81 |
82 |
83 |
84 |
85 | 88 | 91 | 94 | 97 |
98 |
99 | 102 |
103 |
104 |
105 | ); 106 | }; 107 | 108 | export default TokenPreview; 109 | -------------------------------------------------------------------------------- /frontend/src/Pages/TokenPreview/index.tsx: -------------------------------------------------------------------------------- 1 | import TokenPreview from "./TokenPreview"; 2 | export default TokenPreview; 3 | -------------------------------------------------------------------------------- /frontend/src/Pages/TokenPreview/tokenPreview.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | justify-content: flex-start; 6 | gap: 24px; 7 | } 8 | 9 | .body { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: stretch; 13 | justify-content: flex-start; 14 | 15 | .social { 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: space-between; 19 | align-items: center; 20 | gap: 24px; 21 | 22 | padding-inline: 24px; 23 | height: 64px; 24 | 25 | background-color: #897149; 26 | 27 | a { 28 | display: flex; 29 | flex-direction: row; 30 | align-items: center; 31 | justify-content: center; 32 | 33 | padding-inline: 16px; 34 | padding-block: 8px; 35 | gap: 10px; 36 | 37 | border: 1px solid white; 38 | border-radius: 100px; 39 | 40 | text-decoration: none; 41 | color: white; 42 | font-size: 16px; 43 | } 44 | } 45 | } 46 | 47 | .tokenCard { 48 | display: flex; 49 | flex-direction: column; 50 | align-items: stretch; 51 | justify-content: flex-start; 52 | gap: 0px; 53 | padding: 24px; 54 | 55 | background: url("/src/Assets/Images/BG/bus.png") rgba(0, 0, 0, 0.64); 56 | background-size: 100% 100%; 57 | background-blend-mode: multiply; 58 | 59 | color: white; 60 | 61 | .tokenCardMain { 62 | display: flex; 63 | flex-direction: row; 64 | align-items: center; 65 | justify-content: space-between; 66 | 67 | padding-bottom: 24px; 68 | border-bottom: 2px dashed #808080; 69 | 70 | .mainDetails { 71 | display: flex; 72 | flex-direction: column; 73 | align-items: center; 74 | justify-content: center; 75 | gap: 40px; 76 | flex: 1; 77 | padding-inline: 40px; 78 | 79 | .pill { 80 | display: flex; 81 | flex-direction: row; 82 | justify-content: space-between; 83 | align-items: center; 84 | gap: 12px; 85 | width: 100%; 86 | 87 | font-weight: bold; 88 | font-size: 20px; 89 | 90 | padding-inline: 24px; 91 | padding-block: 8px; 92 | 93 | border: 2px solid white; 94 | border-radius: 100px; 95 | } 96 | } 97 | } 98 | 99 | .tokenCardDetails { 100 | display: flex; 101 | flex-direction: row; 102 | align-items: center; 103 | justify-content: space-between; 104 | gap: 50px; 105 | padding-top: 24px; 106 | 107 | .verticalHr { 108 | width: 0px; 109 | border-left: 2px dashed #808080; 110 | align-self: stretch; 111 | } 112 | 113 | .infoColumn { 114 | display: flex; 115 | flex-direction: column; 116 | align-items: flex-start; 117 | justify-content: center; 118 | flex: 1; 119 | 120 | gap: 24px; 121 | 122 | .tokenInfo { 123 | display: flex; 124 | flex-direction: row; 125 | align-items: center; 126 | justify-content: space-between; 127 | width: 100%; 128 | white-space: nowrap; 129 | gap: 12px; 130 | } 131 | } 132 | } 133 | } 134 | 135 | .footer { 136 | display: flex; 137 | flex-direction: column; 138 | align-items: stretch; 139 | justify-content: flex-start; 140 | gap: 24px; 141 | 142 | div { 143 | display: flex; 144 | flex-direction: row; 145 | align-items: center; 146 | justify-content: flex-start; 147 | gap: 8px; 148 | flex-wrap: wrap; 149 | 150 | button { 151 | display: flex; 152 | flex-direction: row; 153 | align-items: center; 154 | justify-content: center; 155 | gap: 8px; 156 | flex-grow: 1; 157 | padding-block: 8px; 158 | padding-inline: 24px; 159 | font-size: 16px; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /frontend/src/Pages/Welcome/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import { WelcomeScene } from "Components/StoryBoardScene"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import useStoryStore from "Store/storyStore"; 4 | import { GetTokensByPid, GetTokensByPidOutput } from "Candid/ts/backend.did"; 5 | import useUserStore, { Token } from "Store/userStore"; 6 | import useAuthStore from "Store/authStore"; 7 | import backendServiceInstance from "Services/backendServices"; 8 | import bitfinityServiceInstance from "Services/bitfinityServices"; 9 | import plugServiceInstance from "Services/plugServices"; 10 | import useAppStore from "Store/appStore"; 11 | import useRouteStore from "Store/routeStore"; 12 | import { whitelist } from "Utils/constants"; 13 | import styles from "./welcome.module.scss"; 14 | import ConnectWalletDialog from "Pages/WelcomeInfo/ConnectWalletDialog"; 15 | 16 | const Welcome = () => { 17 | const goTo = useRouteStore((state) => state.goTo); 18 | const setStage = useStoryStore((state) => state.setStage); 19 | const setPrincipalId = useAuthStore((state) => state.setPrincipalId); 20 | const setIsLoading = useAppStore((s) => s.setIsLoading); 21 | 22 | const idleTimeoutRef = useRef(); 23 | 24 | const [showWalletDialog, setShowWalletDialog] = useState(false); 25 | const setStory = useStoryStore((state) => state.setStage); 26 | const walletPrincipalId = useAuthStore((state) => state.principalId); 27 | 28 | const noWalletIdleTimeOutRef = useRef(); 29 | 30 | // const handlePress = () => { 31 | // clearTimeout(idleTimeoutRef.current); 32 | // goTo("welcome-info"); 33 | // }; 34 | 35 | const handleConnectWalletPress = () => { 36 | setShowWalletDialog(true); 37 | setStory("connect_wallet"); 38 | }; 39 | 40 | useEffect(() => { 41 | noWalletIdleTimeOutRef.current = setTimeout(() => { 42 | if (walletPrincipalId === "") { 43 | setStory("no_wallet_connected"); 44 | } 45 | }, 30000); 46 | 47 | return () => { 48 | clearTimeout(noWalletIdleTimeOutRef.current); 49 | }; 50 | }, [walletPrincipalId, setStory]); 51 | 52 | const fetchUserTokens = async () => { 53 | const tokens: GetTokensByPidOutput | undefined = 54 | await backendServiceInstance.getUserTokens(); 55 | console.log("fetching - tokens", tokens); 56 | 57 | if (tokens !== undefined) { 58 | if ( 59 | typeof (tokens as { err: Array }).err !== "undefined" && 60 | (tokens as { err: Array }).err.length > 0 61 | ) { 62 | throw new Error("get tokens response error"); 63 | } 64 | 65 | if (typeof (tokens as { ok: Array }).ok !== "undefined") { 66 | const setTokens = useUserStore.getState().setTokens; 67 | const userTokens: Token[] = ( 68 | tokens as { ok: Array } 69 | ).ok.map((token) => ({ 70 | name: token.name, 71 | symbol: token.symbol, 72 | coreTraits: token.core_traits.split(","), 73 | supply: Number(token.initial_supply), 74 | evaporation: Number(token.bits_evaporated), 75 | transaction: Number(token.initial_fee), 76 | image: token.logo_url, 77 | mintedAt: token.token_created_at.toString(), 78 | canisterId: token.canister_id, 79 | })); 80 | console.log("setting userTokens", userTokens); 81 | setTokens(userTokens); 82 | } else { 83 | throw "Unable to fetch user tokens"; 84 | } 85 | } 86 | }; 87 | 88 | useEffect(() => { 89 | console.log("autoLogin - wallet useeffect"); 90 | const autoLogin = async () => { 91 | var wallet = localStorage.getItem("_loginType"); 92 | console.log("autoLogin - wallet", wallet); 93 | if (wallet) { 94 | try { 95 | switch (wallet) { 96 | case "plug": 97 | const connected = await plugServiceInstance.isConnected(); 98 | if (connected) { 99 | if (!window.ic.plug.agent) { 100 | await window.ic.plug.createAgent({ whitelist }); 101 | } 102 | 103 | const principal = await window.ic.plug.agent.getPrincipal(); 104 | if (principal) { 105 | setIsLoading(true); 106 | setPrincipalId(principal.toString()); 107 | await plugServiceInstance.initIcpLedgerActor(); 108 | await backendServiceInstance.initThroughPlug(); 109 | await fetchUserTokens(); 110 | setIsLoading(false); 111 | goTo("dashboard"); 112 | } 113 | } 114 | break; 115 | case "bitfinity": 116 | const isConnected = await bitfinityServiceInstance.isConnected(); 117 | if (isConnected) { 118 | const principal = await window.ic.infinityWallet.getPrincipal(); 119 | if (principal) { 120 | setIsLoading(true); 121 | setPrincipalId(principal.toString()); 122 | await bitfinityServiceInstance.initIcpLedgerActor(); 123 | await backendServiceInstance.initThroughBitfinity(); 124 | await fetchUserTokens(); 125 | setIsLoading(false); 126 | goTo("dashboard"); 127 | } 128 | } 129 | break; 130 | default: 131 | break; 132 | } 133 | } catch (e) { 134 | setIsLoading(false); 135 | console.log(e); 136 | throw e; 137 | } 138 | } else { 139 | } 140 | }; 141 | autoLogin(); 142 | }, []); 143 | 144 | useEffect(() => { 145 | idleTimeoutRef.current = setTimeout(() => { 146 | setStage("welcome_idle"); 147 | }, 30000); 148 | 149 | return () => { 150 | clearTimeout(idleTimeoutRef.current); 151 | }; 152 | }, [setStage]); 153 | 154 | return ( 155 | <> 156 | {showWalletDialog ? ( 157 | { 159 | setShowWalletDialog(false); 160 | setStory("welcome_info"); 161 | }} 162 | /> 163 | ) : null} 164 |
165 |
166 |
167 | 168 |
169 |
170 |

BitWizard Toko Blaster

171 |

172 | Yo, What up! Jesse here to guide you through the BitWizard Toko 173 | Blaster. Together, we'll give birth to some epic ideas and bring 174 | them to life. Follow my steps, and we'll have your very own token 175 | ready to hit the streets. Let's get started and break some bits, 176 | yo! 177 |

178 |
179 |
180 |
181 |
182 | 185 |
186 |
187 | 188 | ); 189 | }; 190 | 191 | export default Welcome; 192 | -------------------------------------------------------------------------------- /frontend/src/Pages/Welcome/index.tsx: -------------------------------------------------------------------------------- 1 | import Welcome from "./Welcome"; 2 | export default Welcome; 3 | -------------------------------------------------------------------------------- /frontend/src/Pages/Welcome/welcome.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | justify-content: center; 6 | gap: 24px; 7 | } 8 | 9 | .welcomeContainer { 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | justify-content: flex-start; 14 | flex-wrap: wrap; 15 | gap: 24px; 16 | 17 | .scene { 18 | aspect-ratio: 294/314; 19 | min-width: 200px; 20 | width: min(50%, 300px); 21 | border: 2px solid var(--c-gray); 22 | } 23 | 24 | .description { 25 | display: flex; 26 | flex-direction: column; 27 | align-items: stretch; 28 | justify-content: center; 29 | gap: 24px; 30 | flex: 1; 31 | } 32 | } 33 | 34 | .welcomeFooter { 35 | button { 36 | width: 100%; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/Pages/WelcomeInfo/ConnectWalletDialog.tsx: -------------------------------------------------------------------------------- 1 | import bitfinity from "Assets/Images/Icons/bit-finity.png"; 2 | import plug from "Assets/Images/Icons/plug.svg"; 3 | import { GetTokensByPid, GetTokensByPidOutput } from "Candid/ts/backend.did"; 4 | import { WebMoney } from "Components/Icons"; 5 | import backendServiceInstance from "Services/backendServices"; 6 | import bitfinityServiceInstance from "Services/bitfinityServices"; 7 | import plugServiceInstance from "Services/plugServices"; 8 | import useAppStore from "Store/appStore"; 9 | import useAuthStore from "Store/authStore"; 10 | import useRouteStore from "Store/routeStore"; 11 | import useUserStore, { Token } from "Store/userStore"; 12 | import styles from "./connectWallet.module.scss"; 13 | 14 | interface ConnectWalletDialogProps { 15 | onClose: () => void; 16 | } 17 | 18 | const ConnectWalletDialog = (props: ConnectWalletDialogProps) => { 19 | const { onClose } = props; 20 | const goTo = useRouteStore((state) => state.goTo); 21 | const setWallet = useAuthStore((state) => state.setWallet); 22 | const setPrincipalId = useAuthStore((state) => state.setPrincipalId); 23 | const setIsLoading = useAppStore((s) => s.setIsLoading); 24 | 25 | const fetchUserTokens = async () => { 26 | const tokens: GetTokensByPidOutput | undefined = 27 | await backendServiceInstance.getUserTokens(); 28 | console.log("fetching - tokens", tokens); 29 | 30 | if (tokens !== undefined) { 31 | if ( 32 | typeof (tokens as { err: Array }).err !== "undefined" && 33 | (tokens as { err: Array }).err.length > 0 34 | ) { 35 | throw new Error("get tokens response error"); 36 | } 37 | 38 | if (typeof (tokens as { ok: Array }).ok !== "undefined") { 39 | const setTokens = useUserStore.getState().setTokens; 40 | const userTokens: Token[] = ( 41 | tokens as { ok: Array } 42 | ).ok.map((token) => ({ 43 | name: token.name, 44 | symbol: token.symbol, 45 | coreTraits: token.core_traits.split(","), 46 | supply: Number(token.initial_supply), 47 | evaporation: Number(token.bits_evaporated), 48 | transaction: Number(token.initial_fee), 49 | image: token.logo_url, 50 | mintedAt: token.token_created_at.toString(), 51 | canisterId: token.canister_id, 52 | })); 53 | console.log("setting userTokens", userTokens); 54 | setTokens(userTokens); 55 | } else { 56 | throw "Unable to fetch user tokens"; 57 | } 58 | } 59 | }; 60 | 61 | const handlePlugPress = async () => { 62 | try { 63 | setWallet("plug"); 64 | // const isConnected = await plugServiceInstance.isConnected(); 65 | // console.log("isConnected", isConnected); 66 | const login = await plugServiceInstance.login(); 67 | console.log( 68 | "login - principalId", 69 | login, 70 | plugServiceInstance.principalId 71 | ); 72 | 73 | if (login) { 74 | setIsLoading(true); 75 | await backendServiceInstance.initThroughPlug(); 76 | if (plugServiceInstance.principalId) { 77 | console.log( 78 | "handlePlugPress - plugServiceInstance.principalId", 79 | plugServiceInstance.principalId 80 | ); 81 | setPrincipalId(plugServiceInstance.principalId.toString()); 82 | await fetchUserTokens(); 83 | 84 | setIsLoading(false); 85 | localStorage.setItem("_loginType", "plug"); 86 | goTo("dashboard"); 87 | } 88 | } 89 | } catch (e) { 90 | setIsLoading(false); 91 | console.log(e); 92 | throw e; 93 | } 94 | 95 | // onClose(); 96 | }; 97 | const handleBitfinityPress = async () => { 98 | try { 99 | setWallet("bitfinity"); 100 | 101 | const login = await bitfinityServiceInstance.login(); 102 | console.log( 103 | "login - bitfinity - principalId", 104 | login, 105 | bitfinityServiceInstance.principalId 106 | ); 107 | 108 | if (login) { 109 | setIsLoading(true); 110 | await backendServiceInstance.initThroughBitfinity(); 111 | if (bitfinityServiceInstance.principalId) { 112 | console.log( 113 | "handleBitfinityPress - bitfinityServiceInstance.principalId", 114 | bitfinityServiceInstance.principalId 115 | ); 116 | setPrincipalId(bitfinityServiceInstance.principalId.toString()); 117 | await fetchUserTokens(); 118 | 119 | setIsLoading(false); 120 | localStorage.setItem("_loginType", "bitfinity"); 121 | goTo("dashboard"); 122 | } 123 | } 124 | } catch (e) { 125 | setIsLoading(false); 126 | console.log(e); 127 | throw e; 128 | } 129 | // onClose(); 130 | }; 131 | 132 | return ( 133 |
134 |
135 |
136 |
Connect Wallet
137 |
138 | 139 |
140 |
141 | 142 |
143 | 144 |
Connect Wallet yo!
145 |
146 |
147 | 150 | 153 |
154 |
155 |
156 | ); 157 | }; 158 | 159 | export default ConnectWalletDialog; 160 | -------------------------------------------------------------------------------- /frontend/src/Pages/WelcomeInfo/WelcomeInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | import ConnectWalletDialog from "./ConnectWalletDialog"; 4 | 5 | import styles from "./welcomeInfo.module.scss"; 6 | import { WelcomeInfoScene } from "Components/StoryBoardScene"; 7 | import useStoryStore from "Store/storyStore"; 8 | import useAuthStore from "Store/authStore"; 9 | 10 | const WelcomeInfo = () => { 11 | // const goBack = useRouteStore((state) => state.goBack); 12 | // const goTo = useRouteStore((state) => state.goTo); 13 | const [showWalletDialog, setShowWalletDialog] = useState(false); 14 | const setStory = useStoryStore((state) => state.setStage); 15 | const walletPrincipalId = useAuthStore((state) => state.principalId); 16 | 17 | const noWalletIdleTimeOutRef = useRef(); 18 | 19 | // const goToDashboard = () => { 20 | // if (walletPrincipalId === "") { 21 | // setStory("no_wallet_connected"); 22 | // } else { 23 | // goTo("dashboard"); 24 | // } 25 | // }; 26 | 27 | const handleConnectWalletPress = () => { 28 | setShowWalletDialog(true); 29 | setStory("connect_wallet"); 30 | }; 31 | 32 | useEffect(() => { 33 | noWalletIdleTimeOutRef.current = setTimeout(() => { 34 | if (walletPrincipalId === "") { 35 | setStory("no_wallet_connected"); 36 | } 37 | }, 30000); 38 | 39 | return () => { 40 | clearTimeout(noWalletIdleTimeOutRef.current); 41 | }; 42 | }, [walletPrincipalId, setStory]); 43 | 44 | return ( 45 | <> 46 | {showWalletDialog ? ( 47 | { 49 | setShowWalletDialog(false); 50 | setStory("welcome_info"); 51 | }} 52 | /> 53 | ) : null} 54 |
55 |
56 |
57 | 58 |
59 |
60 |

61 | Hey, listen up! Before we dive into the nitty-gritty, here's what 62 | you need to know about our BitWizard launcher: 63 |

64 |

Token Standard: ICRC-3

65 |
66 |
Black Holed Canister
67 |

68 | Next, the canister's gonna be black holed. No controller wallet 69 | here, just pure, uncut security. Once it's out there, it's out 70 | there for good. 71 |

72 |
73 |
74 |
Burning Bits
75 |

76 | We've set aside 15% just for burning bits. Sometimes you gotta 77 | burn things to make'em shine brighter, right? 78 |

79 |
80 |
81 |
Stable Price
82 |

83 | The price is stable, pegged to dollars but paid in ICP. We're 84 | talking $19.98, give or take in ICP, yo. 85 |

86 |
87 |
88 |
89 |
90 |
91 | {/* */} 94 | 97 | {/* */} 100 |
101 |
102 | 103 | ); 104 | }; 105 | 106 | export default WelcomeInfo; 107 | -------------------------------------------------------------------------------- /frontend/src/Pages/WelcomeInfo/connectWallet.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade { 2 | from { 3 | opacity: 0; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | .backdrop { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | inset: 0; 15 | left: 50%; 16 | top: 50%; 17 | transform: translate(-50%, -50%); 18 | background-color: rgba(0, 0, 0, 0.3); 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | 23 | animation: fade 200ms ease-in-out forwards; 24 | z-index: 100; 25 | } 26 | 27 | .container { 28 | width: 320px; 29 | } 30 | 31 | .header { 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | justify-content: flex-start; 36 | gap: 8px; 37 | padding-inline: 16px; 38 | padding-block: 8px; 39 | } 40 | 41 | .body { 42 | display: flex; 43 | flex-direction: column; 44 | gap: 8px; 45 | padding-block-end: 25px; 46 | padding-block-start: 8px; 47 | padding-inline: 16px; 48 | 49 | button { 50 | display: flex !important; 51 | flex-direction: row !important; 52 | justify-content: space-between !important; 53 | align-items: center; 54 | font-size: 16px !important; 55 | line-height: 24px !important; 56 | padding-block: 8px !important; 57 | padding-inline: 7.5px !important; 58 | 59 | img { 60 | aspect-ratio: 1; 61 | height: 32px; 62 | border-radius: 50%; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/Pages/WelcomeInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import WelcomeInfo from "./WelcomeInfo"; 2 | export default WelcomeInfo; 3 | -------------------------------------------------------------------------------- /frontend/src/Pages/WelcomeInfo/welcomeInfo.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | justify-content: center; 6 | gap: 24px; 7 | position: relative; 8 | } 9 | 10 | .welcomeContainer { 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | justify-content: flex-start; 15 | flex-wrap: wrap; 16 | gap: 24px; 17 | 18 | .scene { 19 | aspect-ratio: 294/314; 20 | min-width: 200px; 21 | align-self: stretch; 22 | width: min(50%, 300px); 23 | border: 2px solid var(--c-gray); 24 | } 25 | 26 | .description { 27 | display: flex; 28 | flex-direction: column; 29 | align-items: stretch; 30 | justify-content: center; 31 | gap: 24px; 32 | flex: 1; 33 | min-width: 250px; 34 | 35 | div { 36 | display: flex; 37 | flex-direction: column; 38 | align-items: stretch; 39 | justify-content: center; 40 | gap: 8px; 41 | } 42 | } 43 | } 44 | 45 | .welcomeFooter { 46 | display: flex; 47 | flex-direction: row; 48 | flex-wrap: wrap; 49 | align-items: center; 50 | justify-content: center; 51 | gap: 8px; 52 | button { 53 | flex-grow: 1; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/Services/backendServices.ts: -------------------------------------------------------------------------------- 1 | import { idlFactory as backendFactory } from "../Candid/js/backend.did.js"; 2 | import { _SERVICE as backendActor } from "../Candid/ts/backend.did.js"; 3 | import { backendCanisterId } from "Utils/constants.js"; 4 | import { host } from "Utils/constants.js"; 5 | import { 6 | UploadFile, 7 | CreateToken, 8 | GetTokensByPidOutput, 9 | } from "../Candid/ts/backend.did.js"; 10 | 11 | class backendServices { 12 | Actor: backendActor | undefined; 13 | constructor() { 14 | this.Actor = undefined; 15 | } 16 | 17 | async initThroughPlug(): Promise { 18 | try { 19 | console.log("initThroughPlug - backendCanisterId : ", backendCanisterId); 20 | this.Actor = await window.ic?.plug?.createActor({ 21 | canisterId: backendCanisterId, 22 | interfaceFactory: backendFactory, 23 | }); 24 | console.log("initThroughPlug - this.Actor : ", this.Actor); 25 | return true; 26 | } catch (e) { 27 | console.error("init Actor through plug error", e); 28 | return false; 29 | } 30 | } 31 | 32 | async initThroughBitfinity(): Promise { 33 | try { 34 | this.Actor = await window.ic?.infinityWallet?.createActor({ 35 | canisterId: backendCanisterId, 36 | interfaceFactory: backendFactory, 37 | host, 38 | }); 39 | return true; 40 | } catch (e) { 41 | console.error("init Actor through bitfinity error", e); 42 | return false; 43 | } 44 | } 45 | 46 | // to upload image use function which in "Utils/functions.ts" bcz it will calling this function 47 | async uploadLogo(params: UploadFile): Promise { 48 | const res = await this.Actor?.uploadImage(params); 49 | return res; 50 | } 51 | 52 | async uniqueTokenName(tokenName: string): Promise { 53 | const res = await this.Actor?.is_unique_token_name(tokenName); 54 | return res; 55 | } 56 | 57 | async uniqueTokenSymbol(tokenSymbol: string): Promise { 58 | const res = await this.Actor?.is_unique_token_symbol(tokenSymbol); 59 | return res; 60 | } 61 | 62 | // this will return icp value already multiply by 10^8 63 | async usdToIcp(usd: number): Promise { 64 | const res = await this.Actor?.usd_to_icp(usd); 65 | return res; 66 | } 67 | 68 | async createToken( 69 | params: CreateToken 70 | ): Promise<{ ok: string } | { err: string[] } | undefined> { 71 | const res = await this.Actor?.create_token(params); 72 | return res; 73 | } 74 | 75 | async getTotalBitsEvaporated(): Promise { 76 | const res = await this.Actor?.get_total_bits_evaporated(); 77 | return res; 78 | } 79 | 80 | async getUserTokens(): Promise { 81 | const res = await this.Actor?.get_user_tokens(); 82 | return res; 83 | } 84 | } 85 | 86 | const backendServiceInstance = new backendServices(); 87 | export default backendServiceInstance; 88 | -------------------------------------------------------------------------------- /frontend/src/Services/bitfinityServices.ts: -------------------------------------------------------------------------------- 1 | import { whitelist, host } from "Utils/constants"; 2 | import { Principal } from "@dfinity/principal"; 3 | import { idlFactory as icpFactory } from "Candid/js/icp.did"; 4 | import { icpLedgerCanister, backendCanisterId } from "Utils/constants"; 5 | import { _SERVICE as icpActor, ApproveArgs, Result_2 } from "Candid/ts/icp.did"; 6 | 7 | class bifinityServices { 8 | principalId: Principal | undefined; 9 | icpLedger: icpActor | undefined; 10 | constructor() { 11 | this.principalId = undefined; 12 | this.icpLedger = undefined; 13 | } 14 | 15 | async isConnected(): Promise { 16 | try { 17 | return await window.ic.infinityWallet.isConnected(); 18 | } catch (error) { 19 | console.error("check bitfinity connection error : ", error); 20 | return false; 21 | } 22 | } 23 | 24 | async initIcpLedgerActor(): Promise { 25 | try { 26 | this.icpLedger = await window.ic?.infinityWallet?.createActor({ 27 | canisterId: icpLedgerCanister, 28 | interfaceFactory: icpFactory, 29 | }); 30 | return true; 31 | } catch (e) { 32 | console.error("init ICP ledger Actor through bitfinity error", e); 33 | return false; 34 | } 35 | } 36 | 37 | async login(): Promise { 38 | try { 39 | await window.ic.infinityWallet.requestConnect({ whitelist }); 40 | this.principalId = await window.ic.infinityWallet.getPrincipal(); 41 | this.icpLedger = await window.ic?.infinityWallet?.createActor({ 42 | canisterId: icpLedgerCanister, 43 | interfaceFactory: icpFactory, 44 | host, 45 | }); 46 | return true; 47 | } catch (error) { 48 | console.error("bitfinity connect error : ", error); 49 | return false; 50 | } 51 | } 52 | 53 | async getICPFee(): Promise { 54 | console.log("canisterId - getICPFee", icpLedgerCanister); 55 | if (typeof icpLedgerCanister === "undefined") { 56 | throw "Some error occured. Please try again."; 57 | } 58 | console.log("this.ledgerActor - getICPFee", this.icpLedger); 59 | const transferFee = await this.icpLedger?.transfer_fee({}); 60 | if (typeof transferFee === "undefined") { 61 | throw "Some error occured. Please try again."; 62 | } 63 | console.log("transfer_fee method - ICP", transferFee.transfer_fee.e8s); 64 | return transferFee.transfer_fee.e8s; 65 | } 66 | 67 | async icp_approve(amount: bigint): Promise { 68 | try { 69 | const fee = await this.getICPFee(); 70 | console.log("fee - icp_approve", fee); 71 | let ApproveArgs: ApproveArgs = { 72 | fee: [BigInt(fee)], 73 | memo: [], 74 | from_subaccount: [], 75 | created_at_time: [BigInt(Date.now() * 1e6)], // in nano second 76 | amount: BigInt(amount), // Amount 77 | expected_allowance: [], 78 | expires_at: [BigInt(Date.now() * 1e6 + 10 * 60 * 1e9)], // expires after (10 min) 79 | spender: { 80 | owner: Principal.fromText(backendCanisterId), 81 | subaccount: [], 82 | }, // Spender 83 | }; 84 | 85 | const res: Result_2 | undefined = await this.icpLedger?.icrc2_approve( 86 | ApproveArgs 87 | ); 88 | if (res !== undefined && "Ok" in res) { 89 | return true; 90 | } else { 91 | return false; 92 | } 93 | } catch (error) { 94 | console.error("bitfinity approve ICP error : ", error); 95 | return false; 96 | } 97 | } 98 | 99 | async logout(): Promise { 100 | try { 101 | await window.ic.infinityWallet.disconnect(); 102 | this.principalId = undefined; 103 | return true; 104 | } catch (error) { 105 | console.error("bitfinity disconnect error : ", error); 106 | return false; 107 | } 108 | } 109 | } 110 | 111 | const bitfinityServiceInstance = new bifinityServices(); 112 | export default bitfinityServiceInstance; 113 | -------------------------------------------------------------------------------- /frontend/src/Services/plugServices.ts: -------------------------------------------------------------------------------- 1 | import { whitelist, host } from "Utils/constants"; 2 | import { Principal } from "@dfinity/principal"; 3 | import { idlFactory as icpFactory } from "Candid/js/icp.did"; 4 | import { icpLedgerCanister, backendCanisterId } from "Utils/constants"; 5 | import { _SERVICE as icpActor, ApproveArgs, Result_2 } from "Candid/ts/icp.did"; 6 | 7 | declare global { 8 | interface Window { 9 | ic: any; 10 | } 11 | } 12 | 13 | class plugServices { 14 | principalId: Principal | undefined; 15 | icpLedger: icpActor | undefined; 16 | constructor() { 17 | this.principalId = undefined; 18 | this.icpLedger = undefined; 19 | } 20 | 21 | async isConnected(): Promise { 22 | try { 23 | return await window.ic.plug.isConnected(); 24 | } catch (error) { 25 | console.error("check plug connection error : ", error); 26 | return false; 27 | } 28 | } 29 | 30 | async initIcpLedgerActor(): Promise { 31 | try { 32 | this.icpLedger = await window.ic?.plug?.createActor({ 33 | canisterId: icpLedgerCanister, 34 | interfaceFactory: icpFactory, 35 | }); 36 | return true; 37 | } catch (e) { 38 | console.error("init ICP ledger Actor through plug error", e); 39 | return false; 40 | } 41 | } 42 | 43 | async login(): Promise { 44 | try { 45 | console.log( 46 | "plug login - whitelist - host - icpLedgerCanister : ", 47 | whitelist, 48 | host, 49 | icpLedgerCanister 50 | ); 51 | await window.ic?.plug?.requestConnect({ whitelist, host }); 52 | this.principalId = window.ic.plug.principalId; 53 | this.icpLedger = await window.ic?.plug?.createActor({ 54 | canisterId: icpLedgerCanister, 55 | interfaceFactory: icpFactory, 56 | }); 57 | return true; 58 | } catch (error) { 59 | console.error("plug connect error : ", error); 60 | return false; 61 | } 62 | } 63 | 64 | async getICPFee(): Promise { 65 | console.log("canisterId - getICPFee", icpLedgerCanister); 66 | if (typeof icpLedgerCanister === "undefined") { 67 | throw "Some error occured. Please try again."; 68 | } 69 | console.log("this.ledgerActor - getICPFee", this.icpLedger); 70 | const transferFee = await this.icpLedger?.transfer_fee({}); 71 | if (typeof transferFee === "undefined") { 72 | throw "Some error occured. Please try again."; 73 | } 74 | console.log("transfer_fee method - ICP", transferFee.transfer_fee.e8s); 75 | return transferFee.transfer_fee.e8s; 76 | } 77 | 78 | async icp_approve(amount: bigint): Promise { 79 | try { 80 | const fee = await this.getICPFee(); 81 | console.log("fee - icp_approve", fee); 82 | let ApproveArgs: ApproveArgs = { 83 | fee: [BigInt(fee)], 84 | memo: [], 85 | from_subaccount: [], 86 | created_at_time: [BigInt(Date.now() * 1e6)], // in nano second 87 | amount: BigInt(amount), // Amount 88 | expected_allowance: [], 89 | expires_at: [BigInt(Date.now() * 1e6 + 10 * 60 * 1e9)], // expires after (10 min) 90 | spender: { 91 | owner: Principal.fromText(backendCanisterId), 92 | subaccount: [], 93 | }, // Spender 94 | }; 95 | 96 | const res: Result_2 | undefined = await this.icpLedger?.icrc2_approve( 97 | ApproveArgs 98 | ); 99 | if (res !== undefined && "Ok" in res) { 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | } catch (error) { 105 | console.error("plug approve ICP error : ", error); 106 | return false; 107 | } 108 | } 109 | 110 | async logout(): Promise { 111 | try { 112 | await window.ic?.plug?.disconnect(); 113 | this.principalId = undefined; 114 | return true; 115 | } catch (error) { 116 | console.error("plug disconnect error : ", error); 117 | return false; 118 | } 119 | } 120 | } 121 | 122 | const plugServiceInstance = new plugServices(); 123 | export default plugServiceInstance; 124 | -------------------------------------------------------------------------------- /frontend/src/Store/appStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface AppStore { 4 | isLoading: boolean; 5 | setIsLoading: (isLoading: boolean) => void; 6 | } 7 | 8 | const useAppStore = create()((set) => ({ 9 | isLoading: false, 10 | setIsLoading: (isLoading) => { 11 | set({ isLoading }); 12 | }, 13 | })); 14 | 15 | export default useAppStore; 16 | -------------------------------------------------------------------------------- /frontend/src/Store/authStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | // Define the interface for the AuthStore state 4 | interface AuthStore { 5 | wallet: string; // Could be "plug" or "bitfinity" 6 | principalId: string; 7 | setWallet: (wallet: string) => void; 8 | setPrincipalId: (principalId: string) => void; 9 | } 10 | 11 | // Create the Zustand store 12 | const useAuthStore = create((set) => ({ 13 | wallet: "", 14 | principalId: "", 15 | setWallet: (wallet) => set({ wallet }), 16 | setPrincipalId: (principalId) => set({ principalId }), 17 | })); 18 | 19 | export default useAuthStore; 20 | -------------------------------------------------------------------------------- /frontend/src/Store/createTokenStore.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from "@dfinity/principal"; 2 | import { create } from "zustand"; 3 | import { Token } from "./userStore"; 4 | 5 | import { CreateToken } from "Candid/ts/backend.did"; 6 | import backendServiceInstance from "Services/backendServices"; 7 | import bitfinityServiceInstance from "Services/bitfinityServices"; 8 | import plugServiceInstance from "Services/plugServices"; 9 | import { upload_img } from "Utils/functions"; 10 | import useAuthStore from "./authStore"; 11 | import useRouteStore from "./routeStore"; 12 | import useStoryStore from "./storyStore"; 13 | import useUserStore from "./userStore"; 14 | 15 | interface CreateTokenStore { 16 | currentStep: number; 17 | totalStep: number; 18 | name: string; 19 | symbol: string; 20 | coreTraits: string[]; 21 | file: File | null; 22 | logoFilePath: string; 23 | supply: number; 24 | fee: number; 25 | isConfirmationVisible: boolean; 26 | isCooking: boolean; 27 | errorMessage: string; 28 | radioOption: string; 29 | goToNextStep: () => void; 30 | goBack: () => void; 31 | setField: ( 32 | key: 33 | | "name" 34 | | "symbol" 35 | | "coreTraits" 36 | | "logoFilePath" 37 | | "supply" 38 | | "fee" 39 | | "file" 40 | | "isCooking" 41 | | "errorMessage" 42 | | "radioOption", 43 | value: string | number | File | boolean | string[] 44 | ) => void; 45 | showConfirmation: () => void; 46 | cookToken: (token: Token, radioOption: string, file: File) => Promise; 47 | resetForm: () => void; 48 | } 49 | 50 | const useCreateTokenStore = create()((set, get) => ({ 51 | currentStep: 1, 52 | totalStep: 3, 53 | name: "", 54 | symbol: "", 55 | coreTraits: [], 56 | file: null, 57 | logoFilePath: "", 58 | supply: 0, 59 | fee: 0, 60 | errorMessage: "", 61 | isConfirmationVisible: false, 62 | isCooking: false, 63 | radioOption: "", 64 | goToNextStep: () => { 65 | set((s) => ({ currentStep: Math.min(3, s.currentStep + 1) })); 66 | }, 67 | goBack: () => { 68 | if (get().isConfirmationVisible) { 69 | set({ isConfirmationVisible: false, currentStep: 3 }); 70 | } else { 71 | set((s) => ({ currentStep: Math.max(s.currentStep - 1, 0) })); 72 | } 73 | }, 74 | setField: (key, value) => { 75 | set({ [key]: value }); 76 | if (key === "logoFilePath") { 77 | useStoryStore.getState().setImageDataUrl(value as string); 78 | } 79 | }, 80 | showConfirmation: () => { 81 | set({ isConfirmationVisible: true }); 82 | }, 83 | resetForm: () => { 84 | set({ 85 | currentStep: 1, 86 | name: "", 87 | symbol: "", 88 | coreTraits: [], 89 | supply: 0, 90 | fee: 0, 91 | logoFilePath: "", 92 | isConfirmationVisible: false, 93 | isCooking: false, 94 | radioOption: "", 95 | }); 96 | }, 97 | cookToken: async (token: Token, radioOption: string, file: File) => { 98 | useStoryStore.getState().setStage("token_cooking"); 99 | await new Promise((resolve) => setTimeout(resolve, 10)); 100 | set({ isCooking: true, errorMessage: "" }); 101 | const { symbol, evaporation: fee, name, coreTraits } = token; 102 | 103 | const principalId = useAuthStore.getState().principalId; 104 | 105 | const usd = await backendServiceInstance.usdToIcp(19.98); 106 | console.log("onConfirm - usd", usd); 107 | if (usd === undefined) { 108 | throw new Error("USD to ICP conversion failed"); 109 | } 110 | 111 | var wallet = localStorage.getItem("_loginType"); 112 | let approvalResp; 113 | if (wallet === "plug") { 114 | approvalResp = await plugServiceInstance.icp_approve(usd); 115 | console.log("onConfirm - approvalResp - plug", approvalResp); 116 | } else { 117 | approvalResp = await bitfinityServiceInstance.icp_approve(usd); 118 | console.log("onConfirm - approvalResp - bitfinity", approvalResp); 119 | } 120 | 121 | if (approvalResp === false) { 122 | throw new Error("ICP Approval failed"); 123 | } 124 | 125 | const logoUrl = await upload_img(file); 126 | console.log("onConfirm - logoUrl", logoUrl); 127 | 128 | if (logoUrl === undefined) { 129 | throw new Error("Unable to fetch logo URL"); 130 | } 131 | 132 | const createTokenPayload: CreateToken = { 133 | token_symbol: symbol, 134 | transfer_fee: BigInt(fee * 1e8), 135 | metadata: [["icrc1:logo", { Text: logoUrl }]], 136 | is_blackholed: radioOption === "black_holed" ? true : false, 137 | initial_balances: [ 138 | [{ owner: Principal.fromText(principalId), subaccount: [] }, BigInt(0)], 139 | ], 140 | icp_amount: BigInt(usd), 141 | token_name: name, 142 | token_core_traits: coreTraits.join(","), 143 | }; 144 | 145 | console.log("onConfirm - createTokenPayload", createTokenPayload); 146 | 147 | const createTokenResp = await backendServiceInstance.createToken( 148 | createTokenPayload 149 | ); 150 | console.log("onConfirm - Token created response - ", createTokenResp); 151 | 152 | if ( 153 | typeof (createTokenResp as { err: Array }).err !== "undefined" && 154 | (createTokenResp as { err: Array }).err.length > 0 155 | ) { 156 | throw new Error("Token creation failed"); 157 | } 158 | 159 | useUserStore.getState().addToken(token); 160 | get().resetForm(); 161 | useRouteStore.getState().openTokenPreview(token); 162 | useStoryStore.getState().setStage("initial"); 163 | }, 164 | })); 165 | 166 | export default useCreateTokenStore; 167 | -------------------------------------------------------------------------------- /frontend/src/Store/routeStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import useStoryStore, { Stage } from "./storyStore"; 3 | import { Token } from "./userStore"; 4 | import useCreateTokenStore from "./createTokenStore"; 5 | 6 | export type RouteName = 7 | | "welcome" 8 | | "welcome-info" 9 | | "dashboard" 10 | | "token-form" 11 | | "token-preview"; 12 | 13 | interface RouteParams { 14 | type: "token"; 15 | value: Token; 16 | } 17 | 18 | interface RouteStore { 19 | currentRoute: RouteName; 20 | params?: RouteParams; 21 | goBack: () => void; 22 | goTo: (route: RouteName) => void; 23 | openTokenPreview: (token: Token) => void; 24 | } 25 | 26 | const BackRouteMap: Record = { 27 | welcome: "welcome", 28 | "welcome-info": "welcome", 29 | dashboard: "welcome-info", 30 | "token-form": "dashboard", 31 | "token-preview": "dashboard", 32 | }; 33 | 34 | // TODO fix this 35 | const RouteStoryMap: Record = { 36 | welcome: "welcome", 37 | dashboard: "welcome_info", 38 | "welcome-info": "welcome_info", 39 | "token-form": "token_form_step_1", 40 | "token-preview": "welcome_info", 41 | }; 42 | 43 | const useRouteStore = create()((set, get) => ({ 44 | currentRoute: "welcome", 45 | params: undefined, 46 | goBack: () => { 47 | const destinationRoute = BackRouteMap[get().currentRoute]; 48 | set({ currentRoute: destinationRoute, params: undefined }); 49 | useStoryStore.getState().setStage(RouteStoryMap[destinationRoute]); 50 | }, 51 | goTo: (route: RouteName) => { 52 | set({ currentRoute: route, params: undefined }); 53 | useStoryStore.getState().setStage(RouteStoryMap[route]); 54 | if (route === "dashboard") { 55 | useCreateTokenStore.getState().resetForm(); 56 | } 57 | }, 58 | openTokenPreview: (token: Token) => { 59 | set({ 60 | currentRoute: "token-preview", 61 | params: { type: "token", value: token }, 62 | }); 63 | useStoryStore.getState().setStage(RouteStoryMap["token-preview"]); 64 | }, 65 | })); 66 | 67 | export default useRouteStore; 68 | -------------------------------------------------------------------------------- /frontend/src/Store/userStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | export interface Token { 4 | name: string; 5 | symbol: string; 6 | coreTraits: string[]; 7 | supply: number; 8 | evaporation: number; 9 | transaction: number; 10 | image: string; 11 | mintedAt: string; // Date.toISOString 12 | canisterId: string; 13 | } 14 | interface UserStore { 15 | tokens: Token[]; 16 | setTokens: (tokens: Token[]) => void; 17 | addToken: (token: Token) => void; 18 | } 19 | 20 | const useUserStore = create()((set) => ({ 21 | tokens: [], 22 | setTokens: (tokens) => { 23 | set({ tokens }); 24 | }, 25 | addToken: (token) => { 26 | set((s) => { 27 | return { tokens: [token, ...s.tokens] }; 28 | }); 29 | }, 30 | })); 31 | 32 | export default useUserStore; 33 | -------------------------------------------------------------------------------- /frontend/src/Utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL = "local"; 2 | 3 | console.log(import.meta.env.VITE_DFX_NETWORK); 4 | export const backendCanisterId = 5 | import.meta.env.VITE_DFX_NETWORK === LOCAL 6 | ? import.meta.env.VITE_LOCAL_CANISTERID 7 | : import.meta.env.VITE_SUBNET_CANISTERID; 8 | console.log("backendCanisterId : ", backendCanisterId); 9 | 10 | export const whitelist = [backendCanisterId]; 11 | 12 | export const host = 13 | import.meta.env.VITE_DFX_NETWORK === LOCAL 14 | ? "http://localhost:4943" 15 | : "https://ic0.app"; 16 | 17 | export const icpLedgerCanister = import.meta.env.VITE_ICPLEDGERCANISTER; 18 | -------------------------------------------------------------------------------- /frontend/src/Utils/functions.ts: -------------------------------------------------------------------------------- 1 | import backendServiceInstance from "Services/backendServices"; 2 | 3 | const fileToUint8Array = async (file: File) => { 4 | const reader = new FileReader(); 5 | return new Promise((resolve, reject) => { 6 | reader.onloadend = () => { 7 | if (reader.error) { 8 | reject(reader.error); 9 | } else { 10 | resolve(new Uint8Array(reader.result as ArrayBuffer)); 11 | } 12 | }; 13 | reader.readAsArrayBuffer(file); 14 | }); 15 | }; 16 | 17 | export const upload_img = async (file: File) => { 18 | let uint8Array: Uint8Array = await fileToUint8Array(file); 19 | console.log("upload_img - uint8Array", uint8Array); 20 | let img_url = await backendServiceInstance.uploadLogo({ 21 | fileBlob: uint8Array, 22 | fileName: file.name, 23 | fileType: file.type, 24 | }); 25 | return img_url; 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/app.module.scss: -------------------------------------------------------------------------------- 1 | $gray: rgba(223, 223, 223, 1); 2 | $dark-gray: rgba(128, 128, 128, 1); 3 | $black: black; 4 | $white: white; 5 | 6 | html, 7 | body { 8 | height: 100%; 9 | width: 100%; 10 | font-size: 16px !important; 11 | line-height: 24px !important; 12 | } 13 | 14 | .bodyContainer { 15 | background-color: $gray; 16 | height: 100%; 17 | width: 100%; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .mainContainer { 25 | padding-inline: min(10%, 50px) !important; 26 | padding-block: min(10%, 50px) !important; 27 | width: min(877px, 100%); 28 | font-size: 1rem !important; 29 | background: $gray !important; 30 | overflow: auto; 31 | 32 | position: relative; 33 | } 34 | 35 | .loader { 36 | position: absolute; 37 | z-index: 100; 38 | background-color: rgba(128, 128, 128, 0.5); 39 | backdrop-filter: blur(1px); 40 | inset: 0; 41 | display: flex; 42 | flex-direction: column; 43 | align-items: stretch; 44 | justify-content: flex-start; 45 | 46 | progress { 47 | block-size: 5px; 48 | inline-size: 100%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "98.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("app-root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | "allowJs": true, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force", 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | "paths": { 28 | "*": ["./src/*"] 29 | }, 30 | 31 | "types": ["vite/client"] 32 | }, 33 | "include": ["src"] 34 | } 35 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true, 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url'; 2 | import { defineConfig } from "vite"; 3 | import react from "@vitejs/plugin-react"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | import dotenv from 'dotenv'; 6 | import environment from 'vite-plugin-environment'; 7 | 8 | dotenv.config({ path: './.env' }); 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | build: { 12 | emptyOutDir: true, 13 | }, 14 | optimizeDeps: { 15 | esbuildOptions: { 16 | define: { 17 | global: "globalThis", 18 | }, 19 | }, 20 | }, 21 | server: { 22 | proxy: { 23 | "/api": { 24 | target: "http://127.0.0.1:4943", 25 | changeOrigin: true, 26 | }, 27 | }, 28 | }, 29 | plugins: [ 30 | tsconfigPaths(), 31 | react(), 32 | environment("all", { prefix: "CANISTER_" }), 33 | environment("all", { prefix: "DFX_" }), 34 | ], 35 | resolve: { 36 | alias: [ 37 | { 38 | find: "declarations", 39 | replacement: fileURLToPath( 40 | new URL("../declarations", import.meta.url) 41 | ), 42 | }, 43 | ], 44 | }, 45 | publicDir: "../public", 46 | }); 47 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breaking-bits", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | --------------------------------------------------------------------------------