UNPKG

@hiero-ledger/sdk

Version:
552 lines (483 loc) 16.2 kB
// SPDX-License-Identifier: Apache-2.0 import FileCreateTransaction from "../file/FileCreateTransaction.js"; import FileAppendTransaction from "../file/FileAppendTransaction.js"; import FileDeleteTransaction from "../file/FileDeleteTransaction.js"; import ContractCreateTransaction from "./ContractCreateTransaction.js"; import * as utf8 from "../encoding/utf8.js"; import * as hex from "../encoding/hex.js"; import PublicKey from "../PublicKey.js"; /** * @typedef {import("../account/AccountId.js").default} AccountId * @typedef {import("../file/FileId.js").default} FileId * @typedef {import("../Key.js").default} Key * @typedef {import("./ContractFunctionParameters.js").default} ContractFunctionParameters * @typedef {import("../Hbar.js").default} Hbar * @typedef {import("../Duration.js").default} Duration * @typedef {import("../channel/Channel.js").default} Channel * @typedef {import("../channel/MirrorChannel.js").default} MirrorChannel * @typedef {import("../transaction/TransactionId.js").default} TransactionId * @typedef {import("../transaction/TransactionResponse.js").default} TransactionResponse * @typedef {import("../transaction/TransactionReceipt.js").default} TransactionReceipt * @typedef {import("../client/Client.js").ClientOperator} ClientOperator * @typedef {import("../Signer.js").Signer} Signer * @typedef {import("../PrivateKey.js").default} PrivateKey * @typedef {import("../transaction/Transaction.js").default} Transaction */ /** * @typedef {import("bignumber.js").BigNumber} BigNumber * @typedef {import("long")} Long */ /** * A convenience flow that handles the creation of a smart contract on the Hedera network. * This flow abstracts away the complexity of the contract creation process by: * * 1. Creating a file to store the contract bytecode * 2. Uploading the contract bytecode in chunks if necessary * 3. Creating the contract instance using the uploaded bytecode * 4. Cleaning up by deleting the bytecode file (if operator key is available) * * This flow is particularly useful when deploying large contracts that exceed the 2048 byte * limit of a single transaction. */ export default class ContractCreateFlow { constructor() { /** @type {Uint8Array | null} */ this._bytecode = null; this._contractCreate = new ContractCreateTransaction(); /** * Read `Transaction._signerPublicKeys` * * @internal * @type {Set<string>} */ this._signerPublicKeys = new Set(); /** * Read `Transaction._publicKeys` * * @private * @type {PublicKey[]} */ this._publicKeys = []; /** * Read `Transaction._transactionSigners` * * @private * @type {((message: Uint8Array) => Promise<Uint8Array>)[]} */ this._transactionSigners = []; this._maxChunks = null; } /** * @returns {number | null} */ get maxChunks() { return this._maxChunks; } /** * @param {number} maxChunks * @returns {this} */ setMaxChunks(maxChunks) { this._maxChunks = maxChunks; return this; } /** * @returns {?Uint8Array} */ get bytecode() { return this._bytecode; } /** * @param {string | Uint8Array} bytecode * @returns {this} */ setBytecode(bytecode) { this._bytecode = bytecode instanceof Uint8Array ? bytecode : utf8.encode(bytecode); return this; } /** * @returns {?Key} */ get adminKey() { return this._contractCreate.adminKey; } /** * @param {Key} adminKey * @returns {this} */ setAdminKey(adminKey) { this._contractCreate.setAdminKey(adminKey); return this; } /** * @returns {?Long} */ get gas() { return this._contractCreate.gas; } /** * @param {number | Long} gas * @returns {this} */ setGas(gas) { this._contractCreate.setGas(gas); return this; } /** * @returns {?Hbar} */ get initialBalance() { return this._contractCreate.initialBalance; } /** * Set the initial amount to transfer into this contract. * * @param {number | string | Long | BigNumber | Hbar} initialBalance * @returns {this} */ setInitialBalance(initialBalance) { this._contractCreate.setInitialBalance(initialBalance); return this; } /** * @deprecated * @returns {?AccountId} */ get proxyAccountId() { // eslint-disable-next-line deprecation/deprecation return this._contractCreate.proxyAccountId; } /** * @deprecated * @param {AccountId | string} proxyAccountId * @returns {this} */ setProxyAccountId(proxyAccountId) { // eslint-disable-next-line deprecation/deprecation this._contractCreate.setProxyAccountId(proxyAccountId); return this; } /** * @returns {Duration} */ get autoRenewPeriod() { return this._contractCreate.autoRenewPeriod; } /** * @param {Duration | Long | number} autoRenewPeriod * @returns {this} */ setAutoRenewPeriod(autoRenewPeriod) { this._contractCreate.setAutoRenewPeriod(autoRenewPeriod); return this; } /** * @returns {?Uint8Array} */ get constructorParameters() { return this._contractCreate.constructorParameters; } /** * @param {Uint8Array | ContractFunctionParameters} constructorParameters * @returns {this} */ setConstructorParameters(constructorParameters) { this._contractCreate.setConstructorParameters(constructorParameters); return this; } /** * @returns {?string} */ get contractMemo() { return this._contractCreate.contractMemo; } /** * @param {string} contractMemo * @returns {this} */ setContractMemo(contractMemo) { this._contractCreate.setContractMemo(contractMemo); return this; } /** * @returns {?number} */ get maxAutomaticTokenAssociation() { return this._contractCreate.maxAutomaticTokenAssociations; } /** * @param {number} maxAutomaticTokenAssociation * @returns {this} */ setMaxAutomaticTokenAssociations(maxAutomaticTokenAssociation) { this._contractCreate.setMaxAutomaticTokenAssociations( maxAutomaticTokenAssociation, ); return this; } /** * @returns {?AccountId} */ get stakedAccountId() { return this._contractCreate.stakedAccountId; } /** * @param {AccountId | string} stakedAccountId * @returns {this} */ setStakedAccountId(stakedAccountId) { this._contractCreate.setStakedAccountId(stakedAccountId); return this; } /** * @returns {?Long} */ get stakedNodeId() { return this._contractCreate.stakedNodeId; } /** * @param {Long | number} stakedNodeId * @returns {this} */ setStakedNodeId(stakedNodeId) { this._contractCreate.setStakedNodeId(stakedNodeId); return this; } /** * @returns {boolean} */ get declineStakingRewards() { return this._contractCreate.declineStakingRewards; } /** * @param {boolean} declineStakingReward * @returns {this} */ setDeclineStakingReward(declineStakingReward) { this._contractCreate.setDeclineStakingReward(declineStakingReward); return this; } /** * @returns {?AccountId} */ get autoRenewAccountId() { return this._contractCreate.autoRenewAccountId; } /** * @param {string | AccountId} autoRenewAccountId * @returns {this} */ setAutoRenewAccountId(autoRenewAccountId) { this._contractCreate.setAutoRenewAccountId(autoRenewAccountId); return this; } /** * Sign the transaction with the private key * **NOTE**: This is a thin wrapper around `.signWith()` * * @param {PrivateKey} privateKey * @returns {this} */ sign(privateKey) { return this.signWith(privateKey.publicKey, (message) => Promise.resolve(privateKey.sign(message)), ); } /** * Sign the transaction with the public key and signer function * * If sign on demand is enabled no signing will be done immediately, instead * the private key signing function and public key are saved to be used when * a user calls an exit condition method (not sure what a better name for this is) * such as `toBytes[Async]()`, `getTransactionHash[PerNode]()` or `execute()`. * * @param {PublicKey} publicKey * @param {(message: Uint8Array) => Promise<Uint8Array>} transactionSigner * @returns {this} */ signWith(publicKey, transactionSigner) { const publicKeyData = publicKey.toBytesRaw(); const publicKeyHex = hex.encode(publicKeyData); if (this._signerPublicKeys.has(publicKeyHex)) { // this public key has already signed this transaction return this; } this._publicKeys.push(publicKey); this._transactionSigners.push(transactionSigner); return this; } /** * @template {Channel} ChannelT * @template {MirrorChannel} MirrorChannelT * @param {import("../client/Client.js").default<ChannelT, MirrorChannelT>} client * @param {number=} requestTimeout * @returns {Promise<TransactionResponse>} */ async execute(client, requestTimeout) { if (this._bytecode == null) { throw new Error("cannot create contract with no bytecode"); } const key = client.operatorPublicKey; const fileCreateTransaction = new FileCreateTransaction() .setKeys(key != null ? [key] : []) .setContents( this._bytecode.subarray( 0, Math.min(this._bytecode.length, 2048), ), ) .freezeWith(client); await addSignersToTransaction( fileCreateTransaction, this._publicKeys, this._transactionSigners, ); let response = await fileCreateTransaction.execute( client, requestTimeout, ); const receipt = await response.getReceipt(client); const fileId = /** @type {FileId} */ (receipt.fileId); if (this._bytecode.length > 2048) { const fileAppendTransaction = new FileAppendTransaction() .setFileId(fileId) .setContents(this._bytecode.subarray(2048)) .freezeWith(client); await addSignersToTransaction( fileAppendTransaction, this._publicKeys, this._transactionSigners, ); await fileAppendTransaction.execute(client, requestTimeout); } this._contractCreate.setBytecodeFileId(fileId).freezeWith(client); await addSignersToTransaction( this._contractCreate, this._publicKeys, this._transactionSigners, ); response = await this._contractCreate.execute(client, requestTimeout); await response.getReceipt(client); if (key != null) { const fileDeleteTransaction = new FileDeleteTransaction() .setFileId(fileId) .freezeWith(client); await addSignersToTransaction( fileDeleteTransaction, this._publicKeys, this._transactionSigners, ); await ( await fileDeleteTransaction.execute(client, requestTimeout) ).getReceipt(client); } return response; } /** * @param {Signer} signer * @returns {Promise<TransactionResponse>} */ async executeWithSigner(signer) { if (this._bytecode == null) { throw new Error("cannot create contract with no bytecode"); } if (signer.getAccountKey == null) { throw new Error( "`Signer.getAccountKey()` is not implemented, but is required for `ContractCreateFlow`", ); } // eslint-disable-next-line @typescript-eslint/await-thenable const key = await signer.getAccountKey(); let formattedPublicKey; if (key instanceof PublicKey) { formattedPublicKey = key; } else { const propertyValues = Object.values( // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access key._key._key._keyData, ); const keyArray = new Uint8Array(propertyValues); formattedPublicKey = PublicKey.fromBytes(keyArray); } const fileCreateTransaction = await new FileCreateTransaction() .setKeys(formattedPublicKey != null ? [formattedPublicKey] : []) .setContents( this._bytecode.subarray( 0, Math.min(this._bytecode.length, 2048), ), ) .freezeWithSigner(signer); await fileCreateTransaction.signWithSigner(signer); await addSignersToTransaction( fileCreateTransaction, this._publicKeys, this._transactionSigners, ); let response = await fileCreateTransaction.executeWithSigner(signer); const receipt = await response.getReceiptWithSigner(signer); const fileId = /** @type {FileId} */ (receipt.fileId); if (this._bytecode.length > 2048) { let fileAppendTransaction = new FileAppendTransaction() .setFileId(fileId) .setContents(this._bytecode.subarray(2048)); if (this._maxChunks != null) { fileAppendTransaction.setMaxChunks(this._maxChunks); } fileAppendTransaction = await fileAppendTransaction.freezeWithSigner(signer); await fileAppendTransaction.signWithSigner(signer); await addSignersToTransaction( fileAppendTransaction, this._publicKeys, this._transactionSigners, ); await fileAppendTransaction.executeWithSigner(signer); } this._contractCreate = await this._contractCreate .setBytecodeFileId(fileId) .freezeWithSigner(signer); this._contractCreate = await this._contractCreate.signWithSigner(signer); await addSignersToTransaction( this._contractCreate, this._publicKeys, this._transactionSigners, ); response = await this._contractCreate.executeWithSigner(signer); await response.getReceiptWithSigner(signer); if (key != null) { const fileDeleteTransaction = await new FileDeleteTransaction() .setFileId(fileId) .freezeWithSigner(signer); await fileDeleteTransaction.signWithSigner(signer); await addSignersToTransaction( fileDeleteTransaction, this._publicKeys, this._transactionSigners, ); await ( await fileDeleteTransaction.executeWithSigner(signer) ).getReceiptWithSigner(signer); } return response; } } /** * @template {Transaction} T * @param {T} transaction * @param {PublicKey[]} publicKeys * @param {((message: Uint8Array) => Promise<Uint8Array>)[]} transactionSigners * @returns {Promise<void>} */ async function addSignersToTransaction( transaction, publicKeys, transactionSigners, ) { for (let i = 0; i < publicKeys.length; i++) { await transaction.signWith(publicKeys[i], transactionSigners[i]); } }