diff --git a/.github/assets/dashboard.png b/.github/assets/dashboard.png deleted file mode 100644 index faf1bba9..00000000 Binary files a/.github/assets/dashboard.png and /dev/null differ diff --git a/.github/assets/dashboardUpload.png b/.github/assets/dashboardUpload.png deleted file mode 100644 index 7c6934bd..00000000 Binary files a/.github/assets/dashboardUpload.png and /dev/null differ diff --git a/.github/assets/ipc-dashboard-add-a-contact.png b/.github/assets/ipc-dashboard-add-a-contact.png new file mode 100644 index 00000000..e9313798 Binary files /dev/null and b/.github/assets/ipc-dashboard-add-a-contact.png differ diff --git a/.github/assets/ipc-dashboard-contacts.png b/.github/assets/ipc-dashboard-contacts.png new file mode 100644 index 00000000..2319b209 Binary files /dev/null and b/.github/assets/ipc-dashboard-contacts.png differ diff --git a/.github/assets/ipc-dashboard-files-shared.png b/.github/assets/ipc-dashboard-files-shared.png new file mode 100644 index 00000000..442fecd0 Binary files /dev/null and b/.github/assets/ipc-dashboard-files-shared.png differ diff --git a/.github/assets/ipc-dashboard-my-profile.png b/.github/assets/ipc-dashboard-my-profile.png new file mode 100644 index 00000000..771ca396 Binary files /dev/null and b/.github/assets/ipc-dashboard-my-profile.png differ diff --git a/.github/assets/ipc-dashboard-share-a-file.png b/.github/assets/ipc-dashboard-share-a-file.png new file mode 100644 index 00000000..89dce481 Binary files /dev/null and b/.github/assets/ipc-dashboard-share-a-file.png differ diff --git a/.github/assets/ipc-dashboard-update-a-contact.png b/.github/assets/ipc-dashboard-update-a-contact.png new file mode 100644 index 00000000..683ccf65 Binary files /dev/null and b/.github/assets/ipc-dashboard-update-a-contact.png differ diff --git a/.github/assets/ipc-dashboard-upload-a-file.png b/.github/assets/ipc-dashboard-upload-a-file.png new file mode 100644 index 00000000..d063194f Binary files /dev/null and b/.github/assets/ipc-dashboard-upload-a-file.png differ diff --git a/.github/assets/ipc-dashboard.png b/.github/assets/ipc-dashboard.png new file mode 100644 index 00000000..543daeff Binary files /dev/null and b/.github/assets/ipc-dashboard.png differ diff --git a/.github/assets/ipc-download-a-file.png b/.github/assets/ipc-download-a-file.png index 2bf343d7..827c28b1 100644 Binary files a/.github/assets/ipc-download-a-file.png and b/.github/assets/ipc-download-a-file.png differ diff --git a/.github/assets/ipc-file-loading.png b/.github/assets/ipc-file-loading.png index 5a98bfc9..6c0341c0 100644 Binary files a/.github/assets/ipc-file-loading.png and b/.github/assets/ipc-file-loading.png differ diff --git a/.github/assets/ipc-graph.png b/.github/assets/ipc-graph.png new file mode 100644 index 00000000..b27d0e4d Binary files /dev/null and b/.github/assets/ipc-graph.png differ diff --git a/.github/assets/ipc-post-message.png b/.github/assets/ipc-post-message.png new file mode 100644 index 00000000..a5debaa1 Binary files /dev/null and b/.github/assets/ipc-post-message.png differ diff --git a/.github/assets/ipc-share-a-file.png b/.github/assets/ipc-share-a-file.png index 7a055f8e..52dfa9dd 100644 Binary files a/.github/assets/ipc-share-a-file.png and b/.github/assets/ipc-share-a-file.png differ diff --git a/.github/assets/ipc-upload-a-file.png b/.github/assets/ipc-upload-a-file.png index 9a647f21..b3cd817d 100644 Binary files a/.github/assets/ipc-upload-a-file.png and b/.github/assets/ipc-upload-a-file.png differ diff --git a/.gitignore b/.gitignore index 755dbaec..78e2f5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ cypress/screenshots cypress/videos .metamask + +/.vscode diff --git a/README.md b/README.md index fe0a53a7..0e94ce8f 100644 --- a/README.md +++ b/README.md @@ -63,17 +63,59 @@ You are now ready to access to your decentralized cloud :boom: !
Dashboard - ![Dashboard](.github/assets/dashboard.png) + ![Dashboard](.github/assets/ipc-dashboard.png)
- Dashboard - Upload document + Dashboard - Upload a file - ![Dashboard Upload](.github/assets/dashboardUpload.png) + ![Dashboard Upload](.github/assets/ipc-dashboard-upload-a-file.png)
+
+ Dashboard - Share a file + +![Dashboard Upload](.github/assets/ipc-dashboard-share-a-file.png) + +
+ +
+ Dashboard - Files shared + +![Dashboard Upload](.github/assets/ipc-dashboard-files-shared.png) + +
+ +
+ Dashboard - Contacts + +![Dashboard Upload](.github/assets/ipc-dashboard-contacts.png) + +
+ +
+ Dashboard - Add a contact + +![Dashboard Upload](.github/assets/ipc-dashboard-add-a-contact.png) + +
+ +
+ Dashboard - Update a contact + +![Dashboard Upload](.github/assets/ipc-dashboard-update-a-contact.png) + +
+ +
+ Dashboard - User's profile + +![Dashboard Upload](.github/assets/ipc-dashboard-my-profile.png) + +
+ ## How ? :thinking: **Technologies 🧑‍💻** @@ -87,6 +129,78 @@ We use [Aleph SDK TS](https://github.com/aleph-im/aleph-sdk-ts#readme). **Security 🛡️** Every file that you upload will be encrypted thanks to [crypto-js](https://www.npmjs.com/package/crypto-js). +**How it works?** + +
+ Full overview + + +
+ +--- + +- For each file, a random key is generated and the content of the file is encrypted with this key. +- The content is pushed into a store message via the aleph network. +- The hash of the store message and the key are added to the 'Contacts' post message. + +
+ Upload a file + + + +
+ +--- + +- For each contacts into the 'Post Message - Contacts', the files and contacts are get. +- An occurrence between the address of the user and the contacts is searched. +- For each file found, metadata about the files are retrieved. + +
+ Load a file + + + +
+ +--- + +- The content is retrieved from the aleph network from his hash. +- The content is decrypt with the key, itself decrypt with the private key of the user. + +
+ Download a file + + + +
+ +--- + +- The hash and the key are encrypted with the public key of the contact. +- These infos are added to the list of shared files of the contact. + +
+ Share a file + + + +
+ +--- + +- One post message, with the list of contacts and the list of shared files for each contacts +- The post message contains the info about the contact, his name, address, public key and a list of shared files + +
+ Post messages + +
+ +
+ +
+ ## Our PoC team :ok_hand: ### September 2021 - Today diff --git a/cypress/integration/dashboard.spec.js b/cypress/integration/dashboard.spec.js index 9c9ccefc..4e933fc0 100644 --- a/cypress/integration/dashboard.spec.js +++ b/cypress/integration/dashboard.spec.js @@ -14,55 +14,6 @@ describe('Create account for DashboardView tests', () => { }); }); -describe('Good front for DashboardView', () => { - it('Go to dashboard view', () => { - cy.visit('http://localhost:3000/login'); - cy.wait(1000); - cy.get('#ipc-loginView-text-area').click().type(dashboardSpecMnemonic); - cy.get('#ipc-loginView-credentials-button').click(); - }); - - it('Good title', () => { - cy.get('#ipc-sideBar-title').should('contain', 'Inter Planetary Cloud'); - }); - - it('Good name for upload button', () => { - cy.get('#ipc-dashboardView-drawer-button').click({force: true}); - cy.get('#ipc-upload-button').should('contain', 'Upload a file'); - }); -}); - -describe('Good Modal Front for DashboardView', () => { - it('Go to upload modal into dashboard view', () => { - cy.visit('http://localhost:3000/login'); - cy.wait(1000); - cy.get('#ipc-loginView-text-area').click().type(dashboardSpecMnemonic); - cy.get('#ipc-loginView-credentials-button').click().wait(3000); - cy.get('#ipc-dashboardView-drawer-button').click({force: true}); - cy.get('#ipc-upload-button').click(); - }); - - it('Good header', () => { - cy.get('header').should('contain', 'Upload a file'); - }); - - it('Good number of buttons', () => { - cy.get('button').should('have.length', 3); - }); - - it('Good number of input', () => { - cy.get('input[type=file]').should('have.length', 1); - }); - - it('Good name for upload a file button', () => { - cy.get('#ipc-dashboardView-upload-file-modal-button').should('contain', 'Upload file'); - }); - - it('Good name for close button', () => { - cy.get('#ipc-modal-close-button').should('contain', 'Close'); - }); -}); - describe('Upload a file modal for DashboardView', () => { const fixtureFile = 'upload_test_file.txt'; @@ -71,20 +22,20 @@ describe('Upload a file modal for DashboardView', () => { cy.wait(1000); cy.get('#ipc-loginView-text-area').click().type(dashboardSpecMnemonic); cy.get('#ipc-loginView-credentials-button').click().wait(3000); - cy.get('#ipc-dashboardView-drawer-button').click({force: true}); - cy.get('#ipc-upload-button').click(); + cy.get('#ipc-dashboardView-drawer-button').click({ force: true }); + cy.get('#ipc-upload-button').click().wait(2500); }); it('Good number of buttons after upload', () => { cy.get('#ipc-dashboardView-upload-file').attachFile(fixtureFile); cy.get('#ipc-dashboardView-upload-file-modal-button').click(); cy.wait(2000); - cy.get('button').should('have.length', 2); + cy.get('button').should('have.length', 8); }); it('Good number of buttons after closing modal', () => { cy.get('#ipc-modal-close-button').click(); - cy.get('button').should('have.length', 2); + cy.get('button').should('have.length', 8); }); }); @@ -94,8 +45,9 @@ describe('Download a file for DashboardView', () => { cy.wait(1000); cy.get('#ipc-loginView-text-area').click().type(dashboardSpecMnemonic); cy.get('#ipc-loginView-credentials-button').click(); + cy.wait(2500); cy.get('#ipc-dashboardView-download-button').click(); - cy.wait(1000); + cy.wait(3000); }); it('Good content for downloaded file', () => { diff --git a/cypress/integration/dashboardFront.spec.js b/cypress/integration/dashboardFront.spec.js new file mode 100644 index 00000000..c94cf049 --- /dev/null +++ b/cypress/integration/dashboardFront.spec.js @@ -0,0 +1,64 @@ +let dashboardSpecMnemonic = ''; + +describe('Create account for DashboardView tests', () => { + it('Connect', () => { + cy.visit('http://localhost:3000/signup'); + cy.wait(1000); + cy.get('#ipc-signupView-credentials-signup-button').click(); + cy.get('#ipc-signupView-text-area') + .invoke('val') + .then((input) => { + dashboardSpecMnemonic = input; + }); + cy.get('#ipc-modal-close-button').click(); + }); +}); + +describe('Good front for DashboardView', () => { + it('Go to dashboard view', () => { + cy.visit('http://localhost:3000/login'); + cy.wait(1000); + cy.get('#ipc-loginView-text-area').click().type(dashboardSpecMnemonic); + cy.get('#ipc-loginView-credentials-button').click(); + }); + + it('Good title', () => { + cy.get('#ipc-sideBar-title').should('contain', 'Inter Planetary Cloud'); + }); + + it('Good name for upload button', () => { + cy.get('#ipc-dashboardView-drawer-button').click({ force: true }); + cy.get('#ipc-upload-button').should('contain', 'Upload a file'); + }); +}); + +describe('Good Modal Front for DashboardView', () => { + it('Go to upload modal into dashboard view', () => { + cy.visit('http://localhost:3000/login'); + cy.wait(1000); + cy.get('#ipc-loginView-text-area').click().type(dashboardSpecMnemonic); + cy.get('#ipc-loginView-credentials-button').click().wait(3000); + cy.get('#ipc-dashboardView-drawer-button').click({ force: true }); + cy.get('#ipc-upload-button').click(); + }); + + it('Good header', () => { + cy.get('header').should('contain', 'Upload a file'); + }); + + it('Good number of buttons', () => { + cy.get('button').should('have.length', 8); + }); + + it('Good number of input', () => { + cy.get('input[type=file]').should('have.length', 1); + }); + + it('Good name for upload a file button', () => { + cy.get('#ipc-dashboardView-upload-file-modal-button').should('contain', 'Upload file'); + }); + + it('Good name for close button', () => { + cy.get('#ipc-modal-close-button').should('contain', 'Close'); + }); +}); diff --git a/package.json b/package.json index 0bf19cf8..8cc37165 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "aleph-sdk-ts": "^2.2.1", "crypto-js": "^4.0.0", "env-var": "^7.1.1", + "eth-crypto": "^2.2.0", "ethers": "^5.5.2", "framer-motion": "^4.1.17", "it-all": "^1.0.5", @@ -23,6 +24,7 @@ "react": "^17.0.2", "react-clipboard.js": "^2.0.16", "react-dom": "^17.0.2", + "react-icons": "^4.3.1", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "typescript": "^4.3.4", diff --git a/src/components/ContactCard.tsx b/src/components/ContactCard.tsx new file mode 100644 index 00000000..0a93ab91 --- /dev/null +++ b/src/components/ContactCard.tsx @@ -0,0 +1,28 @@ +import { Box, Flex, Text, VStack } from '@chakra-ui/react'; +import { IPCContact } from '../types/types'; + +type FileCardProps = { + contact: IPCContact; + children: JSX.Element; +}; + +export const ContactCard = ({ contact, children }: FileCardProps): JSX.Element => ( + + + {contact.name} + + {children} + + + +); diff --git a/src/components/ContactCards.tsx b/src/components/ContactCards.tsx new file mode 100644 index 00000000..39614ddb --- /dev/null +++ b/src/components/ContactCards.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Box, Button, Divider, Tooltip, VStack } from '@chakra-ui/react'; +import { CopyIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons'; +import { IPCContact } from '../types/types'; +import { ContactCard } from './ContactCard'; + +type ContactCardsProps = { + contacts: IPCContact[]; + setContactInfo: React.Dispatch>; + onOpenContactUpdate: () => void; + onOpenContactAdd: () => void; + deleteContact: (contactToDelete: IPCContact) => Promise; +}; + +export const ContactCards = ({ + contacts, + setContactInfo, + onOpenContactUpdate, + onOpenContactAdd, + deleteContact, +}: ContactCardsProps): JSX.Element => ( + <> + + + + + + {contacts.map((contact, index) => { + if (index !== 0) + return ( + + <> + + + + + + + + + + + + ); + return ; + })} + +); diff --git a/src/components/CustomButtons.tsx b/src/components/CustomButtons.tsx new file mode 100644 index 00000000..b6568b94 --- /dev/null +++ b/src/components/CustomButtons.tsx @@ -0,0 +1,19 @@ +import { Button } from '@chakra-ui/react'; + +type UploadButtonProps = { + text: string; + onClick: () => void; + isLoading: boolean; +}; + +export const UploadButton = ({ text, onClick, isLoading }: UploadButtonProps): JSX.Element => ( + +); + +export const ContactButton = ({ text, onClick, isLoading }: UploadButtonProps): JSX.Element => ( + +); diff --git a/src/components/DisplayFileCards.tsx b/src/components/DisplayFileCards.tsx new file mode 100644 index 00000000..07d49863 --- /dev/null +++ b/src/components/DisplayFileCards.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { IPCContact, IPCFile } from '../types/types'; +import { FileCards } from './FileCards'; +import { ContactCards } from './ContactCards'; +import { ProfileCard } from './ProfileCard'; + +type FileCardsProps = { + myFiles: IPCFile[]; + sharedFiles: IPCFile[]; + contacts: IPCContact[]; + index: number; + downloadFile: (file: IPCFile) => Promise; + isDownloadLoading: boolean; + setSelectedFile: React.Dispatch>; + onOpenShare: () => void; + setContactInfo: React.Dispatch>; + onOpenContactUpdate: () => void; + onOpenContactAdd: () => void; + deleteContact: (contactToDelete: IPCContact) => Promise; +}; + +export const DisplayFileCards = ({ + myFiles, + sharedFiles, + contacts, + index, + downloadFile, + isDownloadLoading, + setSelectedFile, + onOpenShare, + setContactInfo, + onOpenContactUpdate, + onOpenContactAdd, + deleteContact, +}: FileCardsProps): JSX.Element => { + if (index === 0) + return ( + + ); + if (index === 1) + return ( + + ); + if (index === 2) + return ( + + ); + return ( + + ); +}; diff --git a/src/components/FileCard.tsx b/src/components/FileCard.tsx index 2b22fa5b..9855e463 100644 --- a/src/components/FileCard.tsx +++ b/src/components/FileCard.tsx @@ -1,6 +1,6 @@ -import { Box, Flex, Text } from '@chakra-ui/react'; +import { Box, Flex, Text, VStack } from '@chakra-ui/react'; -import { IPCFile } from 'lib/drive'; +import { IPCFile } from 'types/types'; type FileCardProps = { file: IPCFile; @@ -19,10 +19,14 @@ const FileCard = ({ file, children }: FileCardProps): JSX.Element => ( display="flex" justifyContent="space-between" > - - {file.name} - {children} - + + + {file.name} + + + {children} + + ); diff --git a/src/components/FileCards.tsx b/src/components/FileCards.tsx new file mode 100644 index 00000000..fe4a9703 --- /dev/null +++ b/src/components/FileCards.tsx @@ -0,0 +1,58 @@ +import { Button, Icon } from '@chakra-ui/react'; +import { DownloadIcon } from '@chakra-ui/icons'; +import { MdPeopleAlt } from 'react-icons/md'; +import React from 'react'; +import FileCard from './FileCard'; +import { IPCFile } from '../types/types'; + +type FileCardsProps = { + files: IPCFile[]; + downloadFile: (file: IPCFile) => Promise; + isDownloadLoading: boolean; + setSelectedFile: React.Dispatch>; + onOpenShare: () => void; +}; + +export const FileCards = ({ + files, + downloadFile, + isDownloadLoading, + setSelectedFile, + onOpenShare, +}: FileCardsProps): JSX.Element => ( + <> + {files.map((file) => ( + + <> + + + + + ))} + +); diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index deb3243e..f6f47052 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -9,7 +9,7 @@ type PopupProps = { onClose: () => void; title: string; children: JSX.Element; - CTA: JSX.Element; + CTA?: JSX.Element; }; const Popup = ({ isOpen, onClose, title, children, CTA }: PopupProps): JSX.Element => ( diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx new file mode 100644 index 00000000..2daac5a9 --- /dev/null +++ b/src/components/ProfileCard.tsx @@ -0,0 +1,60 @@ +import { Box, VStack, Text, Flex, Tooltip, Button } from '@chakra-ui/react'; +import { CopyIcon, EditIcon } from '@chakra-ui/icons'; +import React from 'react'; +import { IPCContact } from '../types/types'; + +type ProfileCardProps = { + profile: IPCContact; + setContactInfo: React.Dispatch>; + onOpenContactUpdate: () => void; +}; + +export const ProfileCard = ({ profile, setContactInfo, onOpenContactUpdate }: ProfileCardProps): JSX.Element => ( + + + {profile.name} + + {profile.address} + + + + + + + + + + + +); diff --git a/src/components/ResponsiveBar.tsx b/src/components/ResponsiveBar.tsx new file mode 100644 index 00000000..2e8a87c8 --- /dev/null +++ b/src/components/ResponsiveBar.tsx @@ -0,0 +1,103 @@ +import { + Box, + Button, + Divider, + Drawer, + DrawerContent, + DrawerOverlay, + HStack, + Icon, + SlideDirection, + Text, + useBreakpointValue, + useDisclosure, + VStack, +} from '@chakra-ui/react'; +import { HamburgerIcon } from '@chakra-ui/icons'; + +import React from 'react'; +import colors from '../theme/foundations/colors'; +import Sidebar from './SideBar'; +import { UploadButton } from './CustomButtons'; + +type BarProps = { + onOpen: () => void; + isUploadLoading: boolean; + setSelectedTab: React.Dispatch>; + selectedTab: number; +}; + +export const LeftBar = ({ onOpen, isUploadLoading, setSelectedTab, selectedTab }: BarProps): JSX.Element => ( + onOpen()} isLoading={isUploadLoading} />} + contactTab="Contacts" + myFilesTab="My files" + profileTab="My profile" + sharedFilesTab="Shared with me" + setSelectedTab={setSelectedTab} + currentTabIndex={selectedTab} + /> +); + +export const BarWithDrawer = ({ onOpen, setSelectedTab, isUploadLoading, selectedTab }: BarProps): JSX.Element => { + const { isOpen: isOpenDrawer, onOpen: onOpenDrawer, onClose: onCloseDrawer } = useDisclosure(); + const placement: SlideDirection = 'left'; + + return ( + + + + + + + + + + + + + + Inter Planetary Cloud + + + + + + + ); +}; + +export const ResponsiveBar = ({ onOpen, setSelectedTab, isUploadLoading, selectedTab }: BarProps): JSX.Element => { + const isDrawerNeeded: boolean = useBreakpointValue({ base: true, xs: true, lg: false }) || false; + + if (!isDrawerNeeded) + return ( + + ); + return ( + + ); +}; diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 647a34a6..6b8fa2a8 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -1,12 +1,27 @@ -import { Text, VStack } from '@chakra-ui/react'; +import { Tab, TabList, Tabs, Text, VStack } from '@chakra-ui/react'; import colors from 'theme/foundations/colors'; +import React from 'react'; type SideBarPropsType = { + contactTab: string; + myFilesTab: string; + sharedFilesTab: string; + profileTab: string; uploadButton: JSX.Element; + setSelectedTab: React.Dispatch>; + currentTabIndex: number; }; -const SideBar = ({ uploadButton }: SideBarPropsType): JSX.Element => ( +const SideBar = ({ + contactTab, + myFilesTab, + sharedFilesTab, + profileTab, + uploadButton, + setSelectedTab, + currentTabIndex, +}: SideBarPropsType): JSX.Element => ( ( bgGradient={`linear-gradient(90deg, ${colors.blue[700]} 0%, ${colors.red[700]} 100%)`} bgClip="text" id="ipc-sideBar-title" + pb="64px" > Inter Planetary Cloud + setSelectedTab(index)}> + + + {myFilesTab} + + + {sharedFilesTab} + + + {contactTab} + + + {profileTab} + + + + {uploadButton} - {uploadButton} ); diff --git a/src/components/UploadButton.tsx b/src/components/UploadButton.tsx deleted file mode 100644 index 34fc2305..00000000 --- a/src/components/UploadButton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Button } from '@chakra-ui/react'; - -type UploadButtonProps = { - text: string; - onClick: () => void; - isLoading: boolean; -}; - -const UploadButton = ({ text, onClick, isLoading }: UploadButtonProps): JSX.Element => ( - -); - -export default UploadButton; diff --git a/src/lib/contact.ts b/src/lib/contact.ts new file mode 100644 index 00000000..8827246f --- /dev/null +++ b/src/lib/contact.ts @@ -0,0 +1,237 @@ +import { accounts, post } from 'aleph-sdk-ts'; + +import { DEFAULT_API_V2 } from 'aleph-sdk-ts/global'; +import { ItemType } from 'aleph-sdk-ts/messages/message'; +import { ALEPH_CHANNEL } from 'config/constants'; + +import { IPCContact, IPCFile, ResponseType } from 'types/types'; +import EthCrypto from 'eth-crypto'; + +class Contact { + public contacts: IPCContact[]; + + public contactsPostHash: string; + + private readonly account: accounts.base.Account | undefined; + + private private_key: string; + + constructor(importedAccount: accounts.base.Account, private_key: string) { + this.contacts = []; + this.contactsPostHash = ''; + this.account = importedAccount; + this.private_key = private_key; + } + + public async load(): Promise { + try { + if (this.account) { + const userData = await post.Get({ + APIServer: DEFAULT_API_V2, + types: '', + pagination: 200, + page: 1, + refs: [], + addresses: [this.account.address], + tags: [], + hashes: [], + }); + + userData.posts.map((postContent) => { + const itemContent = JSON.parse(postContent.item_content); + if (itemContent.content.header === 'InterPlanetaryCloud2.0 - Contacts') { + this.contactsPostHash = postContent.hash; + if (itemContent.content.contacts.length > 0) { + itemContent.content.contacts.map((contact: IPCContact) => { + this.contacts.push(contact); + return true; + }); + } + return true; + } + return false; + }); + + if (this.contactsPostHash === '') { + console.log('Create Post Message for Contacts'); + this.contacts.push({ + name: 'Owner (Me)', + address: this.account.address, + publicKey: this.account.publicKey, + files: [], + }); + const newPostPublishResponse = await post.Publish({ + APIServer: DEFAULT_API_V2, + channel: ALEPH_CHANNEL, + inlineRequested: true, + storageEngine: ItemType.ipfs, + account: this.account, + postType: '', + content: { + header: 'InterPlanetaryCloud2.0 - Contacts', + contacts: this.contacts, + }, + }); + this.contactsPostHash = newPostPublishResponse.item_hash; + } + return { success: true, message: 'Contacts loaded' }; + } + return { success: false, message: 'Failed to load account' }; + } catch (err) { + console.log(err); + return { success: false, message: 'Failed to load contacts' }; + } + } + + public async add(contactToAdd: IPCContact): Promise { + try { + if (this.account) { + if (this.contacts.find((contact) => contact.address === contactToAdd.address)) { + return { success: false, message: 'Contact already exist' }; + } + this.contacts.push(contactToAdd); + + await post.Publish({ + APIServer: DEFAULT_API_V2, + channel: ALEPH_CHANNEL, + inlineRequested: true, + storageEngine: ItemType.ipfs, + account: this.account, + postType: 'amend', + content: { + header: 'InterPlanetaryCloud2.0 - Contacts', + contacts: this.contacts, + }, + ref: this.contactsPostHash, + }); + return { success: true, message: 'Contact added' }; + } + return { success: false, message: 'Failed to load account' }; + } catch (err) { + console.log(err); + return { success: false, message: 'Failed to add this contact' }; + } + } + + public async remove(contactAddress: string): Promise { + try { + if (this.account) { + if (contactAddress !== this.account.address) { + this.contacts.map((contact, index) => { + if (contact.address === contactAddress) { + this.contacts.splice(index, 1); + return true; + } + return false; + }); + + await post.Publish({ + APIServer: DEFAULT_API_V2, + channel: ALEPH_CHANNEL, + inlineRequested: true, + storageEngine: ItemType.ipfs, + account: this.account, + postType: 'amend', + content: { + header: 'InterPlanetaryCloud2.0 - Contacts', + contacts: this.contacts, + }, + ref: this.contactsPostHash, + }); + return { success: true, message: 'Contact deleted' }; + } + return { success: false, message: "You can't delete your account" }; + } + return { success: false, message: 'Failed to load account' }; + } catch (err) { + console.log(err); + return { success: false, message: 'Failed to delete this contact' }; + } + } + + public async update(contactAddress: string, newName: string): Promise { + try { + if (this.account) { + if ( + this.contacts.find((contact, index) => { + if (contact.address === contactAddress) { + this.contacts[index].name = newName; + return true; + } + return false; + }) + ) { + await post.Publish({ + APIServer: DEFAULT_API_V2, + channel: ALEPH_CHANNEL, + inlineRequested: true, + storageEngine: ItemType.ipfs, + account: this.account, + postType: 'amend', + content: { + header: 'InterPlanetaryCloud2.0 - Contacts', + contacts: this.contacts, + }, + ref: this.contactsPostHash, + }); + return { success: true, message: 'Contact updated' }; + } + return { success: false, message: 'Contact does not exist' }; + } + return { success: false, message: 'Failed to load account' }; + } catch (err) { + console.log(err); + return { success: false, message: 'Failed to update this contact' }; + } + } + + public async addFileToContact(contactAddress: string, mainFile: IPCFile): Promise { + try { + if (this.account) { + if ( + await Promise.all( + this.contacts.map(async (contact, contactIndex) => { + if (contact.address === contactAddress) { + if (this.contacts[contactIndex].files.find((file) => file.hash === mainFile.hash)) { + return { success: false, message: 'The file is already shared' }; + } + this.contacts[contactIndex].files.push({ + hash: mainFile.hash, + key: await EthCrypto.encryptWithPublicKey( + contact.publicKey.slice(2), + await EthCrypto.decryptWithPrivateKey(this.private_key, mainFile.key), + ), + created_at: mainFile.created_at, + name: mainFile.name, + }); + await post.Publish({ + APIServer: DEFAULT_API_V2, + channel: ALEPH_CHANNEL, + inlineRequested: true, + storageEngine: ItemType.ipfs, + account: this.account!, + postType: 'amend', + content: { + header: 'InterPlanetaryCloud2.0 - Contacts', + contacts: this.contacts, + }, + ref: this.contactsPostHash, + }); + return true; + } + return false; + }), + ) + ) + return { success: true, message: 'File shared with the contact' }; + return { success: false, message: 'Contact does not exist' }; + } + return { success: false, message: 'Failed to load account' }; + } catch (err) { + console.log(err); + return { success: false, message: 'Failed to share the file with the contact' }; + } + } +} + +export default Contact; diff --git a/src/lib/drive.ts b/src/lib/drive.ts index 5075ac15..1c0557cc 100644 --- a/src/lib/drive.ts +++ b/src/lib/drive.ts @@ -10,21 +10,15 @@ import CryptoJS from 'crypto-js'; import { ArraybufferToString } from 'utils/arraytbufferToString'; -type IPCFile = { - name: string; - content: string; - created_at: number; -}; - -type ResponseType = { - success: boolean; - message: string; -}; +import { IPCContact, IPCFile, ResponseType } from 'types/types'; +import EthCrypto from 'eth-crypto'; class Drive { public files: IPCFile[]; - public postsHash: string; + public sharedFiles: IPCFile[]; + + public filesPostHash: string; private readonly account: accounts.base.Account | undefined; @@ -32,71 +26,65 @@ class Drive { constructor(importedAccount: accounts.base.Account, private_key: string) { this.files = []; + this.sharedFiles = []; this.account = importedAccount; - this.postsHash = ''; + this.filesPostHash = ''; this.private_key = private_key; } - public async load(): Promise { + public async loadShared(contacts: IPCContact[]): Promise { try { if (this.account) { - const userData = await post.Get({ - APIServer: DEFAULT_API_V2, - types: '', - pagination: 200, - page: 1, - refs: [], - addresses: [this.account.address], - tags: [], - hashes: [], - }); - - const postMessage = userData.posts.map((postContent) => { - const itemContent = JSON.parse(postContent.item_content); - if (itemContent.content.header === 'InterPlanetaryCloud2.0 Header') { - this.postsHash = postContent.hash; - if (itemContent.content.files.length > 0) { - itemContent.content.files[0].map((file: IPCFile) => { - this.files.push(file); - return true; - }); - } - return true; - } - return false; - }); - if (postMessage.length !== 1) { - if (postMessage.length > 1) { - return { success: false, message: 'Too many post messages' }; - } - console.log('Create Post Message'); - const newPostPublishResponse = await post.Publish({ - APIServer: DEFAULT_API_V2, - channel: ALEPH_CHANNEL, - inlineRequested: true, - storageEngine: ItemType.ipfs, - account: this.account, - postType: '', - content: { - header: 'InterPlanetaryCloud2.0 Header', - files: this.files, - }, - }); - this.postsHash = newPostPublishResponse.item_hash; - } - return { success: true, message: 'Drive loaded' }; + await Promise.all( + contacts.map(async (contact) => { + const userData = await post.Get({ + APIServer: DEFAULT_API_V2, + types: '', + pagination: 200, + page: 1, + refs: [], + addresses: [contact.address], + tags: [], + hashes: [], + }); + + await Promise.all( + userData.posts.map(async (postContent) => { + const itemContent = JSON.parse(postContent.item_content); + + if (itemContent.content.header === 'InterPlanetaryCloud2.0 - Contacts') { + console.log('Post contacts founded'); + await Promise.all( + itemContent.content.contacts.map(async (contactToFind: IPCContact) => { + if (contactToFind.address === this.account!.address) { + if (contact.address === this.account!.address) + this.files = this.files.concat(contactToFind.files); + else this.sharedFiles = this.sharedFiles.concat(contactToFind.files); + return true; + } + return false; + }), + ); + return true; + } + return false; + }), + ); + }), + ); + return { success: true, message: 'Shared drive loaded' }; } return { success: false, message: 'Failed to load account' }; } catch (err) { - console.log(err); - return { success: false, message: 'Failed to load drive' }; + console.error(err); + return { success: false, message: 'Failed to load shared drive' }; } } - public async upload(file: IPCFile): Promise { + public async upload(file: IPCFile, key: string): Promise { try { if (this.account) { - const encryptedContentFile = CryptoJS.AES.encrypt(file.content, this.private_key).toString(); + const encryptedContentFile = CryptoJS.AES.encrypt(file.hash, key).toString(); const newStoreFile = new File([encryptedContentFile], file.name, { type: 'text/plain', @@ -112,24 +100,12 @@ class Drive { const newFile: IPCFile = { name: file.name, - content: fileHashPublishStore.content.item_hash, + hash: fileHashPublishStore.content.item_hash, created_at: file.created_at, + key: await EthCrypto.encryptWithPublicKey(this.account.publicKey.slice(2), key), }; this.files.push(newFile); - await post.Publish({ - APIServer: DEFAULT_API_V2, - channel: ALEPH_CHANNEL, - inlineRequested: true, - storageEngine: ItemType.ipfs, - account: this.account, - postType: 'amend', - content: { - header: 'InterPlanetaryCloud2.0 Header', - files: [this.files], - }, - ref: this.postsHash, - }); return { success: true, message: 'File uploaded' }; } @@ -145,15 +121,18 @@ class Drive { if (this.account) { const storeFile = await store.Get({ APIServer: DEFAULT_API_V2, - fileHash: file.content, + fileHash: file.hash, }); - const decryptedContentFile = CryptoJS.AES.decrypt(ArraybufferToString(storeFile), this.private_key).toString( + const keyFile = await EthCrypto.decryptWithPrivateKey(this.private_key.slice(2), file.key); + const decryptedContentFile = CryptoJS.AES.decrypt(ArraybufferToString(storeFile), keyFile).toString( CryptoJS.enc.Utf8, ); - const blob = new Blob([decryptedContentFile]); - fileDownload(blob, file.name); + const newFile = new File([decryptedContentFile], file.name, { + type: 'plain/text', + }); + fileDownload(newFile, file.name); return { success: true, message: 'File downloaded' }; } return { success: false, message: 'Failed to load account' }; @@ -164,6 +143,4 @@ class Drive { } } -export type { IPCFile }; - export default Drive; diff --git a/src/lib/user.ts b/src/lib/user.ts index 22da0a2d..59c55fbb 100644 --- a/src/lib/user.ts +++ b/src/lib/user.ts @@ -4,14 +4,19 @@ import { mnemonicToPrivateKey } from 'utils/mnemonicToPrivateKey'; import Drive from './drive'; +import Contact from './contact'; + class User { public account: accounts.base.Account | undefined; public drive: Drive; + public contact: Contact; + constructor(importedAccount: accounts.base.Account, mnemonic: string) { this.account = importedAccount; this.drive = new Drive(this.account, mnemonicToPrivateKey(mnemonic)); + this.contact = new Contact(this.account, mnemonicToPrivateKey(mnemonic)); } } diff --git a/src/theme/components/button.ts b/src/theme/components/button.ts index ed04250a..57ec4ca6 100644 --- a/src/theme/components/button.ts +++ b/src/theme/components/button.ts @@ -32,6 +32,10 @@ const Button = { bgGradient: `linear-gradient(90deg, ${colors.blue[700]} 0%, ${colors.red[700]} 100%)`, color: 'white', }, + reverseInline: { + bgGradient: `linear-gradient(90deg, ${colors.red[700]} 0%, ${colors.blue[700]} 100%)`, + color: 'white', + }, }, defaultProps: { size: 'md', diff --git a/src/theme/index.ts b/src/theme/index.ts index cf882b58..ccb266d3 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -1,7 +1,7 @@ import { extendTheme } from '@chakra-ui/react'; import { createBreakpoints } from '@chakra-ui/theme-tools'; -// Foundations overrides +// Foundation overrides import fonts from './foundations/fonts'; import colors from './foundations/colors'; import radius from './foundations/borderRadius'; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..01b6cfc5 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,20 @@ +import { Encrypted } from 'eth-crypto'; + +export type IPCFile = { + hash: string; + key: Encrypted; + name: string; + created_at: number; +}; + +export type IPCContact = { + name: string; + address: string; + publicKey: string; + files: IPCFile[]; +}; + +export type ResponseType = { + success: boolean; + message: string; +}; diff --git a/src/utils/fileManipulation.ts b/src/utils/fileManipulation.ts new file mode 100644 index 00000000..177a04d2 --- /dev/null +++ b/src/utils/fileManipulation.ts @@ -0,0 +1,6 @@ +export const extractFilename = (filepath: string): string => { + const result = /[^\\]*$/.exec(filepath); + return result && result.length ? result[0] : ''; +}; + +export const getFileContent = async (file: unknown): Promise => (file as Blob).text(); diff --git a/src/utils/generateFileKey.ts b/src/utils/generateFileKey.ts new file mode 100644 index 00000000..48ef1d4d --- /dev/null +++ b/src/utils/generateFileKey.ts @@ -0,0 +1,3 @@ +import CryptoJS from 'crypto-js'; + +export const generateFileKey = (): string => CryptoJS.lib.WordArray.random(256 / 8).toString(); diff --git a/src/views/DashboardView.tsx b/src/views/DashboardView.tsx index 98c9e7f8..783e0d67 100644 --- a/src/views/DashboardView.tsx +++ b/src/views/DashboardView.tsx @@ -8,74 +8,81 @@ import { useDisclosure, useToast, Input, - useBreakpointValue, - Drawer, - DrawerOverlay, - DrawerContent, - SlideDirection, - Icon, Text, + Flex, + Spacer, Divider, } from '@chakra-ui/react'; -import { DownloadIcon, HamburgerIcon } from '@chakra-ui/icons'; +import { CheckIcon } from '@chakra-ui/icons'; + +import EthCrypto from 'eth-crypto'; import { useUserContext } from 'contexts/user'; -import { IPCFile } from 'lib/drive'; +import { IPCFile, IPCContact } from 'types/types'; import Modal from 'components/Modal'; -import Sidebar from 'components/SideBar'; -import FileCard from 'components/FileCard'; -import UploadButton from 'components/UploadButton'; -import colors from 'theme/foundations/colors'; - -const extractFilename = (filepath: string) => { - const result = /[^\\]*$/.exec(filepath); - return result && result.length ? result[0] : ''; -}; -const getFileContent = (file: unknown): Promise => - new Promise((resolve, reject) => { - const reader = new window.FileReader(); - reader.onload = (event: unknown) => { - // eslint-disable-next-line - resolve((event as any).target.result); - }; - reader.onerror = (event) => { - reject(event); - }; - reader.readAsText(file as Blob); - }); +import { generateFileKey } from 'utils/generateFileKey'; + +import { getFileContent, extractFilename } from '../utils/fileManipulation'; + +import { ResponsiveBar } from '../components/ResponsiveBar'; +import { DisplayFileCards } from '../components/DisplayFileCards'; const Dashboard = (): JSX.Element => { const toast = useToast(); const { user } = useUserContext(); const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen: isOpenContactAdd, onOpen: onOpenContactAdd, onClose: onCloseContactAdd } = useDisclosure(); + const { isOpen: isOpenContactUpdate, onOpen: onOpenContactUpdate, onClose: onCloseContactUpdate } = useDisclosure(); + const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure(); const [files, setFiles] = useState([]); + const [sharedFiles, setSharedFiles] = useState([]); + const [contacts, setContacts] = useState([]); + const [contactInfos, setContactInfo] = useState({ + name: '', + address: '', + publicKey: '', + files: [], + }); + const [selectedTab, setSelectedTab] = useState(0); const [isUploadLoading, setIsUploadLoading] = useState(false); const [isDownloadLoading, setIsDownloadLoading] = useState(false); const [fileEvent, setFileEvent] = useState | undefined>(undefined); + const [contactsNameEvent, setContactNameEvent] = useState | undefined>(undefined); + const [contactsPublicKeyEvent, setContactPublicKeyEvent] = useState | undefined>( + undefined, + ); + const [selectedFile, setSelectedFile] = useState({ + name: '', + hash: '', + created_at: 0, + key: { iv: '', ephemPublicKey: '', ciphertext: '', mac: '' }, + }); useEffect(() => { (async () => { - await loadDrive(); + await loadContact(); + await loadSharedDrive(); })(); }, []); - const loadDrive = async () => { + const loadSharedDrive = async () => { try { - const load = await user.drive.load(); + const loadShared = await user.drive.loadShared(user.contact.contacts); toast({ - title: load.message, - status: load.success ? 'success' : 'error', + title: loadShared.message, + status: loadShared.success ? 'success' : 'error', duration: 2000, isClosable: true, }); setFiles(user.drive.files); + setSharedFiles(user.drive.sharedFiles); } catch (error) { console.error(error); toast({ - title: 'Unable to load drive', + title: 'Unable to load shared drive', status: 'error', duration: 2000, isClosable: true, @@ -87,21 +94,49 @@ const Dashboard = (): JSX.Element => { if (!fileEvent) return; const filename = extractFilename(fileEvent.target.value); const fileContent = await getFileContent(fileEvent.target.files ? fileEvent.target.files[0] : []); + const key = generateFileKey(); + if (!filename || !fileContent) return; setIsUploadLoading(true); try { - const upload = await user.drive.upload({ - name: filename, - content: fileContent, - created_at: Date.now(), - }); - toast({ - title: upload.message, - status: upload.success ? 'success' : 'error', - duration: 2000, - isClosable: true, - }); + if (user.account) { + const upload = await user.drive.upload( + { + name: filename, + hash: fileContent, + created_at: Date.now(), + key: { iv: '', ephemPublicKey: '', ciphertext: '', mac: '' }, + }, + key, + ); + if (!upload.success) { + toast({ + title: upload.message, + status: upload.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + } else { + const shared = await user.contact.addFileToContact( + user.account.address, + user.drive.files[user.drive.files.length - 1], + ); + toast({ + title: shared.success ? upload.message : 'Failed to upload the file', + status: shared.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + } + } else { + toast({ + title: 'Failed to load account', + status: 'error', + duration: 2000, + isClosable: true, + }); + } onClose(); } catch (error) { console.error(error); @@ -137,74 +172,180 @@ const Dashboard = (): JSX.Element => { setIsDownloadLoading(false); }; - const LeftBar = (): JSX.Element => ( - onOpen()} isLoading={isUploadLoading} />} - /> - ); + const shareFile = async (contact: IPCContact) => { + setIsDownloadLoading(true); + try { + console.log(selectedFile.key); + const share = await user.contact.addFileToContact(contact.address, selectedFile); + onCloseShare(); + toast({ + title: share.message, + status: share.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + } catch (error) { + console.log(error); + toast({ + title: 'Unable to share the file', + status: 'error', + duration: 2000, + isClosable: true, + }); + } + setIsDownloadLoading(false); + }; - const BarWithDrawer = () => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const { isOpen, onOpen, onClose } = useDisclosure(); - const placement: SlideDirection = 'left'; - - return ( - - - - - - - - - - - - - - Inter Planetary Cloud - - - - - - - ); + const loadContact = async () => { + try { + const load = await user.contact.load(); + toast({ + title: load.message, + status: load.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + + setContacts(user.contact.contacts); + } catch (error) { + console.log(error); + toast({ + title: 'Unable to load contacts', + status: 'error', + duration: 2000, + isClosable: true, + }); + } + }; + + const addContact = async () => { + try { + if (contactsNameEvent && contactsPublicKeyEvent) { + const add = await user.contact.add({ + name: contactsNameEvent.target.value, + address: EthCrypto.publicKey.toAddress(contactsPublicKeyEvent.target.value.slice(2)), + publicKey: contactsPublicKeyEvent.target.value, + files: [], + }); + + toast({ + title: add.message, + status: add.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + setContacts(user.contact.contacts); + } else { + toast({ + title: 'Bad contact infos', + status: 'error', + duration: 2000, + isClosable: true, + }); + } + onCloseContactAdd(); + } catch (error) { + console.log(error); + toast({ + title: 'Unable to add this contact', + status: 'error', + duration: 2000, + isClosable: true, + }); + } }; - const ResponsiveBar = () => { - const isDrawerNeeded: boolean = useBreakpointValue({ base: true, xs: true, lg: false }) || false; + const updateContact = async () => { + try { + if (contactsPublicKeyEvent) { + const update = await user.contact.update( + contactInfos.address, + contactsNameEvent ? contactsNameEvent.target.value : contactInfos.name, + ); + toast({ + title: update.message, + status: update.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + setContacts(user.contact.contacts); + } else { + toast({ + title: 'Invalid address', + status: 'error', + duration: 2000, + isClosable: true, + }); + } + onCloseContactUpdate(); + } catch (error) { + console.log(error); + toast({ + title: 'Unable to update this contact', + status: 'error', + duration: 2000, + isClosable: true, + }); + } + }; + + const deleteContact = async (contactToDelete: IPCContact) => { + try { + const deletedContact = contacts.find((contact) => contact === contactToDelete); + + if (deletedContact) { + const deleteResponse = await user.contact.remove(contactToDelete.address); - if (!isDrawerNeeded) return ; - return ; + toast({ + title: deleteResponse.message, + status: deleteResponse.success ? 'success' : 'error', + duration: 2000, + isClosable: true, + }); + setContacts(user.contact.contacts); + } else { + toast({ + title: 'Unable to find this contact', + status: 'error', + duration: 2000, + isClosable: true, + }); + } + } catch (error) { + console.log(error); + toast({ + title: 'Unable to delete this contact', + status: 'error', + duration: 2000, + isClosable: true, + }); + } }; return ( - + - - {files.map((file) => ( - - - - ))} + + { id="ipc-dashboardView-upload-file" /> + + Add the contact + + } + > + <> + ) => setContactNameEvent(e)} + id="ipc-dashboardView-input-contact-name" + /> + ) => setContactPublicKeyEvent(e)} + id="ipc-dashboardView-input-contact-public-key" + /> + + + + Update the contact + + } + > + <> + New name * + ) => setContactNameEvent(e)} + id="ipc-dashboardView-input-contact-name" + /> + * Fill, to update the info + + + + + {contacts.map((contact) => { + if (user.account && contact.address !== user.account.address) + return ( + + + {contact.name} + {contact.address} + + + + + ); + return ; + })} + + ); }; diff --git a/yarn.lock b/yarn.lock index b88b41ab..769f0f17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1032,6 +1032,13 @@ core-js-pure "^3.19.0" regenerator-runtime "^0.13.4" +"@babel/runtime@7.16.7", "@babel/runtime@^7.11.2": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.16.3" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz" @@ -1039,13 +1046,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.11.2": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" - integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/runtime@^7.15.4": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.0.tgz#b8d142fc0f7664fb3d9b5833fd40dcbab89276c0" @@ -1851,7 +1851,7 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.3" -"@ethereumjs/tx@^3.3.2": +"@ethereumjs/tx@3.4.0", "@ethereumjs/tx@^3.3.2": version "3.4.0" resolved "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.4.0.tgz" integrity sha512-WWUwg1PdjHKZZxPPo274ZuPsJCWV3SqATrEKQP1n2DrVYVP1aZIYpo/mFaA0BDoE0tIQmBeimRCEA0Lgil+yYw== @@ -3029,6 +3029,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/bn.js@5.1.0", "@types/bn.js@^5.1.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz" + integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== + dependencies: + "@types/node" "*" + "@types/bn.js@^4.11.5", "@types/bn.js@^4.11.6": version "4.11.6" resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz" @@ -3036,13 +3043,6 @@ dependencies: "@types/node" "*" -"@types/bn.js@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz" - integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== - dependencies: - "@types/node" "*" - "@types/bs58@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz" @@ -3722,6 +3722,11 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" + integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz" @@ -4462,6 +4467,13 @@ bip39@^3.0.4: pbkdf2 "^3.0.9" randombytes "^2.0.1" +bip66@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" + integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI= + dependencies: + safe-buffer "^5.0.1" + bl@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" @@ -4585,7 +4597,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.2.0: +browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6, browserify-aes@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz" integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== @@ -6327,6 +6339,15 @@ dotenv@8.2.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== +drbg.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" + integrity sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs= + dependencies: + browserify-aes "^1.0.6" + create-hash "^1.1.2" + create-hmac "^1.1.4" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" @@ -6355,6 +6376,18 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +eccrypto@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/eccrypto/-/eccrypto-1.1.6.tgz#846bd1222323036f7a3515613704386399702bd3" + integrity sha512-d78ivVEzu7Tn0ZphUUaL43+jVPKTMPFGtmgtz1D0LrFn7cY3K8CdrvibuLz2AAkHBLKZtR8DMbB2ukRYFk987A== + dependencies: + acorn "7.1.1" + elliptic "6.5.4" + es6-promise "4.2.8" + nan "2.14.0" + optionalDependencies: + secp256k1 "3.7.1" + eciesjs@^0.3.12: version "0.3.13" resolved "https://registry.yarnpkg.com/eciesjs/-/eciesjs-0.3.13.tgz#456a89b9aa68e71d6019b7fd903640c388154b72" @@ -6386,7 +6419,7 @@ electron-to-chromium@^1.3.564, electron-to-chromium@^1.4.17: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.23.tgz" integrity sha512-q3tB59Api3+DMbLnDPkW/UBHBO7KTGcF+rDCeb0GAGyqFj562s6y+c/2tDKTS/y5lbC+JOvT4MSUALJLPqlcSA== -elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: +elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.4.1, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -6541,7 +6574,7 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-promise@^4.0.3: +es6-promise@4.2.8, es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== @@ -6901,6 +6934,19 @@ etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eth-crypto@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eth-crypto/-/eth-crypto-2.2.0.tgz#8fa9bd7b04ee256d0e755d73e9a0a6c7e977c5b9" + integrity sha512-g4YKmHcPNFkIGylWVMYwhoxtBphXX9xc0rttoYQGqH1Mg0YuQk2rYa9jcnTzSyduG0GDL60Cib8Uhhrx13CS4w== + dependencies: + "@babel/runtime" "7.16.7" + "@ethereumjs/tx" "3.4.0" + "@types/bn.js" "5.1.0" + eccrypto "1.1.6" + ethereumjs-util "7.1.3" + ethers "5.5.4" + secp256k1 "4.0.3" + eth-ens-namehash@2.0.8: version "2.0.8" resolved "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz" @@ -6958,7 +7004,7 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" -ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.3: +ethereumjs-util@7.1.3, ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.3: version "7.1.3" resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz" integrity sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw== @@ -6969,7 +7015,7 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.3: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@^5.4.6: +ethers@5.5.4, ethers@^5.4.6: version "5.5.4" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.4.tgz#e1155b73376a2f5da448e4a33351b57a885f4352" integrity sha512-N9IAXsF8iKhgHIC6pquzRgPBJEzc9auw3JoRkaKe+y4Wl/LFBtDDunNe7YmdomontECAcC5APaAgWZBiu1kirw== @@ -10376,7 +10422,12 @@ multihashes@^0.4.15, multihashes@~0.4.15: multibase "^0.7.0" varint "^5.0.0" -nan@^2.12.1, nan@^2.13.2: +nan@2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nan@^2.12.1, nan@^2.13.2, nan@^2.14.0: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== @@ -12230,6 +12281,11 @@ react-focus-lock@2.5.2: use-callback-ref "^1.2.5" use-sidecar "^1.0.5" +react-icons@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" + integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -12962,16 +13018,21 @@ scryptsy@^2.1.0: resolved "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz" integrity sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w== -secp256k1@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz" - integrity sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg== +secp256k1@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.7.1.tgz#12e473e0e9a7c2f2d4d4818e722ad0e14cc1e2f1" + integrity sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g== dependencies: - elliptic "^6.5.2" - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" + bindings "^1.5.0" + bip66 "^1.1.5" + bn.js "^4.11.8" + create-hash "^1.2.0" + drbg.js "^1.0.1" + elliptic "^6.4.1" + nan "^2.14.0" + safe-buffer "^5.1.2" -secp256k1@^4.0.2, secp256k1@^4.0.3: +secp256k1@4.0.3, secp256k1@^4.0.2, secp256k1@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== @@ -12980,6 +13041,15 @@ secp256k1@^4.0.2, secp256k1@^4.0.3: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" +secp256k1@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz" + integrity sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg== + dependencies: + elliptic "^6.5.2" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz"