Skip to content

User Transactions

User transactions are created by any users interacting with the network. For this example, we'll show you an interesting way of verifying and validating a user's email address in a decentralized fashion.

Generating

Since these transactions are meant to be generated by users, they will resemble a CLI command or frontend form submission. In this example, we will use the vorpal npm package to create the initial transaction data using a CLI command.

async function injectTx(tx) {
  try {
    const res = await axios.post(`http://${HOST}/inject`, tx)
    return res.data
  } catch (err) {
    return err.message
  }
}

vorpal.command('email', 'registers your email address to the network').action(async function(_, callback) {
  const answer = await this.prompt({
    type: 'input',
    name: 'email',
    message: 'Enter your email address: ',
    validate: result => {
      const regex = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
      if (!regex.test(result)) { // Check to make sure email is valid
        return 'You need to provide a valid email address'
      } else {
        return true
      }
    },
  })
  const signedTx = {
    emailHash: crypto.hash(answer.email), // Send a signed tx with the hash of the email so nobody can publicly see it
    from: USER.address,
  }
  crypto.signObj(signedTx, USER.keys.secretKey, USER.keys.publicKey)
  const tx = {
    type: 'email', // the type of transaction being submitted
    signedTx, // The signed transaction data
    email: answer.email, // The answered form email
    timestamp: Date.now(), // timestamp of the transaction
  }
  injectTx(tx).then(res => { // Submit transaction to the network
    this.log(res)
    callback()
  })
})

Keys

The keys of any transaction are going to resemble the public keys of the accounts associated with the transaction. We are required to specify these in crack(). The only key involved with this transaction is the signed from address (the public key of the user who submitted it).

case 'email':
    result.sourceKeys = [tx.signedTx.from]
    break

Validation

First, we will quickly validate the types and attributes of the transaction data using validate().

case 'email': {
    if (typeof tx.signedTx !== 'object') {
        success = false
        reason = '"signedTx" must be an object.'
        throw new Error(reason)
    }
    const signedTx = tx.signedTx
    if (signedTx) {
        if (typeof signedTx !== 'object') {
        success = false
        reason = '"signedTx" must be a object.'
        throw new Error(reason)
        }
        if (typeof signedTx.sign !== 'object') {
        success = false
        reason = '"sign" property on signedTx must be an object.'
        throw new Error(reason)
        }
        if (typeof signedTx.from !== 'string') {
        success = false
        reason = '"From" must be a string.'
        throw new Error(reason)
        }
        if (typeof signedTx.emailHash !== 'string') {
        success = false
        reason = '"emailHash" must be a string.'
        throw new Error(reason)
        }
    }
    if (typeof tx.email !== 'string') {
        success = false
        reason = '"email" must be a string.'
        throw new Error(reason)
    }
    if (tx.email.length > 30) {
        success = false
        reason = '"Email" length must be less than 31 characters (30 max)'
        throw new Error(reason)
    }
    break
}

Next, we validate the transaction using actual account data using the helper function validateTransaction.

const response: Shardus.IncomingTransactionResult = {
    success: false,
    reason: 'Transaction is not valid.',
    txnTimestamp: tx.timestamp,
}

const from = wrappedStates[tx.from] && wrappedStates[tx.from].data

case 'email': {
    const source: UserAccount = wrappedStates[tx.signedTx.from] && wrappedStates[tx.signedTx.from].data
    if (!source) {
        response.reason = 'no account associated with address in signed tx'
        return response
    }
    if (tx.signedTx.sign.owner !== tx.signedTx.from) {
        response.reason = 'not signed by from account'
        return response
    }
    if (crypto.verifyObj(tx.signedTx) === false) {
        response.reason = 'incorrect signing'
        return response
    }
    if (tx.signedTx.emailHash !== crypto.hash(tx.email)) {
        response.reason = 'Hash of the email does not match the signed email hash'
        return response
    }
    response.success = true
    response.reason = 'This transaction is valid!'
    return response
}

Applying

The way this transaction works is that it selectively chooses one random node whose ID is closest to the address of the user who initiated the transaction. This "lucky" node can then make a single request to an email server API that will send a confirmation email to the user with a random 6-digit verification number. The tricky part is that only this node knows what the random number is, so we have to implement another transaction type that should be immediately injected during the apply phase of the email transaction. This new transaction will be gossiped to every node, and they will be given the hash of the original verification number, so no user could see the original. Later, you could implement a verify transaction that requires the user to send the verification number to the network and change the state of that user's account data to reflect the validation in some way.

case 'email': {
    const source: UserAccount = wrappedStates[tx.signedTx.from].data
    const nodeId = dapp.getNodeId()
    const { address } = dapp.getNode(nodeId)
    const [closest] = dapp.getClosestNodes(tx.signedTx.from, 5)

    if (nodeId === closest) {
        const baseNumber = 99999
        const randomNumber = Math.floor(Math.random() * 899999) + 1
        const verificationNumber = baseNumber + randomNumber

        axios.post('http://somehost.com/mailAPI/index.cgi', {
            from: 'liberdus.verify',
            to: `${tx.email}`,
            subject: 'Verify your email for liberdus',
            message: `Please verify your email address by sending a "verify" transaction with the number: ${verificationNumber}`,
            secret: 'some-secret-code',
        })

        dapp.put({
            type: 'gossip_email_hash',
            nodeId,
            account: source.id,
            from: address,
            emailHash: tx.signedTx.emailHash,
            verified: crypto.hash(`${verificationNumber}`),
            timestamp: Date.now(),
        })
    }

    dapp.log('Applied email tx', source)
    break
}