Skip to content
Closed
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
5 changes: 3 additions & 2 deletions modules/account-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
"coverage": "npm run gen-coverage && npm run upload-coverage",
"gen-protobuf": "pbjs -t static-module -w commonjs -o ./resources/trx/protobuf/tron.js ./resources/trx/protobuf/Discover.proto ./resources/trx/protobuf/Contract.proto ./resources/trx/protobuf/tron.proto",
"gen-protobufts": "pbts -o ./resources/trx/protobuf/tron.d.ts ./resources/trx/protobuf/tron.js",
"hedera-gen-protobuf": "pbjs -t static-module -w commonjs -o ./resources/hbar/protobuf/hedera.js ./resources/hbar/protobuf/Timestamp.proto ./resources/hbar/protobuf/BasicTypes.proto ./resources/hbar/protobuf/Duration.proto ./resources/hbar/protobuf/CryptoCreate.proto ./resources/hbar/protobuf/CryptoTransfer.proto ./resources/hbar/protobuf/TransactionBody.proto ./resources/hbar/protobuf/Transaction.proto",
"hedera-gen-protobuf": "pbjs -t static-module -w commonjs -o ./resources/hbar/protobuf/hedera.js ./resources/hbar/protobuf/Timestamp.proto ./resources/hbar/protobuf/BasicTypes.proto ./resources/hbar/protobuf/Duration.proto ./resources/hbar/protobuf/CryptoCreate.proto ./resources/hbar/protobuf/CryptoTransfer.proto ./resources/hbar/protobuf/TransactionBody.proto ./resources/hbar/protobuf/Transaction.proto ./resources/hbar/protobuf/TokenCreate.proto",
"hedera-gen-protobufts": "pbts -o ./resources/hbar/protobuf/hedera.d.ts ./resources/hbar/protobuf/hedera.js",
"lint": "eslint 'src/**/*.ts' && eslint 'test/**/*.ts' || true",
"lint-fix": "eslint 'src/**/*.ts' --fix && eslint 'test/**/*.ts' --fix || true",
"prepublishOnly": "npm run compile",
"prepare": "npm run gen-protobuf && npm run gen-protobufts && npm run hedera-gen-protobuf && npm run hedera-gen-protobufts && tsc && cp -r ./resources ./dist",
"unit-test": "nyc -- mocha --opts test/mocha.opts \"test/unit/**/*.ts\"",
"test": "npm run unit-test"
"test": "npm run unit-test",
"test-hbar-tx": "nyc -- mocha --opts test/mocha.opts \"test/unit/coin/hbar/transactionBuilder/*.ts\""
},
"repository": {
"type": "git",
Expand Down
58 changes: 58 additions & 0 deletions modules/account-lib/resources/hbar/protobuf/TokenCreate.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
syntax = "proto3";

package proto;

/*-
* ‌
* Hedera Network Services Protobuf
* ​
* Copyright (C) 2018 - 2020 Hedera Hashgraph, LLC
* ​
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ‍
*/

option java_package = "com.hederahashgraph.api.proto.java";
option java_multiple_files = true;

import "Duration.proto";
import "BasicTypes.proto";
import "Timestamp.proto";

/*
Create a new token. After the token is created, the Token ID for it is in the receipt.
The specified Treasury Account is receiving the initial supply of tokens as-well as the tokens from the Token Mint operation once executed. The balance of the treasury account is decreased when the Token Burn operation is executed.

The supply that is going to be put in circulation is going to be the initial supply provided. The maximum supply a token can have is 2^63-1.
Example:
Token A has initial supply set to 10_000 and decimals set to 2. The tokens that will be put into circulation are going be 100.
Token B has initial supply set to 10_012_345_678 and decimals set to 8. The number of tokens that will be put into circulation are going to be 100.12345678

Creating immutable token: Token can be created as immutable if the adminKey is omitted. In this case, the name, symbol, treasury, management keys, expiry and renew properties cannot be updated. If a token is created as immutable, anyone is able to extend the expiry time by paying the fee.
*/
message TokenCreateTransactionBody {
string name = 1; // The publicly visible name of the token, specified as a string of only ASCII characters
string symbol = 2; // The publicly visible token symbol. It is UTF-8 capitalized alphabetical string identifying the token
uint32 decimals = 3; // The number of decimal places a token is divisible by. This field can never be changed!
uint64 initialSupply = 4; // Specifies the initial supply of tokens to be put in circulation. The initial supply is sent to the Treasury Account. The supply is in the lowest denomination possible.
AccountID treasury = 5; // The account which will act as a treasury for the token. This account will receive the specified initial supply
Key adminKey = 6; // The key which can perform update/delete operations on the token. If empty, the token can be perceived as immutable (not being able to be updated/deleted)
Key kycKey = 7; // The key which can grant or revoke KYC of an account for the token's transactions. If empty, KYC is not required, and KYC grant or revoke operations are not possible.
Key freezeKey = 8; // The key which can sign to freeze or unfreeze an account for token transactions. If empty, freezing is not possible
Key wipeKey = 9; // The key which can wipe the token balance of an account. If empty, wipe is not possible
Key supplyKey = 10; // The key which can change the supply of a token. The key is used to sign Token Mint/Burn operations
bool freezeDefault = 11; // The default Freeze status (frozen or unfrozen) of Hedera accounts relative to this token. If true, an account must be unfrozen before it can receive the token
Timestamp expiry = 13; // The epoch second at which the token should expire; if an auto-renew account and period are specified, this is coerced to the current epoch second plus the autoRenewPeriod
AccountID autoRenewAccount = 14; // An account which will be automatically charged to renew the token's expiration, at autoRenewPeriod interval
Duration autoRenewPeriod = 15; // The interval at which the auto-renew account will be charged to extend the token's expiry
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "CryptoTransfer.proto";
import "Duration.proto";
import "BasicTypes.proto";

import "TokenCreate.proto";

/* A single transaction. All transaction types are possible here. */
message TransactionBody {
Expand All @@ -21,5 +22,6 @@ message TransactionBody {
CryptoCreateTransactionBody cryptoCreateAccount = 11; // Create a new cryptocurrency account
CryptoTransferTransactionBody cryptoTransfer = 14; // Transfer amount between accounts

TokenCreateTransactionBody tokenCreation = 29; // Creates a token instance
}
}
2 changes: 2 additions & 0 deletions modules/account-lib/src/coin/baseCoin/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export enum TransactionType {
StakingUnlock,
// Withdraw
StakingWithdraw,
// Create a token on-chain (e.g. Hedera Token Service)
TokenCreation,
}

/**
Expand Down
283 changes: 283 additions & 0 deletions modules/account-lib/src/coin/hbar/tokenCreateBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics/dist/src/base';
import Long from 'long';
import { AccountId, Ed25519PublicKey } from '@hashgraph/sdk';
import { proto } from '../../../resources/hbar/protobuf/hedera';
import { BuildTransactionError, InvalidParameterValueError } from '../baseCoin/errors';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction';
import { isValidAddress, isValidAmount, isValidPublicKey, isValidTimeString, stringifyAccountId, toHex } from './utils';
import { TransactionType } from '../baseCoin';
import BigNumber from 'bignumber.js';

export class TokenCreateBuilder extends TransactionBuilder {
private _txBodyData: proto.TokenCreateTransactionBody;
private _tokenName: string;
private _tokenSymbol: string;
private _decimals: string;
private _initialSupply: string;
private _treasuryAccountId: string;
private _adminKey: string;
private _kycKey: string;
private _freezeKey: string;
private _wipeKey: string;
private _supplyKey: string;
private _freezeDefault: boolean;
private _expirationTime: string;
private _autoRenewAccountId: string;
private _autoRenewPeriod: string;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._txBodyData = new proto.TokenCreateTransactionBody();
this._txBody.tokenCreation = this._txBodyData;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this._txBodyData.name = this._tokenName;
this._txBodyData.symbol = this._tokenSymbol;
this._txBodyData.treasury = this.buildAccountID(this._treasuryAccountId);
this._txBodyData.expiry = this.buildTimestamp(this._expirationTime);
if (this._decimals) {
this._txBodyData.decimals = parseInt(this._decimals);
}
if (this._initialSupply) {
this._txBodyData.initialSupply = Long.fromString(this._initialSupply);
}
if (this._adminKey) {
this._txBodyData.adminKey = this.buildKey(this._adminKey);
}
if (this._kycKey) {
this._txBodyData.kycKey = this.buildKey(this._kycKey);
}
if (this._freezeKey) {
this._txBodyData.freezeKey = this.buildKey(this._freezeKey);
}
if (this._wipeKey) {
this._txBodyData.wipeKey = this.buildKey(this._wipeKey);
}
if (this._supplyKey) {
this._txBodyData.supplyKey = this.buildKey(this._supplyKey);
}
if (this._freezeDefault) {
this._txBodyData.freezeDefault = this._freezeDefault;
}
if (this._autoRenewAccountId) {
this._txBodyData.autoRenewAccount = this.buildAccountID(this._autoRenewAccountId);
}
if (this._autoRenewPeriod) {
this._txBodyData.autoRenewPeriod = this.buildDuration(this._autoRenewPeriod);
} else {
this._txBodyData.autoRenewPeriod = this.buildDuration('7890000');
}
this.transaction.setTransactionType(TransactionType.TokenCreation);
return await super.buildImplementation();
}

private buildAccountID(address: string): proto.AccountID {
const accountData = new AccountId(address);
return new proto.AccountID({
accountNum: accountData.account,
realmNum: accountData.realm,
shardNum: accountData.shard,
});
}

private buildKey(key: string): proto.Key {
const keyData = Ed25519PublicKey.fromString(key);
return new proto.Key({
ed25519: keyData.toBytes(),
});
}

private buildTimestamp(timestamp: string): proto.Timestamp {
const timeParts = timestamp.split('.').map(v => new BigNumber(v).toNumber());
return new proto.Timestamp({ seconds: timeParts[0], nanos: timeParts[1] });
}

private buildDuration(duration: string): proto.Duration {
const timeParts = duration.split('.').map(v => new BigNumber(v).toNumber());
return new proto.Timestamp({ seconds: timeParts[0], nanos: timeParts[1] });
}

/** @inheritdoc */
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.transaction.setTransactionType(TransactionType.TokenCreation);
const data = tx.txBody.tokenCreation;
this.expirationTime((Date.now() + 7776000).toString());
this.autoRenewPeriod('7890000');
if (data && data.name) {
this.name(data.name!);
}
if (data && data.symbol) {
this.symbol(data.symbol!);
}
if (data && data.decimals) {
this.decimal(data.decimals!.toString());
}
if (data && data.initialSupply) {
this.initialSupply(data.initialSupply!.toString());
}
if (data && data.treasury) {
this.treasuryAccount(stringifyAccountId(data.treasury!));
}
if (data && data.adminKey) {
this.adminKey(toHex(data.adminKey.ed25519!));
}
if (data && data.kycKey) {
this.kycKey(toHex(data.kycKey.ed25519!));
}
if (data && data.freezeKey) {
this.freezeKey(toHex(data.freezeKey.ed25519!));
}
if (data && data.wipeKey) {
this.wipeKey(toHex(data.wipeKey.ed25519!));
}
if (data && data.supplyKey) {
this.supplyKey(toHex(data.supplyKey.ed25519!));
}
if (data && data.freezeDefault) {
this.freezeDefault(data.freezeDefault!);
}
if (data && data.expiry) {
this.expirationTime(data.expiry.seconds!.toString());
}
if (data && data.autoRenewAccount) {
this.autoRenewAccount(stringifyAccountId(data.autoRenewAccount!));
}
if (data && data.autoRenewPeriod) {
this.autoRenewPeriod(data.autoRenewPeriod.seconds!.toString());
}
}

//region TokenCreateTransaction fields
name(name: string): this {
if (name.length > 100) {
throw new InvalidParameterValueError('Name cannot be longer than 100 characters');
}
this._tokenName = name;
return this;
}

symbol(symbol: string): this {
if (symbol.length > 100) {
throw new InvalidParameterValueError('Symbol cannot be longer than 100 characters');
}
this._tokenSymbol = symbol;
return this;
}

decimal(decimal: string): this {
if (!isValidAmount(decimal)) {
throw new InvalidParameterValueError('Invalid decimal');
}
this._decimals = decimal;
return this;
}

initialSupply(initialSupply: string): this {
if (!isValidAmount(initialSupply)) {
throw new InvalidParameterValueError('Invalid initial supply');
}
this._initialSupply = initialSupply;
return this;
}

treasuryAccount(address: string): this {
if (!isValidAddress(address)) {
throw new InvalidParameterValueError('Invalid treasury account');
}
this._treasuryAccountId = address;
return this;
}

adminKey(key: string): this {
if (!isValidPublicKey(key)) {
throw new InvalidParameterValueError('Invalid admin key');
}
this._adminKey = key;
return this;
}

kycKey(key: string): this {
if (!isValidPublicKey(key)) {
throw new InvalidParameterValueError('Invalid kyc key');
}
this._kycKey = key;
return this;
}

freezeKey(key: string): this {
if (!isValidPublicKey(key)) {
throw new InvalidParameterValueError('Invalid freeze key');
}
this._freezeKey = key;
return this;
}

wipeKey(key: string): this {
if (!isValidPublicKey(key)) {
throw new InvalidParameterValueError('Invalid wipe key');
}
this._wipeKey = key;
return this;
}

supplyKey(key: string): this {
if (!isValidPublicKey(key)) {
throw new InvalidParameterValueError('Invalid supply key');
}
this._supplyKey = key;
return this;
}

freezeDefault(freeze: boolean): this {
this._freezeDefault = freeze;
return this;
}

expirationTime(timestamp: string): this {
if (!isValidTimeString(timestamp)) {
throw new InvalidParameterValueError('Invalid timestamp');
}
this._expirationTime = timestamp;
return this;
}

autoRenewAccount(address: string): this {
if (!isValidAddress(address)) {
throw new InvalidParameterValueError('Invalid auto renew account');
}
this._autoRenewAccountId = address;
return this;
}

autoRenewPeriod(duration: string): this {
if (!isValidTimeString(duration)) {
throw new InvalidParameterValueError('Invalid auto renew period');
}
this._autoRenewPeriod = duration;
return this;
}

//endregion

//region Validators
validateMandatoryFields(): void {
if (this._tokenName === undefined) {
throw new BuildTransactionError('Invalid transaction: missing token name');
}
if (this._tokenSymbol === undefined) {
throw new BuildTransactionError('Invalid transaction: missing token symbol');
}
if (this._treasuryAccountId === undefined) {
throw new BuildTransactionError('Invalid transaction: missing treasury account id');
}
if (this._expirationTime === undefined) {
throw new BuildTransactionError('Invalid transaction: missing expiration time');
}
super.validateMandatoryFields();
}
//endregion
}
Loading