@hethers/abstract-signer
Version:
An Abstract Class for describing an Hedera Hashgraph Signer for hethers.
324 lines • 14 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexlify } from "@ethersproject/bytes";
import { defineReadOnly, resolveProperties, shallowCopy } from "@ethersproject/properties";
import { Logger } from "@hethers/logger";
import { version } from "./_version";
import { asAccountString, getAddressFromAccount, getChecksumAddress } from "@hethers/address";
import { AccountId, ContractCallQuery, ContractId, PublicKey as HederaPubKey, TransactionId, PrivateKey, Hbar } from "@hashgraph/sdk";
const logger = new Logger(version);
const allowedTransactionKeys = [
"accessList", "chainId", "customData", "data", "from", "gasLimit", "maxFeePerGas", "maxPriorityFeePerGas", "to", "type", "value",
"nodeId"
];
;
;
function checkError(method, error, txRequest) {
switch (error.status._code) {
// insufficient gas
case 30:
return logger.throwError("insufficient funds for gas cost", Logger.errors.CALL_EXCEPTION, { tx: txRequest });
// insufficient payer balance
case 10:
return logger.throwError("insufficient funds in payer account", Logger.errors.INSUFFICIENT_FUNDS, { tx: txRequest });
// insufficient tx fee
case 9:
return logger.throwError("transaction fee too low", Logger.errors.INSUFFICIENT_FUNDS, { tx: txRequest });
// invalid signature
case 7:
return logger.throwError("invalid transaction signature", Logger.errors.UNKNOWN_ERROR, { tx: txRequest });
// invalid contract id
case 16:
return logger.throwError("invalid contract address", Logger.errors.INVALID_ARGUMENT, { tx: txRequest });
// contract revert
case 33:
return logger.throwError("contract execution reverted", Logger.errors.CALL_EXCEPTION, { tx: txRequest });
}
throw error;
}
export class Signer {
///////////////////
// Sub-classes MUST call super
constructor() {
logger.checkAbstract(new.target, Signer);
defineReadOnly(this, "_isSigner", true);
}
getGasPrice() {
return __awaiter(this, void 0, void 0, function* () {
this._checkProvider("getGasPrice");
return yield this.provider.getGasPrice();
});
}
///////////////////
// Sub-classes MAY override these
getBalance() {
return __awaiter(this, void 0, void 0, function* () {
this._checkProvider("getBalance");
return yield this.provider.getBalance(this.getAddress());
});
}
// Populates "from" if unspecified, and estimates the gas for the transaction
estimateGas(transaction) {
return __awaiter(this, void 0, void 0, function* () {
this._checkProvider("estimateGas");
const tx = yield resolveProperties(this.checkTransaction(transaction));
// cost-answer query on hedera
return yield this.provider.estimateGas(tx);
});
}
// super classes should override this for now
call(txRequest) {
return __awaiter(this, void 0, void 0, function* () {
this._checkProvider("call");
const tx = yield resolveProperties(this.checkTransaction(txRequest));
const to = asAccountString(tx.to);
const from = asAccountString(yield this.getAddress());
const nodeID = AccountId.fromString(asAccountString(tx.nodeId));
const paymentTxId = TransactionId.generate(from);
const hederaTx = new ContractCallQuery()
.setFunctionParameters(arrayify(tx.data))
.setNodeAccountIds([nodeID])
.setGas(BigNumber.from(tx.gasLimit).toNumber())
.setPaymentTransactionId(paymentTxId);
if (tx.customData.usingContractAlias) {
hederaTx.setContractId(ContractId.fromEvmAddress(0, 0, tx.to.toString()));
}
else {
hederaTx.setContractId(to);
}
const walletKey = this.isED25519Type
? PrivateKey.fromStringED25519(this._signingKey().privateKey)
: PrivateKey.fromStringECDSA(this._signingKey().privateKey);
const sdkClient = this.provider.getHederaClient();
sdkClient.setOperator(AccountId.fromString(from), walletKey);
const MULTIPLIER = 1.1;
const cost = yield hederaTx.getCost(sdkClient);
const costWithBuffer = Hbar.fromTinybars(cost._valueInTinybar.multipliedBy(MULTIPLIER).toFixed(0));
hederaTx.setQueryPayment(costWithBuffer);
try {
const response = yield hederaTx.execute(sdkClient);
return hexlify(response.bytes);
}
catch (error) {
return checkError('call', error, tx);
}
});
}
/**
* Composes a transaction which is signed and sent to the provider's network.
* @param transaction - the actual tx
*/
sendTransaction(transaction) {
return __awaiter(this, void 0, void 0, function* () {
const tx = yield resolveProperties(transaction);
if (tx.to) {
const signed = yield this.signTransaction(tx);
return yield this.provider.sendTransaction(signed);
}
else {
const contractByteCode = tx.data;
let chunks = splitInChunks(Buffer.from(contractByteCode.toString()).toString(), 4096);
const fileCreate = {
customData: {
fileChunk: chunks[0],
fileKey: HederaPubKey.fromString(this._signingKey().compressedPublicKey)
}
};
const signedFileCreate = yield this.signTransaction(fileCreate);
const resp = yield this.provider.sendTransaction(signedFileCreate);
for (let chunk of chunks.slice(1)) {
const fileAppend = {
customData: {
fileId: resp.customData.fileId.toString(),
fileChunk: chunk
}
};
const signedFileAppend = yield this.signTransaction(fileAppend);
yield this.provider.sendTransaction(signedFileAppend);
}
const contractCreate = {
gasLimit: tx.gasLimit,
value: tx.value || 0,
customData: {
bytecodeFileId: resp.customData.fileId.toString(),
// @ts-ignore
maxAutomaticTokenAssociations: transaction.customData.maxAutomaticTokenAssociations
}
};
const signedContractCreate = yield this.signTransaction(contractCreate);
return yield this.provider.sendTransaction(signedContractCreate);
}
});
}
getChainId() {
return __awaiter(this, void 0, void 0, function* () {
this._checkProvider("getChainId");
const network = yield this.provider.getNetwork();
return network.chainId;
});
}
/**
* Checks if the given transaction is usable.
* Properties - `from`, `nodeId`, `gasLimit`
* @param transaction - the tx to be checked
*/
checkTransaction(transaction) {
for (const key in transaction) {
if (allowedTransactionKeys.indexOf(key) === -1) {
logger.throwArgumentError("invalid transaction key: " + key, "transaction", transaction);
}
}
const tx = shallowCopy(transaction);
if (!tx.nodeId) {
this._checkProvider();
// provider present, we can go on
const submittableNodeIDs = this.provider.getHederaNetworkConfig();
if (submittableNodeIDs.length > 0) {
tx.nodeId = submittableNodeIDs[randomNumBetween(0, submittableNodeIDs.length - 1)].toString();
}
else {
logger.throwError("Unable to find submittable node ID. The signer's provider is not connected to any usable network");
}
}
if (tx.from == null) {
tx.from = this.getAddress();
}
else {
// Make sure any provided address matches this signer
tx.from = Promise.all([
Promise.resolve(tx.from),
this.getAddress()
]).then((result) => {
if (result[0].toString().toLowerCase() !== result[1].toLowerCase()) {
logger.throwArgumentError("from address mismatch", "transaction", transaction);
}
return result[0];
});
}
tx.gasLimit = transaction.gasLimit;
return tx;
}
/**
* Populates any missing properties in a transaction request.
* Properties affected - `to`, `chainId`
* @param transaction
*/
populateTransaction(transaction) {
return __awaiter(this, void 0, void 0, function* () {
const tx = yield resolveProperties(this.checkTransaction(transaction));
if (tx.to != null) {
tx.to = Promise.resolve(tx.to).then((to) => __awaiter(this, void 0, void 0, function* () {
if (to == null) {
return null;
}
return getChecksumAddress(getAddressFromAccount(to));
}));
// Prevent this error from causing an UnhandledPromiseException
tx.to.catch((error) => { });
}
let isCryptoTransfer = false;
if (tx.to && tx.value) {
if (!tx.data && !tx.gasLimit) {
isCryptoTransfer = true;
}
else if (tx.data && !tx.gasLimit) {
logger.throwError("gasLimit is not provided. Cannot execute a Contract Call");
}
else if (!tx.data && tx.gasLimit) {
this._checkProvider();
if ((yield this.provider.getCode(tx.to)) === '0x') {
logger.throwError("receiver is an account. Cannot execute a Contract Call");
}
}
}
tx.customData = Object.assign(Object.assign({}, tx.customData), { isCryptoTransfer });
const customData = yield tx.customData;
// FileCreate and FileAppend always carry a customData.fileChunk object
const isFileCreateOrAppend = customData && customData.fileChunk;
// CreateAccount always has a publicKey
const isCreateAccount = customData && customData.publicKey;
if (!isFileCreateOrAppend && !isCreateAccount && !tx.customData.isCryptoTransfer && tx.gasLimit == null) {
return logger.throwError("cannot estimate gas; transaction requires manual gas limit", Logger.errors.UNPREDICTABLE_GAS_LIMIT, { tx: tx });
}
return yield resolveProperties(tx);
});
}
///////////////////
// Sub-classes SHOULD leave these alone
_checkProvider(operation) {
if (!this.provider) {
logger.throwError("missing provider", Logger.errors.UNSUPPORTED_OPERATION, {
operation: (operation || "_checkProvider")
});
}
}
static isSigner(value) {
return !!(value && value._isSigner);
}
}
export class VoidSigner extends Signer {
constructor(address, provider) {
logger.checkNew(new.target, VoidSigner);
super();
defineReadOnly(this, "address", address);
defineReadOnly(this, "provider", provider || null);
}
getAddress() {
return Promise.resolve(this.address);
}
_fail(message, operation) {
return Promise.resolve().then(() => {
logger.throwError(message, Logger.errors.UNSUPPORTED_OPERATION, { operation: operation });
});
}
signMessage(message) {
return this._fail("VoidSigner cannot sign messages", "signMessage");
}
signTransaction(transaction) {
return this._fail("VoidSigner cannot sign transactions", "signTransaction");
}
createAccount(pubKey, initialBalance) {
return this._fail("VoidSigner cannot create accounts", "createAccount");
}
_signTypedData(domain, types, value) {
return this._fail("VoidSigner cannot sign typed data", "signTypedData");
}
connect(provider) {
return new VoidSigner(this.address, provider);
}
}
/**
* Generates a random integer in the given range
* @param min - range start
* @param max - range end
*/
function randomNumBetween(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Splits data (utf8) into chunks with the given size
* @param data
* @param chunkSize
*/
function splitInChunks(data, chunkSize) {
const chunks = [];
let num = 0;
while (num <= data.length) {
const slice = data.slice(num, chunkSize + num);
num += chunkSize;
chunks.push(slice);
}
return chunks;
}
//# sourceMappingURL=index.js.map