Chat App
This guide will show you how to create a simple, scalable chat application using Shardus.
This app was built for an older shardus interface, the overall concepts are the same but there may be some discrepencies with specific implementation.
This guide will not cover the basic setup of a shardus application. If you wish to learn how to create an entire application from start to finish using Shardus, see Basic Dapp. We are going to be starting from where we left off in the Basic Dapp guide, so if you wish to follow along step by step we highly recommend implementing that first.
Modify Accounts
Currently, our User Account
data structure from the basic dapp guide looks something like this:
function createAccount(id) {
const account = {
id: id,
data: {
balance: 0,
},
hash: '',
timestamp: 0,
};
account.hash = crypto.hashObj(account);
return account;
}
In order to add chat functionality to our application, we need to modify the User Account
in some way so that we can store the message data between users. One feature we thought would be interesting is the ability for users to request a toll
for the privelege of sending them messages. This prevents people you don't know from being able to spam your inbox constantly with messages or ads. We will be allowing any user to change their requested toll to any amount they desire, but we also want the user to be able to add friends so that people they chat with often don't always need to be sending them tokens. We are also going to add an alias
field so that users can register and use aliases to interact with each other instead of copying and pasting long hexadecimal addresses. Lets add a few lines to the createAccount
function in order to start implementing this functionality.
// CREATE A USER ACCOUNT
function createAccount(id) {
const account = {
id: id,
data: {
balance: 50,
toll: null,
chats: {},
friends: {},
},
alias: null,
hash: '',
timestamp: 0,
};
account.hash = crypto.hashObj(account);
return account;
}
Now we need to add another account for storing the chat data between users. All the state data that exists on a shardus network exists in the form of accounts (See Accounts for a more detailed explanation of what we mean). So the next step in implementing our chat functionality is to create an account for holding the chat data. We will do this by adding the following function:
function createChat(id) {
const chat = {
id: id,
messages: [], // The messages between two users in a chat
timestamp: 0,
hash: '',
};
chat.hash = crypto.hashObj(chat);
return chat;
}
Now, let's do the same thing for storing our aliases.
function createAlias(id) {
const alias = {
id: id,
hash: '',
inbox: '',
address: '',
timestamp: 0,
};
alias.hash = crypto.hashObj(alias);
return alias;
}
API Routes
Next up on our list of things to do server side is to create API routes that grab chat, alias, friend, and toll data.
Here is a reference for the route names we will be using here
'account/:id/alias'; // Get the alias for an account
'account/:id/toll'; // Get the toll for an account
'address/:name'; // Get the address (publicKey) from an alias
'account/:id/:friendId/toll'; // Get the toll for a user chatting with another user
'account/:id/friends'; // Get the friend list of a user
'messages/:chatId'; // Get the messages between two users
The first route deals with getting the alias of an account from their publicKey. You can implement this by grabbing the id
from the request parameters, calling getLocalOrRemoteAccount and passing in the id
to get the account data, then returning the alias field on the account.
dapp.registerExternalGet('account/:id/alias', async (req, res) => {
try {
const id = req.params['id'];
const account = await dapp.getLocalOrRemoteAccount(id);
res.json({ handle: account && account.data.alias });
} catch (error) {
res.json({ error });
}
});
The second route will deal with grabbing the toll
data field from an account. Same technique applies here, just grab the id from the parameters, and call getLocalOrRemoteAccount to grab the account data, then return the toll.
dapp.registerExternalGet('account/:id/toll', async (req, res) => {
try {
const id = req.params['id'];
const account = await dapp.getLocalOrRemoteAccount(id);
if (account) {
if (!account.data.data.toll) {
res.json({ toll: 0 });
} else {
res.json({ toll: account.data.data.toll });
}
} else {
res.json({ error: 'No account with the given id' });
}
} catch (error) {
res.json({ error });
}
});
Next up is the route that deals with mapping an alias to an account. We will be creating this address when we create our alias accounts through a transaction called register
by hashing the name of the alias. We are creating this route so that we can grab the actual user account address info by querying the hash of their alias (the alias account). By now you probably have an idea on how to do this, but here's the full example:
dapp.registerExternalGet('address/:name', async (req, res) => {
try {
const name = req.params['name'];
const account = await dapp.getLocalOrRemoteAccount(name);
if (account && account.data) {
res.json({ address: account.data.address });
} else {
res.json({ error: 'No account exists for the given handle' });
}
} catch (error) {
res.json({ error });
}
});
Now let's talk about the toll
API route. Because the toll can differ depending on whether or not the account you are grabbing it from is a friend of the user requesting the info, we need to have two request parameters. id
is the public key of the account you are requesting the toll from and friendId
will be the publicKey of the user requesting the toll info. Here's how that looks inside of the API route:
dapp.registerExternalGet('account/:id/:friendId/toll', async (req, res) => {
const id = req.params['id'];
const friendId = req.params['friendId'];
if (!id) {
res.json({ error: 'No provided id in the route: account/:id/:friendId/toll' });
}
if (!friendId) {
res.json({ error: 'No provided friendId in the route: account/:id/:friendId/toll' });
}
try {
const account = await dapp.getLocalOrRemoteAccount(id);
if (account) {
if (account.data.data.friends[friendId]) {
res.json({ toll: 0 });
} else {
if (account.data.data.toll === null) {
res.json({ toll: 0 });
} else {
res.json({ toll: account.data.data.toll });
}
}
} else {
res.json({ error: 'No account with the given id' });
}
} catch (error) {
res.json({ error });
}
});
Now, let's add a route for grabbing the friend list of an account:
dapp.registerExternalGet('account/:id/friends', async (req, res) => {
try {
const id = req.params['id'];
const account = await dapp.getLocalOrRemoteAccount(id);
if (account) {
res.json({ friends: account.data.data.friends });
} else {
res.json({ error: 'No account for given id' });
}
} catch (error) {
dapp.log(error);
res.json({ error });
}
});
Finally, let's add the route for grabbing messages shared between two users on the network. We will be using a chatId
created by hashing two users addresses together, so that a unique chat account will exist for any given two users. Here's how we accomplish that:
dapp.registerExternalGet('messages/:chatId', async (req, res) => {
try {
const { chatId } = req.params;
const chat = await dapp.getLocalOrRemoteAccount(chatId);
dapp.log(chat);
if (!chat) {
res.json({ error: "Chat doesn't exist" });
return;
}
if (!chat.data.messages) {
res.json({ error: 'no chat history for this request' });
} else {
res.json({ messages: chat.data.messages });
}
} catch (error) {
res.json({ error });
}
});
Setup functions
Now it's time for modifying the setup functions to handle the new transaction types we will be creating.
We are going to be adding the following transaction types (You can name yours whatever you wish)
register
message
toll
friend
remove_friend
Validate Transactions
Lets begin with validateTransaction. We want to create new paths for validation code to be executed by adding cases to our existing switch
statement.
In this application, we need to pass wrappedStates
into validateTransaction
from apply
. Make sure that you modify the function like so:
validateTransaction(tx, wrappedStates) {}
register
In order to implement verification for the register
transaction type, we need to make sure the following items are accomplished:
- Validate the signature on the transaction and ensure it was signed by the account that submitted it.
- Ensure no
alias
account already exists with the givenaliasHash
of the transaction.
switch (tx.type) {
case 'register': {
const alias = wrappedStates[tx.aliasHash] && wrappedStates[tx.aliasHash].data;
if (tx.sign.owner !== tx.from) {
response.reason = 'not signed by from account';
return response;
}
if (crypto.verifyObj(tx) === false) {
response.reason = 'incorrect signing';
return response;
}
if (!alias) {
response.reason = 'Alias account was not found for some reason';
return response;
}
if (alias.inbox === tx.alias) {
response.reason = 'This alias is already taken';
return response;
}
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
}
}
message
In order to implement verification for the message
transaction type, we need to make sure the following items are accomplished:
- Validate the signature on the transaction and ensure it was signed by the account that submitted it.
- Ensure both
to
andfrom
accounts exists on the network. - If the sender (
from
) isn't a friend of the receiver (to
), then ensure the sender has enough balance to cover the receiver's toll
switch (tx.type) {
case 'message': {
const from = wrappedStates[tx.from].data;
const to = wrappedStates[tx.to].data;
if (tx.sign.owner !== tx.from) {
response.reason = 'not signed by from account';
return response;
}
if (crypto.verifyObj(tx) === false) {
response.reason = 'incorrect signing';
return response;
}
if (typeof from === 'undefined' || from === null) {
response.reason = '"from" account does not exist.';
return response;
}
if (typeof to === 'undefined' || to === null) {
response.reason = '"target" account does not exist.';
return response;
}
if (to.data.friends[tx.from]) {
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
} else {
if (to.data.toll === null) {
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
} else {
if (from.data.balance < to.data.toll) {
response.reason = 'from account does not have sufficient funds.';
return response;
}
}
}
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
}
}
toll
In order to implement verification for the toll
transaction type, we need to make sure the following items are accomplished:
- Validate the signature on the transaction and ensure it was signed by the account that submitted it.
- Ensure
toll
is greater than or equal to at least 1 token (totally optional, you can do this however you want, but here is where you would do it)
switch (tx.type) {
case 'toll': {
const from = wrappedStates[tx.from].data;
if (tx.sign.owner !== tx.from) {
response.reason = 'not signed by from account';
return response;
}
if (crypto.verifyObj(tx) === false) {
response.reason = 'incorrect signing';
return response;
}
if (!from) {
response.reason = 'from account does not exist';
return response;
}
if (!tx.toll) {
response.reason = 'Toll was not defined in the transaction';
return response;
}
if (tx.toll < 1) {
response.reason = 'Toll must be greater than or equal to 1';
return response;
}
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
}
}
friend
In order to implement verification for the friend
transaction type, we need to make sure the following items are accomplished:
- Validate the signature on the transaction and ensure it was signed by the account that submitted it.
- Ensure that the sender account exists
- That's it!
switch (tx.type) {
case 'friend': {
const from = wrappedStates[tx.from].data;
if (typeof from === 'undefined' || from === null) {
response.reason = 'from account does not exist';
return response;
}
if (tx.sign.owner !== tx.from) {
response.reason = 'not signed by from account';
return response;
}
if (crypto.verifyObj(tx) === false) {
response.reason = 'incorrect signing';
return response;
}
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
}
}
remove_friend
In order to implement verification for the remove_friend
transaction type, we need to make sure the following items are accomplished:
- Validate the signature on the transaction and ensure it was signed by the account that submitted it.
- Ensure that the sender account exists
- That's it!
switch (tx.type) {
case 'remove_friend': {
const from = wrappedStates[tx.from].data;
if (typeof from === 'undefined' || from === null) {
response.reason = 'from account does not exist';
return response;
}
if (tx.sign.owner !== tx.from) {
response.reason = 'not signed by from account';
return response;
}
if (crypto.verifyObj(tx) === false) {
response.reason = 'incorrect signing';
return response;
}
response.result = 'pass';
response.reason = 'This transaction is valid!';
return response;
}
}
Validate Transaction Fields
Now we want to do the same thing for the code paths in the switch
statement within validateTxnFields. All you need to do in these validation cases is type check the fields in the transactions to ensure they are in the format that you expect. You can copy the following code examples if you just want to follow along (Keep in mind I have removed the create
and transfer
cases because we already implemented those in the last example we built)
register
switch (tx.type) {
case 'register': {
if (typeof tx.aliasHash !== 'string') {
success = false;
reason = '"aliasHash" must be a string.';
throw new Error(reason);
}
if (typeof tx.from !== 'string') {
success = false;
reason = '"From" must be a string.';
throw new Error(reason);
}
if (typeof tx.alias !== 'string') {
success = false;
reason = '"alias" must be a string.';
throw new Error(reason);
}
if (tx.alias.length >= 20) {
success = false;
reason = '"alias" must be less than 21 characters (20 max)';
throw new Error(reason);
}
break;
}
case 'message': {
if (typeof tx.from !== 'string') {
success = false;
reason = '"From" must be a string.';
throw new Error(reason);
}
if (typeof tx.to !== 'string') {
success = false;
reason = '"To" must be a string.';
throw new Error(reason);
}
if (typeof tx.message !== 'string') {
success = false;
reason = '"Message" must be a string.';
throw new Error(reason);
}
if (tx.message.length > 5000) {
success = false;
reason = '"Message" length must be less than 5000 characters.';
throw new Error(reason);
}
break;
}
case 'toll': {
if (typeof tx.from !== 'string') {
success = false;
reason = '"From" must be a string.';
throw new Error(reason);
}
if (typeof tx.toll !== 'number') {
success = false;
reason = '"Toll" must be a number.';
throw new Error(reason);
}
if (tx.toll < 1) {
success = false;
reason = 'Minimum "toll" allowed is 1 token';
throw new Error(reason);
}
if (tx.toll > 1000000) {
success = false;
reason = 'Maximum toll allowed is 1,000,000 tokens.';
throw new Error(reason);
}
break;
}
case 'friend': {
if (typeof tx.from !== 'string') {
success = false;
reason = '"From" must be a string.';
throw new Error(reason);
}
if (typeof tx.to !== 'string') {
success = false;
reason = '"To" must be a string.';
throw new Error(reason);
}
if (typeof tx.alias !== 'string') {
success = false;
reason = '"Message" must be a string.';
throw new Error(reason);
}
break;
}
case 'remove_friend': {
if (typeof tx.from !== 'string') {
success = false;
reason = '"From" must be a string.';
throw new Error(reason);
}
if (typeof tx.to !== 'string') {
success = false;
reason = '"To" must be a string.';
throw new Error(reason);
}
break;
}
}
Get Keys From Transactions
Next up in our list of setup functions to modify is getKeyFromTransaction. This doesn't require a lot of code to add, we just need to give shardus the expected keys for the accounts that will be modified with respect to each transaction type. Add the following cases for the switch
statement within your getKeysFromTransaction
function.
switch (tx.type) {
case 'register':
result.sourceKeys = [tx.from]; // Account registering the alias
result.targetKeys = [tx.aliasHash]; // Alias account that holds the mapping to the User Account
break;
case 'message':
result.sourceKeys = [tx.from]; // Account sending the message
result.targetKeys = [tx.to, tx.chatId]; // [Account receiving message, Chat account holding the messages between the users]
break;
case 'toll':
result.sourceKeys = [tx.from]; // Account setting the toll
break;
case 'friend':
result.sourceKeys = [tx.from]; // Account adding the friend
break;
case 'remove_friend':
result.sourceKeys = [tx.from]; // Account removing the friend
break;
}
Get Relevant Data
getRelevantData is where we will actually create the accounts specific to each transaction. Remember we added two new account types (chat
and alias
accounts). We need to parse the transaction type and determine whether the account needs to be "created" or "accessed". Since the only two transactions that create different "types" of accounts so far are register
and message
, we will add code paths that deal with creating accounts when those transactions are processed.
getRelevantData(accountId, tx) {
let account = accounts[accountId]
let accountCreated = false
// Create the account if it doesn't exist
if (typeof account === 'undefined' || account === null) {
if (tx.type === 'register') {
if (accountId === tx.aliasHash) {
account = createAlias(accountId)
accounts[accountId] = account
accountCreated = true
}
} else if (tx.type === 'message') {
if (accountId === tx.chatId) {
account = createChat(accountId)
accounts[accountId] = account
accountCreated = true
}
}
}
// All other transactions will default to creating "User Accounts"
if (typeof account === 'undefined' || account === null) {
account = createAccount(accountId, tx.timestamp)
accounts[accountId] = account
accountCreated = true
}
// Wrap it for Shardus
const wrapped = dapp.createWrappedResponse(accountId, accountCreated, account.hash, account.timestamp, account)
return wrapped
}
Apply
Finally, we can implement the business logic of our transactions within apply.
Remember from our last app we built how we called our helper validateTransaction
function to make sure it passed our validation requirements? Well This time, we need to pass an extra argument to that function called wrappedStates
. If you have been following along carefully, you may have noticed that we used this variable in our validateTransaction
setup we did earlier. Just remember to pass it into the function as an argument so that you don't get an error. The start of your apply
function should now look something like this:
apply(tx, wrappedStates) {
// Validate the tx
const { result, reason } = this.validateTransaction(tx, wrappedStates)
if (result !== 'pass') {
throw new Error(`invalid transaction, reason: ${reason}. tx: ${JSON.stringify(tx)}`)
}
// Create an applyResponse which will be used to tell Shardus that the tx has been applied
const txId = crypto.hashObj(tx) // compute txId from tx
const applyResponse = dapp.createApplyResponse(txId, tx.timestamp)
// switch statement code
// ...
}
register
switch (tx.type) {
case 'register': {
const from = wrappedStates[tx.from].data; // grab account data from wrappedStates
const alias = wrappedStates[tx.aliasHash].data;
alias.inbox = tx.alias; // set inbox field to the alias
from.alias = tx.alias; // set alias field on the sender account
alias.address = tx.from; // set address of sender on the alias account
alias.timestamp = tx.timestamp; // set timestamps on accounts that were modified
from.timestamp = tx.timestamp;
dapp.log('Applied register tx', tx);
break;
}
}
message
switch (tx.type) {
case 'message': {
const from = wrappedStates[tx.from].data; // grab sender account data from wrappedStates
const to = wrappedStates[tx.to].data; // grab receiver account data from wrappedStates
const chat = wrappedStates[tx.chatId].data; // grab chat account data from wrappedStates
// check whether or not to apply the toll
if (!to.data.friends[from.id]) {
if (to.data.toll) {
from.data.balance -= to.data.toll;
to.data.balance += to.data.toll;
}
}
// Create a mapping to the chat in each user's chat list
// so that it can be referenced with ease later
if (!from.data.chats[tx.to]) from.data.chats[tx.to] = tx.chatId;
if (!to.data.chats[tx.from]) to.data.chats[tx.from] = tx.chatId;
// Add the actual chat data to the account holding the messages
chat.messages.push(tx.message);
// Add timestamps to the modified accounts
chat.timestamp = tx.timestamp;
from.timestamp = tx.timestamp;
to.timestamp = tx.timestamp;
dapp.log('Applied message tx', tx);
break;
}
}
toll
switch (tx.type) {
case 'toll': {
const from = wrappedStates[tx.from].data; // grab sender account data from wrappedStates
from.data.toll = tx.toll; // Set the toll field on the sender account
from.timestamp = tx.timestamp; // Add timestamp to the sender account
dapp.log('Applied toll tx', tx);
break;
}
}
friend
switch (tx.type) {
case 'friend': {
const from = wrappedStates[tx.from].data;
from.data.friends[tx.to] = tx.alias;
from.timestamp = tx.timestamp;
dapp.log('Applied friend tx', from);
break;
}
}
remove_friend
switch (tx.type) {
case 'remove_friend': {
const from = wrappedStates[tx.from].data;
delete from.data.friends[tx.to];
from.timestamp = tx.timestamp;
dapp.log('Applied remove_friend tx', from);
break;
}
}
Don't forget to return the replyResponse
at the end of the apply
function.
return applyResponse;
Extend CLI
That's it for the server side implementation of our chat application. Now in order to interact with everything we just added, we need to extend the CLI we created in the last example. Start by creating helper methods for interacting with our new API routes:
// USED TO GET THE TOLL AMOUNT BETWEEN 2 USERS
async function getToll(friendId, yourId) {
try {
const res = await axios.get(`http://${HOST}/account/${friendId}/${yourId}/toll`);
return { toll: res.data.toll };
} catch (error) {
return { error: error };
}
}
// USED TO GET THE PUBLIC_KEY OF OF AN ACCOUNT FROM THIER ALIAS
async function getAddress(handle) {
if (handle.length === 64) return handle;
try {
const res = await axios.get(`http://${HOST}/address/${crypto.hash(handle)}`);
const { address, error } = res.data;
if (error) {
console.log(error);
} else {
return address;
}
} catch (error) {
console.log(error);
}
}
// USED TO QUERY MESSAGES
async function queryMessages(to, from) {
try {
const res = await axios.get(`http://${HOST}/messages/${crypto.hash([from, to].sort().join``)}`);
const { messages } = res.data;
return messages;
} catch (error) {
return error;
}
}
Add transaction commands
Now it's time for us to add the commands to interact with the transactions we created.
We've gone ahead and modified the
transfer
command from the previous example application in order to utilize our newalias
feature. Now, instead of grabbing the address from our local wallet, we can query the address from thealias account
associated with that user's alias on the network. We also utilize the crypto module to sign our transactions, as this provides an extra layer of security for our application. Make sure you modify thetransfer
command to the following:
// COMMAND TO TRANSFER TOKENS FROM ONE ACCOUNT TO ANOTHER
vorpal.command('transfer', 'transfers tokens to another account').action(async function(_, callback) {
const answers = await this.prompt([
{
type: 'input',
name: 'target',
message: 'Enter the target account: ',
},
{
type: 'number',
name: 'amount',
message: 'How many tokens do you want to send: ',
default: 50,
filter: (value) => parseInt(value),
},
]);
const to = await getAddress(answers.target);
const tx = {
type: 'transfer',
from: USER.address,
to: to,
amount: answers.amount,
timestamp: Date.now(),
};
crypto.signObj(tx, USER.keys.secretKey, USER.keys.publicKey);
injectTx(tx).then((res) => {
this.log(res);
callback();
});
});
register
command
To implement register
prompt the user to enter an alias
, then hash that alias
and stick it onto the aliasHash
field in the transaction.
// COMMAND TO REGISTER AN ALIAS FOR A USER ACCOUNT
vorpal.command('register', 'registers a unique alias for your account').action(async function(args, callback) {
const answer = await this.prompt({
type: 'input',
name: 'alias',
message: 'Enter the alias you want: ',
});
const tx = {
type: 'register',
aliasHash: crypto.hash(answer.alias),
from: USER.address,
alias: answer.alias,
timestamp: Date.now(),
};
crypto.signObj(tx, USER.keys.secretKey, USER.keys.publicKey);
injectTx(tx).then((res) => {
this.log(res);
callback();
});
});
message
command
The
message
command is a bit more involved. First, we prompt the user for the alias of the account they want to message. Then we query the API for the toll that we need to send (if any). Then we prompt the user once again with a confirmation about how much it will cost them to send a message. Finally, if the user responds to the prompt with a "yes", then create a stringified message object with the following fields:
const message = stringify({
body: answers.message,
handle, // We get this from querying our own alias
timestamp: Date.now(),
});
After stringifying the message
, use the encrypt
function of the crypto module in order to encrypt the message data so that nobody can see your chat other than this other user. Create the unique chatId
by hashing the result of combining and sorting both user addresses like so:
crypto.hash([USER.address, to].sort().join``);
Finally, sign the transaction and send it off:
// COMMAND TO SEND A MESSAGE TO ANOTHER USER ON THE NETWORK
vorpal.command('message', 'sends a message to another user').action(async function(_, callback) {
const answers = await this.prompt([
{
type: 'input',
name: 'to',
message: 'Enter the alias or publicKey of the target: ',
},
{
type: 'input',
name: 'message',
message: 'Enter the message: ',
},
]);
const to = await getAddress(answers.to);
const data = await getAccountData(USER.address);
const handle = data.account.alias;
if (to === undefined || to === null) {
this.log("Account doesn't exist for: ", answers.to);
callback();
}
const result = await getToll(to, USER.address);
if (result.error) {
this.log(`There was an error retrieving the toll for ${answers.to}`);
this.log(result.error);
callback();
} else {
const answer = await this.prompt({
type: 'list',
name: 'confirm',
message: `The toll for sending this user a message is ${result.toll}, continue? `,
choices: [
{ name: 'yes', value: true, short: true },
{ name: 'no', value: false, short: false },
],
default: 'yes',
});
if (answer.confirm) {
const message = stringify({
body: answers.message,
handle,
timestamp: Date.now(),
});
const encryptedMsg = crypto.encrypt(message, crypto.convertSkToCurve(USER.keys.secretKey), crypto.convertPkToCurve(to));
const tx = {
type: 'message',
from: USER.address,
to: to,
chatId: crypto.hash([USER.address, to].sort().join``),
message: encryptedMsg,
timestamp: Date.now(),
};
crypto.signObj(tx, USER.keys.secretKey, USER.keys.publicKey);
injectTx(tx).then((res) => {
this.log(res);
callback();
});
} else {
callback();
}
}
});
toll
command
// COMMAND TO SET A TOLL FOR PEOPLE NOT ON YOUR FRIENDS LIST THAT SEND YOU MESSAGES
vorpal.command('toll', 'sets a toll people must you in order to send you messages').action(async function(_, callback) {
const answer = await this.prompt({
type: 'number',
name: 'toll',
message: 'Enter the toll: ',
filter: (value) => parseInt(value),
});
const tx = {
type: 'toll',
from: USER.address,
toll: answer.toll,
timestamp: Date.now(),
};
crypto.signObj(tx, USER.keys.secretKey, USER.keys.publicKey);
injectTx(tx).then((res) => {
this.log(res);
callback();
});
});
add friend
command
// COMMAND TO ADD A FRIEND TO YOUR USER ACCOUNT'S FRIEND LIST
vorpal.command('add friend', 'adds a friend to your account').action(async function(args, callback) {
const answer = await this.prompt({
type: 'input',
name: 'friend',
message: 'Enter the alias or publicKey of the friend: ',
});
const to = await getAddress(answer.friend);
if (to === undefined || to === null) {
this.log("Target account doesn't exist for: ", answer.friend);
callback();
}
const tx = {
type: 'friend',
alias: answer.friend,
from: USER.address,
to: to,
timestamp: Date.now(),
};
crypto.signObj(tx, USER.keys.secretKey, USER.keys.publicKey);
injectTx(tx).then((res) => {
this.log(res);
callback();
});
});
remove friend
command
// COMMAND TO REMOVE A FRIEND FROM YOUR USER ACCOUNT'S FRIEND LIST
vorpal.command('remove friend', 'removes a friend from your account').action(async function(_, callback) {
const answer = await this.prompt({
type: 'input',
name: 'friend',
message: 'Enter the alias or publicKey of the friend to remove: ',
});
const to = await getAddress(answer.friend);
if (to === undefined || to === null) {
this.log("Target account doesn't exist for:", answer.friend);
callback();
}
const tx = {
type: 'remove_friend',
from: USER.address,
to: to,
timestamp: Date.now(),
};
crypto.signObj(tx, USER.keys.secretKey, USER.keys.publicKey);
injectTx(tx).then((res) => {
this.log(res);
callback();
});
});
message poll
command
In order to decrypt the messages between two users, you need to call the
decrypt
function on the crypto module for each message object. We can use ourqueryMessages
function we created earlier to get themessages
, then map each message to the result ofJSON.parse(crypto.decrypt(message, crypto.convertSkToCurve(<your secret key>), crypto.convertPkToCurve(<target's public key>)).message))
.
// COMMAND TO POLL FOR MESSAGES BETWEEN 2 USERS AFTER A SPECIFIED TIMESTAMP
vorpal.command('message poll <to>', 'gets messages between you and <to>').action(async function(args, callback) {
const to = await getAddress(args.to);
let messages = await queryMessages(USER.address, to);
messages = messages.map((message) =>
JSON.parse(crypto.decrypt(message, crypto.convertSkToCurve(USER.keys.secretKey), crypto.convertPkToCurve(to)).message)
);
this.log(messages);
callback();
});
Interact
Now run the CLI and interact using the following commands:
> help
Commands:
help [command...] Provides help for a given command.
exit Exits application.
register registers a unique alias for your account
create creates tokens for an account
transfer transfers tokens to another account
message sends a message to another user
toll sets a toll people must you in order to send you messages
add friend adds a friend to your account
remove friend removes a friend from your account
message poll <to> gets messages between you and <to>
query [account] gets data for the account associated with the given [wallet]. Otherwise, gets all network data.
wallet create <name> creates a wallet <name>
wallet list [name] lists wallet for [name]. Otherwise, lists all wallets
use <name> uses <name> wallet for transactions
use host <host> uses <host> as the node for queries and transactions
init sets the user wallet if it exists, else creates it
Here's an example of me testing out all the transaction functionality we just added.
❯ node client
Loaded wallet entries from '/home/kyle/Code/shardus/applications/test/wallet.json'.
Using localhost:9001 as coin-app node for queries and transactions.
>
Enter wallet name: kyle
> register
Enter the alias you want: kyle
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> use test
Now using wallet: test
> register
Enter the alias you want: test
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> add friend
Enter the alias or publicKey of the friend: kyle
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> toll
Enter the toll: 10
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> use kyle
Now using wallet: kyle
> message
Enter the alias or publicKey of the target: test
Enter the message: yo
? The toll for sending this user a message is 0, continue? true
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> use test
Now using wallet: test
> remove friend
Enter the alias or publicKey of the friend to remove: kyle
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> use kyle
Now using wallet: kyle
> message
Enter the alias or publicKey of the target: test
Enter the message: yo 2
? The toll for sending this user a message is 10, continue? true
{result: { success: true, reason: 'Transaction queued, poll for results.' }}
> message poll test
[{ body: 'yo', handle: 'kyle', timestamp: 1597800151040 },{ body: 'yo 2', handle: 'kyle', timestamp: 1597800187558 }]
> use test
Now using wallet: test
> message poll kyle
[{ body: 'yo', handle: 'kyle', timestamp: 1597800151040 },{ body: 'yo 2', handle: 'kyle', timestamp: 1597800187558 }]
Encryption and Decryption
When we query the "chat" account on the server that was created in the example above, you can see that they're encrypted and nobody other than the two users who belong to that chat can decode any of the messages you see here:
{
account: {
id: '4187f99aae66a79a96727aad302f71cb07033130d000524cdff352c4e3125c3a',
messages: [
'["9875fbcbc0a376c24f162962da0a17da67d5a66e50f02cc2dfcb20d5f488743406788f8de787bd03a885cb0c513be5894102860c8b91cb249e65c3468ba12bf9b99d8c8f19b3ac","71c428a0fd2c1d506b15ec39396313f11a6c0708be0c73de"]',
'["1e96886f837c9ea4a4fd3ba498e71009bc462ed33c1561d8b59fadb63f7bbdeaee393bf76a86338668bc488930e62316f74a706995224969b3a0fe98875bc69c8711bde8a35e438097","0260018df3afaf12938e0f067f39a6ba6947caf3a2e51253"]'
],
timestamp: 1597800187558,
hash: '5ba70e88d7eac3d301e0089d16a8b7d82be9579215378314d465fa6a81feb9b7'
}
}
That just about wraps it up for the second example. By now, you're probably familiar with the process of creating transactions and extending functionality within a shardus network. Extra credit for those who extend this chat application with group messaging on their own ;)
Seriously build something cool with this! You're going to be surprised how easy it is and how much you can do with this library.