diff --git a/ride4dapps/waveschat/README.md b/ride4dapps/waveschat/README.md new file mode 100644 index 0000000..7b88489 --- /dev/null +++ b/ride4dapps/waveschat/README.md @@ -0,0 +1,14 @@ +# WavesChat +WavesChat is a decentralized messenger with an end-to-end encryption +Basically works like a chat-roulette with an +1. Optional channel +2. Optional payment, which forces to pay the same amount to talk with you + +Try now at https://chat.waves.today (Requires Waves Keeper **with InvokeScript support in TESTNET mode**) + +# How it works +1. First user (Bob) invokes script function `init(bobPublicKey)` +2. Second user (Alice) invokes `init(alicePublicKey)` +3. Script matches Bob's and Alice's public keys, so that they can get the recipients public key from data entry `Base58(Blake2b256(publicKey))` +4. Bob gets Alice's public key, encrypts message with it, and then invokes `sendMessage(bobPublicKey, cipherText)` +5. Bob to Alice conversation id is `Base58(Blake2b256(bobPublicKey + alicePublicKey))`, so Alice can get message count from `conversationId + "_n"` and messages from `conversationId + "_" + 0...msgCount` data entries accordingly, and decrypt it with her private key diff --git a/ride4dapps/waveschat/waveschat.ride b/ride4dapps/waveschat/waveschat.ride new file mode 100644 index 0000000..dc38f4d --- /dev/null +++ b/ride4dapps/waveschat/waveschat.ride @@ -0,0 +1,83 @@ +{-# STDLIB_VERSION 3 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +let defaultAmount = 0 +let maxAmount = 5_0000_0000 + +@Callable(invoke) +func init(publicKey: ByteVector, channel: String) = { + let publicKeyHash = toBase58String(blake2b256(publicKey)) + let channelId = toBase58String(blake2b256(toBytes(channel))) + let currentKey = "current_" + channelId + let currentAmountKey = "current_amount_" + channelId + let isPublicChannel = size(channel) < 6 + + let pmtAmount = match invoke.payment { + case pmt:AttachedPayment => + if (isDefined(pmt.assetId)) then 0 + else pmt.amount + case _ => + 0 + } + + let requiredAmount = match getInteger(this, currentAmountKey) { + case currentAmount: Int => currentAmount + case _ => 0 + } + + let newRequiredAmount = if (isPublicChannel && pmtAmount >= maxAmount) then maxAmount else pmtAmount + + if (pmtAmount < requiredAmount) + then throw("Init requires " + toString(requiredAmount / 100000000) + " waves") + else { + match getBinary(this, publicKeyHash + "_owner") { + case b: ByteVector => + if (b != invoke.caller.bytes) then throw("Public key is not yours") + else { + match getBinary(this, currentKey) { + case left:ByteVector => + let leftHash = toBase58String(blake2b256(left)) + WriteSet([DataEntry(publicKeyHash + "_owner", invoke.caller.bytes), DataEntry(leftHash, publicKey), DataEntry(publicKeyHash, left), DataEntry(currentKey, 0), DataEntry(currentAmountKey, defaultAmount)]) + + case _ => + WriteSet([DataEntry(publicKeyHash + "_owner", invoke.caller.bytes), DataEntry(currentKey, publicKey), DataEntry(currentAmountKey, newRequiredAmount)]) + } + } + + case _ => + match getBinary(this, currentKey) { + case left:ByteVector => + let leftHash = toBase58String(blake2b256(left)) + WriteSet([DataEntry(publicKeyHash + "_owner", invoke.caller.bytes), DataEntry(leftHash, publicKey), DataEntry(publicKeyHash, left), DataEntry(currentKey, 0), DataEntry(currentAmountKey, defaultAmount)]) + + case _ => + WriteSet([DataEntry(publicKeyHash + "_owner", invoke.caller.bytes), DataEntry(currentKey, publicKey), DataEntry(currentAmountKey, newRequiredAmount)]) + } + } + } +} + +@Callable(invoke) +func sendMessage(publicKey: ByteVector, message: ByteVector) = { + let publicKeyHash = toBase58String(blake2b256(publicKey)) + + match getBinary(this, publicKeyHash + "_owner") { + case assignedCaller: ByteVector => + if (assignedCaller != invoke.caller.bytes) then throw("Public keys not match") + else match getBinary(this, publicKeyHash) { + case right: ByteVector => + let conversationId = toBase58String(blake2b256(publicKey + right)) + let nonceKey = conversationId + "_n" + let nonce = match getInteger(this, nonceKey) { + case i: Int => i + 1 + case _ => 1 + } + WriteSet([DataEntry(conversationId + "_" + toString(nonce), message), DataEntry(nonceKey, nonce)]) + + case _ => throw("Not initialized") + } + + case _ => throw("Owner not defined") + } +} diff --git a/ride4dapps/waveschat/waveschat_test.js b/ride4dapps/waveschat/waveschat_test.js new file mode 100644 index 0000000..5cfe25b --- /dev/null +++ b/ride4dapps/waveschat/waveschat_test.js @@ -0,0 +1,27 @@ +describe('Messenger test', () => { + const dappAddress = "3N3st6Cp9ZLz8kmT33EY41AVjwKqBVebvyq" + + const firstPK = "FxKjemCJ9s9yrG5RuDXAtGXZNsTsAr1FMhzNmUKG4GyE" + const secondPK = "5nRF8WDnjWrGhZeYEV8zh7MG1kgpYbnJaEt5G3vsR4e2" + + const firstPKHash = "4CfQDi9nwsps25XA9yrMAw4XWFaG5NwLf6gFVjrTSGL7" + + it('Register for a chat', async function(){ + const tx = invokeScript({ fee: 900000, dApp: dappAddress, call:{function:"init",args:[{"type": "binary", "value": firstPK}, {"type": "string", "value": "test"}]}, + payment: [{amount: 1000000, assetId: null}]}) + const tx1 = invokeScript({ fee: 900000, dApp: dappAddress, call:{function:"init",args:[{"type": "binary", "value": secondPK}, {"type": "string", "value": "test"}]}, + payment: [{amount: 1000000, assetId: null}]}) + + await broadcast(tx) + await broadcast(tx1) + await waitForTx(tx1.id) + }) + + it('Send message', async function(){ + const tx = invokeScript({ fee: 900000, dApp: dappAddress, call:{function:"sendMessage",args:[{"type": "binary", "value": firstPK}, {"type": "binary", "value": "4CfQDi9nwsps25XA9yrMAw4XWFaG5NwLf6gFVjrTSGL7"}]}, + payment: [{amount: 1000000, assetId: null}]}) + + await broadcast(tx) + await waitForTx(tx.id) + }) +})