Skip to content

Commit

Permalink
Merge pull request #25 from trustenterprises/feature/bequest_account
Browse files Browse the repository at this point in the history
Create and bequest tokens to an account
  • Loading branch information
mattsmithies authored Jul 13, 2021
2 parents c4ef810 + 280e1bd commit 0ca2247
Show file tree
Hide file tree
Showing 13 changed files with 1,908 additions and 2 deletions.
1,641 changes: 1,641 additions & 0 deletions Serverless Hedera Consensus.postman_collection.json

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions __tests__/e2e/hashgraph_client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,35 @@ test("The client can create a token", async () => {
expect(token.symbol).toBe(tokenData.symbol)
expect(token.name).toBe(tokenData.name)
}, 20000)

test("The client can create an account", async () => {
const account = await client.createAccount()

expect(account.accountId).toBeDefined()
expect(account.encryptedKey).toBeDefined()
expect(account.publicKey).toBeDefined()
}, 20000)


test("The client can bequest an account with tokens", async () => {

const tokenData = {
supply: "10",
name: 'e2e-hedera-token-test',
symbol: 'te-e2e',
memo: 'BEQUEST TOKEN ACCOUNT TEST',
}

const token = await client.createToken(tokenData)
const account = await client.createAccount()

const bequest = await client.bequestToken({
encrypted_receiver_key: account.encryptedKey,
token_id: token.tokenId,
receiver_id: account.accountId,
amount: 1
})

expect(bequest.amount).toBeDefined()
expect(bequest.receiver_id).toBeDefined()
}, 20000)
4 changes: 3 additions & 1 deletion app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const {
API_SECRET_KEY,
API_URL,
HIDE_STATUS,
WEBHOOK_URL
WEBHOOK_URL,
ENCRYPTION_KEY
} = process.env

const AUTH_KEY_MIN_LENGTH = 10
Expand All @@ -20,6 +21,7 @@ export default {
accountId: HEDERA_ACCOUNT_ID,
privateKey: HEDERA_PRIVATE_KEY,
authenticationKey: API_SECRET_KEY,
encryptionKey: ENCRYPTION_KEY,
apiUrl: API_URL,
hideStatus: HIDE_STATUS,
webhookUrl: WEBHOOK_URL
Expand Down
4 changes: 4 additions & 0 deletions app/constants/language/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module.exports = {
noApikey: 'Please set "x-api-key" in your header',
invalidApikey: 'Unable to validate with the supplied "x-api-key"'
},
ensureEncryptionKey: {
noEncryptionKey:
'Unable to process encryption action, 32 character length "ENCRYPTION_KEY" not set in config'
},
onlyPostResponse: {
notAllowed: method => `Method ${method} is not allowed on this route`
}
Expand Down
30 changes: 30 additions & 0 deletions app/handler/bequestTokenHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import bequestTokenRequest from "app/validators/bequestTokenRequest"
import Response from "app/response"

async function BequestTokenHandler(req, res) {
const validationErrors = bequestTokenRequest(req.body)

if (validationErrors) {
return Response.unprocessibleEntity(res, validationErrors)
}

const { encrypted_receiver_key, token_id, receiver_id, amount } = req.body
const bequestPayload = {
encrypted_receiver_key,
token_id,
receiver_id,
amount
}

const { hashgraphClient } = req.context
const bequestResponse = await hashgraphClient.bequestToken(bequestPayload)

if (bequestResponse) {
return Response.json(res, bequestResponse)
}

// This has to be bolstered up with correct error handling
return Response.badRequest(res)
}

export default BequestTokenHandler
10 changes: 10 additions & 0 deletions app/handler/createAccountHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Response from "app/response"

async function CreateAccountHandler(req, res) {
const { hashgraphClient } = req.context
const account = await hashgraphClient.createAccount()

Response.json(res, account)
}

export default CreateAccountHandler
83 changes: 82 additions & 1 deletion app/hashgraph/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import {
TopicId,
TokenCreateTransaction,
Hbar,
HbarUnit
HbarUnit,
AccountCreateTransaction,
TokenAssociateTransaction,
TokenId,
TransferTransaction
} from "@hashgraph/sdk"
import HashgraphClientContract from "./contract"
import HashgraphNodeNetwork from "./network"
import Config from "app/config"
import sleep from "app/utils/sleep"
import Encryption from "app/utils/encryption"
import Explorer from "app/utils/explorer"
import sendWebhookMessage from "app/utils/sendWebhookMessage"
import Specification from "app/hashgraph/tokens/specifications"
Expand Down Expand Up @@ -144,6 +149,82 @@ class HashgraphClient extends HashgraphClientContract {
return messageTransactionResponse
}

// Before transferring token to other account association is require
async associateToAccount({ privateKey, tokenIds, accountId }) {
const client = this.#client

const transaction = await new TokenAssociateTransaction()
.setAccountId(accountId)
.setTokenIds(tokenIds)
.freezeWith(client)

const accountPrivateKey = PrivateKey.fromString(privateKey)
const signTx = await transaction.sign(accountPrivateKey)

return await signTx.execute(client)
}

bequestToken = async ({
specification = Specification.Fungible,
encrypted_receiver_key,
token_id,
receiver_id,
amount
}) => {
const client = this.#client

// Extract PV from encrypted
const privateKey = await Encryption.decrypt(encrypted_receiver_key)

// Associate with the token
await this.associateToAccount({
privateKey,
tokenIds: [token_id],
accountId: receiver_id
})

const { tokens } = await new AccountBalanceQuery()
.setAccountId(Config.accountId)
.execute(client)

const token = tokens.get(TokenId.fromString(token_id))
const adjustedAmountBySpec = amount * 10 ** specification.decimals

if (token.low < adjustedAmountBySpec) {
return false
}

await new TransferTransaction()
.addTokenTransfer(token_id, Config.accountId, -adjustedAmountBySpec)
.addTokenTransfer(token_id, receiver_id, adjustedAmountBySpec)
.execute(client)

return {
amount,
receiver_id
}
}

createAccount = async () => {
const privateKey = await PrivateKey.generate()
const publicKey = privateKey.publicKey
const client = this.#client
const transaction = new AccountCreateTransaction()
.setKey(publicKey)
.setInitialBalance(0.1)

const txResponse = await transaction.execute(client)
const receipt = await txResponse.getReceipt(client)
const accountId = receipt.accountId.toString()
const encryptedKey = await Encryption.encrypt(privateKey.toString())

return {
accountId,
encryptedKey,
publicKey: publicKey.toString()
}
}

createToken = async tokenCreation => {
const {
specification = Specification.Fungible,
Expand Down
19 changes: 19 additions & 0 deletions app/middleware/ensureEncryptionKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Language from "app/constants/language"
import Response from "app/response"
import Config from "app/config"

const { noEncryptionKey } = Language.middleware.ensureEncryptionKey

const ENC_KEY_LEN = 32

function ensureEncryptionKey(handler) {
return async (req, res) => {
if (Config.encryptionKey?.length === ENC_KEY_LEN) {
return handler(req, res)
}

return Response.unprocessibleEntity(res, noEncryptionKey)
}
}

export default ensureEncryptionKey
39 changes: 39 additions & 0 deletions app/utils/encryption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import crypto from "crypto"
import Config from "app/config"

const IV_LENGTH = 16 // For AES, this is always 16

function encrypt(text, encryptionKey = Config.encryptionKey) {
const iv = Buffer.from(crypto.randomBytes(IV_LENGTH))
.toString("hex")
.slice(0, IV_LENGTH)
const cipher = crypto.createCipheriv(
"aes-256-cbc",
Buffer.from(encryptionKey),
iv
)
const encrypted = cipher.update(text)
const buffer = Buffer.concat([encrypted, cipher.final()])

return iv + ":" + buffer.toString("hex")
}

function decrypt(text, encryptionKey = Config.encryptionKey) {
const textParts = text.includes(":") ? text.split(":") : []
const iv = Buffer.from(textParts.shift() || "", "binary")
const encryptedText = Buffer.from(textParts.join(":"), "hex")
const decipher = crypto.createDecipheriv(
"aes-256-cbc",
Buffer.from(encryptionKey),
iv
)
const decrypted = decipher.update(encryptedText)
const buffer = Buffer.concat([decrypted, decipher.final()])

return buffer.toString()
}

export default {
decrypt,
encrypt
}
20 changes: 20 additions & 0 deletions app/validators/bequestTokenRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const Joi = require("@hapi/joi")

const schema = Joi.object({
encrypted_receiver_key: Joi.string()
.length(241)
.required(),
token_id: Joi.string().required(),
receiver_id: Joi.string().required(),
amount: Joi.number().required()
})

function consensusMessageRequest(candidate = {}) {
const validation = schema.validate(candidate || {})

if (validation.error) {
return validation.error.details.map(error => error.message)
}
}

export default consensusMessageRequest
2 changes: 2 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
HEDERA_ENVIRONMENT=
HEDERA_ACCOUNT_ID=
HEDERA_PRIVATE_KEY=

API_SECRET_KEY=
ENCRYPTION_KEY=

API_URL=
HIDE_STATUS=
Expand Down
13 changes: 13 additions & 0 deletions pages/api/account/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import onlyPost from "app/middleware/onlyPost"
import withAuthentication from "app/middleware/withAuthentication"
import useHashgraphContext from "app/context/useHashgraphContext"
import prepare from "app/utils/prepare"
import CreateAccountHandler from "app/handler/createAccountHandler"
import ensureEncryptionKey from "app/middleware/ensureEncryptionKey"

export default prepare(
onlyPost,
ensureEncryptionKey,
withAuthentication,
useHashgraphContext
)(CreateAccountHandler)
13 changes: 13 additions & 0 deletions pages/api/token/bequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import onlyPost from "app/middleware/onlyPost"
import withAuthentication from "app/middleware/withAuthentication"
import useHashgraphContext from "app/context/useHashgraphContext"
import prepare from "app/utils/prepare"
import BequestTokenHandler from "app/handler/bequestTokenHandler"
import ensureEncryptionKey from "app/middleware/ensureEncryptionKey"

export default prepare(
onlyPost,
ensureEncryptionKey,
withAuthentication,
useHashgraphContext
)(BequestTokenHandler)

0 comments on commit 0ca2247

Please sign in to comment.