├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── eslint.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ └── sui.mock.ts ├── __tests__ ├── components │ ├── EthosConnectProvider.tsx │ ├── __snapshots__ │ │ └── EthosConnectProvider.tsx.snap │ └── styled │ │ ├── SignInButton.test.tsx │ │ ├── SignInModal.test.tsx │ │ └── __snapshots__ │ │ ├── SignInButton.test.tsx.snap │ │ └── SignInModal.test.tsx.snap ├── hooks │ ├── useConnect.test.ts │ └── useWindowDimensions.test.ts ├── index.test.ts └── lib │ ├── apiCall.test.ts │ ├── getWalletContents.test.ts │ └── login.test.ts ├── babel.config.json ├── build └── index.cjs ├── commitlint.config.js ├── jest.config.cjs ├── jest.setup.js ├── jest ├── create-jest-config.cjs └── custom-matchers.ts ├── package.json ├── scripts ├── build.sh ├── lint.sh ├── resolve-files.cjs ├── rewrite-imports.cjs ├── test.sh └── watch.sh ├── src ├── components │ ├── ConnectContext.tsx │ ├── DetachedEthosConnectProvider.tsx │ ├── EthosConnectProvider.tsx │ ├── headless │ │ ├── HoverColorButton.tsx │ │ ├── WorkingButton.tsx │ │ └── index.js │ ├── keyboard.ts │ ├── styled │ │ ├── AddressWidget.tsx │ │ ├── CopyWalletAddressButton.tsx │ │ ├── Dialog.tsx │ │ ├── Email.tsx │ │ ├── EmailSent.tsx │ │ ├── FontProvider.tsx │ │ ├── Header.tsx │ │ ├── IconButton.tsx │ │ ├── InstallWallet.tsx │ │ ├── LogoutButton.tsx │ │ ├── MenuButton.tsx │ │ ├── MobileWallet.tsx │ │ ├── ModalWrapper.tsx │ │ ├── Or.tsx │ │ ├── SignInButton.tsx │ │ ├── SignInModal.tsx │ │ ├── WalletExplorerButton.tsx │ │ ├── Wallets.tsx │ │ ├── index.js │ │ └── signInModalStyles.ts │ └── svg │ │ ├── CheckMark.tsx │ │ ├── Email.tsx │ │ ├── Ethos.tsx │ │ ├── EthosEnclosed.tsx │ │ ├── EthosWalletIcon.tsx │ │ ├── FallbackLogo.tsx │ │ ├── Github.tsx │ │ ├── Google.tsx │ │ ├── InstallWalletIcon.tsx │ │ ├── Loader.tsx │ │ ├── NoticeIcon.tsx │ │ ├── Sui.tsx │ │ ├── SuiEnclosed.tsx │ │ └── WalletsIcon.tsx ├── enums │ ├── AddressWidgetButtons.ts │ ├── Breakpoints.ts │ ├── Chain.ts │ └── EthosConnectStatus.ts ├── hooks │ ├── hooks.ts │ ├── useAccount.ts │ ├── useAddress.ts │ ├── useClientAndSigner.ts │ ├── useConnect.ts │ ├── useContents.ts │ ├── useContext.ts │ ├── useEthosConnect.ts │ ├── useModal.ts │ ├── useWallet.ts │ ├── useWalletKit.ts │ └── useWindowDimensions.ts ├── index.ts ├── lib │ ├── activeUser.ts │ ├── apiCall.ts │ ├── bigNumber.ts │ ├── checkForAssetType.ts │ ├── connectSui.ts │ ├── connectWallet.ts │ ├── constants.ts │ ├── dripSui.ts │ ├── event.ts │ ├── executeTransactionBlock.ts │ ├── fetchSui.ts │ ├── generateQRCode.ts │ ├── getConfiguration.ts │ ├── getDisplay.ts │ ├── getEthosSigner.ts │ ├── getIframe.ts │ ├── getKioskNFT.ts │ ├── getMobileConnetionUrl.ts │ ├── getWalletContents.ts │ ├── hideWallet.ts │ ├── hostedInteraction.ts │ ├── initializeEthos.ts │ ├── lib.ts │ ├── listenForMobileConnection.ts │ ├── log.ts │ ├── login.ts │ ├── logout.ts │ ├── nameService.ts │ ├── postIFrameMessage.ts │ ├── preapprove.ts │ ├── showWallet.ts │ ├── signMessage.ts │ ├── signTransactionBlock.ts │ ├── transact.ts │ ├── truncateMiddle.ts │ └── useHandleElementWithIdClicked.ts ├── types.ts └── types │ ├── ClientAndSigner.ts │ ├── ConnectContextContents.ts │ ├── ConvenienceSuiObject.ts │ ├── EthosConfiguration.ts │ ├── EthosExecuteTransactionBlockInput.ts │ ├── EthosProvider.ts │ ├── EthosSignAndExecuteTransactionBlockInput.ts │ ├── EthosSignMessageInput.ts │ ├── EthosSignTransactionBlockInput.ts │ ├── ExtendedSuiObjectData.ts │ ├── InvalidPackages.ts │ ├── MenuButtonProps.ts │ ├── ModalContextContents.ts │ ├── NFT.ts │ ├── Preapproval.ts │ ├── Signer.ts │ ├── TokenTransferInformation.ts │ ├── User.ts │ ├── Wallet.ts │ ├── WalletContents.ts │ ├── WalletContextContents.ts │ └── WorkingButtonProps.ts ├── tsconfig.json ├── types └── jest.d.ts ├── ui_automation_tests ├── chromedriver └── testCaptcha.py └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true 4 | }, 5 | "extends": [ 6 | "plugin:storybook/recommended", 7 | "next", 8 | "next/core-web-vitals", 9 | "eslint:recommended" 10 | ], 11 | "globals": { 12 | "React": "readonly", 13 | "NodeJS": true, 14 | "JSX": true, 15 | "BigInt": true 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["*.stories.@(ts|tsx|js|jsx|mjs|cjs)"], 20 | "rules": { 21 | // example of overriding a rule 22 | "storybook/hierarchy-separator": "error" 23 | } 24 | } 25 | ], 26 | "rules": { 27 | "no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }], 28 | "no-console": ["warn"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "yarn" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install Modules 9 | run: yarn 10 | - name: Build App 11 | run: yarn build 12 | - name: Run Tests 13 | run: yarn test 14 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | schedule: 19 | - cron: '45 11 * * 1' 20 | 21 | jobs: 22 | eslint: 23 | name: Run eslint scanning 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | security-events: write 28 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Install Modules 34 | run: yarn 35 | 36 | - name: Run ESLint 37 | run: npx eslint . 38 | --config .eslintrc.js 39 | --ext .js,.jsx,.ts,.tsx 40 | --format @microsoft/eslint-formatter-sarif 41 | --output-file eslint-results.sarif 42 | continue-on-error: true 43 | 44 | - name: Upload analysis results to GitHub 45 | uses: github/codeql-action/upload-sarif@v2 46 | with: 47 | sarif_file: eslint-results.sarif 48 | wait-for-processing: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | dist 24 | 25 | # VS Code 26 | .vscode -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | .next 3 | dist 4 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | _Note: this file is kept from HeadlessUI to serve as an example but should be updated with our info._ 2 | 3 | # Changelog 4 | 5 | All notable changes to this project will be documented in this file. 6 | 7 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 8 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 9 | 10 | ## [Unreleased] 11 | 12 | ### Added 13 | 14 | - Add `by` prop for `Listbox`, `Combobox` and `RadioGroup` ([#1482](https://github.com/tailwindlabs/headlessui/pull/1482)) 15 | - Add `@headlessui/tailwindcss` plugin ([#1487](https://github.com/tailwindlabs/headlessui/pull/1487)) 16 | 17 | ### Fixed 18 | 19 | - Fix incorrect transitionend/transitioncancel events for the Transition component ([#1537](https://github.com/tailwindlabs/headlessui/pull/1537)) 20 | - Improve outside click of `Dialog` component ([#1546](https://github.com/tailwindlabs/headlessui/pull/1546)) 21 | 22 | ## [0.0.0] - 2022-06-01 23 | 24 | ### Fixed 25 | 26 | - Ensure `Escape` propagates correctly in `Combobox` component ([#1511](https://github.com/tailwindlabs/headlessui/pull/1511)) 27 | - Remove leftover code in Combobox component ([#1514](https://github.com/tailwindlabs/headlessui/pull/1514)) 28 | - Fix event handlers with arity > 1 ([#1515](https://github.com/tailwindlabs/headlessui/pull/1515)) 29 | - Fix transition `enter` bug ([#1519](https://github.com/tailwindlabs/headlessui/pull/1519)) 30 | - Fix render prop data in `RadioGroup` component ([#1522](https://github.com/tailwindlabs/headlessui/pull/1522)) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethos Connect 2 | 3 | ### Documentation 4 | 5 | For full documentation on Ethos Connect please visit [Ethos Connect](https://ethoswallet.xyz/dev). 6 | 7 | 8 | ### Setting up local development 9 | 10 | From the root folder of the project you are working on (not this project) 11 | 12 | ``` 13 | yarn remove ethos-connect 14 | cd node_modules/react 15 | yarn unlink 16 | yarn link 17 | cd ../react-dom 18 | yarn unlink 19 | yarn link 20 | cd ../.. 21 | ``` 22 | 23 | From the root directory of this project: 24 | 25 | ``` 26 | yarn unlink react 27 | yarn unlink react-dom 28 | yarn link 29 | yarn install 30 | yarn link react 31 | yarn link react-dom 32 | yarn build 33 | ``` 34 | 35 | From the root folder of the project you are working on (not this project) 36 | 37 | ``` 38 | yarn link ethos-connect 39 | ``` 40 | 41 | ## To reset local 42 | 43 | ### To reset your UI (consumer of the NPM package) 44 | 45 | ``` 46 | yarn unlink ethos-connect 47 | yarn unlink react 48 | yarn unlink react-dom 49 | yarn add ethos-connect react react-dom 50 | ``` 51 | 52 | ### To reset and unlink in the NPM package repo 53 | 54 | ``` 55 | yarn unlink 56 | yarn unlink react 57 | yarn unlink react-dom 58 | yarn add react react-dom 59 | ``` 60 | 61 | You can also reset all your linked packages by running (mac only): 62 | 63 | ``` 64 | rm -rf cd ~/.config/yarn/* 65 | ``` 66 | 67 | Or, for windows powershell: 68 | 69 | ``` 70 | Remove-Item C:\Users\\AppData\Local\Yarn\Data\link\* -Recurse -Force 71 | ``` 72 | 73 | # Publishing 74 | 75 | When you're ready to publish your changes, update the `package.json` file with a new version number following [Semantic Versioning guidelines](https://zellwk.com/blog/semantic-versioning/). Then run: 76 | 77 | ``` 78 | npm publish 79 | ``` 80 | 81 | This will run the `prepublishOnly` script and publish the new version to NPM. 82 | -------------------------------------------------------------------------------- /__mocks__/sui.mock.ts: -------------------------------------------------------------------------------- 1 | import { newBN, sumBN } from "../src/lib/bigNumber" 2 | 3 | const nft = { 4 | data: { 5 | type: 'PACKAGE::MODULE::NFT', 6 | content: { 7 | dataType: "moveObject", 8 | fields: { 9 | url: "IMAGE", 10 | name: "NAME" 11 | } 12 | }, 13 | objectId: 'NFT', 14 | version: 1, 15 | digest: "NFT" 16 | } 17 | } 18 | 19 | const suiCoin = { 20 | data: { 21 | type: '0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>', 22 | content: { 23 | dataType: "moveObject", 24 | fields: { 25 | balance: newBN(10000) 26 | } 27 | }, 28 | objectId: 'COIN1', 29 | version: 2, 30 | digest: "COIN1" 31 | } 32 | } 33 | 34 | const suiCoin2 = { 35 | data: { 36 | type: '0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>', 37 | content: { 38 | dataType: "moveObject", 39 | fields: { 40 | balance: newBN(5000) 41 | } 42 | }, 43 | objectId: 'COIN2', 44 | version: 6, 45 | digest: "COIN2" 46 | } 47 | } 48 | 49 | const suiCoin3 = { 50 | data: { 51 | type: '0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>', 52 | content: { 53 | dataType: "moveObject", 54 | fields: { 55 | balance: newBN(50000) 56 | } 57 | }, 58 | objectId: 'COIN3', 59 | version: 36, 60 | digest: "COIN3" 61 | } 62 | } 63 | 64 | const getOwnedObjects = jest.fn( 65 | () => Promise.resolve({ 66 | data: [suiCoin, suiCoin3, nft] 67 | }) 68 | ) 69 | 70 | const multiGetObjects = jest.fn( 71 | ({ ids }: { ids: string[] }) => { 72 | return [suiCoin, suiCoin2, suiCoin3, nft].filter( 73 | (o: any) => ids.includes(o.data.objectId) 74 | ) 75 | } 76 | ) 77 | 78 | const getAllBalances = jest.fn( 79 | () => { 80 | return [ 81 | { 82 | coinType: '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI', 83 | totalBalance: [suiCoin, suiCoin3].reduce( 84 | (acc, c) => sumBN(acc, c.data.content.fields.balance), 85 | newBN(0) 86 | ).toString(), 87 | } 88 | ] 89 | } 90 | ) 91 | 92 | export default { 93 | suiCoin, 94 | suiCoin2, 95 | suiCoin3, 96 | nft, 97 | getOwnedObjects, 98 | multiGetObjects, 99 | getAllBalances, 100 | client: { 101 | getOwnedObjects, 102 | multiGetObjects, 103 | getAllBalances 104 | } 105 | } -------------------------------------------------------------------------------- /__tests__/components/EthosConnectProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create, act } from 'react-test-renderer' 3 | 4 | import EthosConnectProvider from '../../src/components/EthosConnectProvider' 5 | import { EthosConfiguration } from '../../src/types/EthosConfiguration' 6 | import lib from '../../src/lib/lib'; 7 | import { SignerType } from '../../src/types/Signer' 8 | import { HostedSigner } from '../../src/types/Signer'; 9 | import BigNumber from 'bignumber.js'; 10 | import { DEFAULT_CHAIN, DEFAULT_NETWORK } from '../../src/lib/constants'; 11 | import type { SuiSignMessageOutput, WalletAccount } from '@mysten/wallet-standard'; 12 | import {SuiClient} from '@mysten/sui.js/client' 13 | 14 | jest.mock('@mysten/sui.js/client') 15 | const mockSuiClient = jest.mocked(SuiClient) 16 | 17 | describe('EthosConnectProvider', () => { 18 | let signer 19 | let receivedClient 20 | let receivedSigner 21 | let onWalletConnected 22 | 23 | beforeEach(() => { 24 | signer = { 25 | type: SignerType.Hosted, 26 | getAddress: () => Promise.resolve("ADDRESS"), 27 | accounts: [] as readonly WalletAccount[], 28 | currentAccount: null, 29 | getAccounts: () => Promise.resolve([]), 30 | signAndExecuteTransactionBlock: (_transactionBlock) => Promise.resolve({} as any), 31 | signTransactionBlock: (_transactionBlock) => Promise.resolve({} as any), 32 | executeTransactionBlock: (_transactionBlock) => Promise.resolve({} as any), 33 | requestPreapproval: (_preApproval) => Promise.resolve(true), 34 | signMessage: (_message) => Promise.resolve({} as SuiSignMessageOutput), 35 | disconnect: () => {}, 36 | logout: () => {}, 37 | // This client instance does not matter, the one created in useConnect 38 | // is the one that actually gets used. An eventual test refactor could 39 | // clean this test suite up. 40 | client: new SuiClient({url: 'fake-url'}) 41 | } as HostedSigner 42 | 43 | jest.spyOn(lib, 'getEthosSigner').mockImplementation(() => { 44 | return Promise.resolve(signer) 45 | }) 46 | 47 | onWalletConnected = jest.fn(({ client: c, signer: s }) => { 48 | receivedClient = c 49 | receivedSigner = s 50 | }); 51 | 52 | jest.spyOn(lib, 'getWalletContents').mockReturnValue(Promise.resolve({ 53 | suiBalance: new BigNumber(0), 54 | balances: {}, 55 | tokens: {}, 56 | nfts: [], 57 | objects: [], 58 | })) 59 | }) 60 | 61 | afterEach(() => { 62 | receivedClient = null 63 | receivedSigner = null 64 | mockSuiClient.mockClear(); 65 | }) 66 | 67 | it('renders nothing but the children provided', async () => { 68 | let ethosWrapper 69 | await act(async () => { 70 | ethosWrapper = create( 71 | 72 | test 73 | 74 | ) 75 | }) 76 | 77 | expect(ethosWrapper.toJSON()).toMatchSnapshot() 78 | }) 79 | 80 | it('calls the onWalletConnected callback', async () => { 81 | await act(async () => { 82 | create( 83 | 84 | test 85 | 86 | ) 87 | }) 88 | 89 | expect(onWalletConnected.mock.calls.length).toBe(1) 90 | // The first call is from the mock above, and is not important. 91 | expect(mockSuiClient).toHaveBeenCalledTimes(2); 92 | const mockInstanceFromSignerMock = mockSuiClient.mock.instances[0] 93 | const mockInstanceFromUseConnect = mockSuiClient.mock.instances[1] 94 | expect(receivedClient).toEqual(mockInstanceFromUseConnect) 95 | expect(receivedSigner).toBe(signer) 96 | }) 97 | 98 | it('should initialize default configuration if no optional values are given', async () => { 99 | const initialEthosConfiguration: EthosConfiguration = { apiKey: 'test-id' } 100 | const expectedEthosConfiguration: EthosConfiguration = { 101 | apiKey: 'test-id', 102 | walletAppUrl: 'https://ethoswallet.xyz', 103 | chain: DEFAULT_CHAIN, 104 | network: DEFAULT_NETWORK 105 | } 106 | 107 | const initializeSpy = jest.spyOn(lib, 'initializeEthos') 108 | 109 | await act(async () => { 110 | create( 111 | 112 | test 113 | 114 | ) 115 | }) 116 | 117 | expect(initializeSpy).toBeCalledWith(expectedEthosConfiguration) 118 | }); 119 | }) 120 | -------------------------------------------------------------------------------- /__tests__/components/styled/SignInButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { act, create } from 'react-test-renderer' 3 | import EthosConnectProvider from '../../../src/components/EthosConnectProvider' 4 | import SignInButton from '../../../src/components/styled/SignInButton' 5 | import WorkingButton from '../../../src/components/headless/WorkingButton' 6 | 7 | describe('SignInButton', () => { 8 | it('renders correctly', () => { 9 | const rendered = create( 10 | 15 | 16 | 17 | ).toJSON() 18 | expect(rendered).toMatchSnapshot() 19 | }) 20 | 21 | it('opens the sign in modal', async () => { 22 | const rendered = create( 23 | 28 | 29 | 30 | ) 31 | 32 | await act(async () => { 33 | rendered.root.findByType(WorkingButton).props.onClick(); 34 | }) 35 | 36 | expect(rendered.toJSON()).toMatchSnapshot() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /__tests__/components/styled/SignInModal.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create, act, ReactTestInstance } from 'react-test-renderer' 3 | 4 | import SignInModal from '../../../src/components/styled/SignInModal' 5 | import FallbackLogo from '../../../src/components/svg/FallbackLogo' 6 | import lib from '../../../src/lib/lib' 7 | import hooks from '../../../src/hooks/hooks' 8 | import { EthosConnectStatus } from '../../../src/enums/EthosConnectStatus'; 9 | 10 | const modalExists = (root: ReactTestInstance) => { 11 | const modal = root.findByProps({ role: 'dialog' }) 12 | return modal.props.style.visibility !== 'hidden' 13 | } 14 | 15 | const expectElementWithRoleToExist = (root: any, role: string, shouldExist: boolean): void => { 16 | if (shouldExist) { 17 | expect(root.findByProps({ role })).toBeTruthy() 18 | } else { 19 | expect(() => root.findByProps({ role })).toThrow() 20 | } 21 | } 22 | 23 | beforeEach(() => { 24 | jest.spyOn(hooks, 'useWallet').mockReturnValue({ 25 | wallets: [], 26 | status: EthosConnectStatus.Loading, 27 | client: null, 28 | selectWallet: () => {}, 29 | setAltAccount: () => {}, 30 | }) 31 | }) 32 | 33 | describe('SignInModal', () => { 34 | describe('closed', () => { 35 | beforeEach(() =>{ 36 | jest.spyOn(hooks, 'useModal').mockReturnValue({ 37 | isModalOpen: false, 38 | openModal: () => {}, 39 | closeModal: () => {} 40 | }) 41 | }) 42 | 43 | it('renders a hidden modal if isOpen is false', async () => { 44 | let signInModal; 45 | await act(async () => { 46 | signInModal = create( 47 | 48 | ) 49 | }) 50 | 51 | const { root } = signInModal; 52 | 53 | expect(modalExists(root)).toBeFalsy() 54 | 55 | expect(signInModal.toJSON()).toMatchSnapshot() 56 | }) 57 | }) 58 | 59 | describe('opened', () => { 60 | beforeEach(() =>{ 61 | jest.spyOn(hooks, 'useModal').mockReturnValue({ 62 | isModalOpen: true, 63 | openModal: () => {}, 64 | closeModal: () => {} 65 | }) 66 | }) 67 | 68 | it('renders a visible modal if isOpen is true', async () => { 69 | let signInModal; 70 | await act(async () => { 71 | signInModal = create( 72 | 73 | ) 74 | }) 75 | 76 | const root = signInModal.root 77 | expect(modalExists(root)).toBeTruthy() 78 | 79 | expect(root.findAllByType(FallbackLogo).length).toBe(0) 80 | 81 | expect(signInModal.toJSON()).toMatchSnapshot() 82 | }) 83 | 84 | it('renders email sign in if hideEmailSignIn is not true and no wallets are present', async () => { 85 | let signInModal; 86 | await act(async () => { 87 | signInModal = create( 88 | 89 | ) 90 | }) 91 | 92 | const { root } = signInModal 93 | expectElementWithRoleToExist(root, 'email-sign-in', true) 94 | expectElementWithRoleToExist(root, 'wallet-sign-in', false) 95 | }) 96 | 97 | it('does NOT render email if hideEmailSignIn is true', async () => { 98 | let signInModal; 99 | await act(async () => { 100 | signInModal = create( 101 | 102 | ) 103 | }) 104 | 105 | const root = signInModal.root 106 | expectElementWithRoleToExist(root, 'email-sign-in', false) 107 | }) 108 | 109 | it('does NOT render wallet if hideWalletSignIn is true', async () => { 110 | let signInModal; 111 | await act(async () => { 112 | signInModal = create( 113 | 114 | ) 115 | }) 116 | 117 | const root = signInModal.root 118 | expectElementWithRoleToExist(root, 'wallet-sign-in', false) 119 | }) 120 | 121 | // having trouble getting this test to build with the async 122 | it.todo("should throw if hideWalletSignIn and hideWalletSignIn are both true") 123 | // it('should throw if hideWalletSignIn and hideWalletSignIn are both true', async () => { 124 | // // Hide console error in test 125 | // console.error = jest.fn(); 126 | 127 | // expect(await act( 128 | // async () => create( 129 | // 130 | // ) 131 | // )).rejects.toThrow() 132 | // }) 133 | 134 | it('sends an email if you click the send email button and the captcha is passed', async () => { 135 | const testEmail = 'test@example.com' 136 | 137 | let emailProvided 138 | jest.spyOn(lib, 'postIFrameMessage').mockImplementation(({ action, data }) => { 139 | expect(action).toBe('login') 140 | emailProvided = data.email 141 | expect(data.apiKey).toBe('test') 142 | return Promise.resolve({}) 143 | }) 144 | 145 | let signInModal; 146 | await act(async () => { 147 | signInModal = create( 148 | null} /> 149 | ) 150 | }) 151 | 152 | const root = signInModal.root 153 | const emailInput = root.findByProps({ type: 'email' }) 154 | const emailForm = emailInput.parent 155 | // let captcha: ReactTestInstance 156 | // await waitFor(() => { 157 | // captcha = root.findByProps({ size: 'invisible' }) 158 | // }); 159 | 160 | act(() => { 161 | emailInput.props.onChange({ target: { value: testEmail } }) 162 | }) 163 | 164 | await act(async () => { 165 | // Pass captcha 166 | // captcha.props.onChange(); 167 | emailForm?.props.onSubmit() 168 | }) 169 | 170 | expect(emailProvided).toBe(testEmail) 171 | }) 172 | 173 | // it('should render captcha as invisible', async () => { 174 | // const signInModal = create( 175 | // null} /> 176 | // ) 177 | 178 | // const root = signInModal.root 179 | // let captcha: ReactTestInstance 180 | // await waitFor(() => { 181 | // captcha = root.findByProps({ size: 'invisible' }) 182 | // expect(captcha).toBeTruthy(); 183 | // }); 184 | // }); 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /__tests__/hooks/useConnect.test.ts: -------------------------------------------------------------------------------- 1 | describe('useConnect', () => { 2 | 3 | it('should work', () => { 4 | expect(true).toBe(true) 5 | }) 6 | 7 | }) 8 | -------------------------------------------------------------------------------- /__tests__/hooks/useWindowDimensions.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react-hooks"; 2 | import useWindowDimensions from "../../src/hooks/useWindowDimensions"; 3 | 4 | describe('useWindowDimensions hook', () => { 5 | it('should update width and height on window size change', () => { 6 | const { result } = renderHook(() => useWindowDimensions()) 7 | 8 | const expectedWidth = 500; 9 | const expectedHeight = 1000; 10 | global.innerWidth = expectedWidth 11 | global.innerHeight = expectedHeight 12 | 13 | act(() => { 14 | global.dispatchEvent(new Event('resize')); 15 | }) 16 | 17 | expect(result.current.width).toBe(expectedWidth) 18 | expect(result.current.height).toBe(expectedHeight) 19 | }); 20 | }); -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as ReactApi from '../src/index' 2 | import { EthosConnectProvider, DetachedEthosConnectProvider, SignInButton } from '../src/index' 3 | 4 | /** 5 | * Looks a bit of a silly test, however this ensures that we don't accidentally expose something to 6 | * the outside world that we didn't want! 7 | */ 8 | it('should expose the correct components', () => { 9 | const exportNames = [ 10 | EthosConnectProvider.name, 11 | DetachedEthosConnectProvider.name, 12 | SignInButton.name, 13 | 'ethos', 14 | 'EthosConnectStatus', 15 | 'TransactionBlock', 16 | 'Chain', 17 | ] 18 | expect(Object.keys(ReactApi)).toEqual(exportNames) 19 | }) 20 | -------------------------------------------------------------------------------- /__tests__/lib/apiCall.test.ts: -------------------------------------------------------------------------------- 1 | import apiCall from '../../src/lib/apiCall' 2 | 3 | describe('apiCall', () => { 4 | let spyFetch: jest.SpyInstance 5 | const expectedHeaders = { 6 | 'Content-Type': 'application/json', 7 | Accept: 'application/json', 8 | } 9 | const relativePath = 'test' 10 | const body = { example: true } 11 | const expectedResponseJson = { balance: '1' } 12 | const expectedStatus = 200 13 | const mockResponse = { 14 | json: async () => expectedResponseJson, 15 | status: expectedStatus, 16 | } as Response 17 | 18 | beforeEach(() => { 19 | global.fetch = jest.fn() 20 | spyFetch = jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse) 21 | }) 22 | 23 | it('should call an API with all parameters', async () => { 24 | const method = 'GET' 25 | const expectedEndpointCalled = `test/api/${relativePath}` 26 | const expectedApiCallParameters = { 27 | method: method, 28 | headers: expectedHeaders, 29 | body: JSON.stringify(body), 30 | } 31 | 32 | const result = await apiCall({ relativePath, body, method }) 33 | 34 | expect(spyFetch).toBeCalledTimes(1) 35 | expect(spyFetch).toBeCalledWith(expectedEndpointCalled, expectedApiCallParameters) 36 | expect(result.json).toEqual(expectedResponseJson) 37 | expect(result.status).toBe(expectedStatus) 38 | }) 39 | 40 | it('should call an API with all REQUIRED parameters', async () => { 41 | const expectedEndpointCalled = 'test/api/' + relativePath 42 | const expectedApiCallParameters = { 43 | method: 'GET', 44 | headers: expectedHeaders, 45 | body: undefined, 46 | } 47 | 48 | const result = await apiCall({ relativePath }) 49 | 50 | expect(spyFetch).toBeCalledTimes(1) 51 | expect(spyFetch).toBeCalledWith(expectedEndpointCalled, expectedApiCallParameters) 52 | expect(result.json).toEqual(expectedResponseJson) 53 | expect(result.status).toBe(expectedStatus) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /__tests__/lib/getWalletContents.test.ts: -------------------------------------------------------------------------------- 1 | import sui from '../../__mocks__/sui.mock' 2 | import { newBN } from '../../src/lib/bigNumber'; 3 | import getWalletContents from '../../src/lib/getWalletContents'; 4 | import { sumBN } from '../../src/lib/bigNumber'; 5 | import { WalletContents } from '../../src/types/WalletContents'; 6 | import { CoinBalance, SuiClient } from '@mysten/sui.js/client'; 7 | 8 | jest.mock('@mysten/sui.js/client') 9 | const mockSuiClient = jest.mocked(SuiClient) 10 | 11 | describe('getWalletBalance', () => { 12 | let spyFetch: jest.SpyInstance 13 | 14 | beforeEach(() => { 15 | // this is a mock response for the invalid packages fetch, that is, 16 | // pretend there are no invalid packages in these tests 17 | global.fetch = jest.fn() 18 | spyFetch = jest.spyOn(global, 'fetch').mockResolvedValue({ json: async () => ([]) } as Response) 19 | 20 | sui.getOwnedObjects.mockClear() 21 | sui.multiGetObjects.mockClear(); 22 | 23 | // ts-ignoring because this is a partial mock, and the strongly typed mock 24 | // wants all 40+ methods 25 | //@ts-ignore 26 | mockSuiClient.mockImplementation(() => { 27 | return { 28 | getAllBalances: sui.getAllBalances, 29 | getOwnedObjects: sui.getOwnedObjects 30 | } 31 | }) 32 | }) 33 | 34 | it('should get balance for given wallet', async () => { 35 | const contents = await getWalletContents({ address: '0x123', network: "TEST" }) 36 | 37 | expect(SuiClient).toHaveBeenCalled(); 38 | expect(mockSuiClient.mock.instances.length).toBe(1) 39 | const balance = sumBN( 40 | sui.suiCoin.data.content.fields.balance, 41 | sui.suiCoin3.data.content.fields.balance 42 | ) 43 | 44 | expect(sui.getOwnedObjects).toBeCalledTimes(1) 45 | expect(contents?.suiBalance).toEqual(balance) 46 | const suiTokens = contents?.tokens['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'] 47 | expect(suiTokens?.balance).toEqual(balance) 48 | expect(suiTokens?.coins.length).toEqual(2) 49 | expect(contents?.nfts.length).toEqual(1) 50 | }) 51 | 52 | // This test is in flux while performance issues are investigated 53 | it('should not request objects that have not changed', async () => { 54 | const contents = await getWalletContents({ network: "test", address: '0x123', existingContents }) 55 | 56 | expect(sui.getOwnedObjects).toBeCalledTimes(1) 57 | expect(contents).toBeNull(); 58 | }) 59 | 60 | it('should add and remove objects as necessary', async () => { 61 | sui.getOwnedObjects.mockReturnValueOnce( 62 | Promise.resolve({ 63 | data: [sui.suiCoin, sui.suiCoin2].map((o: any) => (o)) 64 | }) 65 | ) 66 | sui.getAllBalances.mockReturnValueOnce( 67 | [{ 68 | coinType: '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI', 69 | totalBalance: [sui.suiCoin, sui.suiCoin2].reduce( 70 | (acc, c) => sumBN(acc, c.data.content.fields.balance), 71 | newBN(0) 72 | ).toString(), 73 | }] 74 | ) 75 | 76 | const contents = await getWalletContents({ network: "test", address: '0x123', existingContents }) 77 | 78 | const totalBalance = sumBN( 79 | sui.suiCoin.data.content.fields.balance, 80 | sui.suiCoin2.data.content.fields.balance 81 | ) 82 | 83 | expect(contents?.nfts.length).toBe(0) 84 | expect(contents?.suiBalance).toStrictEqual(totalBalance) 85 | expect(contents?.tokens['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'].balance).toStrictEqual(totalBalance); 86 | expect(contents?.tokens['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'].coins.length).toBe(2); 87 | expect(contents?.tokens['0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI'].coins[1].balance).toStrictEqual(sui.suiCoin2.data.content.fields.balance) 88 | }) 89 | }) 90 | 91 | const existingContents: WalletContents = { 92 | "suiBalance": newBN("60000"), 93 | "balances": { 94 | "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI": { 95 | "coinType": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", 96 | "totalBalance": "60000" 97 | } as CoinBalance 98 | }, 99 | "tokens": { 100 | "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI": { 101 | "balance": newBN("60000"), 102 | "coins": [ 103 | { 104 | "objectId": "COIN1", 105 | "type": "0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>", 106 | "balance": newBN("10000"), 107 | "digest": "COIN1", 108 | "version": 2 109 | }, 110 | { 111 | "objectId": "COIN3", 112 | "type": "0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>", 113 | "balance": newBN("50000"), 114 | "digest": "COIN3", 115 | "version": 36 116 | } 117 | ] 118 | } 119 | }, 120 | "nfts": [ 121 | { 122 | "type": "PACKAGE::MODULE::NFT", 123 | "packageObjectId": "PACKAGE", 124 | "moduleName": "MODULE", 125 | "structName": "NFT", 126 | "chain": "Sui", 127 | "address": "NFT", 128 | "objectId": "NFT", 129 | "name": "NAME", 130 | "imageUrl": "IMAGE", 131 | "fields": { 132 | "url": "IMAGE", 133 | "name": "NAME" 134 | }, 135 | "links": { 136 | "Explorer": "https://explorer.sui.io/objects/NFT" 137 | } 138 | } 139 | ], 140 | "objects": [ 141 | { 142 | "type": "0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>", 143 | "content": { 144 | "dataType": "moveObject", 145 | "type": "COIN", 146 | "fields": { 147 | "balance": "10000" 148 | }, 149 | "hasPublicTransfer": true 150 | }, 151 | "objectId": "COIN1", 152 | "version": "2", 153 | "digest": "COIN1", 154 | "packageObjectId": "0x2", 155 | "moduleName": "coin", 156 | "structName": "Coin", 157 | "imageUrl": "", 158 | "fields": { 159 | "balance": "10000" 160 | }, 161 | "isCoin": true 162 | }, 163 | { 164 | "type": "0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>", 165 | "content": { 166 | "dataType": "moveObject", 167 | "type": "COIN", 168 | "fields": { 169 | "balance": "50000" 170 | }, 171 | "hasPublicTransfer": true 172 | }, 173 | "objectId": "COIN3", 174 | "version": "36", 175 | "digest": "COIN3", 176 | "packageObjectId": "0x2", 177 | "moduleName": "coin", 178 | "structName": "Coin", 179 | "imageUrl": "", 180 | "fields": { 181 | "balance": "50000" 182 | }, 183 | "isCoin": true 184 | }, 185 | { 186 | "type": "PACKAGE::MODULE::NFT", 187 | "content": { 188 | "dataType": "moveObject", 189 | "type": "NFT", 190 | "fields": { 191 | "url": "IMAGE", 192 | "name": "NAME" 193 | }, 194 | "hasPublicTransfer": true 195 | }, 196 | "objectId": "NFT", 197 | "version": "1", 198 | "digest": "NFT", 199 | "packageObjectId": "PACKAGE", 200 | "moduleName": "MODULE", 201 | "structName": "NFT", 202 | "name": "NAME", 203 | "imageUrl": "IMAGE", 204 | "fields": { 205 | "url": "IMAGE", 206 | "name": "NAME" 207 | }, 208 | "isCoin": false 209 | } 210 | ] 211 | } -------------------------------------------------------------------------------- /__tests__/lib/login.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import store from 'store2' 6 | import { User } from '../../src/types/User' 7 | import lib from '../../src/lib/lib' 8 | import login from '../../src/lib/login' 9 | 10 | describe('login', () => { 11 | let spyPostMessage: jest.SpyInstance 12 | let actualUser: User 13 | const email = 'test@t.co' 14 | const apiKey = '123abc' 15 | const wallet = '0x0' 16 | const expectedUser: User = { email, wallet } 17 | 18 | const postIFrameMessageReturn = { 19 | json: { user: expectedUser }, 20 | status: 200, 21 | } 22 | 23 | beforeEach(async () => { 24 | jest.spyOn(window, 'addEventListener').mockImplementation((topic, listener) => { 25 | expect(topic).toBe('message'); 26 | const listenerFunction = listener as any; 27 | listenerFunction({ 28 | origin: 'test', 29 | data: { 30 | action: 'login', 31 | data: 'user' 32 | } 33 | }) 34 | }) 35 | spyPostMessage = jest.spyOn(lib, 'postIFrameMessage').mockReturnValue() 36 | 37 | actualUser = await login({ email, apiKey }) 38 | }) 39 | 40 | it('should call the /login endpoint', async () => { 41 | expect(spyPostMessage).toBeCalledTimes(1) 42 | expect(spyPostMessage).toBeCalledWith({ 43 | "action": "login", 44 | "data": { 45 | email, 46 | apiKey, 47 | returnTo: window.location.href, 48 | provider: undefined 49 | } 50 | }) 51 | }) 52 | 53 | it('should save the user to the userStore', async () => { 54 | const userStore = store.namespace('users') 55 | 56 | expect(actualUser).toEqual(userStore('current')) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "edge": "17", 8 | "firefox": "60", 9 | "chrome": "67", 10 | "safari": "11.1" 11 | }, 12 | "useBuiltIns": "usage", 13 | "corejs": "3.22.3" 14 | } 15 | ], 16 | "@babel/preset-react" 17 | ], 18 | "plugins": [ 19 | [ 20 | "inline-dotenv", 21 | { 22 | "path": ".env" 23 | } 24 | ] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /build/index.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./ethos-connect.prod.cjs') 5 | } else { 6 | module.exports = require('./ethos-connect.dev.cjs') 7 | } 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 2 | // ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 3 | // docs: Documentation only changes 4 | // feat: A new feature 5 | // fix: A bug fix 6 | // perf: A code change that improves performance 7 | // refactor: A code change that neither fixes a bug nor adds a feature 8 | // style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 9 | // test: Adding missing tests or correcting existing tests 10 | 11 | module.exports = { 12 | extends: ['@commitlint/config-conventional'], 13 | rules: { 14 | 'body-leading-blank': [1, 'always'], 15 | 'body-max-line-length': [2, 'always', 100], 16 | 'footer-leading-blank': [1, 'always'], 17 | 'footer-max-line-length': [2, 'always', 100], 18 | 'header-max-length': [2, 'always', 100], 19 | 'scope-case': [2, 'always', 'lower-case'], 20 | 'subject-case': [ 21 | 2, 22 | 'never', 23 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], 24 | ], 25 | 'subject-empty': [2, 'never'], 26 | 'subject-full-stop': [2, 'never', '.'], 27 | 'type-case': [2, 'always', 'lower-case'], 28 | 'type-empty': [2, 'never'], 29 | 'type-enum': [ 30 | 2, 31 | 'always', 32 | [ 33 | 'build', 34 | 'chore', 35 | 'ci', 36 | 'docs', 37 | 'feat', 38 | 'fix', 39 | 'perf', 40 | 'refactor', 41 | 'revert', 42 | 'style', 43 | 'test', 44 | 'translation', 45 | 'security', 46 | 'changeset', 47 | ], 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | let create = require('./jest/create-jest-config.cjs') 2 | module.exports = create(__dirname, { 3 | displayName: 'React', 4 | setupFilesAfterEnv: ['./jest.setup.js'], 5 | testEnvironment: 'jsdom', 6 | transformIgnorePatterns: [ "/node_modules/?!uuid" ] 7 | }) 8 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | globalThis.IS_REACT_ACT_ENVIRONMENT = true 2 | 3 | import { TextEncoder, TextDecoder } from 'util' 4 | globalThis.TextEncoder = TextEncoder; 5 | globalThis.TextDecoder = TextDecoder; 6 | 7 | import sui from './__mocks__/sui.mock' 8 | 9 | jest.mock('./src/lib/lib', () => { 10 | return { 11 | __esModule: true, 12 | ...jest.requireActual('./src/lib/lib') 13 | }; 14 | }); 15 | 16 | jest.mock('./src/lib/getConfiguration', () => ({ 17 | __esModule: true, 18 | default: () => ({ 19 | walletAppUrl: 'test', 20 | apiKey: 'test', 21 | network: 'test', 22 | chain: 'test', 23 | }) 24 | })); 25 | 26 | jest.mock('@mysten/sui.js/utils', () => ({ 27 | SUI_TYPE_ARG: "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" 28 | })); 29 | -------------------------------------------------------------------------------- /jest/create-jest-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function createJestConfig(root, options) { 2 | let { setupFilesAfterEnv = [], transform = {}, ...rest } = options 3 | return Object.assign( 4 | { 5 | rootDir: root, 6 | setupFilesAfterEnv: ['/jest/custom-matchers.ts', ...setupFilesAfterEnv], 7 | transform: { 8 | '^.+\\.(t|j)sx?$': '@swc/jest', 9 | ...transform, 10 | }, 11 | }, 12 | rest 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /jest/custom-matchers.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | 3 | // Assuming requestAnimationFrame is roughly 60 frames per second 4 | let frame = 1000 / 60 5 | let amountOfFrames = 2 6 | 7 | let formatter = new Intl.NumberFormat('en') 8 | 9 | expect.extend({ 10 | toBeWithinRenderFrame(actual, expected) { 11 | let min = expected - frame * amountOfFrames 12 | let max = expected + frame * amountOfFrames 13 | 14 | let pass = actual >= min && actual <= max 15 | 16 | return { 17 | message: pass 18 | ? () => { 19 | return `expected ${actual} not to be within range of a frame ${formatter.format( 20 | min 21 | )} - ${formatter.format(max)}` 22 | } 23 | : () => { 24 | return `expected ${actual} not to be within range of a frame ${formatter.format( 25 | min 26 | )} - ${formatter.format(max)}` 27 | }, 28 | pass, 29 | } 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethos-connect", 3 | "version": "0.0.194", 4 | "description": "Build on Sui with ease with the Ethos Wallet APIs. Connect with all wallets and users with no wallet.", 5 | "main": "dist/index.cjs", 6 | "typings": "dist/index.d.ts", 7 | "module": "dist/ethos-connect.esm.js", 8 | "license": "MIT", 9 | "files": [ 10 | "README.md", 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/ethos-connect.esm.js", 15 | "require": "./dist/index.cjs", 16 | "types": "./dist/index.d.ts" 17 | }, 18 | "type": "module", 19 | "sideEffects": false, 20 | "engines": { 21 | "node": ">=10" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/EthosWallet/react-api.git" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "scripts": { 31 | "prepublishOnly": "npm run build", 32 | "build": "./scripts/build.sh --external:react --external:react-dom", 33 | "watch": "./scripts/watch.sh --external:react --external:react-dom", 34 | "test": "./scripts/test.sh", 35 | "test:watch": "./scripts/test.sh --watch", 36 | "test:snapshot": "npx jest --update-snapshot", 37 | "lint": "./scripts/lint.sh", 38 | "playground": "yarn workspace playground-react dev", 39 | "clean": "rimraf ./dist" 40 | }, 41 | "peerDependencies": { 42 | "react": "^18", 43 | "react-dom": "^18" 44 | }, 45 | "devDependencies": { 46 | "@microsoft/eslint-formatter-sarif": "2.1.7", 47 | "@swc/core": "^1.2.197", 48 | "@swc/jest": "^0.2.21", 49 | "@testing-library/jest-dom": "^5.16.4", 50 | "@testing-library/react": "^13.0.0", 51 | "@testing-library/react-hooks": "^8.0.1", 52 | "@types/jest": "^28.1.0", 53 | "@types/lodash-es": "^4.17.7", 54 | "@types/qrcode": "^1.4.2", 55 | "@types/react": "^18.0.28", 56 | "@types/react-dom": "^18.0.11", 57 | "@types/react-google-recaptcha": "^2.1.5", 58 | "@types/react-test-renderer": "^18.0.0", 59 | "esbuild": "^0.11.18", 60 | "fast-glob": "^3.2.11", 61 | "jest": "^28.1.1", 62 | "jest-environment-jsdom": "^28.1.1", 63 | "prettier": "^2.6.2", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0", 66 | "react-test-renderer": "^18.2.0", 67 | "rimraf": "^3.0.2", 68 | "snapshot-diff": "^0.8.1", 69 | "typescript": "^4.7.3" 70 | }, 71 | "dependencies": { 72 | "@mysten/sui.js": "^0.42.0", 73 | "@mysten/wallet-kit-core": "^0.6.3", 74 | "@open-rpc/client-js": "^1.8.1", 75 | "@superstate/react": "^0.1.0", 76 | "bignumber.js": "^9.1.1", 77 | "cross-fetch": "^3.1.5", 78 | "encoding": "^0.1.13", 79 | "eslint": "^8.36.0", 80 | "events": "^3.3.0", 81 | "lodash-es": "^4.17.21", 82 | "qrcode": "^1.5.1", 83 | "react-google-recaptcha": "^2.1.0", 84 | "store2": "^2.14.2" 85 | }, 86 | "lint-staged": { 87 | "*": "yarn lint" 88 | }, 89 | "prettier": { 90 | "printWidth": 100, 91 | "semi": false, 92 | "singleQuote": true, 93 | "trailingComma": "es5" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | # set -x 4 | 5 | SCRIPT_DIR=$(cd ${0%/*} && pwd -P) 6 | 7 | # Known variables 8 | SRC='./src' 9 | DST='./dist' 10 | name="ethos-connect" 11 | input="./${SRC}/index.ts" 12 | 13 | # Find executables 14 | esbuild=$(yarn bin esbuild) 15 | tsc=$(yarn bin tsc) 16 | resolver="${SCRIPT_DIR}/resolve-files.cjs" 17 | rewriteImports="${SCRIPT_DIR}/rewrite-imports.cjs" 18 | 19 | # Setup shared options for esbuild 20 | sharedOptions=() 21 | sharedOptions+=("--platform=browser") 22 | sharedOptions+=("--target=es2020") 23 | 24 | # Generate actual builds 25 | # ESM 26 | resolverOptions=() 27 | resolverOptions+=($SRC) 28 | resolverOptions+=('/**/*.{ts,tsx}') 29 | resolverOptions+=('--ignore=.test.,__mocks__') 30 | INPUT_FILES=$($resolver ${resolverOptions[@]}) 31 | 32 | NODE_ENV=production $esbuild $INPUT_FILES --format=esm --outdir=$DST --outbase=$SRC --minify --pure:React.createElement ${sharedOptions[@]} & 33 | NODE_ENV=production $esbuild $input --format=esm --outfile=$DST/$name.esm.js --outbase=$SRC --minify --pure:React.createElement ${sharedOptions[@]} & 34 | 35 | # Common JS 36 | NODE_ENV=production $esbuild $input --format=cjs --outfile=$DST/$name.prod.cjs --minify --bundle --pure:React.createElement ${sharedOptions[@]} $@ & 37 | NODE_ENV=development $esbuild $input --format=cjs --outfile=$DST/$name.dev.cjs --bundle --pure:React.createElement ${sharedOptions[@]} $@ & 38 | 39 | # Generate types 40 | tsc --emitDeclarationOnly --outDir $DST & 41 | 42 | # Copy build files over 43 | cp -rf ./build/ $DST 44 | 45 | # Wait for all the scripts to finish 46 | wait 47 | 48 | # Rewrite ESM imports 😤 49 | $rewriteImports "$DST" '/**/*.js' 50 | 51 | # Remove test related files 52 | rm -rf `$resolver "$DST" '/**/*.{test,__mocks__,}.*'` 53 | rm -rf `$resolver "$DST" '/**/test-utils/*'` 54 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env bash 2 | # set -e 3 | 4 | # ROOT_DIR="$(git rev-parse --show-toplevel)/" 5 | # TARGET_DIR="$(pwd)" 6 | # RELATIVE_TARGET_DIR="${TARGET_DIR/$ROOT_DIR/}" 7 | 8 | # # INFO: This script is always run from the root of the repository. If we execute this script from a 9 | # # package then the filters (in this case a path to $RELATIVE_TARGET_DIR) will be applied. 10 | 11 | # pushd $ROOT_DIR > /dev/null 12 | 13 | # prettierArgs=() 14 | 15 | # if ! [ -z "$CI" ]; then 16 | # prettierArgs+=("--check") 17 | # else 18 | # prettierArgs+=("--write") 19 | # fi 20 | 21 | # # Add default arguments 22 | # prettierArgs+=('--ignore-unknown') 23 | 24 | # # Passthrough arguments and flags 25 | # prettierArgs+=($@) 26 | 27 | # # Ensure that a path is passed, otherwise default to the current directory 28 | # if [ -z "$@" ]; then 29 | # prettierArgs+=("$RELATIVE_TARGET_DIR") 30 | # fi 31 | 32 | # # Execute 33 | # yarn prettier "${prettierArgs[@]}" 34 | 35 | # popd > /dev/null 36 | -------------------------------------------------------------------------------- /scripts/resolve-files.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | let fastGlob = require('fast-glob') 3 | 4 | let parts = process.argv.slice(2) 5 | let [args, flags] = parts.reduce( 6 | ([args, flags], part) => { 7 | if (part.startsWith('--')) { 8 | flags[part.slice(2, part.indexOf('='))] = part.slice(part.indexOf('=') + 1) 9 | } else { 10 | args.push(part) 11 | } 12 | return [args, flags] 13 | }, 14 | [[], {}] 15 | ) 16 | 17 | flags.ignore = flags.ignore ?? '' 18 | flags.ignore = flags.ignore.split(',').filter(Boolean) 19 | 20 | console.log( 21 | fastGlob 22 | .sync(args.join('')) 23 | .filter((file) => { 24 | for (let ignore of flags.ignore) { 25 | if (file.includes(ignore)) { 26 | return false 27 | } 28 | } 29 | return true 30 | }) 31 | .join('\n') 32 | ) 33 | -------------------------------------------------------------------------------- /scripts/rewrite-imports.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let fs = require('fs') 4 | let path = require('path') 5 | let fastGlob = require('fast-glob') 6 | 7 | console.time('Rewrote imports in') 8 | fastGlob.sync([process.argv.slice(2).join('')]).forEach((file) => { 9 | file = path.resolve(process.cwd(), file) 10 | let content = fs.readFileSync(file, 'utf8') 11 | let result = content.replace(/(import|export)([^"']*?)(["'])\.(.*?)\3;/g, '$1$2".$4.js";') 12 | result = result.replace(/.js.js/g, '.js') 13 | 14 | if (result !== content) { 15 | fs.writeFileSync(file, result, 'utf8') 16 | } 17 | }) 18 | console.timeEnd('Rewrote imports in') 19 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | node="yarn node" 5 | jestArgs=() 6 | 7 | # Add default arguments 8 | jestArgs+=("--passWithNoTests") 9 | 10 | # Add arguments based on environment variables 11 | if ! [ -z "$CI" ]; then 12 | jestArgs+=("--maxWorkers=4") 13 | jestArgs+=("--ci") 14 | fi 15 | 16 | # Passthrough arguments and flags 17 | jestArgs+=($@) 18 | 19 | # Execute 20 | $node "$(yarn bin jest)" "${jestArgs[@]}" 21 | -------------------------------------------------------------------------------- /scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Known variables 5 | outdir="./dist" 6 | name="ethos-connect" 7 | input="./src/index.ts" 8 | 9 | # Find executables 10 | esbuild=$(yarn bin esbuild) 11 | tsc=$(yarn bin tsc) 12 | 13 | # Setup shared options for esbuild 14 | sharedOptions=() 15 | sharedOptions+=("--bundle") 16 | sharedOptions+=("--platform=browser") 17 | sharedOptions+=("--target=es2020") 18 | 19 | # Generate actual builds 20 | $esbuild $input --format=esm --outfile=$outdir/$name.esm.js --sourcemap ${sharedOptions[@]} $@ --watch 21 | 22 | -------------------------------------------------------------------------------- /src/components/ConnectContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { ConnectContextContents } from '../types/ConnectContextContents'; 3 | 4 | const defaultContents: ConnectContextContents = { 5 | init: () => {} 6 | } 7 | 8 | const ConnectContext = createContext(defaultContents); 9 | 10 | export default ConnectContext; -------------------------------------------------------------------------------- /src/components/DetachedEthosConnectProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode 3 | } from 'react' 4 | import SignInModal from './styled/SignInModal' 5 | 6 | export interface DetachedEthosConnectProviderProps { 7 | context: any, 8 | connectMessage?: string | ReactNode 9 | dappName?: string 10 | dappIcon?: string | ReactNode 11 | children: ReactNode 12 | } 13 | 14 | const DetachedEthosConnectProvider = ({ context, connectMessage, dappName, dappIcon, children }: DetachedEthosConnectProviderProps) => { 15 | return ( 16 | <> 17 | {children} 18 | 19 | 28 | 29 | ) 30 | } 31 | 32 | export default DetachedEthosConnectProvider 33 | -------------------------------------------------------------------------------- /src/components/EthosConnectProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode 3 | } from 'react' 4 | import { EthosConfiguration } from '../types/EthosConfiguration' 5 | import { ClientAndSigner } from '../types/ClientAndSigner' 6 | import SignInModal from './styled/SignInModal' 7 | import ConnectContext from './ConnectContext' 8 | import useContext from '../hooks/useContext' 9 | 10 | export interface EthosConnectProviderProps { 11 | ethosConfiguration?: EthosConfiguration 12 | onWalletConnected?: ({ client, signer }: ClientAndSigner) => void, 13 | connectMessage?: string | ReactNode 14 | dappName?: string 15 | dappIcon?: string | ReactNode 16 | children: ReactNode 17 | } 18 | 19 | const EthosConnectProvider = ({ ethosConfiguration, onWalletConnected, connectMessage, dappName, dappIcon, children }: EthosConnectProviderProps) => { 20 | const context = useContext({ configuration: ethosConfiguration || {}, onWalletConnected }); 21 | 22 | return ( 23 | 24 | {children} 25 | 26 | 35 | 36 | ) 37 | } 38 | 39 | export default EthosConnectProvider 40 | -------------------------------------------------------------------------------- /src/components/headless/HoverColorButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useCallback, useState } from "react"; 2 | import WorkingButton from "./WorkingButton"; 3 | import { WorkingButtonProps } from '../../types/WorkingButtonProps' 4 | import { primaryColor } from "../../lib/constants"; 5 | 6 | export interface HoverColorButtonProps extends WorkingButtonProps { 7 | hoverBackgroundColor?: string 8 | hoverChildren: ReactNode 9 | } 10 | 11 | const HoverColorButton = (props: HoverColorButtonProps) => { 12 | const { hoverBackgroundColor, hoverChildren, children, style, ...workingButtonProps} = props; 13 | const [hover, setHover] = useState(false); 14 | 15 | const onMouseEnter = useCallback(() => { 16 | setHover(true); 17 | }, []) 18 | 19 | const onMouseLeave = useCallback(() => { 20 | setHover(false); 21 | }, []) 22 | 23 | return ( 24 | 34 | {hover ? hoverChildren : children} 35 | 36 | ) 37 | } 38 | 39 | export default HoverColorButton; -------------------------------------------------------------------------------- /src/components/headless/WorkingButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { WorkingButtonProps } from '../../types/WorkingButtonProps' 3 | 4 | import Loader from '../svg/Loader' 5 | 6 | const WorkingButton = (props: WorkingButtonProps) => { 7 | const { children, isWorking, workingComponent, ...reactProps } = props 8 | 9 | return ( 10 | 21 | ) 22 | } 23 | export default WorkingButton 24 | -------------------------------------------------------------------------------- /src/components/headless/index.js: -------------------------------------------------------------------------------- 1 | import WorkingButton from './WorkingButton.jsx' 2 | import HoverColorButton from './HoverColorButton.jsx' 3 | 4 | export { 5 | WorkingButton, 6 | HoverColorButton 7 | } 8 | -------------------------------------------------------------------------------- /src/components/keyboard.ts: -------------------------------------------------------------------------------- 1 | // TODO: This must already exist somewhere, right? 🤔 2 | // Ref: https://www.w3.org/TR/uievents-key/#named-key-attribute-values 3 | export enum Keys { 4 | Space = ' ', 5 | Enter = 'Enter', 6 | Escape = 'Escape', 7 | Backspace = 'Backspace', 8 | Delete = 'Delete', 9 | 10 | ArrowLeft = 'ArrowLeft', 11 | ArrowUp = 'ArrowUp', 12 | ArrowRight = 'ArrowRight', 13 | ArrowDown = 'ArrowDown', 14 | 15 | Home = 'Home', 16 | End = 'End', 17 | 18 | PageUp = 'PageUp', 19 | PageDown = 'PageDown', 20 | 21 | Tab = 'Tab', 22 | } 23 | -------------------------------------------------------------------------------- /src/components/styled/AddressWidget.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { ReactNode, useCallback, useState } from 'react'; 3 | 4 | import SignInButton from './SignInButton'; 5 | 6 | import useWallet from '../../hooks/useWallet'; 7 | import { formatBalance } from '../../lib/bigNumber'; 8 | import truncateMiddle from '../../lib/truncateMiddle'; 9 | 10 | import Sui from "../svg/Sui"; 11 | import CopyWalletAddressButton from './CopyWalletAddressButton'; 12 | import WalletExplorerButton from './WalletExplorerButton'; 13 | import LogoutButton from './LogoutButton'; 14 | import { primaryColor } from '../../lib/constants'; 15 | import { useEffect } from 'react'; 16 | import { AddressWidgetButtons } from '../../enums/AddressWidgetButtons'; 17 | 18 | export interface AddressWidgetProps { 19 | includeMenu?: boolean 20 | buttonColor?: string 21 | extraButtons?: ReactNode[] 22 | excludeButtons?: AddressWidgetButtons[] 23 | externalContext?: any 24 | } 25 | 26 | const AddressWidget = ({ 27 | includeMenu = true, 28 | buttonColor = primaryColor, 29 | extraButtons = [], 30 | excludeButtons = [], 31 | externalContext 32 | }: AddressWidgetProps) => { 33 | const { wallet } = externalContext?.wallet || useWallet(); 34 | const [showMenu, setShowMenu] = useState(false); 35 | 36 | useEffect(() => { 37 | if (!wallet) { 38 | setShowMenu(false); 39 | } 40 | }, [wallet]) 41 | 42 | const onMouseEnter = useCallback(() => { 43 | if (!wallet) return; 44 | 45 | setShowMenu(true); 46 | }, [wallet]) 47 | 48 | const onMouseLeave = useCallback(() => { 49 | if (!wallet) return; 50 | 51 | setShowMenu(false); 52 | }, [wallet]) 53 | 54 | return ( 55 |
56 |
57 |
58 | 59 |
60 | {wallet ? ( 61 | <> 62 |
63 | {formatBalance(wallet.contents?.suiBalance)}{' '} 64 | Sui 65 |
66 |
67 | {wallet.icon && ( 68 | 72 | )} 73 | {truncateMiddle(wallet.address)} 74 |
75 | 76 | ) : ( 77 | 78 | )} 79 |
80 | {includeMenu && showMenu && ( 81 |
82 | {!excludeButtons.includes(AddressWidgetButtons.CopyWalletAddress) && ( 83 | 87 | )} 88 | 89 | {!excludeButtons.includes(AddressWidgetButtons.WalletExplorer) && ( 90 | 93 | )} 94 | {extraButtons} 95 | {!excludeButtons.includes(AddressWidgetButtons.Logout) && ( 96 | 100 | )} 101 |
102 | )} 103 | 104 |
105 | ) 106 | } 107 | 108 | export default AddressWidget; 109 | 110 | export const container = () => ( 111 | { 112 | position: "relative", 113 | backgroundColor: 'white', 114 | padding: "6px 12px 6px 18px", 115 | boxShadow: "1px 1px 3px 1px #dfdfe0", 116 | borderRadius: '18px', 117 | fontSize: '14px', 118 | color: 'black' 119 | } as React.CSSProperties 120 | ) 121 | 122 | export const primary = () => ( 123 | { 124 | display: 'flex', 125 | justifyContent: 'center', 126 | alignItems: 'center', 127 | gap: '12px', 128 | } as React.CSSProperties 129 | ) 130 | 131 | export const sui = () => ( 132 | { 133 | whiteSpace: "nowrap" 134 | } as React.CSSProperties 135 | ) 136 | 137 | 138 | export const address = () => ( 139 | { 140 | borderRadius: "30px", 141 | backgroundColor: "#f2f1f0", 142 | padding: "6px 12px", 143 | display: "flex", 144 | alignItems: "center", 145 | gap: "6px" 146 | } as React.CSSProperties 147 | ) 148 | 149 | export const menu = () => ( 150 | { 151 | display: "flex", 152 | flexDirection: "column", 153 | gap: "6px", 154 | padding: "12px 18px", 155 | position: "absolute", 156 | bottom: 0, 157 | left: "12px", 158 | right: "12px", 159 | transform: "translateY(100%)", 160 | boxShadow: "1px 1px 3px 1px #dfdfe0", 161 | borderBottomLeftRadius: '18px', 162 | borderBottomRightRadius: '18px', 163 | backgroundColor: 'white', 164 | zIndex: "99" 165 | } as React.CSSProperties 166 | ) 167 | 168 | export const signIn = () => ( 169 | { 170 | padding: "0 12px 0 0", 171 | background: "none", 172 | whiteSpace: "nowrap" 173 | } as React.CSSProperties 174 | ); 175 | 176 | export const walletIcon = () => ( 177 | { 178 | width: "20px", 179 | height: "20px" 180 | } as React.CSSProperties 181 | ); 182 | 183 | export const svg = () => ( 184 | { 185 | verticalAlign: 'middle', 186 | display: 'block' 187 | } 188 | ) 189 | 190 | -------------------------------------------------------------------------------- /src/components/styled/CopyWalletAddressButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | 3 | import MenuButton from './MenuButton'; 4 | 5 | import useWallet from '../../hooks/useWallet'; 6 | import { MenuButtonProps } from '../../types/MenuButtonProps'; 7 | 8 | 9 | const CopyWalletAddressButton = (props: MenuButtonProps) => { 10 | const { externalContext, ...buttonProps } = props; 11 | const { wallet } = externalContext?.wallet || useWallet(); 12 | 13 | const children = useCallback((hover: boolean) => ( 14 | <> 15 | 16 | 23 | 24 | Copy Wallet Address 25 | 26 | ), []); 27 | 28 | const onClick = useCallback((e: any) => { 29 | const element = e.target; 30 | const innerHTML = element.innerHTML; 31 | element.innerHTML = "Copied!" 32 | navigator.clipboard.writeText(wallet?.address || '') 33 | setTimeout(() => { 34 | element.innerHTML = innerHTML; 35 | }, 1000); 36 | }, [wallet]) 37 | 38 | return ( 39 | 44 | { children(false) } 45 | 46 | ) 47 | } 48 | 49 | export default CopyWalletAddressButton; -------------------------------------------------------------------------------- /src/components/styled/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react" 2 | import FontProvider from './FontProvider' 3 | import * as styles from './signInModalStyles' 4 | 5 | export type DialogProps = { 6 | isOpenAll: boolean, 7 | children: ReactNode 8 | } 9 | 10 | const Dialog = ({ isOpenAll, children }: DialogProps) => { 11 | return ( 12 | 13 |
14 |
15 | {children} 16 |
17 | 18 | ) 19 | } 20 | 21 | export default Dialog; -------------------------------------------------------------------------------- /src/components/styled/Email.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useCallback, useMemo, useState } from 'react' 2 | // import ReCAPTCHA from 'react-google-recaptcha' 3 | // import { captchaSiteKey } from '../../lib/constants' 4 | import getConfiguration from '../../lib/getConfiguration' 5 | import event from '../../lib/event' 6 | import login from '../../lib/login' 7 | import * as styles from './signInModalStyles' 8 | import IconButton from './IconButton' 9 | 10 | export type EmailProps = { 11 | setSigningIn: (signingIn: boolean) => void, 12 | setEmailSent: (emailSent: boolean) => void, 13 | // captchaRef: MutableRefObject, 14 | width: number 15 | } 16 | 17 | const Email = ({ setSigningIn, setEmailSent, width }: EmailProps) => { 18 | const { apiKey } = getConfiguration() 19 | const [email, setEmail] = useState('') 20 | 21 | const validEmail = useMemo(() => { 22 | if (!email) return false; 23 | if (email.length === 0) return false; 24 | return !!email.match(/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/) 25 | }, [email]) 26 | 27 | const sendEmail = useCallback(async () => { 28 | if (!validEmail) return 29 | await login({ email, apiKey }) 30 | setEmail('') 31 | setSigningIn(false) 32 | setEmailSent(true) 33 | event({ action: 'send_email', category: 'sign_in', label: email, value: 1 }) 34 | }, [validEmail, login, email, apiKey]); 35 | 36 | const _handleChange = useCallback((e: any) => { 37 | setEmail(e.target.value) 38 | }, []) 39 | 40 | const onSubmit = useCallback(async (e: FormEvent) => { 41 | if (!validEmail) { 42 | e.preventDefault() 43 | return 44 | } 45 | setSigningIn(true) 46 | sendEmail() 47 | // if (captchaRef && captchaRef.current && process.env.NODE_ENV !== 'development') { 48 | // try { 49 | // await captchaRef.current.execute() 50 | // } catch (e) { 51 | // console.log('CAPTCHA ERROR', e) 52 | // sendEmail() 53 | // } 54 | // } else { 55 | // sendEmail() 56 | // } 57 | }, [sendEmail]); 58 | 59 | return ( 60 |
61 |
62 | 69 | 76 | 77 | {/*
78 | 84 |
*/} 85 |
86 | ); 87 | } 88 | 89 | export default Email; -------------------------------------------------------------------------------- /src/components/styled/EmailSent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './Header'; 3 | import * as styles from './signInModalStyles' 4 | import EthosWalletIcon from '../svg/EthosWalletIcon'; 5 | 6 | const EmailSent = () => { 7 | return ( 8 |
} 11 | > 12 |
13 |

14 | An email has been sent to you with a link to login. 15 |

16 |

17 | If you don't receive it, please check your spam folder or contact us at: 18 |

19 |

20 | support@ethoswallet.xyz 21 |

22 |
23 |
24 | ) 25 | } 26 | 27 | export default EmailSent; -------------------------------------------------------------------------------- /src/components/styled/FontProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const FontProvider = ({ children }: React.HTMLAttributes) => { 4 | const styles = () => ( 5 | { 6 | fontFamily: "'Inter', sans-serif", 7 | color: 'black', 8 | lineHeight: '1.5', 9 | fontSize: '16px' 10 | } as React.CSSProperties 11 | ) 12 | return ( 13 | <> 14 | 15 |
16 | {children} 17 |
18 | 19 | ); 20 | } 21 | 22 | export default FontProvider; -------------------------------------------------------------------------------- /src/components/styled/Header.tsx: -------------------------------------------------------------------------------- 1 | import Ethos from '../svg/Ethos' 2 | import React, { ReactNode } from 'react' 3 | import * as styles from './signInModalStyles' 4 | 5 | export type HeaderProps = { 6 | title?: string | ReactNode, 7 | subTitle?: string | ReactNode, 8 | dappIcon?: string | ReactNode, 9 | showEthos?: boolean, 10 | children: ReactNode 11 | } 12 | 13 | const Header = ({ title, subTitle, dappIcon, showEthos=false, children }: HeaderProps) => { 14 | return ( 15 |
16 |
17 |
18 | {dappIcon && ( 19 | typeof dappIcon === 'string' ? : dappIcon 20 | )} 21 | {showEthos && ()} 22 | 23 |
24 | {title && ( 25 |
26 | {title} 27 |
28 | )} 29 | {subTitle && ( 30 |
31 | {subTitle} 32 |
33 | )} 34 |
35 | {children} 36 |
37 | ) 38 | } 39 | 40 | export default Header; -------------------------------------------------------------------------------- /src/components/styled/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes, ReactNode } from "react"; 2 | import * as styles from './signInModalStyles' 3 | 4 | export interface IconButtonProps extends HTMLAttributes { 5 | text: string, 6 | icon?: ReactNode, 7 | width: number, 8 | disabled?: boolean, 9 | primary?: boolean, 10 | type?: 'button' | 'submit' | 'reset' | undefined; 11 | } 12 | 13 | const IconButton = (props: IconButtonProps) => { 14 | const { text, icon, width, disabled, primary, type, ...reactProps } = props; 15 | return ( 16 | 26 | ) 27 | } 28 | 29 | export default IconButton; -------------------------------------------------------------------------------- /src/components/styled/InstallWallet.tsx: -------------------------------------------------------------------------------- 1 | import InstallWalletIcon from "../svg/InstallWalletIcon"; 2 | import React, { ReactNode } from "react"; 3 | import Header from "./Header"; 4 | import * as styles from './signInModalStyles' 5 | // import SuiEnclosed from "../svg/SuiEnclosed"; 6 | import EthosWalletIcon from "../svg/EthosWalletIcon"; 7 | 8 | export type WalletInstallInfo = { 9 | name: string, 10 | icon: string, 11 | link: string 12 | } 13 | 14 | export type InstallWalletProps = { 15 | walletInfos?: WalletInstallInfo[] 16 | width: number 17 | } 18 | 19 | const InstallWallet = ({ walletInfos, width }: InstallWalletProps) => { 20 | const icon = (data?: string | ReactNode) => { 21 | if (!data) return <>; 22 | 23 | if (typeof data === "string") { 24 | return ( 25 | 26 | ) 27 | } 28 | 29 | return data; 30 | } 31 | 32 | const installWallets = [ 33 | { 34 | name: "Ethos Wallet", 35 | icon: , 36 | link: "https://chrome.google.com/webstore/detail/ethos-wallet/mcbigmjiafegjnnogedioegffbooigli" 37 | }, 38 | // { 39 | // name: "Sui Wallet", 40 | // icon: , 41 | // link: "https://chrome.google.com/webstore/detail/sui-wallet/opcgpfmipidbgpenhmajoajpbobppdil" 42 | // }, 43 | ...(walletInfos || []) 44 | ] 45 | 46 | return ( 47 |
} 49 | title="Install A Wallet" 50 | subTitle="Wallets allow you to interact with, store, send, and receive digital assets." 51 | > 52 |
53 |
54 | {installWallets?.map( 55 | (installWallet, index) => ( 56 | 62 | {installWallet.name} 63 |
64 | {icon(installWallet.icon)} 65 |
66 |
67 | ) 68 | )} 69 |
70 |
71 |
72 | ) 73 | } 74 | 75 | export default InstallWallet; -------------------------------------------------------------------------------- /src/components/styled/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import MenuButton from './MenuButton'; 3 | import type { MenuButtonProps } from '../../types/MenuButtonProps'; 4 | 5 | import useWallet from '../../hooks/useWallet'; 6 | 7 | const LogoutButton = (props: MenuButtonProps) => { 8 | const { externalContext, ...buttonProps } = props; 9 | const { wallet } = externalContext?.wallet || useWallet(); 10 | 11 | const children = useCallback((hover: boolean) => ( 12 | <> 13 | 14 | 18 | 19 | Log Out 20 | 21 | ), []); 22 | 23 | return ( 24 | 29 | { children(false) } 30 | 31 | ) 32 | } 33 | 34 | export default LogoutButton; -------------------------------------------------------------------------------- /src/components/styled/MenuButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HoverColorButton, { HoverColorButtonProps } from '../headless/HoverColorButton'; 3 | 4 | const MenuButton = (props: HoverColorButtonProps) => { 5 | return ( 6 | 10 | ) 11 | } 12 | 13 | export default MenuButton; 14 | 15 | export const button = () => ( 16 | { 17 | width: "100%", 18 | borderRadius: "12px", 19 | textAlign: 'left', 20 | padding: "6px 12px", 21 | display: "flex", 22 | alignItems: 'center', 23 | gap: "6px", 24 | border: "none", 25 | fontFamily: 'inherit', 26 | cursor: 'pointer' 27 | } as React.CSSProperties 28 | ) -------------------------------------------------------------------------------- /src/components/styled/MobileWallet.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as styles from './signInModalStyles' 3 | 4 | export type MobileWalletInfo = { 5 | name: string, 6 | icon: string, 7 | link: string 8 | } 9 | 10 | export type MobileWalletProps = { 11 | walletInfos?: MobileWalletInfo[] 12 | width: number 13 | } 14 | 15 | const MobileWallet = () => { 16 | return ( 17 |
18 | 19 | Connect A Mobile Wallet 20 | 21 |
22 |

23 | There are no mobile wallets yet on Sui. 24 |

25 |
26 |
27 | ) 28 | } 29 | 30 | export default MobileWallet; -------------------------------------------------------------------------------- /src/components/styled/ModalWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import * as styles from './signInModalStyles' 3 | 4 | export type ModalWrapperProps = { 5 | closeOnClickId: string, 6 | onClose: () => void, 7 | isOpenAll: boolean, 8 | width: number, 9 | back: (() => void) | null, 10 | children: ReactNode 11 | } 12 | 13 | const ModalWrapper = ({ closeOnClickId, onClose, isOpenAll, width, back, children }: ModalWrapperProps) => { 14 | return ( 15 |
16 |
17 |
18 |
19 | 20 | {back && ( 21 | 22 | ← 23 | Back 24 | 25 | )} 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | {children} 34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | export default ModalWrapper; -------------------------------------------------------------------------------- /src/components/styled/Or.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as styles from './signInModalStyles' 3 | 4 | const Or = () => { 5 | return ( 6 |
7 |
8 | or 9 |
10 |
11 | ) 12 | } 13 | 14 | export default Or; -------------------------------------------------------------------------------- /src/components/styled/SignInButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, useCallback } from 'react' 2 | import WorkingButton from '../headless/WorkingButton' 3 | import { WorkingButtonProps } from '../../types/WorkingButtonProps' 4 | import useModal from '../../hooks/useModal' 5 | 6 | export interface SignInButtonProps extends WorkingButtonProps { 7 | onLoaded?: () => void 8 | externalContext?: any 9 | } 10 | 11 | const SignInButton = (props: SignInButtonProps) => { 12 | const { children, onClick, externalContext, ...reactProps } = props 13 | const { openModal } = externalContext?.modal || useModal(); 14 | 15 | const _onClick = useCallback((e: any) => { 16 | openModal() 17 | onClick && onClick(e) 18 | }, [openModal, onClick]) 19 | 20 | return ( 21 | <> 22 | 23 | {children || <>Sign In} 24 | 25 | 26 | ) 27 | } 28 | export default SignInButton 29 | 30 | export const buttonDefault = (providedStyles: CSSProperties | undefined) => ( 31 | { 32 | lineHeight: '21px', 33 | border: 'none', 34 | cursor: 'pointer', 35 | fontFamily: 'inherit', 36 | fontSize: '14px', 37 | ...providedStyles || {} 38 | } as React.CSSProperties 39 | ) 40 | -------------------------------------------------------------------------------- /src/components/styled/SignInModal.tsx: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | ethosInternal: any 4 | } 5 | } 6 | 7 | import React, { useCallback, useEffect, useMemo, useState, ReactNode } from 'react' 8 | 9 | import Loader from '../svg/Loader' 10 | import WalletsIcon from '../svg/WalletsIcon' 11 | import * as styles from './signInModalStyles' 12 | import useHandleElementWithIdClicked from '../../lib/useHandleElementWithIdClicked' 13 | import EmailSent from './EmailSent' 14 | import Wallets from './Wallets' 15 | import Email from './Email' 16 | import Dialog from './Dialog' 17 | import ModalWrapper from './ModalWrapper' 18 | import InstallWallet from './InstallWallet' 19 | import IconButton from './IconButton' 20 | import hooks from '../../hooks/hooks' 21 | import MobileWallet from './MobileWallet' 22 | import Header from './Header' 23 | import Or from './Or' 24 | import log from '../../lib/log'; 25 | import type { WalletWithSuiFeatures } from '@mysten/wallet-standard'; 26 | 27 | export type SignInModalProps = { 28 | connectMessage?: string | ReactNode, 29 | dappName?: string, 30 | dappIcon?: string | ReactNode, 31 | isOpen: boolean 32 | onClose?: () => void 33 | hideEmailSignIn?: boolean 34 | hideWalletSignIn?: boolean, 35 | externalContext?: any, 36 | preferredWallets?: string[] 37 | } 38 | 39 | export function showSignInModal() { 40 | window.ethosInternal.showSignInModal() 41 | } 42 | 43 | export function hideSignInModal() { 44 | window.ethosInternal.hideSignInModal() 45 | } 46 | 47 | const SignInModal = ({ 48 | connectMessage, 49 | dappName, 50 | dappIcon, 51 | hideEmailSignIn, 52 | hideWalletSignIn, 53 | externalContext, 54 | preferredWallets 55 | }: SignInModalProps) => { 56 | const { wallets, selectWallet } = externalContext?.wallet || hooks.useWallet() 57 | const { isModalOpen, openModal, closeModal } = externalContext?.modal || hooks.useModal() 58 | const [isOpenAll, setIsOpenAll] = useState(isModalOpen) 59 | const [signingIn, setSigningIn] = useState(false) 60 | 61 | const [emailSent, setEmailSent] = useState(false) 62 | const { width } = hooks.useWindowDimensions() 63 | // const captchaRef = useRef(null) 64 | const closeOnClickId = 'ethos-close-on-click' 65 | 66 | const [showEmail, setShowEmail] = useState(false); 67 | const [showMobile, setShowMobile] = useState(false); 68 | const [showInstallWallet, setShowInstallWallet] = useState(false); 69 | 70 | const [safeDappName, setSafeDappName] = useState(dappName); 71 | const [safeWallets, setSafeWallets] = useState(); 72 | 73 | useHandleElementWithIdClicked(closeOnClickId, closeModal) 74 | 75 | useEffect(() => { 76 | window.ethosInternal ||= {} 77 | 78 | window.ethosInternal.showSignInModal = () => { 79 | openModal() 80 | } 81 | 82 | window.ethosInternal.hideSignInModal = () => { 83 | closeModal() 84 | } 85 | 86 | setIsOpenAll(isModalOpen) 87 | }, [isModalOpen, setIsOpenAll, openModal, closeModal]) 88 | 89 | useEffect(() => { 90 | if (hideEmailSignIn && hideWalletSignIn) { 91 | throw new Error("hideEmailSignIn and hideWalletSignIn cannot both be true"); 92 | } 93 | }, [hideEmailSignIn, hideWalletSignIn]) 94 | 95 | useEffect(() => { 96 | if (!safeDappName) { 97 | setSafeDappName(document.title) 98 | } 99 | }, [safeDappName]) 100 | 101 | useEffect(() => { 102 | let safeWallets: WalletWithSuiFeatures[] = wallets || []; 103 | if (preferredWallets && preferredWallets.length > 0) { 104 | safeWallets = safeWallets.sort( 105 | (a: any, b: any) => { 106 | let aIndex = preferredWallets.indexOf(a.name); 107 | if (aIndex === -1) { 108 | aIndex = safeWallets.length; 109 | } 110 | 111 | let bIndex = preferredWallets.indexOf(b.name); 112 | if (bIndex === -1) { 113 | bIndex = safeWallets.length; 114 | } 115 | 116 | return aIndex - bIndex; 117 | } 118 | ) 119 | } 120 | log("preferredWallets", preferredWallets, safeWallets) 121 | 122 | setSafeWallets(safeWallets) 123 | }, [wallets, preferredWallets, log]) 124 | 125 | // const _toggleMobile = useCallback(() => { 126 | // setShowMobile((prev) => !prev) 127 | // }, []) 128 | 129 | const _toggleInstallWallet = useCallback(() => { 130 | setShowInstallWallet((prev) => !prev) 131 | }, []) 132 | 133 | const _toggleEmail = useCallback(() => { 134 | setShowEmail((prev) => !prev) 135 | }, []) 136 | 137 | const _reset = useCallback(() => { 138 | setShowInstallWallet(false) 139 | setShowMobile(false) 140 | setShowEmail(false) 141 | }, []) 142 | 143 | const safeConnectMessage = useMemo(() => { 144 | if (connectMessage) return connectMessage; 145 | 146 | if (!safeDappName) { 147 | return <> 148 | } 149 | 150 | return ( 151 | <>Connect to {safeDappName} 152 | ) 153 | }, [safeDappName, connectMessage]); 154 | 155 | const modalContent = useMemo(() => { 156 | if (!safeWallets) { 157 | return <> 158 | } 159 | 160 | if (showMobile) { 161 | return 162 | } 163 | 164 | if (showInstallWallet || (hideEmailSignIn && safeWallets.length === 0)) { 165 | return 166 | } 167 | 168 | if (hideWalletSignIn) { 169 | return ( 170 | 176 | ) 177 | } 178 | 179 | if (!showEmail && safeWallets.length > 0) return ( 180 |
185 | 190 | {!hideEmailSignIn && ( 191 | <> 192 | 193 | 194 |
195 | 201 |
202 | 203 | )} 204 |
205 | ) 206 | 207 | return ( 208 |
213 | 219 | {!hideWalletSignIn && ( 220 | <> 221 | 222 | 223 |
224 | {safeWallets.length > 0 ? ( 225 | } 227 | text="Select One Of Your Wallets" 228 | onClick={_toggleEmail} 229 | width={width} 230 | /> 231 | ) : ( 232 | } 234 | text="Install A Wallet" 235 | onClick={_toggleInstallWallet} 236 | width={width} 237 | /> 238 | )} 239 |
240 | 241 | )} 242 |
243 | ) 244 | }, [safeConnectMessage, safeDappName, hideEmailSignIn, hideWalletSignIn, safeWallets, showEmail, showMobile, showInstallWallet]) 245 | 246 | const subpage = useMemo(() => { 247 | return showMobile || showInstallWallet 248 | }, [showMobile, showInstallWallet]) 249 | 250 | const loader = useMemo(() => ( 251 |
252 | 253 |
254 | ), []) 255 | 256 | return ( 257 | 258 | 265 | {emailSent ? ( 266 | 267 | ) : ( 268 | signingIn ? loader : modalContent 269 | )} 270 | 271 | 272 | ) 273 | } 274 | 275 | export default SignInModal 276 | -------------------------------------------------------------------------------- /src/components/styled/WalletExplorerButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import MenuButton from './MenuButton'; 3 | import type { MenuButtonProps } from '../../types/MenuButtonProps'; 4 | 5 | const WalletExplorerButton = (props: MenuButtonProps) => { 6 | const children = useCallback((hover: boolean) => ( 7 | <> 8 | 9 | 16 | 17 | Wallet Explorer 18 | 19 | ), []); 20 | 21 | const onClick = useCallback(() => { 22 | window.open("https://ethoswallet.xyz/dashboard", "_blank") 23 | }, []) 24 | 25 | return ( 26 | 31 | { children(false) } 32 | 33 | ) 34 | } 35 | 36 | export default WalletExplorerButton; -------------------------------------------------------------------------------- /src/components/styled/Wallets.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import * as styles from './signInModalStyles' 3 | import IconButton from "./IconButton"; 4 | import type { WalletWithSuiFeatures } from '@mysten/wallet-standard'; 5 | 6 | export type WalletProps = { 7 | wallets?: WalletWithSuiFeatures[], 8 | selectWallet?: ((name: string) => void), 9 | width: number 10 | } 11 | 12 | const Wallets = ({ wallets, selectWallet, width }: WalletProps) => { 13 | const _connectExtension = useCallback((e: any) => { 14 | if (!selectWallet) return; 15 | 16 | let element = e.target; 17 | let name; 18 | while (!name && element.parentNode) { 19 | name = element.dataset.name; 20 | element = element.parentNode; 21 | } 22 | selectWallet(name); 23 | }, []); 24 | 25 | const icon = useCallback((wallet: WalletWithSuiFeatures) => { 26 | return ( 27 | 28 | ) 29 | }, []); 30 | 31 | return ( 32 |
33 |
34 | {wallets?.map( 35 | (wallet, index) => ( 36 | 44 | ) 45 | )} 46 |
47 |
48 | ) 49 | } 50 | 51 | export default Wallets; -------------------------------------------------------------------------------- /src/components/styled/index.js: -------------------------------------------------------------------------------- 1 | import SignInButton, { SignInButtonProps } from './SignInButton.jsx' 2 | import SignInModal, { SignInModalProps } from './SignInModal.jsx' 3 | import AddressWidget, { AddressWidgetProps } from './AddressWidget.jsx' 4 | 5 | export { 6 | SignInButton, SignInButtonProps, 7 | SignInModal, SignInModalProps, 8 | AddressWidget, AddressWidgetProps 9 | } 10 | -------------------------------------------------------------------------------- /src/components/svg/CheckMark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const CheckMark = ({ width = 24, color = '#1e293b' }: { width?: number; color?: string }) => ( 4 | 12 | 17 | 18 | ) 19 | 20 | export default CheckMark 21 | -------------------------------------------------------------------------------- /src/components/svg/Email.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Email = ({ width = 24, }: { width?: number }) => ( 4 | 5 | 6 | 7 | ) 8 | 9 | export default Email -------------------------------------------------------------------------------- /src/components/svg/Ethos.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Ethos = ({ width = 24, color = '#1e293b' }: { width?: number; color?: string }) => ( 4 | 11 | 16 | 20 | 21 | ) 22 | 23 | export default Ethos 24 | -------------------------------------------------------------------------------- /src/components/svg/EthosEnclosed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const EthosEnclosed = ({ width = 24, color = '#6D28D9' }: { width?: number; color?: string }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default EthosEnclosed 20 | -------------------------------------------------------------------------------- /src/components/svg/EthosWalletIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EthosWalletIcon = ({ width=52 }: { width?: number }) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default EthosWalletIcon; -------------------------------------------------------------------------------- /src/components/svg/FallbackLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FallbackLogo = ({ width = 24}: { width?: number; color?: string }) => ( 4 | 5 | 6 | 7 | ) 8 | 9 | export default FallbackLogo 10 | -------------------------------------------------------------------------------- /src/components/svg/Github.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Github = ({ width = 24, color="#1B1F23" }: { width?: number; color?: string }) => ( 4 | 5 | 6 | 7 | ) 8 | 9 | export default Github -------------------------------------------------------------------------------- /src/components/svg/Google.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Google = ({ width = 24 }: { width?: number; color?: string }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | 14 | export default Google 15 | -------------------------------------------------------------------------------- /src/components/svg/InstallWalletIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const InstallWalletIcon = ({ width=60 }: { width?: number }) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default InstallWalletIcon; -------------------------------------------------------------------------------- /src/components/svg/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loader = ({ width = 100, color = '#333' }: { width?: number; color?: string }) => ( 4 | 15 | 16 | 23 | 24 | 25 | 32 | 33 | 34 | 41 | 42 | 43 | ) 44 | 45 | export default Loader 46 | -------------------------------------------------------------------------------- /src/components/svg/NoticeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NoticeIcon = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | 11 | export default NoticeIcon; -------------------------------------------------------------------------------- /src/components/svg/Sui.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Sui = ({ width = 24, color = '#6EBCEE' }: { width?: number; color?: string }) => ( 4 | 12 | 16 | 17 | ) 18 | 19 | export default Sui 20 | -------------------------------------------------------------------------------- /src/components/svg/SuiEnclosed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SuiEnclosed = ({ width = 32 }: { width?: number }) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default SuiEnclosed; -------------------------------------------------------------------------------- /src/components/svg/WalletsIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const WalletsIcon = ({ width = 32 }) => ( 4 | 5 | ); 6 | 7 | export default WalletsIcon 8 | 9 | const dataUri = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAAqACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAAQdIdCAAAACXBIWXMAAAsTAAALEwEAmpwYAAACZmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjU2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiB3yYcAAAgwSURBVFgJrZddiJ1HGcefmXfe9z3n7G42yTYamy9sklZT0XilAeuFdwXxQrD4gVKwF6IXohdS8CYXJWJFwQvprXhTaOhVFakFjbGWioqItKYXprVda0OaNNnd8/F+jr//nN1k16RCwLM778w8M2f+//k/zzzzHme3+Zw+/Zuw+94TK7mvDucxHCws3h2ie19u9p68d3vy3kaFuYG3diH4We/9dJaF2SzLKG58xWfTyz4b/zvPJqsun6zW9eS1j2785ap76Gz333Buu+H0M2+OwrQ5Wzj/YBbNhWiWR2eAG+CxiJ6+WeijK5KNMWssy6YGCdWR4pwbWwgTbGNsam9QxtH5jSfqS6vfuv+hs/UW7g0C33l69WARu38GMx/Mxaw3l5uAXQypPScjQihgxSY51cE3mwQmAApY9Rx8Xs8JZNm6y/z6uCjHRw5+7GdXRMLr8bmnYtZZe6Hx0TdQalUYaVy02qKTjfpGUb/xLjbeotpdn1uMZey60vp+sFlKm/dxYJRddR77mC+0bfZyjKcTNhs221e89khjbhTZTc+DDbrAwmgWIx0sUMGgj6MbkQE3yOY8hTnWFy74HlB1OnOuTaXvW8BZ3eUOEtE5EQn73njxwleY+FMRcJ23M1BL6wQWjJDQ4n3sXX+TiAMSQoBDTNj84SAm43XYQqSkyZS+x9IB1lJvEaghUtOvWB8Sff6DRODrT720UFm717uMfTrWnxPp5VuBg8KSLnrUoEMcYmQO+8eDPMRHIokWoduVLkge1yXwmwTkkgqCBTVh7sPKhec/sxTqpeye2MKSBWPEsS5jB2wGcJFgYzBXu8MGCW1Wam0S2VJLdqbyBUK4G9CUC+SKDkAFacPUGn2IpjgDo3QL+cL7Q9O3xwnF6NmlAc6IdgtgFg/sLu3LJ1fcgeVCS+uTMObN2z53jM/GtV144YJV641IMFZBpKZdooACM9wXWl8fz5JgWfSA9z7AFAlQ5Esn73YXrkztr29xtFAjFThkYCM2tUs29jzvy0ZRX8d3tOjcoRNH7JUX1llPKtSclArwASoUwJTHyTHN0R4JCDnAgQDcm1c02PdffD1WtUAUIT7mSB9UEwvzXOBSPiixFXBWTijx2QAxKW5A0AyH5Jx+gd12gDeAb5EYupDNjobeNUcURYouoOZuYEGcZSujwj1y6qAd2lVK7y15t+rtPridzaYbnf3t3FWbrin4dCIatK6oZxAiX8TyMATa/TEpAAGCjJ0TTiLU2Vc/csxevrJmf740D3+FmcY0C0QiZu4WuUJp+0ZNWxlz12Kwwx9espfP1wQmIU1K0zH0viJTzmLXTvZLgZUEmBbPmCAAojLL4mN//LubNQJnaSnU4wg8zHFjVsAluAIg+bsg4AesgQtSe8isAbbdRWBmsEG3IAWso2RZzcGb4o7RXaHz7aLETwBKe0CwbgJZGZX2tQ/da0eWRtvltrcnjT3661f5SmYd5Fpi49EHD9reUUqsW+5wG+PWfnv+HZtWMCFdu3YXB4yk39aAJwUW2G49FKuOC2WzxN63Ue1vn/zALeBictcot6JouTMq7oTaFtnCJvgOoosLwT5+atnGxPaEUsXSte0ujuEuFFgiNS+XvnUtV3zDGaAA2rua01CTnmvbNxzsWHB75wv3v9f2LMRE5NP3LW8f2tFe2V0kAmOO9QTPVt3QmmY5EWm7xSKwW6U7PImL0pNZ8rfy/c3I37GoOg8cWEnlloHbGDa0lBJLRzzpr1s03yovVBbKjszEaJ/h+5TUcS2HheeWL2+z5J2Z1nRLEictSyrLKF35djdx0MQwaGrdXSRBT4Q6FXI+9wH1ncG8++w13VESgA2S7ri5kKHjZNV72jDqmoqoJxB1oyYCUoIY+N8ueHe4W0fGWU3A5rzgoIRncyDo7vTdoAnDtpr20Q+VfuZF11Jq/98UaMIYF5Sx4RqucUUdUJvXUw79LAyb9hqS7NXZl9vlfmKCE+Hj6vXL7uDyvlu3dAeWy7OpVdm6Zb7idJXWdgOrRQYNeO275kdVdWnU1kaJo7oylYWmjov1zD39p1/atcnaHcDtnHpx4x0789IfrM7XrQprbPe6zYprcZJfd1fyqb0Z2kth2NcX0fyUAkMxoIPCoZASce3tVffjZ35CctTdKGWQDWWSSvhQth7VFFyKGQq+9lw7ngSVWRMCL668H2WB3Sqd58R/4ZqsjDnvBE278I8wqGbnvPdf1GKQSGBC2GzLTyI1J5cIklXlrnRSAYXuZuyk+gYZEfEqEMnmZQaZJuSxznJXe37a+Py5EKb1s2WZ6fbV4dAjPclJMuEphkDTmKaIkKZIKeWt+Ty1PQTS24RebrFLFdRhXg8JyEglq7MM8GD8lLK1xp8XsHvyiW+us+ZIgHMeetObExGHBK7BTRLCTqQSmTkx2cQwqYUdYonknKyIY9h0JW7D6378jYfP7ArazJM/XPvgMC9f1wJbsFpbfX2SCKigQyLkZEqDEkZC6V/jtJMkvGGk9uZyvNUq/2HizYpET32tqk5gSotRm/388YcfKDN/PnW0fxoq+iCiFhbXTTt1wo2WycjZVl9trjjJqJd8ARF0OANb1ne8pDCgV+wufvKe7/3id/O1E8T88exjnz+0lGWvADwUqAIg1VKG72q3XFWprcX17qzzke4aEjpgAqSvObKnLwlUDX1/YzKLJ4796FdvbMFubXKrr9r9/rufPTwo4icG0T5F/yjy7WeHe1iIG94VLM7VlX6qCEgy8g9i1M9FXvzMJkhyFa5vMXiRS/D5qqueO/b4uX9p4naw/wDv1ZplvOGRpgAAAABJRU5ErkJggg==`; 10 | -------------------------------------------------------------------------------- /src/enums/AddressWidgetButtons.ts: -------------------------------------------------------------------------------- 1 | export enum AddressWidgetButtons { 2 | CopyWalletAddress = "copy_wallet_address", 3 | WalletExplorer = "wallet_explorer", 4 | Logout = "logout" 5 | } -------------------------------------------------------------------------------- /src/enums/Breakpoints.ts: -------------------------------------------------------------------------------- 1 | export enum Breakpoints { 2 | 'sm' = 640, 3 | 'md' = 768, 4 | 'lg' = 1024, 5 | 'xl' = 1280, 6 | '2xl' = 1536, 7 | } 8 | -------------------------------------------------------------------------------- /src/enums/Chain.ts: -------------------------------------------------------------------------------- 1 | export enum Chain { 2 | SUI_MAINNET = 'sui:mainnet', 3 | SUI_TESTNET = 'sui:testnet', 4 | SUI_DEVNET = 'sui:devnet', 5 | SUI_CUSTOM = 'sui:custom', 6 | } 7 | -------------------------------------------------------------------------------- /src/enums/EthosConnectStatus.ts: -------------------------------------------------------------------------------- 1 | export enum EthosConnectStatus { 2 | Loading = "loading", 3 | NoConnection = "no_connection", 4 | Connected = "connected" 5 | } -------------------------------------------------------------------------------- /src/hooks/hooks.ts: -------------------------------------------------------------------------------- 1 | import useModal from "./useModal"; 2 | import useWallet from "./useWallet"; 3 | import useWindowDimensions from './useWindowDimensions' 4 | 5 | const hooks = { 6 | useModal, 7 | useWallet, 8 | useWindowDimensions 9 | } 10 | 11 | export default hooks; -------------------------------------------------------------------------------- /src/hooks/useAccount.ts: -------------------------------------------------------------------------------- 1 | import getWalletContents from '../lib/getWalletContents'; 2 | import { useEffect, useRef, useState } from 'react' 3 | import { Signer } from 'types/Signer'; 4 | import { WalletContents } from '../types/WalletContents'; 5 | import { WalletAccount } from '@mysten/wallet-standard'; 6 | import { InvalidPackages } from '../types/InvalidPackages'; 7 | 8 | export type Account = { 9 | address?: string; 10 | contents?: WalletContents; 11 | } 12 | 13 | const useAccount = (signer: Signer | null, network: string, explicitInterval?: number, invalidPackages?: InvalidPackages) => { 14 | const [altAccount, setAltAccount] = useState(); 15 | const [account, setAccount] = useState({}); 16 | const latestNetwork = useRef(network); 17 | const existingContents = useRef(); 18 | 19 | useEffect(() => { 20 | if (!signer) return; 21 | latestNetwork.current = network; 22 | 23 | const initAccount = async () => { 24 | const address = altAccount?.address ?? signer.currentAccount?.address 25 | if (!address) { 26 | return 27 | } 28 | setAccount((prev) => { 29 | if (prev.address === address) return prev; 30 | return { ...prev, address } 31 | }); 32 | 33 | const contents = await getWalletContents({ 34 | address, 35 | network, 36 | existingContents: existingContents.current, 37 | invalidPackageModifications: invalidPackages 38 | }); 39 | 40 | if (!contents || network !== latestNetwork.current || JSON.stringify(existingContents.current) === JSON.stringify(contents)) return; 41 | 42 | existingContents.current = contents; 43 | setAccount((prev) => ({ ...prev, contents })) 44 | } 45 | 46 | initAccount(); 47 | const interval = setInterval(initAccount, explicitInterval ?? 5000); 48 | 49 | return () => clearInterval(interval); 50 | }, [network, signer, altAccount]) 51 | 52 | return { account, altAccount, setAltAccount }; 53 | } 54 | 55 | export default useAccount; -------------------------------------------------------------------------------- /src/hooks/useAddress.ts: -------------------------------------------------------------------------------- 1 | import useProviderAndSigner from "./useClientAndSigner"; 2 | 3 | const useAddress = () => { 4 | const { signer } = useProviderAndSigner(); 5 | 6 | return signer?.currentAccount?.address; 7 | } 8 | 9 | export default useAddress; -------------------------------------------------------------------------------- /src/hooks/useClientAndSigner.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ClientAndSigner } from 'types/ClientAndSigner'; 3 | import ConnectContext from '../components/ConnectContext'; 4 | 5 | const useClientAndSigner = (): ClientAndSigner => { 6 | const { clientAndSigner } = useContext(ConnectContext); 7 | 8 | return clientAndSigner || { client: null, signer: null }; 9 | } 10 | 11 | export default useClientAndSigner; -------------------------------------------------------------------------------- /src/hooks/useConnect.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react' 2 | import log from '../lib/log' 3 | import useWalletKit from './useWalletKit' 4 | // import listenForMobileConnection from '../lib/listenForMobileConnection' 5 | import { ClientAndSigner } from '../types/ClientAndSigner' 6 | import { ExtensionSigner, HostedSigner } from 'types/Signer' 7 | import lib from '../lib/lib' 8 | import { EthosConfiguration } from '../types/EthosConfiguration' 9 | import { DEFAULT_CHAIN, DEFAULT_NETWORK } from '../lib/constants'; 10 | import { WalletKitCoreConnectionStatus } from '@mysten/wallet-kit-core' 11 | import { SuiClient } from '@mysten/sui.js/client' 12 | 13 | const useConnect = (ethosConfiguration?: EthosConfiguration, onWalletConnected?: (clientAndSigner: ClientAndSigner) => void) => { 14 | const signerFound = useRef(false) 15 | const methodsChecked = useRef({ 16 | 'ethos': false, 17 | // 'mobile': false, 18 | 'extension': false 19 | }) 20 | 21 | const client = useMemo(() => { 22 | const network = typeof ethosConfiguration?.network === "string" ? ethosConfiguration.network : DEFAULT_NETWORK 23 | const client = new SuiClient({url: network}) 24 | return client 25 | }, [ethosConfiguration]) 26 | 27 | const [clientAndSigner, setClientAndSigner] = useState({client: null, signer: null}) 28 | 29 | const { 30 | wallets, 31 | status: suiStatus, 32 | signer: suiSigner, 33 | getState, 34 | connect 35 | } = useWalletKit({ 36 | client, 37 | defaultChain: ethosConfiguration?.chain ?? DEFAULT_CHAIN, 38 | preferredWallets: ethosConfiguration?.preferredWallets, 39 | disableAutoConnect: ethosConfiguration?.disableAutoConnect 40 | }); 41 | 42 | const disconnect = useCallback(() => { 43 | signerFound.current = false; 44 | methodsChecked.current = { 45 | 'ethos': false, 46 | // 'mobile': false, 47 | 'extension': false 48 | } 49 | 50 | setClientAndSigner((prev) => ({ 51 | ...prev, 52 | signer: null 53 | })); 54 | }, []) 55 | 56 | useEffect(() => { 57 | signerFound.current = false; 58 | methodsChecked.current = { 59 | 'ethos': false, 60 | // 'mobile': false, 61 | 'extension': false 62 | } 63 | }, [ethosConfiguration]); 64 | 65 | useEffect(() => { 66 | const { client, signer } = clientAndSigner; 67 | if (!client && !signer) return; 68 | 69 | const extensionState = getState(); 70 | if (extensionState.isConnecting || extensionState.isError) return; 71 | 72 | onWalletConnected && onWalletConnected(clientAndSigner) 73 | }, [suiStatus, clientAndSigner, onWalletConnected, getState]); 74 | 75 | const checkSigner = useCallback((signer: ExtensionSigner | HostedSigner | null, type?: string) => { 76 | log("useConnect", "trying to set clientAndSigner", type, signerFound.current, methodsChecked.current) 77 | if (signerFound.current) return; 78 | 79 | if (type) { 80 | methodsChecked.current[type] = true; 81 | } 82 | 83 | const allMethodsChecked = !Object.values(methodsChecked.current).includes(false) 84 | if (!signer && !allMethodsChecked) return; 85 | 86 | signerFound.current = !!signer; 87 | 88 | if (signer) { 89 | const _disconnect = signer?.disconnect; 90 | signer.disconnect = () => { 91 | _disconnect(); 92 | disconnect(); 93 | } 94 | } 95 | 96 | setClientAndSigner({ client, signer }) 97 | }, [client, disconnect]); 98 | 99 | useEffect(() => { 100 | if (suiStatus === WalletKitCoreConnectionStatus.DISCONNECTED) { 101 | methodsChecked.current["extension"] = false; 102 | signerFound.current = false; 103 | setClientAndSigner((prev) => ({ 104 | ...prev, 105 | signer: null 106 | })) 107 | } 108 | }, [suiStatus]) 109 | 110 | useEffect(() => { 111 | if (!ethosConfiguration) return; 112 | 113 | log("mobile", "listening to mobile connection from EthosConnectProvider") 114 | // listenForMobileConnection( 115 | // (mobileSigner: any) => { 116 | // log('useConnect', 'Setting clientAndSigner mobile', mobileSigner) 117 | // log("mobile", "Setting client and signer", mobileSigner) 118 | // checkSigner(mobileSigner, 'mobile') 119 | // } 120 | // ) 121 | }, [checkSigner, ethosConfiguration]) 122 | 123 | useEffect(() => { 124 | if (!ethosConfiguration) return; 125 | 126 | const state = getState(); 127 | log('useConnect', 'Setting clientAndSigner extension', state) 128 | if (state.isConnecting || state.isError) return 129 | 130 | checkSigner(suiSigner, 'extension') 131 | }, [suiStatus, getState, checkSigner, suiSigner, ethosConfiguration]) 132 | 133 | useEffect(() => { 134 | if (!ethosConfiguration) return; 135 | 136 | if (!ethosConfiguration.apiKey) { 137 | log('useConnect', 'Setting null clientAndSigner ethos'); 138 | checkSigner(null, 'ethos'); 139 | return; 140 | } 141 | 142 | const fetchEthosSigner = async () => { 143 | const signer = await lib.getEthosSigner({ client, defaultChain: ethosConfiguration.chain ?? DEFAULT_CHAIN }) 144 | log('useConnect', 'Setting clientAndSigner ethos', signer) 145 | checkSigner(signer, 'ethos'); 146 | } 147 | 148 | fetchEthosSigner() 149 | }, [client, checkSigner, ethosConfiguration]) 150 | 151 | return { wallets, clientAndSigner, connect, getState }; 152 | } 153 | 154 | export default useConnect; -------------------------------------------------------------------------------- /src/hooks/useContents.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import ConnectContext from '../components/ConnectContext'; 3 | 4 | const useContents = () => { 5 | const contents = useContext(ConnectContext).wallet?.wallet?.contents; 6 | 7 | return contents; 8 | } 9 | 10 | export default useContents; -------------------------------------------------------------------------------- /src/hooks/useContext.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useCallback, 3 | useEffect, 4 | useMemo, 5 | useState, 6 | } from 'react' 7 | import lib from '../lib/lib' 8 | import log from '../lib/log' 9 | import { WalletContextContents } from '../types/WalletContextContents' 10 | import useAccount from './useAccount' 11 | import useConnect from './useConnect' 12 | import { EthosConnectStatus } from '../enums/EthosConnectStatus' 13 | import { ModalContextContents } from '../types/ModalContextContents'; 14 | import { ConnectContextContents } from '../types/ConnectContextContents'; 15 | import { EthosConfiguration } from '../types/EthosConfiguration'; 16 | import { ClientAndSigner } from '../types/ClientAndSigner'; 17 | import { DEFAULT_NETWORK, DEFAULT_CHAIN } from '../lib/constants'; 18 | 19 | export interface UseContextArgs { 20 | configuration?: EthosConfiguration, 21 | onWalletConnected?: (clientAndSigner: ClientAndSigner) => void 22 | } 23 | 24 | const DEFAULT_CONFIGURATION = { 25 | network: DEFAULT_NETWORK, 26 | chain: DEFAULT_CHAIN, 27 | walletAppUrl: 'https://ethoswallet.xyz' 28 | } 29 | 30 | const useContext = ({ configuration, onWalletConnected }: UseContextArgs): ConnectContextContents => { 31 | const [ethosConfiguration, setEthosConfiguration] = useState({ 32 | ...DEFAULT_CONFIGURATION, 33 | ...configuration 34 | }) 35 | const [isModalOpen, setIsModalOpen] = useState(false); 36 | 37 | const init = useCallback((config: EthosConfiguration) => { 38 | log('EthosConnectProvider', 'EthosConnectProvider Configuration:', config) 39 | const fullConfiguration = { 40 | ...DEFAULT_CONFIGURATION, 41 | ...config 42 | } 43 | lib.initializeEthos(fullConfiguration) 44 | setEthosConfiguration((prev) => { 45 | if (JSON.stringify(fullConfiguration) !== JSON.stringify(prev)) { 46 | return fullConfiguration; 47 | } else { 48 | return prev; 49 | } 50 | }) 51 | }, []); 52 | 53 | useEffect(() => { 54 | lib.initializeEthos(ethosConfiguration) 55 | }, [ethosConfiguration]) 56 | 57 | useEffect(() => { 58 | if (!configuration) return; 59 | if (JSON.stringify(ethosConfiguration) === JSON.stringify(configuration)) return; 60 | init(configuration); 61 | }, [ethosConfiguration, configuration]); 62 | 63 | const _onWalletConnected = useCallback((clientAndSigner: ClientAndSigner) => { 64 | setIsModalOpen(false); 65 | onWalletConnected && onWalletConnected(clientAndSigner); 66 | }, [onWalletConnected]); 67 | 68 | const { 69 | wallets, 70 | connect: selectWallet, 71 | clientAndSigner, 72 | getState 73 | } = useConnect(ethosConfiguration, _onWalletConnected) 74 | 75 | const { 76 | account: { address, contents }, 77 | altAccount, 78 | setAltAccount 79 | } = useAccount( 80 | clientAndSigner.signer, 81 | ethosConfiguration?.network ?? DEFAULT_NETWORK, 82 | ethosConfiguration?.pollingInterval, 83 | ethosConfiguration?.invalidPackages 84 | ) 85 | 86 | const modal: ModalContextContents = useMemo(() => { 87 | const openModal = () => { 88 | setIsModalOpen(true) 89 | } 90 | 91 | const closeModal = () => { 92 | setIsModalOpen(false) 93 | } 94 | 95 | return { 96 | isModalOpen, 97 | openModal, 98 | closeModal 99 | } 100 | }, [isModalOpen, setIsModalOpen]) 101 | 102 | const wallet = useMemo(() => { 103 | const { client, signer } = clientAndSigner; 104 | const extensionState = getState(); 105 | let status; 106 | 107 | if (signer?.type === 'hosted') { 108 | status = EthosConnectStatus.Connected 109 | } else if (extensionState.isConnecting) { 110 | status = EthosConnectStatus.Loading 111 | } else if (client && extensionState.isConnected && signer) { 112 | status = EthosConnectStatus.Connected 113 | } else { 114 | status = EthosConnectStatus.NoConnection 115 | } 116 | 117 | const context: WalletContextContents = { 118 | status, 119 | wallets: wallets.map(w => ({ 120 | ...w, 121 | name: w.name, 122 | icon: w.icon, 123 | })), 124 | selectWallet, 125 | client, 126 | altAccount, 127 | setAltAccount 128 | } 129 | 130 | if (signer && address) { 131 | context.wallet = { 132 | ...signer, 133 | address, 134 | contents 135 | } 136 | } 137 | 138 | return context; 139 | }, [ 140 | wallets, 141 | selectWallet, 142 | address, 143 | altAccount, 144 | setAltAccount, 145 | clientAndSigner, 146 | contents, 147 | ethosConfiguration 148 | ]) 149 | 150 | useEffect(() => { 151 | if (isModalOpen) { 152 | document.getElementsByTagName('html').item(0)?.setAttribute('style', 'overflow: hidden;') 153 | } else { 154 | document.getElementsByTagName('html').item(0)?.setAttribute('style', '') 155 | } 156 | }, [isModalOpen]) 157 | 158 | const value = useMemo(() => ({ 159 | wallet, 160 | modal, 161 | clientAndSigner 162 | }), [wallet, modal, clientAndSigner]); 163 | 164 | return { ...value, ethosConfiguration, init } 165 | } 166 | 167 | export default useContext; -------------------------------------------------------------------------------- /src/hooks/useEthosConnect.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import ConnectContext from '../components/ConnectContext'; 3 | 4 | const useEthosConnect = () => { 5 | return useContext(ConnectContext) 6 | } 7 | 8 | export default useEthosConnect; -------------------------------------------------------------------------------- /src/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import ConnectContext from '../components/ConnectContext'; 3 | 4 | const useModal = () => { 5 | const { modal } = useContext(ConnectContext) 6 | 7 | return modal 8 | } 9 | 10 | export default useModal; -------------------------------------------------------------------------------- /src/hooks/useWallet.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import ConnectContext from '../components/ConnectContext'; 3 | import { WalletContextContents } from '../types/WalletContextContents'; 4 | import { EthosConnectStatus } from "../enums/EthosConnectStatus"; 5 | 6 | const useWallet = (): WalletContextContents => { 7 | const { wallet } = useContext(ConnectContext); 8 | 9 | return wallet ?? { status: EthosConnectStatus.Loading, client: null, setAltAccount: () => { } }; 10 | } 11 | 12 | export default useWallet; -------------------------------------------------------------------------------- /src/hooks/useWalletKit.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react' 2 | import { createWalletKitCore } from '@mysten/wallet-kit-core' 3 | import type { WalletKitCore, StorageAdapter } from '@mysten/wallet-kit-core' 4 | import { ExtensionSigner, SignerType } from '../types/Signer'; 5 | import { EthosSignMessageInput } from '../types/EthosSignMessageInput'; 6 | import { EthosSignAndExecuteTransactionBlockInput } from '../types/EthosSignAndExecuteTransactionBlockInput'; 7 | import { EthosSignTransactionBlockInput } from '../types/EthosSignTransactionBlockInput'; 8 | import { DEFAULT_CHAIN } from '../lib/constants'; 9 | import { Preapproval } from 'types/Preapproval'; 10 | import { Chain } from 'enums/Chain'; 11 | import { SuiClient, SuiTransactionBlockResponse } from '@mysten/sui.js/client'; 12 | import { EthosExecuteTransactionBlockInput } from 'types/EthosExecuteTransactionBlockInput'; 13 | import { SuiSignPersonalMessageOutput, SuiSignTransactionBlockOutput, SuiSignMessageInput } from '@mysten/wallet-standard'; 14 | 15 | export interface UseWalletKitArgs { 16 | defaultChain: Chain 17 | client: SuiClient; 18 | features?: string[]; 19 | enableUnsafeBurner?: boolean; 20 | preferredWallets?: string[]; 21 | storageAdapter?: StorageAdapter; 22 | storageKey?: string; 23 | disableAutoConnect?: boolean; 24 | } 25 | 26 | const useWalletKit = ({ defaultChain, client, preferredWallets, storageAdapter, storageKey, disableAutoConnect }: UseWalletKitArgs) => { 27 | const walletKitRef = useRef(null); 28 | if (!walletKitRef.current) { 29 | walletKitRef.current = createWalletKitCore({ 30 | preferredWallets, 31 | storageAdapter, 32 | storageKey, 33 | }); 34 | } 35 | 36 | // Automatically trigger the autoconnect logic when we mount, and whenever wallets change: 37 | const { wallets, status, currentWallet, accounts, currentAccount } = useSyncExternalStore( 38 | walletKitRef.current.subscribe, 39 | walletKitRef.current.getState, 40 | walletKitRef.current.getState 41 | ); 42 | 43 | useEffect(() => { 44 | if (!disableAutoConnect) { 45 | walletKitRef.current?.autoconnect(); 46 | } 47 | }, [status, wallets]); 48 | 49 | const { autoconnect, ...walletFunctions } = walletKitRef.current; 50 | 51 | const signAndExecuteTransactionBlock = useCallback((input: EthosSignAndExecuteTransactionBlockInput): Promise => { 52 | if (!currentWallet || !currentAccount) { 53 | throw new Error("No wallet connect to sign message"); 54 | } 55 | 56 | const account = input.account || currentAccount 57 | const chain = input.chain || defaultChain || DEFAULT_CHAIN 58 | return currentWallet.features['sui:signAndExecuteTransactionBlock'].signAndExecuteTransactionBlock({ 59 | ...input, 60 | account, 61 | chain 62 | }) 63 | }, [currentWallet, currentAccount, defaultChain]) 64 | 65 | const executeTransactionBlock = useCallback((input: EthosExecuteTransactionBlockInput): Promise => { 66 | return client.executeTransactionBlock(input) 67 | }, [client]) 68 | 69 | const signTransactionBlock = useCallback((input: EthosSignTransactionBlockInput): Promise => { 70 | if (!currentWallet || !currentAccount) { 71 | throw new Error("No wallet connect to sign message"); 72 | } 73 | 74 | const account = input.account || currentAccount 75 | const chain = input.chain || defaultChain || DEFAULT_CHAIN 76 | 77 | return currentWallet.features['sui:signTransactionBlock'].signTransactionBlock({ 78 | ...input, 79 | account, 80 | chain 81 | }) 82 | }, [currentWallet, currentAccount, defaultChain]) 83 | 84 | const signPersonalMessage = useCallback((input: EthosSignMessageInput): Promise => { 85 | if (!currentWallet || !currentAccount) { 86 | throw new Error("No wallet connect to sign message"); 87 | } 88 | 89 | const account = input.account || currentAccount 90 | 91 | const message = typeof input.message === 'string' ? 92 | new TextEncoder().encode(input.message) : 93 | input.message; 94 | 95 | const legacySignMessage = async (input: SuiSignMessageInput) => { 96 | const response = await currentWallet.features['sui:signMessage']?.signMessage(input) 97 | return { 98 | ...response, 99 | bytes: response?.messageBytes 100 | } 101 | } 102 | 103 | const signFunction = currentWallet.features['sui:signPersonalMessage']?.signPersonalMessage ?? legacySignMessage 104 | 105 | return signFunction({ 106 | ...input, 107 | message, 108 | account, 109 | }) 110 | }, [currentWallet, currentAccount]) 111 | 112 | const requestPreapproval = useCallback(async (preapproval: Preapproval) => { 113 | if (!currentWallet || !currentAccount) { 114 | throw new Error("No wallet connect to preapprove transactions"); 115 | } 116 | 117 | const ethosWallet = (window as any).ethosWallet 118 | if (!ethosWallet || ["Ethos Wallet", "Ethos Mobile"].indexOf(currentWallet.name) === -1) { 119 | console.log("Wallet does not support preapproval") 120 | return false; 121 | } 122 | 123 | if (!preapproval.address) { 124 | preapproval.address = currentAccount.address; 125 | } 126 | 127 | if (!preapproval.chain) { 128 | preapproval.chain = defaultChain ?? DEFAULT_CHAIN; 129 | } 130 | 131 | return ethosWallet.requestPreapproval(preapproval) 132 | }, [currentWallet, currentAccount, defaultChain]) 133 | 134 | const constructedSigner = useMemo(() => { 135 | if (!currentWallet || !currentAccount) return null; 136 | 137 | return { 138 | type: SignerType.Extension, 139 | name: currentWallet.name, 140 | icon: currentWallet.icon, 141 | getAddress: async () => currentAccount?.address, 142 | accounts, 143 | currentAccount, 144 | signAndExecuteTransactionBlock, 145 | executeTransactionBlock, 146 | signTransactionBlock, 147 | requestPreapproval, 148 | signPersonalMessage, 149 | disconnect: () => { 150 | // currentWallet.features['standard:connect'].connect(); 151 | walletKitRef.current?.disconnect(); 152 | }, 153 | client 154 | } 155 | }, [ 156 | currentWallet, 157 | accounts, 158 | currentAccount, 159 | signAndExecuteTransactionBlock, 160 | executeTransactionBlock, 161 | requestPreapproval, 162 | signPersonalMessage, 163 | client 164 | ]); 165 | 166 | return { 167 | wallets, 168 | status, 169 | signer: constructedSigner, 170 | ...walletFunctions 171 | } 172 | } 173 | 174 | export default useWalletKit -------------------------------------------------------------------------------- /src/hooks/useWindowDimensions.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export type WindowDimensions = { 4 | width: number, 5 | height: number 6 | } 7 | 8 | function getWindowDimensions(): WindowDimensions { 9 | if (typeof window === 'undefined') return { width: 0, height: 0 } 10 | const { innerWidth: width, innerHeight: height } = window 11 | return { 12 | width, 13 | height, 14 | } 15 | } 16 | 17 | export default function useWindowDimensions() { 18 | const [windowDimensions, setWindowDimensions] = useState( 19 | { width: 0, height: 0 } 20 | ) 21 | 22 | useEffect(() => { 23 | setWindowDimensions(getWindowDimensions()) 24 | 25 | function handleResize() { 26 | setWindowDimensions(getWindowDimensions()) 27 | } 28 | 29 | window.addEventListener('resize', handleResize) 30 | return () => window.removeEventListener('resize', handleResize) 31 | }, []) 32 | return windowDimensions 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { SuiClient } from '@mysten/sui.js/client'; 2 | import { TransactionBlock } from '@mysten/sui.js/transactions'; 3 | import { WalletAccount } from '@mysten/wallet-standard'; 4 | 5 | import EthosConnectProvider from './components/EthosConnectProvider'; 6 | import SignInButton from './components/styled/SignInButton'; 7 | 8 | import { hideSignInModal, showSignInModal } from './components/styled/SignInModal'; 9 | 10 | import HoverColorButton from './components/headless/HoverColorButton'; 11 | import AddressWidget from './components/styled/AddressWidget'; 12 | import MenuButton from './components/styled/MenuButton'; 13 | 14 | import getWalletContents, { ipfsConversion } from './lib/getWalletContents'; 15 | import login from './lib/login'; 16 | import logout from './lib/logout'; 17 | import preapprove from './lib/preapprove'; 18 | import signMessage from './lib/signMessage'; 19 | import transact from './lib/transact'; 20 | import signTransactionBlock from './lib/signTransactionBlock'; 21 | import executeTransactionBlock from './lib/executeTransactionBlock'; 22 | import checkForAssetType from './lib/checkForAssetType'; 23 | 24 | import hideWallet from './lib/hideWallet'; 25 | import showWallet from './lib/showWallet'; 26 | 27 | import { formatBalance } from './lib/bigNumber'; 28 | import dripSui from './lib/dripSui'; 29 | import { getSuiAddress, getSuiName } from './lib/nameService'; 30 | import truncateMiddle from './lib/truncateMiddle'; 31 | 32 | import useAddress from './hooks/useAddress'; 33 | import useContents from './hooks/useContents'; 34 | import useClientAndSigner from './hooks/useClientAndSigner'; 35 | import useWallet from './hooks/useWallet'; 36 | 37 | // Enums (must be exported as objects, NOT types) 38 | import { AddressWidgetButtons } from './enums/AddressWidgetButtons'; 39 | import { Chain } from './enums/Chain'; 40 | import { EthosConnectStatus } from './enums/EthosConnectStatus'; 41 | 42 | // Types, interfaces, and enums 43 | import { ClientAndSigner } from './types/ClientAndSigner'; 44 | import { Signer } from './types/Signer'; 45 | import { Wallet } from './types/Wallet'; 46 | import { SuiNFT, Token, WalletContents } from './types/WalletContents'; 47 | 48 | import DetachedEthosConnectProvider from './components/DetachedEthosConnectProvider'; 49 | import useContext from './hooks/useContext'; 50 | import { ExtendedSuiObjectData } from './types/ExtendedSuiObjectData'; 51 | 52 | const components = { 53 | AddressWidget, 54 | MenuButton, 55 | headless: { 56 | HoverColorButton 57 | } 58 | } 59 | 60 | const enums = { 61 | AddressWidgetButtons 62 | } 63 | 64 | 65 | const ethos = { 66 | login, 67 | logout, 68 | 69 | signMessage, 70 | transact, 71 | signTransactionBlock, 72 | executeTransactionBlock, 73 | preapprove, 74 | 75 | showWallet, 76 | hideWallet, 77 | 78 | showSignInModal, 79 | hideSignInModal, 80 | 81 | useClientAndSigner, 82 | useAddress, 83 | useContents, 84 | useWallet, 85 | useContext, 86 | getWalletContents, 87 | checkForAssetType, 88 | 89 | dripSui, 90 | getSuiName, 91 | getSuiAddress, 92 | formatBalance, 93 | truncateMiddle, 94 | ipfsConversion, 95 | 96 | components, 97 | 98 | enums 99 | } 100 | 101 | export { 102 | EthosConnectProvider, 103 | DetachedEthosConnectProvider, 104 | SignInButton, 105 | ethos, 106 | EthosConnectStatus, 107 | TransactionBlock, 108 | Chain 109 | }; 110 | 111 | export type { 112 | Wallet, 113 | WalletAccount, 114 | WalletContents, 115 | ClientAndSigner, 116 | Signer, 117 | SuiNFT, 118 | Token, 119 | ExtendedSuiObjectData, 120 | SuiClient 121 | }; 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/lib/activeUser.ts: -------------------------------------------------------------------------------- 1 | import getConfiguration from './getConfiguration' 2 | import getIframe from './getIframe' 3 | import log from './log' 4 | import postIFrameMessage from './postIFrameMessage' 5 | 6 | const activeUser = () => { 7 | log('activeUser', 'Calling Active User') 8 | const { walletAppUrl, apiKey } = getConfiguration() 9 | 10 | log('activeUser', 'Configuration', walletAppUrl, apiKey); 11 | const resolver = (resolve: any) => { 12 | const listener = (message: any) => { 13 | log('activeUser', 'Message Origin: ', message.origin, walletAppUrl, message) 14 | if (message.origin === walletAppUrl) { 15 | const { action, data } = message.data 16 | log('activeUser', "Message From Wallet", action, data) 17 | if (action === 'user' && data.apiKey === apiKey) { 18 | window.removeEventListener('message', listener) 19 | resolve(data?.user) 20 | } 21 | } 22 | } 23 | window.addEventListener('message', listener) 24 | 25 | const message = { action: 'activeUser' } 26 | log('activeUser', 'getIframe'); 27 | getIframe() 28 | log('activeUser", "Post message to the iframe', message) 29 | postIFrameMessage(message) 30 | } 31 | 32 | return new Promise(resolver) 33 | } 34 | 35 | export default activeUser 36 | -------------------------------------------------------------------------------- /src/lib/apiCall.ts: -------------------------------------------------------------------------------- 1 | import lib from './lib' 2 | const { getConfiguration } = lib; 3 | 4 | type ApiCallProps = { 5 | relativePath: string 6 | method?: string 7 | body?: any 8 | } 9 | 10 | const apiCall = async ({ relativePath, method = 'GET', body }: ApiCallProps) => { 11 | const { walletAppUrl } = getConfiguration() 12 | const data = { 13 | method: method, 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | Accept: 'application/json', 17 | }, 18 | } as any 19 | 20 | if (body) { 21 | data.body = JSON.stringify(body) 22 | } 23 | 24 | const response = await fetch(`${walletAppUrl}/api/${relativePath}`, data) 25 | const json = await response.json() 26 | const { status } = response 27 | return { json, status } 28 | } 29 | 30 | export default apiCall 31 | -------------------------------------------------------------------------------- /src/lib/bigNumber.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export const newBN = (value: number | string) => new BigNumber(value); 4 | 5 | export const sumBN = (balance: BigNumber | string | number, addition: BigNumber | string | number): BigNumber => { 6 | let bn = new BigNumber(balance.toString()); 7 | let bnAddition = new BigNumber(addition.toString()); 8 | return bn.plus(bnAddition); 9 | }; 10 | 11 | export const formatBalance = (balance?: string | bigint | number, decimals: number = 9) => { 12 | if (balance === undefined) return '---'; 13 | 14 | let postfix = ''; 15 | let bn = new BigNumber(balance.toString()).shiftedBy(-1 * decimals); 16 | 17 | if (bn.gte(1_000_000_000)) { 18 | bn = bn.shiftedBy(-9); 19 | postfix = ' B'; 20 | } else if (bn.gte(1_000_000)) { 21 | bn = bn.shiftedBy(-6); 22 | postfix = ' M'; 23 | } else if (bn.gte(10_000)) { 24 | bn = bn.shiftedBy(-3); 25 | postfix = ' K'; 26 | } 27 | 28 | if (bn.gte(1)) { 29 | bn = bn.decimalPlaces(3, BigNumber.ROUND_DOWN); 30 | } else { 31 | bn = bn.decimalPlaces(6, BigNumber.ROUND_DOWN); 32 | } 33 | 34 | return bn.toFormat() + postfix; 35 | } -------------------------------------------------------------------------------- /src/lib/checkForAssetType.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from "../types/Signer"; 2 | import { Wallet } from '../types/Wallet'; 3 | import { getKioskObjects } from "./getKioskNFT"; 4 | import { SuiObjectDataOptions, SuiObjectData, PaginatedObjectsResponse, SuiObjectDataFilter } from '@mysten/sui.js/client' 5 | 6 | export interface CheckForAssetTypeArgs { 7 | signer?: Signer; 8 | wallet?: Wallet; 9 | type?: string; 10 | cursor?: PaginatedObjectsResponse['nextCursor']; 11 | options?: SuiObjectDataOptions; 12 | filter?: SuiObjectDataFilter; 13 | } 14 | 15 | const checkForAssetType = async ({ signer, wallet, type, cursor, options, filter }: CheckForAssetTypeArgs) => { 16 | let owner; 17 | if (wallet) { 18 | owner = wallet.address; 19 | } else if (signer) { 20 | owner = signer.currentAccount?.address; 21 | } 22 | 23 | if (!owner) return; 24 | 25 | const client = (signer ?? wallet)?.client; 26 | 27 | if (!client) return; 28 | 29 | let kioskAssets: SuiObjectData[] = []; 30 | if (!cursor) { 31 | const kioskTokens = await client.getOwnedObjects({ 32 | owner, 33 | filter: { 34 | StructType: "0x95a441d389b07437d00dd07e0b6f05f513d7659b13fd7c5d3923c7d9d847199b::ob_kiosk::OwnerToken" 35 | }, 36 | options: options ?? { 37 | showContent: true, 38 | showType: true, 39 | }, 40 | cursor 41 | }) 42 | 43 | for (const kioskToken of kioskTokens.data) { 44 | if (kioskToken.data) { 45 | const kioskObjects = await getKioskObjects(client, kioskToken.data) 46 | for (const kioskObject of kioskObjects) { 47 | if (kioskObject.data && kioskObject.data?.type === type) { 48 | kioskAssets.push(kioskObject.data); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | const assets = await client.getOwnedObjects({ 56 | owner, 57 | filter: filter ?? { 58 | StructType: type ?? '' 59 | }, 60 | options: options ?? { 61 | showContent: true, 62 | showDisplay: true 63 | }, 64 | cursor 65 | }) 66 | 67 | return { 68 | assets: (assets.data ?? []).map((a) => a.data).concat(kioskAssets), 69 | nextCursor: assets.nextCursor 70 | } 71 | } 72 | 73 | export default checkForAssetType; -------------------------------------------------------------------------------- /src/lib/connectSui.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import log from './log' 3 | 4 | const connectSui = async (walletIdentifier: string) => { 5 | if (typeof window === 'undefined') return 6 | 7 | const suiWallet = (window as any)[walletIdentifier] 8 | const suiStore = store.namespace('sui') 9 | 10 | if (!suiWallet) { 11 | return false 12 | } 13 | 14 | try { 15 | let confirmed = await suiWallet.hasPermissions() 16 | if (!confirmed) { 17 | confirmed = await suiWallet.requestPermissions() 18 | } 19 | 20 | if (confirmed) { 21 | suiStore('disconnected', null); 22 | 23 | const accounts = await suiWallet.getAccounts() 24 | 25 | if (!accounts || accounts.length === 0) return false 26 | 27 | const storeResult = suiStore('account', accounts[0]) 28 | const success = window.dispatchEvent(new Event('ethos-storage-changed')) 29 | log('connectSui', 'Dispatch event-storage-changed', storeResult, success) 30 | } 31 | } catch (e) { 32 | console.log("Error connecting to Sui Wallet", e) 33 | return false; 34 | } 35 | 36 | 37 | return true 38 | } 39 | 40 | export default connectSui 41 | -------------------------------------------------------------------------------- /src/lib/connectWallet.ts: -------------------------------------------------------------------------------- 1 | import getConfiguration from './getConfiguration' 2 | 3 | const connectWallet = (apiKey: string) => { 4 | return new Promise((resolve) => { 5 | const { walletAppUrl } = getConfiguration() 6 | window.addEventListener('message', (message) => { 7 | if (message.origin === walletAppUrl) { 8 | const { action, data } = message.data 9 | if (action === 'user') { 10 | resolve(data.user) 11 | } 12 | } 13 | }) 14 | 15 | const returnTo = encodeURIComponent(window.location.href) 16 | window.open( 17 | `${walletAppUrl}/connect?apiKey=${apiKey}&returnTo=${returnTo}`, 18 | '_blank', 19 | `popup,top=100,left=${window.screen.width - 500},width=390,height=420` 20 | ) 21 | }) 22 | } 23 | 24 | export default connectWallet 25 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { Chain } from '../enums/Chain' 2 | 3 | export const primaryColor = "#6f53e4"; 4 | export const appBaseUrl = 5 | typeof window !== 'undefined' && window.location.origin.indexOf('http://localhost') === 0 6 | ? 'http://localhost:3000' 7 | : 'https://ethoswallet.onrender.com' 8 | 9 | export const captchaSiteKey = '6LcXUDshAAAAAPTZ3E7xi3-335IA9rncYVoey_ls'; 10 | export const DEFAULT_NETWORK = "https://fullnode.testnet.sui.io/" 11 | export const DEFAULT_FAUCET = "https://faucet.testnet.sui.io/" 12 | export const DEFAULT_CHAIN = Chain.SUI_TESTNET -------------------------------------------------------------------------------- /src/lib/dripSui.ts: -------------------------------------------------------------------------------- 1 | import { getFaucetHost, requestSuiFromFaucetV0 } from '@mysten/sui.js/faucet'; 2 | 3 | type DripSuiParams = { 4 | address: string 5 | networkName: Parameters[0] 6 | } 7 | 8 | const dripSui = async ({ address, networkName }: DripSuiParams) => { 9 | return requestSuiFromFaucetV0({ 10 | host: getFaucetHost(networkName), 11 | recipient: address 12 | }) 13 | } 14 | 15 | export default dripSui 16 | -------------------------------------------------------------------------------- /src/lib/event.ts: -------------------------------------------------------------------------------- 1 | import postIFrameMessage from './postIFrameMessage' 2 | 3 | type EventProps = { 4 | action: string, 5 | category: string, 6 | label: string, 7 | value: number 8 | } 9 | 10 | const event = async (eventProps: EventProps) => { 11 | postIFrameMessage({ 12 | action: 'event', 13 | data: eventProps, 14 | }) 15 | } 16 | 17 | export default event 18 | -------------------------------------------------------------------------------- /src/lib/executeTransactionBlock.ts: -------------------------------------------------------------------------------- 1 | import log from './log' 2 | 3 | import { ExtensionSigner, HostedSigner } from '../types/Signer'; 4 | import { EthosExecuteTransactionBlockInput } from 'types/EthosExecuteTransactionBlockInput'; 5 | 6 | type TransactArgs = { 7 | signer: HostedSigner | ExtensionSigner 8 | transactionInput: EthosExecuteTransactionBlockInput 9 | } 10 | 11 | const executeTransactionBlock = async ({ 12 | signer, 13 | transactionInput 14 | }: TransactArgs) => { 15 | log("transact", "Starting transaction", signer, transactionInput) 16 | return signer.executeTransactionBlock(transactionInput) 17 | } 18 | 19 | export default executeTransactionBlock 20 | -------------------------------------------------------------------------------- /src/lib/fetchSui.ts: -------------------------------------------------------------------------------- 1 | const fetchSui = async (network: string, method: string, params: string[]) => { 2 | const data = { 3 | method: 'POST', 4 | headers: { 'Content-Type': 'application/json' }, 5 | body: JSON.stringify({ 6 | "jsonrpc": "2.0", 7 | "method": method, 8 | "params": params, 9 | "id": 1 10 | }) 11 | } 12 | 13 | const response = await fetch(network, data); 14 | const json = await response.json(); 15 | // console.log("SUI JSON", json, response, json.result?.EffectResponse?.effects, method, params) 16 | 17 | if (json.error?.message) { 18 | return { error: json.error?.message } 19 | } 20 | 21 | return json.result; 22 | } 23 | 24 | export default fetchSui; -------------------------------------------------------------------------------- /src/lib/generateQRCode.ts: -------------------------------------------------------------------------------- 1 | import QRCode from 'qrcode' 2 | 3 | const generateQRCode = async (url: string): Promise => { 4 | return new Promise((resolve) => { 5 | QRCode.toDataURL(url, { 6 | width: 300, 7 | margin: 2, 8 | color: { 9 | dark: '#000', 10 | light: '#FFF' 11 | } 12 | }, (err: any, qrCodeUrl: any) => { 13 | if (err) return console.error(err) 14 | resolve(qrCodeUrl) 15 | }) 16 | }); 17 | } 18 | 19 | export default generateQRCode -------------------------------------------------------------------------------- /src/lib/getConfiguration.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import { EthosConfiguration } from '../types/EthosConfiguration' 3 | 4 | const getConfiguration = (): EthosConfiguration => { 5 | const ethosStore = store.namespace('ethos') 6 | const configuration = ethosStore('configuration') 7 | return configuration || {} 8 | } 9 | 10 | export default getConfiguration 11 | -------------------------------------------------------------------------------- /src/lib/getDisplay.ts: -------------------------------------------------------------------------------- 1 | import type { SuiObjectData } from '@mysten/sui.js/client'; 2 | 3 | const getDisplay = ( 4 | display?: SuiObjectData['display'] | Record 5 | ): Record | null => { 6 | if (!display) return null; 7 | if ("data" in display && display.data && typeof display.data === "object") { 8 | return display.data; 9 | } 10 | return display as Record; 11 | }; 12 | 13 | export default getDisplay; 14 | -------------------------------------------------------------------------------- /src/lib/getEthosSigner.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import { HostedSigner, SignerType } from '../types/Signer' 3 | import activeUser from './activeUser' 4 | import hostedInteraction, { HostedInteractionResponse } from './hostedInteraction' 5 | 6 | import type { 7 | SuiSignPersonalMessageOutput, 8 | SuiSignTransactionBlockOutput, 9 | WalletAccount, 10 | WalletIcon 11 | } from '@mysten/wallet-standard'; 12 | import { EthosSignMessageInput } from '../types/EthosSignMessageInput' 13 | import { EthosSignAndExecuteTransactionBlockInput } from '../types/EthosSignAndExecuteTransactionBlockInput' 14 | import { EthosSignTransactionBlockInput } from '../types/EthosSignTransactionBlockInput' 15 | import { DEFAULT_CHAIN } from '../lib/constants'; 16 | import { Chain } from 'enums/Chain' 17 | import { EthosExecuteTransactionBlockInput } from 'types/EthosExecuteTransactionBlockInput' 18 | import { SuiClient, SuiTransactionBlockResponse } from '@mysten/sui.js/dist/cjs/client' 19 | 20 | const getEthosSigner = async ({ client, defaultChain }: { client: SuiClient, defaultChain: Chain }): Promise => { 21 | 22 | const user: any = await activeUser() 23 | 24 | const accounts: WalletAccount[] = (user?.accounts || []).filter((account: any) => account.chain === 'sui') 25 | 26 | const currentAccount = accounts[0] 27 | 28 | const signAndExecuteTransactionBlock = (input: EthosSignAndExecuteTransactionBlockInput): Promise => { 29 | return new Promise((resolve, reject) => { 30 | const transactionEventListener = ({ approved, data }: HostedInteractionResponse) => { 31 | if (approved) { 32 | resolve(data.response); 33 | } else { 34 | reject({ error: data?.response?.error || "User rejected transaction."}) 35 | } 36 | } 37 | 38 | const serializedTransaction = input.transactionBlock.serialize(); 39 | const account = input.account ?? currentAccount.address 40 | const chain = input.chain ?? defaultChain ?? DEFAULT_CHAIN 41 | 42 | hostedInteraction({ 43 | action: 'transaction', 44 | data: { 45 | input, 46 | serializedTransaction, 47 | account, 48 | chain 49 | }, 50 | onResponse: transactionEventListener, 51 | showWallet: true 52 | }) 53 | }); 54 | } 55 | 56 | const executeTransactionBlock = (input: EthosExecuteTransactionBlockInput): Promise => { 57 | return client.executeTransactionBlock(input); 58 | } 59 | 60 | const signTransactionBlock = (input: EthosSignTransactionBlockInput): Promise => { 61 | return new Promise((resolve, reject) => { 62 | const transactionEventListener = ({ approved, data }: HostedInteractionResponse) => { 63 | if (approved) { 64 | resolve(data.response); 65 | } else { 66 | reject({ error: data?.response?.error || "User rejected transaction."}) 67 | } 68 | } 69 | 70 | const serializedTransaction = input.transactionBlock.serialize(); 71 | const account = input.account ?? currentAccount.address 72 | const chain = input.chain ?? defaultChain ?? DEFAULT_CHAIN 73 | 74 | hostedInteraction({ 75 | action: 'transaction', 76 | data: { 77 | input, 78 | serializedTransaction, 79 | account, 80 | chain 81 | }, 82 | onResponse: transactionEventListener, 83 | showWallet: true 84 | }) 85 | }); 86 | } 87 | 88 | const requestPreapproval = () => { 89 | return Promise.resolve(true); 90 | } 91 | 92 | const signPersonalMessage = (input: EthosSignMessageInput): Promise => { 93 | return new Promise((resolve, reject) => { 94 | const transactionEventListener = ({ approved, data }: HostedInteractionResponse) => { 95 | if (approved) { 96 | resolve({ 97 | ...data.response, 98 | bytes: data.response.messageBytes 99 | }); 100 | } else { 101 | reject({ error: data?.response?.error || "User rejected signing."}) 102 | } 103 | } 104 | 105 | hostedInteraction({ 106 | action: 'sign', 107 | data: { ...input, signData: input.message }, 108 | onResponse: transactionEventListener, 109 | showWallet: true 110 | }) 111 | }); 112 | } 113 | 114 | const disconnect = (fromWallet = false) => { 115 | return new Promise((resolve) => { 116 | const transactionEventListener = () => { 117 | resolve(true); 118 | } 119 | 120 | hostedInteraction({ 121 | action: 'logout', 122 | data: { 123 | fromWallet: typeof fromWallet === 'boolean' ? fromWallet : false 124 | }, 125 | onResponse: transactionEventListener 126 | }) 127 | 128 | store.namespace('auth')('access_token', null) 129 | }); 130 | } 131 | 132 | const logout = () => { 133 | return disconnect(true); 134 | } 135 | 136 | return user ? { 137 | type: SignerType.Hosted, 138 | name: "Ethos", 139 | icon: dataIcon, 140 | email: user.email, 141 | getAddress: async () => currentAccount?.address, 142 | accounts, 143 | currentAccount, 144 | signAndExecuteTransactionBlock, 145 | executeTransactionBlock, 146 | signTransactionBlock, 147 | requestPreapproval, 148 | signPersonalMessage, 149 | disconnect, 150 | logout, 151 | client 152 | } : null 153 | 154 | } 155 | 156 | export default getEthosSigner; 157 | 158 | const dataIcon: WalletIcon = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iOCIgZmlsbD0iIzZEMjhEOSIvPgo8cGF0aCBvcGFjaXR5PSIwLjgiIGQ9Ik05LjEyMTg3IDYuODU3MDZIMTkuOTU4M0MyMC40NTcxIDYuODU3MDYgMjAuODYxNCA3LjI2MTQxIDIwLjg2MTQgNy43NjAyVjE5Ljk4ODZDMjAuODYxNCAyMC40ODc0IDIwLjQ1NzEgMjAuODkxOCAxOS45NTgzIDIwLjg5MThIOS4xMjE4N0M4LjYyMzA4IDIwLjg5MTggOC4yMTg3MiAyMC40ODc0IDguMjE4NzIgMTkuOTg4NlY3Ljc2MDJDOC4yMTg3MiA3LjI2MTQxIDguNjIzMDggNi44NTcwNiA5LjEyMTg3IDYuODU3MDZaIiBzdHJva2U9InVybCgjcGFpbnQwX2xpbmVhcl82OTlfMjY5OCkiIHN0cm9rZS13aWR0aD0iMC40NTE1NzIiLz4KPHBhdGggZD0iTTguNzEyNzQgNy40NTQ1OUwxNi4wOTQ1IDEwLjg4OTRDMTYuNDEyOSAxMS4wMzc2IDE2LjYxNjYgMTEuMzU3IDE2LjYxNjYgMTEuNzA4M1YyMy44MUMxNi42MTY2IDI0LjQ2MzUgMTUuOTQ0IDI0LjkwMDcgMTUuMzQ2OCAyNC42MzUzTDcuOTY1MDIgMjEuMzU1NkM3LjYzODgyIDIxLjIxMDcgNy40Mjg1OCAyMC44ODcyIDcuNDI4NTggMjAuNTMwM1Y4LjI3MzQzQzcuNDI4NTggNy42MTMxMSA4LjExNDA2IDcuMTc2MDIgOC43MTI3NCA3LjQ1NDU5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIzLjM3ODIgMTUuMzc2N0MyMy40MzAzIDE1LjEzMjEgMjMuNTUzOCAxNC45MDg2IDIzLjczMzIgMTQuNzM0M0MyMy45MTI1IDE0LjU2IDI0LjEzOTYgMTQuNDQzIDI0LjM4NTYgMTQuMzk3OUwyNS4wNDA0IDE0LjI3ODRMMjQuMzg1NSAxNC4xNTg4SDI0LjM4NTZDMjQuMTM5NiAxNC4xMTM3IDIzLjkxMjUgMTMuOTk2NyAyMy43MzMyIDEzLjgyMjRDMjMuNTUzOCAxMy42NDgxIDIzLjQzMDMgMTMuNDI0NiAyMy4zNzgyIDEzLjE4TDIzLjIzNDEgMTIuNTAxM0wyMy4wOSAxMy4xOEMyMy4wMzc5IDEzLjQyNDYgMjIuOTE0NCAxMy42NDgxIDIyLjczNTEgMTMuODIyNEMyMi41NTU4IDEzLjk5NjcgMjIuMzI4NyAxNC4xMTM4IDIyLjA4MjcgMTQuMTU4OEwyMS40Mjc4IDE0LjI3ODRMMjIuMDgyNyAxNC4zOTc5SDIyLjA4MjdDMjIuMzI4NyAxNC40NDMgMjIuNTU1NyAxNC41NiAyMi43MzUgMTQuNzM0M0MyMi45MTQ0IDE0LjkwODYgMjMuMDM3OSAxNS4xMzIxIDIzLjA5IDE1LjM3NjdMMjMuMjM0MSAxNi4wNTU0TDIzLjM3ODIgMTUuMzc2N1oiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjk5XzI2OTgiIHgxPSIyMC44NjE0IiB5MT0iMTAuNTkyNiIgeDI9IjE0LjUzOTgiIHkyPSIxMy43NTM0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IndoaXRlIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0id2hpdGUiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=`; 159 | -------------------------------------------------------------------------------- /src/lib/getIframe.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import getConfiguration from './getConfiguration' 3 | import log from './log' 4 | import postIFrameMessage from './postIFrameMessage' 5 | 6 | const getIframe = (show?: boolean) => { 7 | const { apiKey, walletAppUrl, network } = getConfiguration() 8 | log('getIframe', 'getIframe', apiKey, walletAppUrl) 9 | 10 | if (!apiKey || !walletAppUrl) return; 11 | 12 | const iframeId = 'ethos-wallet-iframe' 13 | let scrollY: number = 0 14 | 15 | let iframe = document.getElementById(iframeId) as HTMLIFrameElement 16 | 17 | const close = () => { 18 | if (!iframe) return 19 | iframe.style.width = '1px' 20 | iframe.style.height = '1px' 21 | } 22 | 23 | const queryParams = new URLSearchParams(window.location.search) 24 | const accessToken = queryParams.get('access_token') 25 | const refreshToken = queryParams.get('refresh_token') 26 | 27 | let fullWalletAppUrl = walletAppUrl + `/wallet?apiKey=${apiKey}&network=${network}` 28 | if (accessToken && refreshToken) { 29 | fullWalletAppUrl += `&access_token=${accessToken}&refresh_token=${refreshToken}` 30 | 31 | queryParams.delete('access_token') 32 | queryParams.delete('refresh_token') 33 | let fullPath = location.protocol + '//' + location.host + location.pathname 34 | if (queryParams.toString().length > 0) { 35 | fullPath += '?' + queryParams.toString() 36 | } 37 | store.namespace('auth')('access_token', accessToken) 38 | store.namespace('auth')('refresh_token', refreshToken) 39 | window.history.pushState({}, '', fullPath) 40 | } else { 41 | const accessToken = store.namespace('auth')('access_token') 42 | const refreshToken = store.namespace('auth')('refresh_token') 43 | if (accessToken && refreshToken) { 44 | fullWalletAppUrl += `&access_token=${accessToken}&refresh_token=${refreshToken}` 45 | } 46 | } 47 | 48 | if (!iframe) { 49 | log('getIframe', 'Load Iframe', fullWalletAppUrl) 50 | iframe = document.createElement('IFRAME') as HTMLIFrameElement 51 | iframe.src = fullWalletAppUrl 52 | iframe.id = iframeId 53 | iframe.style.border = 'none' 54 | iframe.style.position = 'absolute' 55 | iframe.style.top = scrollY - 1 + 'px' 56 | iframe.style.right = '60px' 57 | iframe.style.width = '1px' 58 | iframe.style.height = '1px' 59 | iframe.style.zIndex = '99'; 60 | iframe.style.backgroundColor = 'transparent'; 61 | iframe.setAttribute('allow', 'payment; clipboard-read; clipboard-write') 62 | document.body.appendChild(iframe) 63 | 64 | window.addEventListener('message', (message) => { 65 | if (message.origin === walletAppUrl) { 66 | 67 | const { action, data } = message.data; 68 | 69 | switch (action) { 70 | case 'close': 71 | close() 72 | break; 73 | case 'resize': 74 | if ((data.width ?? 0) > 1 && (data.height ?? 0) > 1) { 75 | iframe.style.width = data.width + 'px' 76 | iframe.style.height = data.height + 'px' 77 | } 78 | break; 79 | case 'ready': 80 | iframe.setAttribute('readyToReceiveMessages', 'true') 81 | 82 | const messageStore = store.namespace('iframeMessages') 83 | const messages = messageStore('messages') || [] 84 | for (const message of messages) { 85 | postIFrameMessage(message) 86 | } 87 | messageStore('messages', null) 88 | 89 | break; 90 | } 91 | } 92 | }) 93 | 94 | window.addEventListener('scroll', () => { 95 | scrollY = window.scrollY 96 | iframe.style.top = scrollY + 'px' 97 | }) 98 | } else if (iframe.src !== fullWalletAppUrl) { 99 | iframe.src = fullWalletAppUrl 100 | } 101 | 102 | if (show) { 103 | iframe.style.width = '360px' 104 | iframe.style.height = '600px' 105 | } else if (show !== undefined) { 106 | close() 107 | } 108 | 109 | return iframe 110 | } 111 | 112 | export default getIframe 113 | -------------------------------------------------------------------------------- /src/lib/getKioskNFT.ts: -------------------------------------------------------------------------------- 1 | import { DynamicFieldInfo, SuiClient, SuiObjectData, SuiObjectResponse } from "@mysten/sui.js/client"; 2 | import get from 'lodash-es/get.js'; 3 | 4 | export const isKiosk = (data: SuiObjectData): boolean => { 5 | return ( 6 | !!data.type && 7 | data.type.includes('kiosk') && 8 | !!data.content && 9 | 'fields' in data.content && ( 10 | 'kiosk' in data.content.fields || 11 | 'for' in data.content.fields 12 | ) 13 | ); 14 | } 15 | 16 | export const getKioskObjects = async ( 17 | client: SuiClient, 18 | data: SuiObjectData 19 | ): Promise => { 20 | if (!isKiosk(data)) return []; 21 | let kiosk = get(data, 'content.fields.kiosk'); 22 | if (!kiosk) kiosk = get(data, 'content.fields.for'); 23 | if (!kiosk) return []; 24 | let allKioskObjects: DynamicFieldInfo[] = []; 25 | let cursor: string | undefined | null; 26 | while (cursor !== null) { 27 | const response = await client.getDynamicFields({ 28 | parentId: kiosk, 29 | cursor 30 | }); 31 | if (!response.data) return []; 32 | allKioskObjects = [...(allKioskObjects || []), ...response.data]; 33 | if (response.hasNextPage && response.nextCursor !== cursor) { 34 | cursor = response.nextCursor; 35 | } else { 36 | cursor = null; 37 | } 38 | } 39 | 40 | const relevantKioskObjects = allKioskObjects.filter( 41 | (kioskObject) => ( 42 | kioskObject.name.type === '0x0000000000000000000000000000000000000000000000000000000000000002::kiosk::Item' || 43 | kioskObject.name.type === '0x2::kiosk::Item' 44 | ) 45 | ); 46 | const objectIds = relevantKioskObjects.map((item) => item.objectId); 47 | 48 | let objects: SuiObjectResponse[] = []; 49 | const groupSize = 30; 50 | for (let i = 0; i < objectIds.length; i += groupSize) { 51 | const group = objectIds.slice(i, i + groupSize); 52 | 53 | const groupObjects = await client.multiGetObjects({ 54 | ids: group, 55 | options: { 56 | showContent: true, 57 | showType: true, 58 | showDisplay: true, 59 | showOwner: true, 60 | }, 61 | }); 62 | 63 | objects = [...objects, ...groupObjects]; 64 | } 65 | 66 | return objects; 67 | } -------------------------------------------------------------------------------- /src/lib/getMobileConnetionUrl.ts: -------------------------------------------------------------------------------- 1 | import getConfiguration from './getConfiguration' 2 | import postIFrameMessage from './postIFrameMessage' 3 | 4 | const getMobileConnectionUrl = async (): Promise => { 5 | const { walletAppUrl } = getConfiguration() 6 | 7 | return new Promise((resolve, _reject) => { 8 | const connectionEventListener = (message: any) => { 9 | if (message.origin === walletAppUrl) { 10 | const { action, data } = message.data 11 | if (action !== 'connect') return 12 | window.removeEventListener('message', connectionEventListener) 13 | resolve(data) 14 | } 15 | } 16 | 17 | window.addEventListener('message', connectionEventListener) 18 | 19 | postIFrameMessage({ 20 | action: 'connect' 21 | }) 22 | }) 23 | } 24 | 25 | export default getMobileConnectionUrl -------------------------------------------------------------------------------- /src/lib/hideWallet.ts: -------------------------------------------------------------------------------- 1 | import { Signer, SignerType } from '../types/Signer' 2 | import getIframe from './getIframe' 3 | 4 | const hideWallet = (signer: Signer) => { 5 | if (signer.type === SignerType.Extension) return; 6 | getIframe(false) 7 | } 8 | 9 | export default hideWallet 10 | -------------------------------------------------------------------------------- /src/lib/hostedInteraction.ts: -------------------------------------------------------------------------------- 1 | import store from "store2"; 2 | import getConfiguration from "./getConfiguration"; 3 | import getIframe from "./getIframe"; 4 | import log from "./log"; 5 | import postIFrameMessage from "./postIFrameMessage"; 6 | 7 | export type HostedInteractionArgs = { 8 | id?: string | number, 9 | action: string, 10 | data: any, 11 | onResponse: (response: HostedInteractionResponse) => void, 12 | showWallet?: boolean 13 | } 14 | 15 | export type HostedInteractionResponse = { 16 | approved: boolean, 17 | data?: any 18 | } 19 | 20 | const hostedInteraction = ({ id, action, data, onResponse, showWallet=false }: HostedInteractionArgs) => { 21 | const { walletAppUrl } = getConfiguration(); 22 | 23 | const iframeListener = (message: any) => { 24 | log("hostedInteraction", "response: ", message) 25 | if (message.origin === walletAppUrl) { 26 | const { approved, action: responseAction, data: responseData } = message.data 27 | if (responseAction !== action) return 28 | onResponse({ approved, data: responseData }); 29 | window.removeEventListener('message', iframeListener); 30 | getIframe(false); 31 | } 32 | } 33 | 34 | window.addEventListener('message', iframeListener) 35 | 36 | const ethosStore = store.namespace('ethos') 37 | const configuration = ethosStore("configuration"); 38 | const { network } = configuration; 39 | 40 | log("hostedInteraction", "Posting interaction", id, action, data) 41 | postIFrameMessage({ id, network, action, data }) 42 | 43 | getIframe(showWallet); 44 | } 45 | 46 | export default hostedInteraction; -------------------------------------------------------------------------------- /src/lib/initializeEthos.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import { EthosConfiguration } from '../types/EthosConfiguration' 3 | import log from './log' 4 | 5 | const initializeEthos = (ethosConfiguration: EthosConfiguration): void => { 6 | const ethosStore = store.namespace('ethos') 7 | log('initialize', 'Ethos Configuration', ethosConfiguration) 8 | ethosStore('configuration', ethosConfiguration) 9 | } 10 | 11 | export default initializeEthos 12 | -------------------------------------------------------------------------------- /src/lib/lib.ts: -------------------------------------------------------------------------------- 1 | // import login from './login' 2 | // import logout from './logout' 3 | // import transact from './transact' 4 | // import preapprove from './preapprove' 5 | import getWalletContents from './getWalletContents' 6 | // import showWallet from './showWallet' 7 | // import hideWallet from './hideWallet' 8 | // import dripSui from './dripSui' 9 | import getConfiguration from './getConfiguration' 10 | import getEthosSigner from './getEthosSigner' 11 | import initializeEthos from './initializeEthos' 12 | import postIFrameMessage from './postIFrameMessage' 13 | import listenForMobileConnection from './listenForMobileConnection' 14 | 15 | const lib = { 16 | // showWallet, 17 | // hideWallet, 18 | // login, 19 | // logout, 20 | // transact, 21 | // preapprove, 22 | getWalletContents, 23 | // dripSui, 24 | postIFrameMessage, 25 | getEthosSigner, 26 | getConfiguration, 27 | initializeEthos, 28 | listenForMobileConnection 29 | } 30 | 31 | export default lib; 32 | -------------------------------------------------------------------------------- /src/lib/listenForMobileConnection.ts: -------------------------------------------------------------------------------- 1 | // import { ProviderAndSigner } from '../types/ProviderAndSigner' 2 | import getConfiguration from './getConfiguration' 3 | import log from './log' 4 | 5 | // const listenForMobileConnection = async (onConnect: (providerAndSigner: ProviderAndSigner) => void) => { 6 | const listenForMobileConnection = async () => { 7 | const { walletAppUrl } = getConfiguration() 8 | 9 | const connectionEventListener = (message: any) => { 10 | if (message.origin === walletAppUrl) { 11 | const { action, data } = message.data 12 | if (action !== 'connect') return 13 | 14 | if (!data.address) { 15 | // onConnect({ provider: {}, signer: null }) 16 | return; 17 | }; 18 | 19 | window.removeEventListener('message', connectionEventListener) 20 | 21 | const signer = { 22 | currentAccount: { address: data.address } 23 | } 24 | 25 | const provider = { 26 | getSigner: signer, 27 | } 28 | 29 | log('mobile', 'Mobile connection established', provider, signer) 30 | // onConnect({ provider, signer } as ProviderAndSigner) 31 | } 32 | } 33 | 34 | window.removeEventListener('message', connectionEventListener) 35 | window.addEventListener('message', connectionEventListener) 36 | } 37 | 38 | export default listenForMobileConnection -------------------------------------------------------------------------------- /src/lib/log.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | 3 | const allowLog = (label: string) => { 4 | const logStore = store.namespace('log') 5 | const allowed = logStore('allowed') || [] 6 | if (allowed.includes(label)) return 7 | logStore('allowed', [...allowed, label]) 8 | return `Logging enabled for ${label}. Call ethos.clearAllowLog() to turn off this logging.` 9 | } 10 | 11 | const clearAllowLog = () => { 12 | const logStore = store.namespace('log') 13 | logStore('allowed', []) 14 | } 15 | 16 | const log = (label: string, ...message: any[]) => { 17 | const logStore = store.namespace('log') 18 | 19 | const allowed = logStore('allowed') 20 | 21 | if (!allowed || !(allowed.includes(label) || allowed.includes('all'))) { 22 | return 23 | } 24 | 25 | console.log(label, ...message) 26 | } 27 | 28 | if (typeof window !== 'undefined') { 29 | ;(window as any).ethos = { 30 | allowLog: allowLog, 31 | clearAllowLog: clearAllowLog, 32 | } 33 | } 34 | 35 | export default log 36 | -------------------------------------------------------------------------------- /src/lib/login.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import { User } from 'types/User' 3 | import lib from './lib' 4 | 5 | export type loginArgs = { 6 | email?: string, 7 | provider?: string, 8 | apiKey?: string 9 | } 10 | 11 | const login = async ({ email, provider, apiKey }: loginArgs) => { 12 | const { walletAppUrl, redirectTo } = lib.getConfiguration(); 13 | const userStore = store.namespace('users') 14 | 15 | if (provider) { 16 | const returnTo = redirectTo ?? location.href; 17 | const fullUrl = `${walletAppUrl}/auth?apiKey=${apiKey}&returnTo=${encodeURIComponent(returnTo)}` 18 | location.href = `${walletAppUrl}/socialauth?provider=${provider}&redirectTo=${encodeURIComponent(fullUrl)}`; 19 | return; 20 | } 21 | 22 | return new Promise((resolve, _reject) => { 23 | const loginEventListener = (message: any) => { 24 | if (message.origin === walletAppUrl) { 25 | const { action, data } = message.data 26 | if (action !== 'login') return 27 | window.removeEventListener('message', loginEventListener) 28 | 29 | userStore('current', data) 30 | resolve(data) 31 | } 32 | } 33 | 34 | window.addEventListener('message', loginEventListener) 35 | 36 | lib.postIFrameMessage({ 37 | action: 'login', 38 | data: { 39 | email, 40 | provider, 41 | returnTo: redirectTo ?? window.location.href, 42 | apiKey 43 | }, 44 | }) 45 | }) 46 | } 47 | 48 | export default login 49 | -------------------------------------------------------------------------------- /src/lib/logout.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionSigner, HostedSigner } from '../types/Signer' 2 | import log from './log' 3 | 4 | const logout = async (signer: ExtensionSigner | HostedSigner, fromWallet: boolean = false) => { 5 | log('logout', `-- Wallet ${fromWallet} --`, `-- Is Extension: ${signer?.type} --`, `-- Disconnect: ${!!signer?.disconnect} --`, "signer", signer) 6 | 7 | if (signer.type === "extension" || !fromWallet) { 8 | await signer.disconnect(); 9 | } else { 10 | await signer.logout() 11 | } 12 | } 13 | 14 | 15 | export default logout 16 | -------------------------------------------------------------------------------- /src/lib/nameService.ts: -------------------------------------------------------------------------------- 1 | import { SuiClient } from '@mysten/sui.js/client' 2 | import { DEFAULT_NETWORK } from './constants' 3 | 4 | export const getSuiName = async (address: string, network?: string) => { 5 | const client = new SuiClient({ url: network ?? DEFAULT_NETWORK }) 6 | return client.resolveNameServiceNames({ address }) 7 | } 8 | 9 | export const getSuiAddress = async (name: string, network?: string) => { 10 | const client = new SuiClient({ url: network ?? DEFAULT_NETWORK }) 11 | return client.resolveNameServiceAddress({ name }) 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/postIFrameMessage.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2' 2 | import getIframe from './getIframe' 3 | import log from './log' 4 | 5 | const postIFrameMessage = (message: any) => { 6 | const iframe = getIframe() 7 | 8 | if (!(iframe as any)?.getAttribute('readyToReceiveMessages')) { 9 | const messageStore = store.namespace('iframeMessages') 10 | const existingMessages = messageStore('messages') || [] 11 | const result = messageStore('messages', [...existingMessages, message]) 12 | log("iframe", "Storing iframe message", result) 13 | return; 14 | } 15 | 16 | iframe?.contentWindow?.postMessage(message, '*') 17 | } 18 | 19 | export default postIFrameMessage 20 | -------------------------------------------------------------------------------- /src/lib/preapprove.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Preapproval } from "types/Preapproval"; 3 | 4 | export type PreapprovalArgs = { 5 | signer: any, 6 | preapproval: Preapproval 7 | } 8 | 9 | const preapprove = async ({ signer, preapproval }: PreapprovalArgs) => { 10 | return signer.requestPreapproval(preapproval) 11 | } 12 | 13 | export default preapprove; -------------------------------------------------------------------------------- /src/lib/showWallet.ts: -------------------------------------------------------------------------------- 1 | import { Signer, SignerType } from '../types/Signer' 2 | import getIframe from './getIframe' 3 | 4 | const showWallet = (signer: Signer) => { 5 | if (signer.type === SignerType.Extension) return; 6 | getIframe(true) 7 | } 8 | 9 | export default showWallet 10 | -------------------------------------------------------------------------------- /src/lib/signMessage.ts: -------------------------------------------------------------------------------- 1 | type signProps = { 2 | signer?: any 3 | message: string | Uint8Array 4 | } 5 | 6 | const signMessage = async ({ signer, message }: signProps): Promise => { 7 | return signer.signMessage({ message }); 8 | } 9 | 10 | export default signMessage 11 | -------------------------------------------------------------------------------- /src/lib/signTransactionBlock.ts: -------------------------------------------------------------------------------- 1 | import log from './log' 2 | 3 | import type { EthosSignTransactionBlockInput } from '../types/EthosSignTransactionBlockInput'; 4 | import { ExtensionSigner, HostedSigner } from '../types/Signer'; 5 | 6 | type TransactArgs = { 7 | signer: HostedSigner | ExtensionSigner 8 | transactionInput: EthosSignTransactionBlockInput 9 | } 10 | 11 | const signTransactionBlock = async ({ 12 | signer, 13 | transactionInput 14 | }: TransactArgs) => { 15 | log("transact", "Starting transaction", signer, transactionInput) 16 | return signer.signTransactionBlock(transactionInput) 17 | } 18 | 19 | export default signTransactionBlock 20 | -------------------------------------------------------------------------------- /src/lib/transact.ts: -------------------------------------------------------------------------------- 1 | import log from './log' 2 | 3 | import type { EthosSignAndExecuteTransactionBlockInput } from '../types/EthosSignAndExecuteTransactionBlockInput'; 4 | import { ExtensionSigner, HostedSigner } from '../types/Signer'; 5 | 6 | type TransactArgs = { 7 | signer: HostedSigner | ExtensionSigner 8 | transactionInput: EthosSignAndExecuteTransactionBlockInput 9 | } 10 | 11 | const transact = async ({ 12 | signer, 13 | transactionInput 14 | }: TransactArgs) => { 15 | log("transact", "Starting transaction", signer, transactionInput) 16 | return signer.signAndExecuteTransactionBlock(transactionInput) 17 | } 18 | 19 | export default transact 20 | -------------------------------------------------------------------------------- /src/lib/truncateMiddle.ts: -------------------------------------------------------------------------------- 1 | const truncateMiddle = (text: string, length: number = 6) => 2 | text ? `${text.slice(0,length)}...${text.slice(length * -1)}` : '' 3 | 4 | export default truncateMiddle; -------------------------------------------------------------------------------- /src/lib/useHandleElementWithIdClicked.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | 3 | export default function useHandleElementWithIdClicked( 4 | clickId: string, 5 | onClickOutside: () => void 6 | ) { 7 | useEffect(() => { 8 | function handleClickOutside(event: any) { 9 | if (event.target.id === clickId) { 10 | onClickOutside() 11 | } 12 | } 13 | document.addEventListener('mousedown', handleClickOutside) 14 | return () => { 15 | // Unbind the event listener on clean up 16 | document.removeEventListener('mousedown', handleClickOutside) 17 | } 18 | }, []) 19 | } 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode, ReactElement } from 'react' 2 | 3 | // A unique placeholder we can use as a default. This is nice because we can use this instead of 4 | // defaulting to null / never / ... and possibly collide with actual data. 5 | // Ideally we use a unique symbol here. 6 | let __ = '1D45E01E-AF44-47C4-988A-19A94EBAF55C' as const 7 | export type __ = typeof __ 8 | 9 | export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never 10 | 11 | export type PropsOf = TTag extends React.ElementType 12 | ? React.ComponentProps 13 | : never 14 | 15 | type PropsWeControl = 'as' | 'children' | 'refName' | 'className' 16 | 17 | // Resolve the props of the component, but ensure to omit certain props that we control 18 | type CleanProps = TOmitableProps extends __ 19 | ? Omit, PropsWeControl> 20 | : Omit, TOmitableProps | PropsWeControl> 21 | 22 | // Add certain props that we control 23 | type OurProps = { 24 | as?: TTag 25 | children?: ReactNode | ((bag: TSlot) => ReactElement) 26 | refName?: string 27 | } 28 | 29 | // Conditionally override the `className`, to also allow for a function 30 | // if and only if the PropsOf already define `className`. 31 | // This will allow us to have a TS error on as={Fragment} 32 | type ClassNameOverride = PropsOf extends { className?: any } 33 | ? { className?: string | ((bag: TSlot) => string) } 34 | : {} 35 | 36 | // Provide clean TypeScript props, which exposes some of our custom API's. 37 | export type Props = CleanProps< 38 | TTag, 39 | TOmitableProps 40 | > & 41 | OurProps & 42 | ClassNameOverride 43 | 44 | type Without = { [P in Exclude]?: never } 45 | export type XOR = T | U extends __ 46 | ? never 47 | : T extends __ 48 | ? U 49 | : U extends __ 50 | ? T 51 | : T | U extends object 52 | ? (Without & U) | (Without & T) 53 | : T | U 54 | -------------------------------------------------------------------------------- /src/types/ClientAndSigner.ts: -------------------------------------------------------------------------------- 1 | import { HostedSigner, ExtensionSigner } from "./Signer" 2 | import {SuiClient} from '@mysten/sui.js/client' 3 | 4 | export type ClientAndSigner = { 5 | client: SuiClient | null 6 | signer: ExtensionSigner | HostedSigner | null 7 | } -------------------------------------------------------------------------------- /src/types/ConnectContextContents.ts: -------------------------------------------------------------------------------- 1 | import { WalletContextContents } from "./WalletContextContents"; 2 | import { ModalContextContents } from './ModalContextContents'; 3 | import { ClientAndSigner } from './ClientAndSigner'; 4 | import { EthosConfiguration } from './EthosConfiguration'; 5 | 6 | export interface ConnectContextContents { 7 | wallet?: WalletContextContents, 8 | modal?: ModalContextContents, 9 | clientAndSigner?: ClientAndSigner, 10 | ethosConfiguration?: EthosConfiguration, 11 | init: (configuration: EthosConfiguration) => void 12 | } -------------------------------------------------------------------------------- /src/types/ConvenienceSuiObject.ts: -------------------------------------------------------------------------------- 1 | import { SuiObjectData } from "@mysten/sui.js/client"; 2 | 3 | export interface ConvenenienceSuiObject extends SuiObjectData { 4 | packageObjectId: string, 5 | moduleName: string, 6 | structName: string, 7 | name?: string, 8 | description?: string 9 | imageUrl?: string, 10 | fields?: Record, 11 | display?: Record | null, 12 | isCoin: boolean, 13 | kiosk?: SuiObjectData 14 | } -------------------------------------------------------------------------------- /src/types/EthosConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { Chain } from '../enums/Chain' 2 | import { InvalidPackages } from '../types/InvalidPackages'; 3 | 4 | export interface EthosConfiguration { 5 | apiKey?: string 6 | walletAppUrl?: string 7 | chain?: Chain 8 | network?: string 9 | hideEmailSignIn?: boolean 10 | hideWalletSignIn?: boolean 11 | preferredWallets?: string[] 12 | redirectTo?: string; 13 | disableAutoConnect?: boolean; 14 | pollingInterval?: number; 15 | invalidPackages?: InvalidPackages; 16 | } 17 | -------------------------------------------------------------------------------- /src/types/EthosExecuteTransactionBlockInput.ts: -------------------------------------------------------------------------------- 1 | import { SerializedSignature } from '@mysten/sui.js/cryptography' 2 | import { ExecuteTransactionRequestType, SuiTransactionBlockResponseOptions } from '@mysten/sui.js/client'; 3 | 4 | export type EthosExecuteTransactionBlockInput = { 5 | transactionBlock: Uint8Array | string; 6 | signature: SerializedSignature | SerializedSignature[]; 7 | options?: SuiTransactionBlockResponseOptions; 8 | requestType?: ExecuteTransactionRequestType; 9 | } -------------------------------------------------------------------------------- /src/types/EthosProvider.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface EthosProvider { 3 | getSigner: () => any 4 | } 5 | -------------------------------------------------------------------------------- /src/types/EthosSignAndExecuteTransactionBlockInput.ts: -------------------------------------------------------------------------------- 1 | import type { TransactionBlock } from '@mysten/sui.js/transactions'; 2 | import type { IdentifierString, SuiSignAndExecuteTransactionBlockInput, WalletAccount } from '@mysten/wallet-standard'; 3 | 4 | export type EthosSignAndExecuteTransactionBlockInput = { 5 | transactionBlock: TransactionBlock; 6 | options?: SuiSignAndExecuteTransactionBlockInput['options']; 7 | requestType?: SuiSignAndExecuteTransactionBlockInput['requestType']; 8 | account?: WalletAccount; 9 | chain?: IdentifierString; 10 | } -------------------------------------------------------------------------------- /src/types/EthosSignMessageInput.ts: -------------------------------------------------------------------------------- 1 | import { WalletAccount } from '@mysten/wallet-standard'; 2 | 3 | export type EthosSignMessageInput = { 4 | message: string | Uint8Array, 5 | account?: WalletAccount, 6 | chain?: string 7 | } -------------------------------------------------------------------------------- /src/types/EthosSignTransactionBlockInput.ts: -------------------------------------------------------------------------------- 1 | import type { TransactionBlock } from '@mysten/sui.js/transactions'; 2 | import type { IdentifierString, WalletAccount } from '@mysten/wallet-standard'; 3 | 4 | export interface EthosSignTransactionBlockInput { 5 | transactionBlock: TransactionBlock; 6 | account?: WalletAccount; 7 | chain?: IdentifierString; 8 | } -------------------------------------------------------------------------------- /src/types/ExtendedSuiObjectData.ts: -------------------------------------------------------------------------------- 1 | import { SuiObjectData } from "@mysten/sui.js/client"; 2 | 3 | export interface ExtendedSuiObjectData extends SuiObjectData { 4 | kiosk?: SuiObjectData; 5 | } -------------------------------------------------------------------------------- /src/types/InvalidPackages.ts: -------------------------------------------------------------------------------- 1 | export interface InvalidPackages { 2 | invalidPackageAdditions: string[]; 3 | invalidPackageSubtractions: string[]; 4 | } -------------------------------------------------------------------------------- /src/types/MenuButtonProps.ts: -------------------------------------------------------------------------------- 1 | import type { WorkingButtonProps } from './WorkingButtonProps'; 2 | 3 | export interface MenuButtonProps extends WorkingButtonProps { 4 | hoverBackgroundColor: string, 5 | externalContext?: any 6 | } -------------------------------------------------------------------------------- /src/types/ModalContextContents.ts: -------------------------------------------------------------------------------- 1 | export interface ModalContextContents { 2 | isModalOpen: boolean, 3 | openModal: () => void, 4 | closeModal: () => void 5 | } -------------------------------------------------------------------------------- /src/types/NFT.ts: -------------------------------------------------------------------------------- 1 | export type NFT = { 2 | name: string 3 | tokenId?: number 4 | imageUri: string 5 | previewUri?: string 6 | chain: string 7 | collection: { 8 | name: string 9 | } 10 | block_number_minted?: number 11 | } 12 | -------------------------------------------------------------------------------- /src/types/Preapproval.ts: -------------------------------------------------------------------------------- 1 | import type { IdentifierString } from '@mysten/wallet-standard'; 2 | 3 | export interface Preapproval { 4 | target: `${string}::${string}::${string}`, 5 | chain: IdentifierString, 6 | address?: string, 7 | objectId: string, 8 | description: string, 9 | totalGasLimit: number; 10 | perTransactionGasLimit: number; 11 | maxTransactionCount: number; 12 | } -------------------------------------------------------------------------------- /src/types/Signer.ts: -------------------------------------------------------------------------------- 1 | import type { Preapproval } from './Preapproval' 2 | import type { SuiClient, SuiTransactionBlockResponse } from '@mysten/sui.js/client'; 3 | import type { 4 | SuiSignPersonalMessageOutput, 5 | SuiSignTransactionBlockOutput, 6 | WalletAccount 7 | } from '@mysten/wallet-standard' 8 | import { EthosSignMessageInput } from './EthosSignMessageInput'; 9 | import { EthosSignTransactionBlockInput } from './EthosSignTransactionBlockInput'; 10 | import { EthosSignAndExecuteTransactionBlockInput } from './EthosSignAndExecuteTransactionBlockInput'; 11 | import { EthosExecuteTransactionBlockInput } from './EthosExecuteTransactionBlockInput'; 12 | 13 | export enum SignerType { 14 | Extension = "extension", 15 | Hosted = "hosted" 16 | } 17 | 18 | export interface Signer { 19 | type: SignerType, 20 | name?: string, 21 | icon?: string, 22 | getAddress: () => Promise 23 | accounts: readonly WalletAccount[], 24 | currentAccount: WalletAccount | null, 25 | signAndExecuteTransactionBlock: (input: EthosSignAndExecuteTransactionBlockInput) => Promise, 26 | executeTransactionBlock: (input: EthosExecuteTransactionBlockInput) => Promise, 27 | signTransactionBlock: (input: EthosSignTransactionBlockInput) => Promise, 28 | requestPreapproval: (preApproval: Preapproval) => Promise, 29 | signPersonalMessage: (input: EthosSignMessageInput) => Promise, 30 | disconnect: () => void, 31 | client: SuiClient 32 | } 33 | 34 | export interface ExtensionSigner extends Signer { 35 | type: SignerType.Extension, 36 | } 37 | 38 | export interface HostedSigner extends Signer { 39 | type: SignerType.Hosted, 40 | email?: string, 41 | logout: () => void 42 | } -------------------------------------------------------------------------------- /src/types/TokenTransferInformation.ts: -------------------------------------------------------------------------------- 1 | export type TokenTransferInformation = { 2 | orderedTransfers: any[] 3 | currentTokenIds: string[] 4 | } 5 | -------------------------------------------------------------------------------- /src/types/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | email: string 3 | wallet: string 4 | } 5 | -------------------------------------------------------------------------------- /src/types/Wallet.ts: -------------------------------------------------------------------------------- 1 | import { WalletContents } from "./WalletContents"; 2 | import { Signer } from "./Signer"; 3 | 4 | export interface Wallet extends Signer { 5 | address: string, 6 | contents?: WalletContents 7 | } -------------------------------------------------------------------------------- /src/types/WalletContents.ts: -------------------------------------------------------------------------------- 1 | import { CoinBalance, SuiObjectData } from "@mysten/sui.js/client" 2 | import BigNumber from "bignumber.js" 3 | import { ConvenenienceSuiObject } from "./ConvenienceSuiObject" 4 | 5 | export interface SuiNFTCollection { 6 | name: string, 7 | type: string 8 | } 9 | 10 | export interface SuiNFT { 11 | chain: string, 12 | type: string, 13 | packageObjectId: string, 14 | moduleName: string 15 | structName: string, 16 | address: string, 17 | objectId: string, 18 | name?: string, 19 | description?: string, 20 | imageUrl?: string, 21 | link?: string, 22 | creator?: string, 23 | projectUrl?: string, 24 | display?: Record | null, 25 | fields?: Record, 26 | collection?: SuiNFTCollection, 27 | links?: Record, 28 | kiosk?: SuiObjectData 29 | } 30 | 31 | export interface Coin { 32 | type: string, 33 | objectId: string, 34 | balance: BigNumber, 35 | digest: string, 36 | version: number, 37 | display?: string | Record 38 | } 39 | 40 | export interface Token { 41 | balance: BigNumber, 42 | coins: Coin[] 43 | } 44 | 45 | export interface WalletContents { 46 | suiBalance: BigNumber, 47 | balances: {[key: string]: CoinBalance}, 48 | tokens: {[key: string]: Token}, 49 | nfts: SuiNFT[] 50 | objects: ConvenenienceSuiObject[], 51 | hasNextPage?: boolean, 52 | nextCursor?: string | { 53 | objectId: string; 54 | atCheckpoint?: number | undefined; 55 | } 56 | } -------------------------------------------------------------------------------- /src/types/WalletContextContents.ts: -------------------------------------------------------------------------------- 1 | import type { Wallet } from './Wallet'; 2 | import type { WalletAccount, WalletWithSuiFeatures } from '@mysten/wallet-standard'; 3 | import type { SuiClient } from '@mysten/sui.js/client' 4 | import { EthosConnectStatus } from '../enums/EthosConnectStatus'; 5 | 6 | export type WalletContextContents = { 7 | wallets?: WalletWithSuiFeatures[], 8 | selectWallet?: ((walletName: string) => void), 9 | status: EthosConnectStatus, 10 | client: SuiClient | null, 11 | wallet?: Wallet; 12 | altAccount?: WalletAccount; 13 | setAltAccount: (_account: WalletAccount) => void; 14 | } -------------------------------------------------------------------------------- /src/types/WorkingButtonProps.ts: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | 3 | export interface WorkingButtonProps extends ButtonHTMLAttributes { 4 | isWorking?: boolean, 5 | workingComponent?: ReactNode 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "lib": ["dom", "esnext", "dom.iterable"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "outDir": "dist", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "downlevelIteration": true, 17 | "moduleResolution": "node", 18 | "baseUrl": "./", 19 | "paths": { 20 | "ethos-connect": ["src"], 21 | "*": ["src/*", "node_modules/*"] 22 | }, 23 | "jsx": "preserve", 24 | "esModuleInterop": true, 25 | "target": "ESNext", 26 | "allowJs": true, 27 | "skipLibCheck": true, 28 | "forceConsistentCasingInFileNames": true, 29 | "resolveJsonModule": true, 30 | "isolatedModules": true 31 | }, 32 | "exclude": ["node_modules", "**/*.test.tsx?"] 33 | } 34 | -------------------------------------------------------------------------------- /types/jest.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare global { 4 | namespace jest { 5 | interface Matchers { 6 | toBeWithinRenderFrame(actual: number): R 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ui_automation_tests/chromedriver: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeyam-ai/ethos-connect/55862f6368ca5ba47a210bcae0d7712221bca8b0/ui_automation_tests/chromedriver -------------------------------------------------------------------------------- /ui_automation_tests/testCaptcha.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.keys import Keys 3 | from selenium.webdriver.common.by import By 4 | from webdriver_manager.chrome import ChromeDriverManager 5 | 6 | driver = webdriver.Chrome(ChromeDriverManager().install()) 7 | # Will wait 5 seconds to find elements: 8 | driver.implicitly_wait(10) 9 | driver.get("https://sui-wallet-staging.onrender.com/") 10 | # elem = driver.find_element(By.ID, "ethos-sign-in-button") 11 | signInButton = driver.find_element(By.XPATH, '//*[@id="__next"]/div/main/div[1]/div/div/div[2]/div/div[1]/div[1]/button') 12 | signInButton.click() 13 | # emailInput = driver.find_element(By.CSS_SELECTOR, "input[type=email]") 14 | # emailInput.send_keys("me@test.com") 15 | 16 | # sendEmailButton = driver.find_element(By.CSS_SELECTOR, "button[type=submit]") 17 | sendEmailButton = driver.find_element(By.XPATH, '//*[@id="__next"]/div/main/div[1]/div/div/div[2]/div/div[1]/div[1]/div[4]/div[3]/div/div/div/div[2]/div[2]/form/button') 18 | sendEmailButton.click() 19 | 20 | emailSentConfirmation = driver.find_element(By.XPATH, '//*[@id="headlessui-dialog-title-:r7:"]') 21 | print('emailSentConfirmation:', emailSentConfirmation) 22 | # elem.clear() 23 | # elem.send_keys("pycon") 24 | # elem.send_keys(Keys.RETURN) 25 | # assert "No results found." not in driver.page_source 26 | # driver.close() --------------------------------------------------------------------------------