Papaya Chat is the chat app that uses decentralized solutions. Made by el-tumero (tumerpl@gmail.com). The purpose of the application is to create safe place for online conversations. This safety comes from encryption and storing user data (username, photo, bio) on blockchain and IPFS. Smart contracts are deployed on Binance Smartchain Testnet (PapayaStorage: 0x5f030595035C3AAdb884260e72Ae5A2ece2c5D8F) (PapayaProfile: 0xBB333CDDBd113b3D2dC7CF207384Efd0a70F8Cb5)
App is using browser's Notification API so to receive notifications I recommend turning it on permision request.
Live demo available at: https://el-tumero.github.io/papaya-chat-dapp/
(Custom emotes should be 32x32 pixel png images)
Demo: https://www.youtube.com/watch?v=UG8eZO9La94
These steps are done under the hood while on first user's entry (initialization):
- User generates a key pair (ECDH) using Subtle Crypto Web API inside a browser
- Then stores them in IndexedDB inside a browser
- Sends public key to blockchain - thanks to that blockchain address of user is connected with public key of application
- Next user creates a profile - name, bio, photo and uploads it to IPFS via nft.storage API
- Then he saves its CID on blockchain and mints NFT
- Account is fully created
These steps are done under the hood while user establish conversation with diffrent user:
- User downloads his mate's public key from blockchain (smart contract)
- Creates common key from his mate's public key and user's private key thanks to Eliptic Curve Diffie Helman algorithm (ECDH) - this common key encrypts conversation with the AES algorithm.
- User downloads his mate's profile from IPFS
- User connect to relay server and start messaging
The presented diagrams in a simplified way show the operation of the application mechanisms. In the application code, the mechanisms are more elaborate and complex. Here I have introduced their essence.
-
Front end written in React and Typescript (src folder)
-
SubtleCrypto Web API - for generating keys and encrypting messages (ECDH)
-
Smart contracts are written in Solidity with OpenZeppelin (stored in contracts folder) compiled using Hardhat framework
-
Relay server is written in Typescript uses Express and Socket.io https://github.com/el-tumero/papaya-relay
-
IPFS and nft.storage API
- Saving profile to IPFS and creating NFT src/components/Keypair.tsx
const imageAsBase64 = await convertToBase64(selectedFile)
const profileObj = {name: profileName, bio: profileBio, photo: imageAsBase64}
const content = new Blob([JSON.stringify(profileObj)])
const ipfsClient = new NFTStorage({token: process.env.REACT_APP_NFTSTORAGE_API_KEY})
try {
const cid = await ipfsClient.storeBlob(content)
const tx = await profileContract.mint(cid)
const receipt = await tx.wait()
setSteps(prev => {
const copy = [...prev]
copy[3] = "done"
return copy
})
alert("Profile created! cid: " + cid)
document.location.reload()
// console.log(receipt)
} catch (error) {
alert(error)
setSteps(prev => {
const copy = [...prev]
copy[3] = "notdone"
return copy
})
}
- Downloading user's profile src/components/Keypair.tsx - it's used for preview
const address = await signer.getAddress()
const cid = await profileContract.activeProfile(address)
if(cid){
setSteps(prev => {
const copy = [...prev]
copy[3] = "loading"
return copy
})
const response = await axios.get(ipfsGateway + cid)
setSteps(prev => {
const copy = [...prev]
copy[3] = "done"
return copy
})
setCurrentProfile(response.data)
- Downloading mate's profile, during the creation of "relation" src/components/RelationList/RelationList.tsx - profile is saved to browser's localStorage
const clientAddress = await signer.getAddress()
if(newRelationAddress.toLowerCase() === clientAddress.toLowerCase()) throw Error("You can't create a relation with youself ;)")
const result = await storageContract.getPublicKey(newRelationAddress)
if(result.length === 0) {
throw Error("Current address has not created an account on this service :(")
}
localStorage.setItem("p"+newRelationAddress.toLowerCase(), result)
const profileCid = await profileContract.activeProfile(newRelationAddress)
//console.log(profileCid)
const response = await axios.get(ipfsGateway + profileCid)
const profile = response.data
localStorage.setItem(newRelationAddress.toLowerCase(), JSON.stringify(profile))
const cookies = cookiesClient
- Uploading custom emote to IPFS src/components/Emotes/Emotes.tsx
const acceptedImageTypes = ['image/jpeg', 'image/png']
if(!acceptedImageTypes.includes(selectedFile['type'])) throw Error("Unsupported image type")
const ipfsClient = new NFTStorage({token: process.env.REACT_APP_NFTSTORAGE_API_KEY})
const imageAsBase64 = await convertToBase64(selectedFile) as string
const {height, width} = await getHeightAndWidthFromDataUrl(imageAsBase64)
if(height !== 32 || width !== 32) throw Error("Emote has unsupported dimensions!")
const emoteObj = new Blob([JSON.stringify({command: emoteCommand, image: imageAsBase64})])
const cid = await ipfsClient.storeBlob(emoteObj)
- Donwloading custom emotes from IPFS src/components/MessageBox/EmoteImage.tsx
async function getEmoteImage(cid:string){
const response = await axios.get("https://"+cid+".ipfs.nftstorage.link")
return response.data.image as string
}