diff --git a/modules/account-lib/package.json b/modules/account-lib/package.json index 23ae4d4360..51b68d84ba 100644 --- a/modules/account-lib/package.json +++ b/modules/account-lib/package.json @@ -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", diff --git a/modules/account-lib/resources/hbar/protobuf/TokenCreate.proto b/modules/account-lib/resources/hbar/protobuf/TokenCreate.proto new file mode 100644 index 0000000000..40ce1d98ec --- /dev/null +++ b/modules/account-lib/resources/hbar/protobuf/TokenCreate.proto @@ -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 +} diff --git a/modules/account-lib/resources/hbar/protobuf/TransactionBody.proto b/modules/account-lib/resources/hbar/protobuf/TransactionBody.proto index 9f700d3f5a..a6078d5305 100644 --- a/modules/account-lib/resources/hbar/protobuf/TransactionBody.proto +++ b/modules/account-lib/resources/hbar/protobuf/TransactionBody.proto @@ -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 { @@ -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 } } \ No newline at end of file diff --git a/modules/account-lib/src/coin/baseCoin/enum.ts b/modules/account-lib/src/coin/baseCoin/enum.ts index 09ffd48514..04ad6803f3 100644 --- a/modules/account-lib/src/coin/baseCoin/enum.ts +++ b/modules/account-lib/src/coin/baseCoin/enum.ts @@ -27,6 +27,8 @@ export enum TransactionType { StakingUnlock, // Withdraw StakingWithdraw, + // Create a token on-chain (e.g. Hedera Token Service) + TokenCreation, } /** diff --git a/modules/account-lib/src/coin/hbar/tokenCreateBuilder.ts b/modules/account-lib/src/coin/hbar/tokenCreateBuilder.ts new file mode 100644 index 0000000000..811cb3c132 --- /dev/null +++ b/modules/account-lib/src/coin/hbar/tokenCreateBuilder.ts @@ -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) { + super(_coinConfig); + this._txBodyData = new proto.TokenCreateTransactionBody(); + this._txBody.tokenCreation = this._txBodyData; + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + 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 +} diff --git a/modules/account-lib/src/coin/hbar/transactionBuilderFactory.ts b/modules/account-lib/src/coin/hbar/transactionBuilderFactory.ts index 55fe69a049..a8cc0814c3 100644 --- a/modules/account-lib/src/coin/hbar/transactionBuilderFactory.ts +++ b/modules/account-lib/src/coin/hbar/transactionBuilderFactory.ts @@ -4,6 +4,7 @@ import { BaseTransactionBuilderFactory } from '../baseCoin'; import { WalletInitializationBuilder } from './walletInitializationBuilder'; import { TransferBuilder } from './transferBuilder'; import { TransactionBuilder } from './transactionBuilder'; +import { TokenCreateBuilder } from './tokenCreateBuilder'; import { Transaction } from './transaction'; import { isValidRawTransactionFormat, toUint8Array } from './utils'; @@ -22,6 +23,11 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new TransferBuilder(this._coinConfig)); } + /** @inheritDoc */ + getTokenCreateBuilder(tx?: Transaction): TokenCreateBuilder { + return this.initializeBuilder(tx, new TokenCreateBuilder(this._coinConfig)); + } + /** @inheritDoc */ from(raw: Uint8Array | string): TransactionBuilder { this.validateRawTransaction(raw); @@ -31,6 +37,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.getTransferBuilder(tx); case 'cryptoCreateAccount': return this.getWalletInitializationBuilder(tx); + case 'tokenCreation': + return this.getTokenCreateBuilder(tx); default: throw new InvalidTransactionError('Invalid transaction ' + tx.txBody.data); } diff --git a/modules/account-lib/test/resources/hbar/hbar.ts b/modules/account-lib/test/resources/hbar/hbar.ts index 636c7a5f32..3213d62ee2 100644 --- a/modules/account-lib/test/resources/hbar/hbar.ts +++ b/modules/account-lib/test/resources/hbar/hbar.ts @@ -99,3 +99,9 @@ export const WALLET_BUILDER_SIGNED_TWICE_TRANSACTION = export const WALLET_INIT_2_OWNERS = '227f0a140a0c089ded8af90510aac5d8b101120418d5d00412021804188094ebdc03220208785a590a4e2a4c080212480a2212205a9111b5e6881ff20b9243a42ac1a9a67fa16cd4f01e58bab30c1fe611ea8cf90a221220592a4fbb7263c59d450e651df96620dc9208ee7c7d9d6f2fdcb91c53f883126110004a0508d0c8e103'; + +export const TOKEN_CREATE_SIGNED_TRANSACTION = + '1a660a640a20d32b7b1eb103c10a6c8f6ec575b8002816e9725d95485b3d5509aa8c89b4528b1a40aee749ffa9c57b88137216835fe0d2e03895803252f51c89c7bfc545e8421a515171000db0204a2cc9f16f34504e1a8830a44b45e0fd93469e6d2cf32a310e0f225b0a180a0c089ded8af90510aac5d8b10112080800100018d5d0041206080010001804188094ebdc0322020878ea012c0a07546f6b656e20311206544f4b454e3120002a080800100018d5d0046a0608e5a8cb80067a0508d0c8e103'; + +export const TOKEN_CREATE_SIGNED_TWICE_TRANSACTION = + '1acc010a640a20d32b7b1eb103c10a6c8f6ec575b8002816e9725d95485b3d5509aa8c89b4528b1a40aee749ffa9c57b88137216835fe0d2e03895803252f51c89c7bfc545e8421a515171000db0204a2cc9f16f34504e1a8830a44b45e0fd93469e6d2cf32a310e0f0a640a205a9111b5e6881ff20b9243a42ac1a9a67fa16cd4f01e58bab30c1fe611ea8cf91a40e78d161f6ac9eedefc7cc5b1906350c99dd9b2823559200803b55d89a87727ac9e6ec9fa571da9927dd32eacb102bffdd23ce2ad989eeb38280409e47e25eb08225b0a180a0c089ded8af90510aac5d8b10112080800100018d5d0041206080010001804188094ebdc0322020878ea012c0a07546f6b656e20311206544f4b454e3120002a080800100018d5d0046a0608e5a8cb80067a0508d0c8e103'; diff --git a/modules/account-lib/test/unit/coin/hbar/transactionBuilder/tokenCreate.ts b/modules/account-lib/test/unit/coin/hbar/transactionBuilder/tokenCreate.ts new file mode 100644 index 0000000000..0df50d27e4 --- /dev/null +++ b/modules/account-lib/test/unit/coin/hbar/transactionBuilder/tokenCreate.ts @@ -0,0 +1,214 @@ +import should from 'should'; +import { register } from '../../../../../src/index'; +import { KeyPair, TransactionBuilderFactory } from '../../../../../src/coin/hbar'; +import * as testData from '../../../../resources/hbar/hbar'; +import { TransactionType } from '../../../../../src/coin/baseCoin'; + +describe('HTS token creation', () => { + const factory = register('thbar', TransactionBuilderFactory); + + const initTxBuilder = () => { + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.fee({ fee: '1000000000' }); + txBuilder.name('Your Token Name'); + txBuilder.symbol('F'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + txBuilder.initialSupply('5000'); + txBuilder.adminKey(testData.OPERATOR.publicKey); + txBuilder.expirationTime((Date.now() + 7776000).toString()); + txBuilder.source({ address: testData.OPERATOR.accountId }); + txBuilder.sign({ key: testData.OPERATOR.privateKey }); + return txBuilder; + }; + + describe('should build ', () => { + it('a valid raw tx for tokenCreate', async () => { + const builder = initTxBuilder(); + const tx = await builder.build(); + const raw = tx.toBroadcastFormat(); + const builder2 = factory.from(raw); + const tx2 = await builder2.build(); + should.deepEqual(tx.signature.length, 1); + should.deepEqual(tx.toJson(), tx2.toJson()); + should.deepEqual(raw, tx2.toBroadcastFormat()); + tx.type.should.equal(TransactionType.TokenCreation); + tx2.type.should.equal(TransactionType.TokenCreation); + }); + + it('a tokenCreate transaction', async () => { + const txBuilder = initTxBuilder(); + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + txJson.fee.should.equal(1000000000); + should.deepEqual(tx.signature.length, 1); + should.equal(txJson.from, testData.OPERATOR.accountId); + tx.type.should.equal(TransactionType.TokenCreation); + }); + + it('a tokenCreate transaction utilizing all available fields', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.fee({ fee: '1000000000' }); + txBuilder.name('Token'); + txBuilder.symbol('TKN'); + txBuilder.decimal('1'); + txBuilder.initialSupply('1000'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + txBuilder.adminKey(testData.OPERATOR.publicKey); + txBuilder.kycKey(testData.OPERATOR.publicKey); + txBuilder.freezeKey(testData.OPERATOR.publicKey); + txBuilder.wipeKey(testData.OPERATOR.publicKey); + txBuilder.supplyKey(testData.OPERATOR.publicKey); + txBuilder.freezeDefault(true); + txBuilder.expirationTime((Date.now() + 7776000).toString()); + txBuilder.autoRenewAccount(testData.OPERATOR.accountId); + txBuilder.autoRenewPeriod('7890000'); + txBuilder.source({ address: testData.OPERATOR.accountId }); + txBuilder.sign({ key: testData.OPERATOR.privateKey }); + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + txJson.fee.should.equal(1000000000); + should.deepEqual(tx.signature.length, 1); + should.equal(txJson.from, testData.OPERATOR.accountId); + tx.type.should.equal(TransactionType.TokenCreation); + }); + + it('offline signing tokenCreate transaction', async () => { + const txBuilder1 = factory.getTokenCreateBuilder(); + txBuilder1.startTime('1596110493.372646570'); + txBuilder1.fee({ fee: '1000000000' }); + txBuilder1.name('Token 1'); + txBuilder1.symbol('TOKEN1'); + txBuilder1.treasuryAccount(testData.OPERATOR.accountId); + txBuilder1.expirationTime('1611846757'); + txBuilder1.source({ address: testData.OPERATOR.accountId }); + const tx1 = await txBuilder1.build(); + const factory2 = register('thbar', TransactionBuilderFactory); + const txBuilder2 = factory2.from(tx1.toBroadcastFormat()); + txBuilder2.sign({ key: testData.OPERATOR.privateKey }); + const tx2 = await txBuilder2.build(); + const factory3 = register('thbar', TransactionBuilderFactory); + const txBuilder3 = factory3.from(tx2.toBroadcastFormat()); + txBuilder3.sign({ key: testData.ACCOUNT_1.prvKeyWithPrefix }); + const tx3 = await txBuilder3.build(); + should.deepEqual(tx2.signature.length, 1); + should.deepEqual(tx3.signature.length, 2); + should.deepEqual(tx2.toBroadcastFormat(), testData.TOKEN_CREATE_SIGNED_TRANSACTION); + should.deepEqual(tx3.toBroadcastFormat(), testData.TOKEN_CREATE_SIGNED_TWICE_TRANSACTION); + }); + + it('a tokenCreate transaction with external signature', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.fee({ fee: '1000000000' }); + txBuilder.name('Token 1'); + txBuilder.symbol('TOKEN1'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + txBuilder.expirationTime('1611846757'); + txBuilder.source({ address: testData.OPERATOR.accountId }); + txBuilder.signature( + '20bc01a6da677b99974b17204de5ff6f34f8e5904f58d6df1ceb39b473e7295dccf60fcedaf4f' + + 'dc3f6bef93edcfbe2a7ec33cc94c893906a063383c27b014f09', + new KeyPair({ pub: testData.ACCOUNT_1.pubKeyWithPrefix }), + ); + + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + should.equal(txJson.from, testData.OPERATOR.accountId); + }); + + it('a tokenCreate transaction with external signature included twice', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.fee({ fee: '1000000000' }); + txBuilder.name('Token 1'); + txBuilder.symbol('TOKEN1'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + txBuilder.expirationTime('1611846757'); + txBuilder.source({ address: testData.OPERATOR.accountId }); + txBuilder.signature( + '20bc01a6da677b99974b17204de5ff6f34f8e5904f58d6df1ceb39b473e7295dccf60fcedaf4f' + + 'dc3f6bef93edcfbe2a7ec33cc94c893906a063383c27b014f09', + new KeyPair({ pub: testData.ACCOUNT_1.pubKeyWithPrefix }), + ); + txBuilder.signature( + '20bc01a6da677b99974b17204de5ff6f34f8e5904f58d6df1ceb39b473e7295dccf60fcedaf4f' + + 'dc3f6bef93edcfbe2a7ec33cc94c893906a063383c27b014f09', + new KeyPair({ pub: testData.ACCOUNT_1.pubKeyWithPrefix }), + ); + + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + should.equal(txJson.from, testData.OPERATOR.accountId); + }); + }); + + describe('should fail to build', () => { + it('a transaction without fee', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.name('Token 1'); + txBuilder.symbol('TOKEN1'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + txBuilder.expirationTime('1611846757'); + txBuilder.source({ address: testData.OPERATOR.accountId }); + await txBuilder.build().should.be.rejectedWith('Invalid transaction: missing fee'); + }); + + it('a transaction with invalid source', async () => { + const factory = register('thbar', TransactionBuilderFactory); + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.fee({ fee: '1000000000' }); + txBuilder.name('Token 1'); + txBuilder.symbol('TOKEN1'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + txBuilder.expirationTime('1611846757'); + await txBuilder.build().should.be.rejectedWith('Invalid transaction: missing source'); + }); + }); + + describe('should validate', () => { + it('a transaction to build', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + should.throws(() => txBuilder.validateTransaction(), 'Invalid transaction: missing fee'); + txBuilder.fee({ fee: '10' }); + should.throws(() => txBuilder.validateTransaction(), 'Invalid transaction: missing source'); + txBuilder.source(testData.VALID_ADDRESS); + should.throws(() => txBuilder.validateTransaction(), 'Invalid transaction: missing token name'); + txBuilder.name('Token 1'); + should.throws(() => txBuilder.validateTransaction(), 'Invalid transaction: missing token symbol'); + txBuilder.symbol('TOKEN1'); + should.throws(() => txBuilder.validateTransaction(), 'Invalid transaction: missing treasury account id'); + txBuilder.treasuryAccount(testData.OPERATOR.accountId); + should.throws(() => txBuilder.validateTransaction(), 'Invalid transaction: missing expiration time'); + txBuilder.expirationTime('1611846757'); + should.doesNotThrow(() => txBuilder.validateTransaction()); + }); + + it('an address', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + txBuilder.validateAddress(testData.VALID_ADDRESS); + should.throws( + () => txBuilder.validateAddress(testData.INVALID_ADDRESS), + 'Invalid address ' + testData.INVALID_ADDRESS, + ); + }); + + it('value should be greater than zero', () => { + const txBuilder = factory.getTokenCreateBuilder(); + should.throws(() => txBuilder.fee({ fee: '-10' })); + should.doesNotThrow(() => txBuilder.fee({ fee: '10' })); + }); + + it('a private key', () => { + const txBuilder = factory.getTokenCreateBuilder(); + should.throws(() => txBuilder.validateKey({ key: 'abc' }), 'Invalid key'); + should.doesNotThrow(() => txBuilder.validateKey({ key: testData.ACCOUNT_1.prvKeyWithPrefix })); + }); + + it('a raw transaction', async () => { + const txBuilder = factory.getTokenCreateBuilder(); + should.doesNotThrow(() => txBuilder.validateRawTransaction(testData.TOKEN_CREATE_SIGNED_TRANSACTION)); + should.throws(() => txBuilder.validateRawTransaction('0x00001000')); + should.throws(() => txBuilder.validateRawTransaction('')); + should.throws(() => txBuilder.validateRawTransaction('pqrs')); + should.throws(() => txBuilder.validateRawTransaction(1234)); + }); + }); +});