@cityofzion/neon-js
Version:
Neon-JS SDK for interacting with NEO blockchain
233 lines • 10.7 kB
JavaScript
import { CONST, rpc, sc, tx, u, wallet } from "@cityofzion/neon-core";
import { GASContract } from "./nep17";
import { smartCalculateNetworkFee } from "@cityofzion/neon-api";
/**
* Calculate the GAS costs for validation and inclusion of the transaction in a block
* @param transaction - the transaction to calculate the network fee for
* @param account -
* @param config -
*
* @deprecated use the smartCalculateNetworkFee helper instead.
*/
export async function calculateNetworkFee(transaction, account, config) {
if (transaction.signers.length < 1) {
throw new Error("Cannot calculate the network fee without a sender in the transaction.");
}
const hashes = transaction.getScriptHashesForVerifying();
let networkFeeSize = transaction.headerSize +
u.getSerializedSize(transaction.signers) +
u.getSerializedSize(transaction.attributes) +
u.getSerializedSize(transaction.script) +
u.getSerializedSize(hashes.length);
const rpcClient = new rpc.RPCClient(config.rpcAddress);
let execFeeFactor = 0;
try {
const response = await rpcClient.invokeFunction(CONST.NATIVE_CONTRACT_HASH.PolicyContract, "getExecFeeFactor");
if (response.state === "FAULT") {
throw Error;
}
execFeeFactor = parseInt(response.stack[0].value);
}
catch (e) {
throw new Error(`Failed to get 'Execution Fee factor' from Policy contract. Error: ${e}`);
}
let networkFee = 0;
hashes.map((hash) => {
let witnessScript;
if (hash === account.scriptHash && account.contract.script !== undefined) {
witnessScript = u.HexString.fromBase64(account.contract.script);
}
if (witnessScript === undefined && transaction.witnesses.length > 0) {
for (const witness of transaction.witnesses) {
if (witness.scriptHash === hash) {
witnessScript = witness.verificationScript;
break;
}
}
}
if (witnessScript === undefined)
// should get the contract script via RPC getcontractstate
// then execute the script with a verification trigger (not yet supported)
// and collect the gas consumed
throw new Error("Using a smart contract as a witness is not yet supported in neon-js");
else if (sc.isSignatureContract(witnessScript)) {
networkFeeSize += 67 + u.getSerializedSize(witnessScript);
networkFee =
execFeeFactor *
(sc.OpCodePrices[sc.OpCode.PUSHDATA1] * 2 +
sc.OpCodePrices[sc.OpCode.SYSCALL] +
sc.getInteropServicePrice(sc.InteropServiceCode.SYSTEM_CRYPTO_CHECKSIG));
}
else if (sc.isMultisigContract(witnessScript)) {
const publicKeyCount = wallet.getPublicKeysFromVerificationScript(witnessScript.toString()).length;
const signatureCount = wallet.getSigningThresholdFromVerificationScript(witnessScript.toString());
const invocationScriptSize = 66 * signatureCount;
networkFeeSize +=
u.getSerializedSize(invocationScriptSize) +
invocationScriptSize +
u.getSerializedSize(witnessScript);
networkFee +=
execFeeFactor * sc.OpCodePrices[sc.OpCode.PUSHDATA1] * signatureCount;
const builder = new sc.ScriptBuilder();
let pushOpcode = sc.fromHex(builder.emitPush(signatureCount).build().slice(0, 2));
// price for pushing the signature count
networkFee += execFeeFactor * sc.OpCodePrices[pushOpcode];
// now do the same for the public keys
builder.reset();
pushOpcode = sc.fromHex(builder.emitPush(publicKeyCount).build().slice(0, 2));
// price for pushing the public key count
networkFee += execFeeFactor * sc.OpCodePrices[pushOpcode];
networkFee +=
execFeeFactor *
(sc.getInteropServicePrice(sc.InteropServiceCode.SYSTEM_CRYPTO_CHECKMULTISIG) *
publicKeyCount);
}
// else { // future supported contract types}
});
try {
const response = await rpcClient.invokeFunction(CONST.NATIVE_CONTRACT_HASH.PolicyContract, "getFeePerByte");
if (response.state === "FAULT") {
throw Error;
}
const nativeContractPolicyFeePerByte = parseInt(response.stack[0].value);
networkFee += networkFeeSize * nativeContractPolicyFeePerByte;
}
catch (e) {
throw new Error(`Failed to get 'fee per byte' from Policy contract. Error: ${e}`);
}
return u.BigInteger.fromDecimal(networkFee, 0);
}
/**
* Get the cost of executing the smart contract script
* @param script - smart contract script
* @param config -
* @param signers - signers to set while running the script
*/
export async function getSystemFee(script, config, signers) {
const rpcClient = new rpc.RPCClient(config.rpcAddress);
try {
const response = await rpcClient.invokeScript(script, signers);
if (response.state === "FAULT") {
throw Error(`Script execution failed. ExecutionEngine state = FAULT. ${response.exception}`);
}
return u.BigInteger.fromDecimal(response.gasconsumed, 0);
}
catch (e) {
throw new Error(`Failed to get system fee. ${e}`);
}
}
/**
* Set the validUntilBlock field on a transaction
*
* If `blocksTillExpiry` is provided then the value is used.
* If `blocksTillExpiry` is not provided, or the value exceeds the maximum allowed,
* then the field is automatically set to the maximum allowed by the network.
* @param transaction - the transaction to set the expiry field on
* @param config -
* @param blocksTillExpiry - number of blocks from the current chain height until the transaction is no longer valid
*/
export async function setBlockExpiry(transaction, config, blocksTillExpiry) {
let blockLifeSpan = tx.Transaction.MAX_TRANSACTION_LIFESPAN;
if (blocksTillExpiry &&
!(blocksTillExpiry > tx.Transaction.MAX_TRANSACTION_LIFESPAN))
blockLifeSpan = blocksTillExpiry;
const rpcClient = new rpc.RPCClient(config.rpcAddress);
transaction.validUntilBlock =
(await rpcClient.getBlockCount()) + blockLifeSpan - 1;
}
/**
* Add system and network fees to a transaction.
* Validates that the source Account has sufficient balance
*
* Note: Witnesses must be present on the transaction. If no witnesses are
* present a temporary single signature account witness will be used for
* fee calculation. For fee calculation using a multi signature account you
* must add the witness yourself. See TransactionBuilder.addEmptyWitnesses()
* for reference how this could be done.
* @param transaction - the transaction to add network and system fees to
* @param config -
*/
export async function addFees(transaction, config) {
if (config.networkFeeOverride && config.prioritisationFee) {
throw new Error("networkFeeOverride and prioritisationFee are mutually exclusive");
}
if (config.systemFeeOverride) {
transaction.systemFee = config.systemFeeOverride;
}
else {
transaction.systemFee = await getSystemFee(transaction.script, config, transaction.signers);
}
if (config.account === undefined)
throw new Error("Cannot determine network fee and validate balances without an account in your config");
if (config.networkFeeOverride) {
transaction.networkFee = config.networkFeeOverride;
}
else {
const rpcClient = new rpc.RPCClient(config.rpcAddress);
const txClone = new tx.Transaction(transaction);
if (txClone.witnesses.length < 1) {
txClone.addWitness(new tx.Witness({
invocationScript: "",
verificationScript: u.HexString.fromBase64(config.account.contract.script).toString(),
}));
}
transaction.networkFee = await smartCalculateNetworkFee(txClone, rpcClient);
}
if (config.prioritisationFee) {
transaction.networkFee = transaction.networkFee.add(u.BigInteger.fromNumber(config.prioritisationFee));
}
const GAS = new GASContract(config);
const gasBalance = await GAS.balanceOf(config.account.address);
const requiredGAS = parseFloat(transaction.systemFee.add(transaction.networkFee).toDecimal(8));
if (gasBalance < requiredGAS) {
throw new Error(`Insufficient GAS. Required: ${requiredGAS} Available: ${gasBalance}`);
}
}
/**
* Deploy a smart contract
* @param nef - A smart contract in Neo executable file format. Commonly created by a NEO compiler and stored as .NEF on disk
* @param manifest - the manifest corresponding to the smart contract
* @param config -
*/
export async function deployContract(nef, manifest, config) {
const builder = new sc.ScriptBuilder();
builder.emitContractCall({
scriptHash: CONST.NATIVE_CONTRACT_HASH.ManagementContract,
operation: "deploy",
callFlags: sc.CallFlags.All,
args: [
sc.ContractParam.byteArray(u.HexString.fromHex(nef.serialize(), true)),
sc.ContractParam.string(JSON.stringify(manifest.toJson())),
],
});
const transaction = new tx.Transaction();
transaction.script = u.HexString.fromHex(builder.build());
await setBlockExpiry(transaction, config, config.blocksTillExpiry);
// add a sender
if (config.account === undefined)
throw new Error("Account in your config cannot be undefined");
transaction.addSigner({
account: config.account.scriptHash,
scopes: "CalledByEntry",
});
await addFees(transaction, config);
transaction.sign(config.account, config.networkMagic);
const rpcClient = new rpc.RPCClient(config.rpcAddress);
return await rpcClient.sendRawTransaction(transaction);
}
/**
* Get the hash that identifies the contract on the chain matching the specified NEF
* @param sender - The sender of the transaction
* @param nefChecksum - The checksum of the Neo Executable File. A NEF file is a smart contract commonly created by a NEO compiler and stored as .NEF on disk
* @param contractName - The name as indicated in the manifest
*/
export function getContractHash(sender, nefChecksum, contractName) {
const assembledScript = new sc.ScriptBuilder()
.emit(sc.OpCode.ABORT)
.emitPush(sender)
.emitPush(nefChecksum)
.emitPush(contractName)
.build();
return u.reverseHex(u.hash160(assembledScript));
}
//# sourceMappingURL=helpers.js.map