Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beta token issuance #21

Merged
merged 9 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions __tests__/e2e/hashgraph_client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,21 @@ test("The client can update the memo of a private topic", async () => {
const updatedTopicInfo = await client.getTopicInfo(newTopic.topic)
expect(updatedTopicInfo.topicMemo).toBe(newMemo)
}, 20000)

test("The client can create a token", async () => {

const tokenData = {
supply: "10",
name: 'e2e-hedera-token-test',
symbol: 'te-e2e',
memo: 'THIS IS A MEMO',
}

const token = await client.createToken(tokenData)

expect(token.tokenId).toBeDefined()
expect(token.memo).toBe(tokenData.memo)
expect(token.supply).toBe(tokenData.supply)
expect(token.symbol).toBe(tokenData.symbol)
expect(token.name).toBe(tokenData.name)
}, 20000)
36 changes: 36 additions & 0 deletions app/handler/createTokenHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import createTokenRequest from "app/validators/createTokenRequest"
import Response from "app/response"
import Specification from "app/hashgraph/tokens/specifications"

async function CreateTokenHandler(req, res) {
const validationErrors = createTokenRequest(req.body)

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

const {
symbol,
name,
supply,
memo,
requires_kyc = false,
can_freeze = false
} = req.body

const { hashgraphClient } = req.context

const token = await hashgraphClient.createToken({
specification: Specification.Fungible,
memo,
name,
symbol,
supply,
requires_kyc,
can_freeze
})

Response.json(res, token)
}

export default CreateTokenHandler
67 changes: 66 additions & 1 deletion app/hashgraph/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import {
TopicUpdateTransaction,
TopicMessageSubmitTransaction,
TransactionRecordQuery,
TopicId
TopicId,
TokenCreateTransaction,
Hbar,
HbarUnit
} from "@hashgraph/sdk"
import HashgraphClientContract from "./contract"
import HashgraphNodeNetwork from "./network"
import Config from "app/config"
import sleep from "app/utils/sleep"
import Explorer from "app/utils/explorer"
import sendWebhookMessage from "app/utils/sendWebhookMessage"
import Specification from "app/hashgraph/tokens/specifications"

class HashgraphClient extends HashgraphClientContract {
// Keep a private internal reference to SDK client
Expand Down Expand Up @@ -139,6 +143,67 @@ class HashgraphClient extends HashgraphClientContract {

return messageTransactionResponse
}

createToken = async tokenCreation => {
const {
specification = Specification.Fungible,
accountId,
memo,
name,
symbol,
supply,
requires_kyc = false,
can_freeze = false
} = tokenCreation

const client = this.#client

const operatorPrivateKey = PrivateKey.fromString(Config.privateKey)
const supplyPrivateKey = PrivateKey.fromString(Config.privateKey)

const supplyWithDecimals = supply * 10 ** specification.decimals

const transaction = new TokenCreateTransaction()
.setTokenName(name)
.setTokenSymbol(symbol)
.setTreasuryAccountId(accountId || Config.accountId)
.setInitialSupply(supplyWithDecimals)
.setDecimals(specification.decimals)
.setFreezeDefault(false)
.setMaxTransactionFee(new Hbar(5, HbarUnit.Hbar)) //Change the default max transaction fee

if (memo) {
transaction.setTokenMemo(memo)
transaction.setTransactionMemo(memo)
}

if (requires_kyc) {
transaction.setKycKey(operatorPrivateKey.publicKey)
}

if (can_freeze) {
transaction.setFreezeKey(operatorPrivateKey.publicKey)
}

transaction.freezeWith(client)

const signTx = await (await transaction.sign(operatorPrivateKey)).sign(
supplyPrivateKey
)

const txResponse = await signTx.execute(client)
const receipt = await txResponse.getReceipt(client)

return {
name,
symbol,
memo,
reference: specification.reference,
supply: String(supply),
supplyWithDecimals: String(supplyWithDecimals),
tokenId: receipt.tokenId.toString()
}
}
}

export default HashgraphClient
97 changes: 97 additions & 0 deletions app/hashgraph/tokens/specifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Update the type definition of Specification when new bits are required
*/
// export type Specification = {
// reference: string;
// decimals: number;
// kyc: boolean;
// wipe: boolean;
// freeze: boolean;
// }

/**
* TODO: more conversations and design about interop and bridging required to reduce precision loss
*
* This is a prototype spec that describes the interop for trading
* imported ERC20 tokens with minted HTS tokens. This provides a
* common shared attributes, the decimal places have been reduced to
* 6 as we wish to have enough precision but not lose the max limit
* for (2 - 1) * (10 ** 64)
*
* As at 01/02/21 provides a precision loss of 1.3 cents for 0.000001 eth
*
* With 6 digits of precision we have have a max HTS limit of for this spec
*
* 9,223,372,036,854.775 (9 trillion)
*
* @type {{decimals: number}}
*/
const Fungible = {
reference: "basic.fungible",
decimals: 6,
kyc: false,
wipe: false,
freeze: false
}

/**
* When creating NFT representations of tokens we require a token to be unique
* and non fungible, it cannot be divided.
*
* @type {{decimals: number}}
*/
const NonFungible = {
reference: "basic.nonfungible",
decimals: 0,
kyc: false,
wipe: false,
freeze: false
}

/**
* A KYC, freeze and wipe compliant fungible token, based on ERC20 that enables a token that has more control
* for regulatory and stricter requirements for investment purposes.
*
* @type {{decimals: number}}
*/
const CompliantFungible = {
...Fungible,
reference: "compliance.fungible",
kyc: true,
wipe: true,
freeze: true
}

/**
* A KYC, freeze and wipe compliant fungible token, based on ERC721/Non fungible that enables a token
* that has more control for regulatory and stricter use cases.
*
* @type {{decimals: number}}
*/
const CompliantNonFungible = {
...NonFungible,
reference: "compliance.nonfungible",
kyc: true,
wipe: true,
freeze: true
}

/**
* A special token that acts as a receipt for deposited assets in a pool. These
* can be returned to the treasury account that minted them. Upon receipt a claimant
* will be returned all tokens deposited and their fair share of the reward distribution.
*
* @type {{decimals: number}}
*/
const UnibarLiquidityProviderReceipt = {
...NonFungible,
reference: "lp.reward.receipt"
}

export default {
Fungible,
NonFungible,
CompliantFungible,
CompliantNonFungible,
UnibarLiquidityProviderReceipt
}
32 changes: 32 additions & 0 deletions app/validators/createTokenRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const Joi = require("@hapi/joi")

const TRILLION = 10 ** 12

const schema = Joi.object({
symbol: Joi.string()
.max(100)
.required(),
name: Joi.string()
.max(100)
.required(),
memo: Joi.string()
.max(100)
.optional(),
supply: Joi.number()
.positive()
.max(TRILLION)
.min(1)
.required(),
requires_kyc: Joi.bool().default(false),
can_freeze: Joi.bool().default(false)
}).options({ allowUnknown: true })

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

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

export default createTokenRequest
Loading