/** * # @harmony-js/staking This package provides a collection of apis to create, sign/send staking transaction, and receive confirm/receipt. ## Installation ``` npm install @harmony-js/staking ``` ## Usage Create a Harmony instance connecting to testnet ```javascript * const { Harmony } = require('@harmony-js/core'); * const { * ChainID, * ChainType, * hexToNumber, * numberToHex, * fromWei, * Units, * Unit, * } = require('@harmony-js/utils'); * const hmy = new Harmony( * 'https://api.s0.b.hmny.io/', * { * chainType: ChainType.Harmony, * chainId: ChainID.HmyTestnet, * }, * ); ``` Below, examples show how to send delegate, undelegate, and collect rewards staking transactions. First, set the chainId, gasLimit, gasPrice for all subsequent staking transactions ```javascript * hmy.stakings.setTxParams({ * gasLimit: 25000, * gasPrice: numberToHex(new hmy.utils.Unit('1').asGwei().toWei()), * chainId: 2 * }); ``` Note: create and edit validator transactions are not fully supported in the sdk Create delegate staking transaction ```javascript * const delegate = hmy.stakings.delegate({ * delegatorAddress: 'one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7', * validatorAddress: 'one1vfqqagdzz352mtvdl69v0hw953hm993n6v26yl', * amount: numberToHex(new Unit(1000).asOne().toWei()) * }); * const delegateStakingTx = delegate.build(); ``` Sign and send the delegate transaction and receive confirmation ```javascript * // key corresponds to one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7, only has testnet balance * hmy.wallet.addByPrivateKey('45e497bd45a9049bcb649016594489ac67b9f052a6cdf5cb74ee2427a60bf25e'); * hmy.wallet.signStaking(delegateStakingTx).then(signedTxn => { * signedTxn.sendTransaction().then(([tx, hash]) => { * console.log(hash); * signedTxn.confirm(hash).then(response => { * console.log(response.receipt); * }); * }); * }); ``` Similarily, undelegate and collect reward transactions can be composed, signed and sent Create undelegate staking transaction ```javascript * const undelegate = hmy.stakings.undelegate({ * delegatorAddress: 'one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7', * validatorAddress: 'one1vfqqagdzz352mtvdl69v0hw953hm993n6v26yl', * amount: numberToHex(new Unit(1000).asOne().toWei()) * }); * const undelegateStakingTx = undelegate.build(); ``` Create collect rewards staking transaction ```javascript * const collectRewards = hmy.stakings.collectRewards({ * delegatorAddress: 'one103q7qe5t2505lypvltkqtddaef5tzfxwsse4z7' * }); * const collectRewardsStakingTx = collectRewards.build(); ``` Also, similar to normal transaction, signing and sending can be performed asynchronously. * @packageDocumentation * @module harmony-staking */ // tslint:disable: max-classes-per-file import { arrayify, BN, encode, hexlify, keccak256, sign, Signature, splitSignature, stripZeros, } from '@harmony-js/crypto'; import { Messenger, RPCMethod } from '@harmony-js/network'; import { defaultMessenger, TransactionBase, TxStatus } from '@harmony-js/transaction'; import { numberToHex, Unit } from '@harmony-js/utils'; import { TextEncoder } from 'text-encoding'; /** @hidden */ export class StakingSettings { public static PRECISION = 18; public static MAX_DECIMAL = 1000000000000000000; } /** @hidden */ export enum Directive { DirectiveCreateValidator, DirectiveEditValidator, DirectiveDelegate, DirectiveUndelegate, DirectiveCollectRewards, } export class StakingTransaction extends TransactionBase { private directive: Directive; private stakeMsg: CreateValidator | EditValidator | Delegate | Undelegate | CollectRewards; private nonce: number | string; private gasLimit: number | string; private gasPrice: number | string; private chainId: number; private rawTransaction: string; private unsignedRawTransaction: string; private signature: Signature; private from: string; constructor( directive: Directive, stakeMsg: CreateValidator | EditValidator | Delegate | Undelegate | CollectRewards, nonce: number | string, gasPrice: number | string, gasLimit: number | string, chainID: number, messenger: Messenger = defaultMessenger, txStatus = TxStatus.INTIALIZED, ) { super(messenger, txStatus); this.directive = directive; this.stakeMsg = stakeMsg; this.nonce = nonce; this.gasLimit = gasLimit; this.gasPrice = gasPrice; this.rawTransaction = '0x'; this.unsignedRawTransaction = '0x'; this.signature = { r: '', s: '', recoveryParam: 0, v: 0, }; this.chainId = chainID; this.from = '0x'; } encode(): [string, any[]] { const raw: Array> = []; // TODO: temporary hack for converting 0x00 to 0x if (!this.directive) { raw.push('0x'); } else { raw.push(hexlify(this.directive)); } raw.push(this.stakeMsg.encode()); if (!this.nonce) { raw.push('0x'); } else { raw.push(hexlify(this.nonce)); } raw.push(hexlify(this.gasPrice)); raw.push(hexlify(this.gasLimit)); if (this.chainId != null && this.chainId !== 0) { raw.push(hexlify(this.chainId)); raw.push('0x'); raw.push('0x'); } return [encode(raw), raw]; } rlpSign(prv: string): [Signature, string] { const [unsignedRawTransaction, raw] = this.encode(); this.setUnsigned(unsignedRawTransaction); const signature = sign(keccak256(unsignedRawTransaction), prv); const signed = this.getRLPSigned(raw, signature); return [signature, signed]; } getRLPSigned(raw: any[], signature: Signature): string { const sig = splitSignature(signature); let v = 27 + (sig.recoveryParam || 0); raw.pop(); raw.pop(); raw.pop(); v += this.chainId * 2 + 8; raw.push(hexlify(v)); raw.push(stripZeros(arrayify(sig.r) || [])); raw.push(stripZeros(arrayify(sig.s) || [])); return encode(raw); } public async sendTransaction(): Promise<[StakingTransaction, string]> { if (this.rawTransaction === 'tx' || this.rawTransaction === undefined) { throw new Error('Transaction not signed'); } if (!this.messenger) { throw new Error('Messenger not found'); } const res = await this.messenger.send( RPCMethod.SendRawStakingTransaction, this.rawTransaction, this.messenger.chainType, this.messenger.currentShard, // 0, // Staking tx always sent to shard 0 ); if (res.isResult()) { this.id = res.result; this.emitTransactionHash(this.id); this.setTxStatus(TxStatus.PENDING); return [this, res.result]; } else if (res.isError()) { this.emitConfirm(`transaction failed:${res.error.message}`); this.setTxStatus(TxStatus.REJECTED); return [this, `transaction failed:${res.error.message}`]; } else { this.emitError('transaction failed'); throw new Error('transaction failed'); } } setUnsigned(unSigned: string) { this.unsignedRawTransaction = unSigned; } setRawTransaction(rawTransaction: string) { this.rawTransaction = rawTransaction; } setSignature(signature: Signature) { this.signature = { r: signature.r, s: signature.s, v: signature.v, recoveryParam: signature.recoveryParam, }; } setNonce(nonce: number) { this.nonce = nonce; } setFromAddress(address: string) { this.from = address; } getUnsignedRawTransaction() { return this.unsignedRawTransaction; } getRawTransaction() { return this.rawTransaction; } getSignature() { return this.signature; } getFromAddress() { return this.from; } async confirm( txHash: string, maxAttempts: number = 20, interval: number = 1000, shardID: number | string = this.messenger.currentShard, toShardID: number | string = 0, ) { const txConfirmed = await this.txConfirm(txHash, maxAttempts, interval, shardID); if (shardID === toShardID) { return txConfirmed; } if (txConfirmed.isConfirmed()) { const cxConfirmed = await this.cxConfirm(txHash, maxAttempts, interval, toShardID); return cxConfirmed; } else { return txConfirmed; } } } /** @hidden */ export class Description { name: string; identity: string; website: string; securityContact: string; details: string; constructor( name: string, identity: string, website: string, securityContact: string, details: string, ) { this.name = name; this.identity = identity; this.website = website; this.securityContact = securityContact; this.details = details; } encode(): any[] { const raw: Array = []; const enc = new TextEncoder(); raw.push(enc.encode(this.name)); raw.push(enc.encode(this.identity)); raw.push(enc.encode(this.website)); raw.push(enc.encode(this.securityContact)); raw.push(enc.encode(this.details)); return raw; } } /** @hidden */ export class Decimal { value: BN; constructor(value: string) { if (value.length === 0) { throw new Error(`decimal string is empty`); } let value1 = value; if (value[0] === '-') { throw new Error(`decimal fraction should be be between [0, 1]`); } if (value[0] === '+') { value1 = value.substr(1); } if (value1.length === 0) { throw new Error(`decimal string is empty`); } const spaced = value1.split(' '); if (spaced.length > 1) { throw new Error(`bad decimal string`); } const splitted = value1.split('.'); let len = 0; let combinedStr = splitted[0]; if (splitted.length === 2) { len = splitted[1].length; if (len === 0 || combinedStr.length === 0) { throw new Error(`bad decimal length`); } if (splitted[1][0] === '-') { throw new Error(`bad decimal string`); } combinedStr += splitted[1]; } else if (splitted.length > 2) { throw new Error(`too many periods to be a decimal string`); } if (len > StakingSettings.PRECISION) { throw new Error( `too much precision: precision should be less than ${StakingSettings.PRECISION}`, ); } const zerosToAdd = StakingSettings.PRECISION - len; combinedStr += '0'.repeat(zerosToAdd); combinedStr = combinedStr.replace(/^0+/, ''); const val = new Unit(combinedStr).asWei().toWei(); if (val.gt(new Unit(StakingSettings.MAX_DECIMAL.toString()).asWei().toWei())) { throw new Error(`too large decimal fraction`); } this.value = val; } encode(): any[] { const raw: Array = []; raw.push(numberToHex(this.value)); return raw; } } /** @hidden */ export class CommissionRate { rate: Decimal; maxRate: Decimal; maxChangeRate: Decimal; constructor(rate: Decimal, maxRate: Decimal, maxChangeRate: Decimal) { this.rate = rate; this.maxRate = maxRate; this.maxChangeRate = maxChangeRate; } encode(): any[] { const raw: Array> = []; raw.push(this.rate.encode()); raw.push(this.maxRate.encode()); raw.push(this.maxChangeRate.encode()); return raw; } } export class CreateValidator { validatorAddress: string; description: Description; commissionRates: CommissionRate; minSelfDelegation: number; maxTotalDelegation: number; slotPubKeys: string[]; amount: number; constructor( validatorAddress: string, description: Description, commissionRates: CommissionRate, minSelfDelegation: number, maxTotalDelegation: number, slotPubKeys: string[], amount: number, ) { this.validatorAddress = validatorAddress; this.description = description; this.commissionRates = commissionRates; this.minSelfDelegation = minSelfDelegation; this.maxTotalDelegation = maxTotalDelegation; this.slotPubKeys = slotPubKeys; this.amount = amount; } encode(): any[] { const raw: Array> = []; raw.push(hexlify(TransactionBase.normalizeAddress(this.validatorAddress))); raw.push(this.description.encode()); raw.push(this.commissionRates.encode()); raw.push(hexlify(this.minSelfDelegation)); raw.push(hexlify(this.maxTotalDelegation)); raw.push(this.encodeArr()); raw.push(hexlify(this.amount)); return raw; } encodeArr(): any[] { const raw: Array> = []; this.slotPubKeys.forEach((pubKey) => { raw.push(pubKey); }); return raw; } } export class EditValidator { validatorAddress: string; description: Description; commissionRate: Decimal; minSelfDelegation: number; maxTotalDelegation: number; slotKeyToRemove: string; slotKeyToAdd: string; constructor( validatorAddress: string, description: Description, commissionRate: Decimal, minSelfDelegation: number, maxTotalDelegation: number, slotKeyToRemove: string, slotKeyToAdd: string, ) { this.validatorAddress = validatorAddress; this.description = description; this.commissionRate = commissionRate; this.minSelfDelegation = minSelfDelegation; this.maxTotalDelegation = maxTotalDelegation; this.slotKeyToRemove = slotKeyToRemove; this.slotKeyToAdd = slotKeyToAdd; } encode(): any[] { const raw: Array> = []; raw.push(hexlify(TransactionBase.normalizeAddress(this.validatorAddress))); raw.push(this.description.encode()); raw.push(this.commissionRate.encode()); raw.push(hexlify(this.minSelfDelegation)); raw.push(hexlify(this.maxTotalDelegation)); raw.push(this.slotKeyToRemove); raw.push(this.slotKeyToAdd); return raw; } } export class Delegate { delegatorAddress: string; validatorAddress: string; amount: number; constructor(delegatorAddress: string, validatorAddress: string, amount: number) { this.delegatorAddress = delegatorAddress; this.validatorAddress = validatorAddress; this.amount = amount; } encode(): any[] { const raw: Array = []; raw.push(hexlify(TransactionBase.normalizeAddress(this.delegatorAddress))); raw.push(hexlify(TransactionBase.normalizeAddress(this.validatorAddress))); raw.push(hexlify(this.amount)); return raw; } } export class Undelegate { delegatorAddress: string; validatorAddress: string; amount: number; constructor(delegatorAddress: string, validatorAddress: string, amount: number) { this.delegatorAddress = delegatorAddress; this.validatorAddress = validatorAddress; this.amount = amount; } encode(): any[] { const raw: Array = []; raw.push(hexlify(TransactionBase.normalizeAddress(this.delegatorAddress))); raw.push(hexlify(TransactionBase.normalizeAddress(this.validatorAddress))); raw.push(hexlify(this.amount)); return raw; } } export class CollectRewards { delegatorAddress: string; constructor(delegatorAddress: string) { this.delegatorAddress = delegatorAddress; } encode(): any[] { const raw: Array = []; raw.push(hexlify(TransactionBase.normalizeAddress(this.delegatorAddress))); return raw; } }