@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
822 lines • 41.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
/**
* SPDX-License-Identifier: Apache-2.0
*/
import * as Base64 from 'js-base64';
import * as constants from './constants.js';
import { IGNORED_NODE_ACCOUNT_ID } from './constants.js';
import { AccountCreateTransaction, AccountId, AccountInfoQuery, AccountUpdateTransaction, Client, FileContentsQuery, FileId, Hbar, HbarUnit, KeyList, Logger, LogLevel, PrivateKey, Status, TransferTransaction, } from '@hashgraph/sdk';
import { MissingArgumentError, ResourceNotFoundError, SoloError } from './errors.js';
import { Templates } from './templates.js';
import { NetworkNodeServicesBuilder } from './network_node_services.js';
import path from 'path';
import { PodName } from './kube/resources/pod/pod_name.js';
import { isNumeric, sleep } from './helpers.js';
import { Duration } from './time/duration.js';
import { inject, injectable } from 'tsyringe-neo';
import { patchInject } from './dependency_injection/container_helper.js';
import { PodRef } from './kube/resources/pod/pod_ref.js';
import { SecretType } from './kube/resources/secret/secret_type.js';
import { InjectTokens } from './dependency_injection/inject_tokens.js';
import { SoloService } from './model/solo_service.js';
const REASON_FAILED_TO_GET_KEYS = 'failed to get keys for accountId';
const REASON_SKIPPED = 'skipped since it does not have a genesis key';
const REASON_FAILED_TO_UPDATE_ACCOUNT = 'failed to update account keys';
const REASON_FAILED_TO_CREATE_K8S_S_KEY = 'failed to create k8s scrt key';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
let AccountManager = class AccountManager {
logger;
k8Factory;
_portForwards;
_forcePortForward = false;
_nodeClient;
constructor(logger, k8Factory) {
this.logger = logger;
this.k8Factory = k8Factory;
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this._portForwards = [];
this._nodeClient = null;
}
/**
* Gets the account keys from the Kubernetes secret from which it is stored
* @param accountId - the account ID for which we want its keys
* @param namespace - the namespace storing the secret
* @param [context]
*/
async getAccountKeysFromSecret(accountId, namespace, context) {
try {
const k8 = this.k8Factory.getK8(context);
const secrets = await k8.secrets().list(namespace, [Templates.renderAccountKeySecretLabelSelector(accountId)]);
if (secrets.length > 0) {
const secret = secrets[0];
return {
accountId: secret.labels['solo.hedera.com/account-id'],
privateKey: Base64.decode(secret.data.privateKey),
publicKey: Base64.decode(secret.data.publicKey),
};
}
}
catch (e) {
if (!(e instanceof ResourceNotFoundError)) {
throw e;
}
}
// if it isn't in the secrets we can load genesis key
return {
accountId,
privateKey: constants.GENESIS_KEY,
publicKey: PrivateKey.fromStringED25519(constants.GENESIS_KEY).publicKey.toString(),
};
}
/**
* Gets the treasury account private key from Kubernetes secret if it exists, else
* returns the Genesis private key, then will return an AccountInfo object with the
* accountId, ed25519PrivateKey, publicKey
* @param namespace - the namespace that the secret is in
* @param [context]
*/
async getTreasuryAccountKeys(namespace, context) {
// check to see if the treasure account is in the secrets
return await this.getAccountKeysFromSecret(constants.TREASURY_ACCOUNT_ID, namespace, context);
}
/**
* batch up the accounts into sets to be processed
* @param [accountRange]
* @returns an array of arrays of numbers representing the accounts to update
*/
batchAccounts(accountRange = constants.SYSTEM_ACCOUNTS) {
const batchSize = constants.ACCOUNT_UPDATE_BATCH_SIZE;
const batchSets = [];
let currentBatch = [];
for (const [start, end] of accountRange) {
let batchCounter = start;
for (let i = start; i <= end; i++) {
currentBatch.push(i);
batchCounter++;
if (batchCounter % batchSize === 0) {
batchSets.push(currentBatch);
currentBatch = [];
batchCounter = 0;
}
}
}
if (currentBatch.length > 0) {
batchSets.push(currentBatch);
}
batchSets.push([constants.TREASURY_ACCOUNT]);
return batchSets;
}
/** stops and closes the port forwards and the _nodeClient */
async close() {
this._nodeClient?.close();
if (this._portForwards) {
for (const srv of this._portForwards) {
await this.k8Factory.default().pods().readByRef(null).stopPortForward(srv);
}
}
this._nodeClient = null;
this._portForwards = [];
this.logger.debug('node client and port forwards have been closed');
}
/**
* loads and initializes the Node Client
* @param namespace - the namespace of the network
* @param clusterRefs - the cluster references to use
* @param deployment - k8 deployment name
* @param context - k8 context name
* @param forcePortForward - whether to force the port forward
*/
async loadNodeClient(namespace, clusterRefs, deployment, forcePortForward, context) {
try {
this.logger.debug(`loading node client: [!this._nodeClient=${!this._nodeClient}, this._nodeClient.isClientShutDown=${this._nodeClient?.isClientShutDown}]`);
if (!this._nodeClient || this._nodeClient?.isClientShutDown) {
this.logger.debug(`refreshing node client: [!this._nodeClient=${!this._nodeClient}, this._nodeClient.isClientShutDown=${this._nodeClient?.isClientShutDown}]`);
await this.refreshNodeClient(namespace, undefined, clusterRefs, deployment, context, forcePortForward);
}
else {
try {
if (!constants.SKIP_NODE_PING) {
await this._nodeClient.ping(this._nodeClient.operatorAccountId);
}
}
catch {
this.logger.debug('node client ping failed, refreshing node client');
await this.refreshNodeClient(namespace, undefined, clusterRefs, deployment, context, forcePortForward);
}
}
return this._nodeClient;
}
catch (e) {
const message = `failed to load node client: ${e.message}`;
this.logger.error(message, e);
throw new SoloError(message, e);
}
}
/**
* loads and initializes the Node Client, throws a SoloError if anything fails
* @param namespace - the namespace of the network
* @param skipNodeAlias - the node alias to skip
* @param [clusterRefs]
* @param [deployment]
* @param [context]
* @param forcePortForward - whether to force the port forward
*/
async refreshNodeClient(namespace, skipNodeAlias, clusterRefs, deployment, context, forcePortForward) {
try {
await this.close();
if (forcePortForward !== undefined) {
this._forcePortForward = forcePortForward;
}
const treasuryAccountInfo = await this.getTreasuryAccountKeys(namespace, context);
const networkNodeServicesMap = await this.getNodeServiceMap(namespace, clusterRefs, deployment);
this._nodeClient = await this._getNodeClient(namespace, networkNodeServicesMap, treasuryAccountInfo.accountId, treasuryAccountInfo.privateKey, skipNodeAlias);
this.logger.debug('node client has been refreshed');
return this._nodeClient;
}
catch (e) {
const message = `failed to refresh node client: ${e.message}`;
this.logger.error(message, e);
throw new SoloError(message, e);
}
}
/**
* if the load balancer IP is not set, then we should use the local host port forward
* @param networkNodeServices
* @returns whether to use the local host port forward
*/
shouldUseLocalHostPortForward(networkNodeServices) {
return this._forcePortForward || !networkNodeServices.haProxyLoadBalancerIp;
}
/**
* Returns a node client that can be used to make calls against
* @param namespace - the namespace for which the node client resides
* @param networkNodeServicesMap - a map of the service objects that proxy the nodes
* @param operatorId - the account id of the operator of the transactions
* @param operatorKey - the private key of the operator of the transactions
* @param skipNodeAlias - the node alias to skip
* @returns a node client that can be used to call transactions
*/
async _getNodeClient(namespace, networkNodeServicesMap, operatorId, operatorKey, skipNodeAlias) {
let nodes = {};
const configureNodeAccessPromiseArray = [];
try {
let localPort = constants.LOCAL_NODE_START_PORT;
for (const networkNodeService of networkNodeServicesMap.values()) {
if (networkNodeService.accountId !== IGNORED_NODE_ACCOUNT_ID &&
networkNodeService.nodeAlias !== skipNodeAlias) {
configureNodeAccessPromiseArray.push(this.configureNodeAccess(networkNodeService, localPort, networkNodeServicesMap.size));
localPort++;
}
}
this.logger.debug(`configuring node access for ${configureNodeAccessPromiseArray.length} nodes`);
await Promise.allSettled(configureNodeAccessPromiseArray).then(results => {
for (const result of results) {
switch (result.status) {
case REJECTED:
throw new SoloError(`failed to configure node access: ${result.reason}`);
case FULFILLED:
nodes = { ...nodes, ...result.value };
break;
}
}
});
this.logger.debug(`configured node access for ${Object.keys(nodes).length} nodes`);
let formattedNetworkConnection = '';
Object.keys(nodes).forEach(key => (formattedNetworkConnection += `${key}:${nodes[key]}, `));
this.logger.info(`creating client from network configuration: [${formattedNetworkConnection}]`);
// scheduleNetworkUpdate is set to false, because the ports 50212/50211 are hardcoded in JS SDK that will not work
// when running locally or in a pipeline
this._nodeClient = Client.fromConfig({ network: nodes, scheduleNetworkUpdate: false });
this._nodeClient.setOperator(operatorId, operatorKey);
this._nodeClient.setLogger(new Logger(LogLevel.Trace, path.join(constants.SOLO_LOGS_DIR, 'hashgraph-sdk.log')));
this._nodeClient.setMaxAttempts(constants.NODE_CLIENT_MAX_ATTEMPTS);
this._nodeClient.setMinBackoff(constants.NODE_CLIENT_MIN_BACKOFF);
this._nodeClient.setMaxBackoff(constants.NODE_CLIENT_MAX_BACKOFF);
this._nodeClient.setRequestTimeout(constants.NODE_CLIENT_REQUEST_TIMEOUT);
// ping the node client to ensure it is working
if (!constants.SKIP_NODE_PING) {
await this._nodeClient.ping(AccountId.fromString(operatorId));
}
// start a background pinger to keep the node client alive, Hashgraph SDK JS has a 90-second keep alive time, and
// 5-second keep alive timeout
this.startIntervalPinger(operatorId);
return this._nodeClient;
}
catch (e) {
throw new SoloError(`failed to setup node client: ${e.message}`, e);
}
}
/**
* pings the node client at a set interval, can throw an exception if the ping fails
* @param operatorId
* @private
*/
startIntervalPinger(operatorId) {
const interval = constants.NODE_CLIENT_PING_INTERVAL;
const intervalId = setInterval(async () => {
if (this._nodeClient || !this._nodeClient?.isClientShutDown) {
this.logger.debug('node client has been closed, clearing node client ping interval');
clearInterval(intervalId);
}
else {
try {
this.logger.debug(`pinging node client at an interval of ${Duration.ofMillis(interval).seconds} seconds`);
if (!constants.SKIP_NODE_PING) {
await this._nodeClient.ping(AccountId.fromString(operatorId));
}
}
catch (e) {
const message = `failed to ping node client while running the interval pinger: ${e.message}`;
this.logger.error(message, e);
throw new SoloError(message, e);
}
}
}, interval);
}
async configureNodeAccess(networkNodeService, localPort, totalNodes) {
this.logger.debug(`configuring node access for node: ${networkNodeService.nodeAlias}`);
const obj = {};
const port = +networkNodeService.haProxyGrpcPort;
const accountId = AccountId.fromString(networkNodeService.accountId);
try {
// if the load balancer IP is set, then we should use that and avoid the local host port forward
if (!this.shouldUseLocalHostPortForward(networkNodeService)) {
const host = networkNodeService.haProxyLoadBalancerIp;
const targetPort = port;
this.logger.debug(`using load balancer IP: ${host}:${targetPort}`);
try {
obj[`${host}:${targetPort}`] = accountId;
await this.pingNetworkNode(obj, accountId);
this.logger.debug(`successfully pinged network node: ${host}:${targetPort}`);
return obj;
}
catch {
// if the connection fails, then we should use the local host port forward
}
}
// if the load balancer IP is not set or the test connection fails, then we should use the local host port forward
const host = '127.0.0.1';
const targetPort = localPort;
if (this._portForwards.length < totalNodes) {
this._portForwards.push(await this.k8Factory
.getK8(networkNodeService.context)
.pods()
.readByRef(PodRef.of(networkNodeService.namespace, networkNodeService.haProxyPodName))
.portForward(localPort, port));
}
this.logger.debug(`using local host port forward: ${host}:${targetPort}`);
obj[`${host}:${targetPort}`] = accountId;
await this.testNodeClientConnection(obj, accountId);
return obj;
}
catch (e) {
throw new SoloError(`failed to configure node access: ${e.message}`, e);
}
}
/**
* pings the network node to ensure that the connection is working
* @param obj - the object containing the network node service and the account id
* @param accountId - the account id to ping
* @throws {@link SoloError} if the ping fails
* @private
*/
async testNodeClientConnection(obj, accountId) {
const maxRetries = constants.NODE_CLIENT_PING_MAX_RETRIES;
const sleepInterval = constants.NODE_CLIENT_PING_RETRY_INTERVAL;
let currentRetry = 0;
let success = false;
try {
while (!success && currentRetry < maxRetries) {
try {
this.logger.debug(`attempting to ping network node: ${Object.keys(obj)[0]}, attempt: ${currentRetry}, of ${maxRetries}`);
await this.pingNetworkNode(obj, accountId);
success = true;
return;
}
catch (e) {
this.logger.error(`failed to ping network node: ${Object.keys(obj)[0]}, ${e.message}`);
currentRetry++;
await sleep(Duration.ofMillis(sleepInterval));
}
}
}
catch (e) {
const message = `failed testing node client connection for network node: ${Object.keys(obj)[0]}, after ${maxRetries} retries: ${e.message}`;
this.logger.error(message, e);
throw new SoloError(message, e);
}
if (currentRetry >= maxRetries) {
throw new SoloError(`failed to ping network node: ${Object.keys(obj)[0]}, after ${maxRetries} retries`);
}
return;
}
/**
* Gets a Map of the Hedera node services and the attributes needed, throws a SoloError if anything fails
* @param namespace - the namespace of the solo network deployment
* @param clusterRefs - the cluster references to use
* @param deployment - the deployment to use
* @returns a map of the network node services
*/
async getNodeServiceMap(namespace, clusterRefs, deployment) {
const labelSelector = 'solo.hedera.com/node-name';
const serviceBuilderMap = new Map();
try {
const services = [];
for (const [clusterRef, context] of Object.entries(clusterRefs)) {
const serviceList = await this.k8Factory.getK8(context).services().list(namespace, [labelSelector]);
services.push(...serviceList.map(service => SoloService.getFromK8Service(service, clusterRef, context, deployment)));
}
// retrieve the list of services and build custom objects for the attributes we need
for (const service of services) {
let nodeId;
const clusterRef = service.clusterRef;
let serviceBuilder = new NetworkNodeServicesBuilder(service.metadata.labels['solo.hedera.com/node-name']);
if (serviceBuilderMap.has(serviceBuilder.key())) {
serviceBuilder = serviceBuilderMap.get(serviceBuilder.key());
}
else {
serviceBuilder = new NetworkNodeServicesBuilder(service.metadata.labels['solo.hedera.com/node-name']);
serviceBuilder.withNamespace(namespace);
serviceBuilder.withClusterRef(clusterRef);
serviceBuilder.withContext(clusterRefs[clusterRef]);
serviceBuilder.withDeployment(deployment);
}
const serviceType = service.metadata.labels['solo.hedera.com/type'];
switch (serviceType) {
// solo.hedera.com/type: envoy-proxy-svc
case 'envoy-proxy-svc':
serviceBuilder
.withEnvoyProxyName(service.metadata.name)
.withEnvoyProxyClusterIp(service.spec.clusterIP)
.withEnvoyProxyLoadBalancerIp(service.status.loadBalancer.ingress ? service.status.loadBalancer.ingress[0].ip : undefined)
.withEnvoyProxyGrpcWebPort(service.spec.ports.filter(port => port.name === 'hedera-grpc-web')[0].port);
break;
// solo.hedera.com/type: haproxy-svc
case 'haproxy-svc':
serviceBuilder
.withHaProxyAppSelector(service.spec.selector.app)
.withHaProxyName(service.metadata.name)
.withHaProxyClusterIp(service.spec.clusterIP)
// @ts-ignore
.withHaProxyLoadBalancerIp(service.status.loadBalancer.ingress ? service.status.loadBalancer.ingress[0].ip : undefined)
.withHaProxyGrpcPort(service.spec.ports.filter(port => port.name === 'non-tls-grpc-client-port')[0].port)
.withHaProxyGrpcsPort(service.spec.ports.filter(port => port.name === 'tls-grpc-client-port')[0].port);
break;
// solo.hedera.com/type: network-node-svc
case 'network-node-svc':
if (service.metadata.labels['solo.hedera.com/node-id'] !== '' &&
isNumeric(service.metadata.labels['solo.hedera.com/node-id'])) {
nodeId = service.metadata.labels['solo.hedera.com/node-id'];
}
else {
nodeId = `${Templates.nodeIdFromNodeAlias(service.metadata.labels['solo.hedera.com/node-name'])}`;
this.logger.warn(`received an incorrect node id of ${service.metadata.labels['solo.hedera.com/node-id']} for ` +
`${service.metadata.labels['solo.hedera.com/node-name']}`);
}
serviceBuilder
.withAccountId(service.metadata.labels['solo.hedera.com/account-id'])
.withNodeServiceName(service.metadata.name)
.withNodeServiceClusterIp(service.spec.clusterIP)
.withNodeServiceLoadBalancerIp(service.status.loadBalancer.ingress ? service.status.loadBalancer.ingress[0].ip : undefined)
.withNodeServiceGossipPort(service.spec.ports.filter(port => port.name === 'gossip')[0].port)
.withNodeServiceGrpcPort(service.spec.ports.filter(port => port.name === 'grpc-non-tls')[0].port)
.withNodeServiceGrpcsPort(service.spec.ports.filter(port => port.name === 'grpc-tls')[0].port);
if (nodeId)
serviceBuilder.withNodeId(nodeId);
break;
}
serviceBuilderMap.set(serviceBuilder.key(), serviceBuilder);
}
// get the pod name for the service to use with portForward if needed
for (const serviceBuilder of serviceBuilderMap.values()) {
const podList = await this.k8Factory
.getK8(serviceBuilder.context)
.pods()
.list(namespace, [`app=${serviceBuilder.haProxyAppSelector}`]);
serviceBuilder.withHaProxyPodName(PodName.of(podList[0].metadata.name));
}
for (const [_, context] of Object.entries(clusterRefs)) {
// get the pod name of the network node
const pods = await this.k8Factory
.getK8(context)
.pods()
.list(namespace, ['solo.hedera.com/type=network-node']);
for (const pod of pods) {
// eslint-disable-next-line no-prototype-builtins
if (!pod.metadata?.labels?.hasOwnProperty('solo.hedera.com/node-name')) {
// TODO Review why this fixes issue
continue;
}
const podName = PodName.of(pod.metadata.name);
const nodeAlias = pod.metadata.labels['solo.hedera.com/node-name'];
const serviceBuilder = serviceBuilderMap.get(nodeAlias);
serviceBuilder.withNodePodName(podName);
}
}
const serviceMap = new Map();
for (const networkNodeServicesBuilder of serviceBuilderMap.values()) {
serviceMap.set(networkNodeServicesBuilder.key(), networkNodeServicesBuilder.build());
}
this.logger.debug('node services have been loaded');
return serviceMap;
}
catch (e) {
throw new SoloError(`failed to get node services: ${e.message}`, e);
}
}
/**
* updates a set of special accounts keys with a newly generated key and stores them in a Kubernetes secret
* @param namespace the namespace of the nodes network
* @param currentSet - the accounts to update
* @param updateSecrets - whether to delete the secret prior to creating a new secret
* @param resultTracker - an object to keep track of the results from the accounts that are being updated
* @returns the updated resultTracker object
*/
async updateSpecialAccountsKeys(namespace, currentSet, updateSecrets, resultTracker) {
const genesisKey = PrivateKey.fromStringED25519(constants.OPERATOR_KEY);
const realm = constants.HEDERA_NODE_ACCOUNT_ID_START.realm;
const shard = constants.HEDERA_NODE_ACCOUNT_ID_START.shard;
const accountUpdatePromiseArray = [];
for (const accountNum of currentSet) {
accountUpdatePromiseArray.push(this.updateAccountKeys(namespace, AccountId.fromString(`${realm}.${shard}.${accountNum}`), genesisKey, updateSecrets));
}
await Promise.allSettled(accountUpdatePromiseArray).then(results => {
for (const result of results) {
// @ts-ignore
switch (result.value.status) {
case REJECTED:
// @ts-ignore
if (result.value.reason === REASON_SKIPPED) {
resultTracker.skippedCount++;
}
else {
// @ts-ignore
this.logger.error(`REJECT: ${result.value.reason}: ${result.value.value}`);
resultTracker.rejectedCount++;
}
break;
case FULFILLED:
resultTracker.fulfilledCount++;
break;
}
}
});
this.logger.debug(`Current counts: [fulfilled: ${resultTracker.fulfilledCount}, ` +
`skipped: ${resultTracker.skippedCount}, ` +
`rejected: ${resultTracker.rejectedCount}]`);
return resultTracker;
}
/**
* update the account keys for a given account and store its new key in a Kubernetes secret
* @param namespace - the namespace of the nodes network
* @param accountId - the account that will get its keys updated
* @param genesisKey - the genesis key to compare against
* @param updateSecrets - whether to delete the secret prior to creating a new secret
* @returns the result of the call
*/
async updateAccountKeys(namespace, accountId, genesisKey, updateSecrets) {
let keys;
try {
keys = await this.getAccountKeys(accountId);
}
catch (e) {
this.logger.error(`failed to get keys for accountId ${accountId.toString()}, e: ${e.toString()}\n ${e.stack}`);
return {
status: REJECTED,
reason: REASON_FAILED_TO_GET_KEYS,
value: accountId.toString(),
};
}
if (!keys || !keys[0]) {
return {
status: REJECTED,
reason: REASON_FAILED_TO_GET_KEYS,
value: accountId.toString(),
};
}
if (constants.OPERATOR_PUBLIC_KEY !== keys[0].toString()) {
this.logger.debug(`account ${accountId.toString()} can be skipped since it does not have a genesis key`);
return {
status: REJECTED,
reason: REASON_SKIPPED,
value: accountId.toString(),
};
}
this.logger.debug(`updating account ${accountId.toString()} since it is using the genesis key`);
const newPrivateKey = PrivateKey.generateED25519();
const data = {
privateKey: Base64.encode(newPrivateKey.toString()),
publicKey: Base64.encode(newPrivateKey.publicKey.toString()),
};
try {
const createdOrUpdated = updateSecrets
? await this.k8Factory
.default()
.secrets()
.replace(namespace, Templates.renderAccountKeySecretName(accountId), SecretType.OPAQUE, data, Templates.renderAccountKeySecretLabelObject(accountId))
: await this.k8Factory
.default()
.secrets()
.create(namespace, Templates.renderAccountKeySecretName(accountId), SecretType.OPAQUE, data, Templates.renderAccountKeySecretLabelObject(accountId));
if (!createdOrUpdated) {
this.logger.error(`failed to create secret for accountId ${accountId.toString()}`);
return {
status: REJECTED,
reason: REASON_FAILED_TO_CREATE_K8S_S_KEY,
value: accountId.toString(),
};
}
}
catch (e) {
this.logger.error(`failed to create secret for accountId ${accountId.toString()}, e: ${e.toString()}`);
return {
status: REJECTED,
reason: REASON_FAILED_TO_CREATE_K8S_S_KEY,
value: accountId.toString(),
};
}
try {
if (!(await this.sendAccountKeyUpdate(accountId, newPrivateKey, genesisKey))) {
this.logger.error(`failed to update account keys for accountId ${accountId.toString()}`);
return {
status: REJECTED,
reason: REASON_FAILED_TO_UPDATE_ACCOUNT,
value: accountId.toString(),
};
}
}
catch (e) {
this.logger.error(`failed to update account keys for accountId ${accountId.toString()}, e: ${e.toString()}`);
return {
status: REJECTED,
reason: REASON_FAILED_TO_UPDATE_ACCOUNT,
value: accountId.toString(),
};
}
return {
status: FULFILLED,
value: accountId.toString(),
};
}
/**
* gets the account info from Hedera network
* @param accountId - the account
* @returns the private key of the account
*/
async accountInfoQuery(accountId) {
if (!this._nodeClient) {
throw new MissingArgumentError('node client is not initialized');
}
return await new AccountInfoQuery()
.setAccountId(accountId)
.setMaxAttempts(3)
.setMaxBackoff(1000)
.execute(this._nodeClient);
}
/**
* gets the account private and public key from the Kubernetes secret from which it is stored
* @param accountId - the account
* @returns the private key of the account
*/
async getAccountKeys(accountId) {
const accountInfo = await this.accountInfoQuery(accountId);
let keys = [];
if (accountInfo.key instanceof KeyList) {
keys = accountInfo.key.toArray();
}
else {
keys.push(accountInfo.key);
}
return keys;
}
/**
* send an account key update transaction to the network of nodes
* @param accountId - the account that will get its keys updated
* @param newPrivateKey - the new private key
* @param oldPrivateKey - the genesis key that is the current key
* @returns whether the update was successful
*/
async sendAccountKeyUpdate(accountId, newPrivateKey, oldPrivateKey) {
if (typeof newPrivateKey === 'string') {
newPrivateKey = PrivateKey.fromStringED25519(newPrivateKey);
}
if (typeof oldPrivateKey === 'string') {
oldPrivateKey = PrivateKey.fromStringED25519(oldPrivateKey);
}
// Create the transaction to update the key on the account
const transaction = new AccountUpdateTransaction()
.setAccountId(accountId)
.setKey(newPrivateKey.publicKey)
.freezeWith(this._nodeClient);
// Sign the transaction with the old key and new key
const signTx = await (await transaction.sign(oldPrivateKey)).sign(newPrivateKey);
// SIgn the transaction with the client operator private key and submit to a Hedera network
// @ts-ignore
const txResponse = await signTx.execute(this._nodeClient);
// Request the receipt of the transaction
// @ts-ignore
const receipt = await txResponse.getReceipt(this._nodeClient);
return receipt.status === Status.Success;
}
/**
* creates a new Hedera account
* @param namespace - the namespace to store the Kubernetes key secret into
* @param privateKey - the private key of type PrivateKey
* @param amount - the amount of HBAR to add to the account
* @param [setAlias] - whether to set the alias of the account to the public key, requires the ed25519PrivateKey supplied to be ECDSA
* @returns a custom object with the account information in it
*/
async createNewAccount(namespace, privateKey, amount, setAlias = false) {
const newAccountTransaction = new AccountCreateTransaction()
.setKey(privateKey)
.setInitialBalance(Hbar.from(amount, HbarUnit.Hbar));
if (setAlias) {
newAccountTransaction.setAlias(privateKey.publicKey.toEvmAddress());
}
// @ts-ignore
const newAccountResponse = await newAccountTransaction.execute(this._nodeClient);
// Get the new account ID
// @ts-ignore
const transactionReceipt = await newAccountResponse.getReceipt(this._nodeClient);
const accountInfo = {
accountId: transactionReceipt.accountId.toString(),
privateKey: privateKey.toString(),
publicKey: privateKey.publicKey.toString(),
balance: amount,
};
// add the account alias if setAlias is true
if (setAlias) {
const accountId = accountInfo.accountId;
const realm = transactionReceipt.accountId.realm;
const shard = transactionReceipt.accountId.shard;
const accountInfoQueryResult = await this.accountInfoQuery(accountId);
accountInfo.accountAlias = `${realm}.${shard}.${accountInfoQueryResult.contractAccountId}`;
}
try {
const accountSecretCreated = await this.k8Factory
.default()
.secrets()
.createOrReplace(namespace, Templates.renderAccountKeySecretName(accountInfo.accountId), SecretType.OPAQUE, {
privateKey: Base64.encode(accountInfo.privateKey),
publicKey: Base64.encode(accountInfo.publicKey),
}, Templates.renderAccountKeySecretLabelObject(accountInfo.accountId));
if (!accountSecretCreated) {
this.logger.error(`new account created [accountId=${accountInfo.accountId}, amount=${amount} HBAR, publicKey=${accountInfo.publicKey}, privateKey=${accountInfo.privateKey}] but failed to create secret in Kubernetes`);
throw new SoloError(`failed to create secret for accountId ${accountInfo.accountId.toString()}, keys were sent to log file`);
}
}
catch (e) {
if (e instanceof SoloError) {
throw e;
}
throw new SoloError(`failed to create secret for accountId ${accountInfo.accountId.toString()}, e: ${e.toString()}`, e);
}
return accountInfo;
}
/**
* transfer the specified amount of HBAR from one account to another
* @param fromAccountId - the account to pull the HBAR from
* @param toAccountId - the account to put the HBAR
* @param hbarAmount - the amount of HBAR
* @returns if the transaction was successfully posted
*/
async transferAmount(fromAccountId, toAccountId, hbarAmount) {
try {
const transaction = new TransferTransaction()
.addHbarTransfer(fromAccountId, new Hbar(-1 * hbarAmount))
.addHbarTransfer(toAccountId, new Hbar(hbarAmount))
.freezeWith(this._nodeClient);
// @ts-ignore
const txResponse = await transaction.execute(this._nodeClient);
// @ts-ignore
const receipt = await txResponse.getReceipt(this._nodeClient);
this.logger.debug(`The transfer from account ${fromAccountId} to account ${toAccountId} for amount ${hbarAmount} was ${receipt.status.toString()} `);
return receipt.status === Status.Success;
}
catch (e) {
const errorMessage = `transfer amount failed with an error: ${e.toString()}`;
this.logger.error(errorMessage);
throw new SoloError(errorMessage, e);
}
}
/**
* Fetch and prepare address book as a base64 string
*/
async prepareAddressBookBase64(namespace, clusterRefs, deployment, operatorId, operatorKey, forcePortForward, context) {
// fetch AddressBook
await this.loadNodeClient(namespace, clusterRefs, deployment, forcePortForward, context);
const client = this._nodeClient;
if (operatorId && operatorKey) {
client.setOperator(operatorId, operatorKey);
}
const query = new FileContentsQuery().setFileId(FileId.ADDRESS_BOOK);
return Buffer.from(await query.execute(client)).toString('base64');
}
async getFileContents(namespace, fileNum, clusterRefs, deployment, forcePortForward, context) {
await this.loadNodeClient(namespace, clusterRefs, deployment, forcePortForward, context);
const client = this._nodeClient;
const fileId = FileId.fromString(`0.0.${fileNum}`);
const queryFees = new FileContentsQuery().setFileId(fileId);
return Buffer.from(await queryFees.execute(client)).toString('hex');
}
/**
* Pings the network node with a grpc call to ensure it is working, throws a SoloError if the ping fails
* @param obj - the network node object where the key is the network endpoint and the value is the account id
* @param accountId - the account id to ping
* @throws {@link SoloError} if the ping fails
* @private
*/
async pingNetworkNode(obj, accountId) {
let nodeClient;
try {
nodeClient = Client.fromConfig({ network: obj, scheduleNetworkUpdate: false });
this.logger.debug(`pinging network node: ${Object.keys(obj)[0]}`);
try {
if (!constants.SKIP_NODE_PING) {
await nodeClient.ping(accountId);
}
this.logger.debug(`ping successful for network node: ${Object.keys(obj)[0]}`);
}
catch (e) {
const message = `failed to ping network node: ${Object.keys(obj)[0]} ${e.message}`;
this.logger.error(message, e);
throw new SoloError(message, e);
}
return;
}
catch (e) {
throw new SoloError(`failed to ping network node: ${Object.keys(obj)[0]} ${e.message}`, e);
}
finally {
if (nodeClient) {
try {
nodeClient.close();
}
catch {
// continue if nodeClient.close() fails
}
}
}
}
};
AccountManager = __decorate([
injectable(),
__param(0, inject(InjectTokens.SoloLogger)),
__param(1, inject(InjectTokens.K8Factory)),
__metadata("design:paramtypes", [Function, Object])
], AccountManager);
export { AccountManager };
//# sourceMappingURL=account_manager.js.map