@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
207 lines (206 loc) • 7.61 kB
JavaScript
import { Buffer as _Buffer } from "buffer";
import { RestError } from '@azure/core-rest-pipeline';
import { hash, isEncoded, verifySignature } from '../utils/crypto.js';
import { ProtocolToVmAbi } from './builder/field-types/ct-version.js';
import { Tag } from './builder/constants.js';
import { buildTx, unpackTx } from './builder/index.js';
import { concatBuffers, isAccountNotFoundError } from '../utils/other.js';
import { Encoding, decode } from '../utils/encoder.js';
import Node from '../Node.js';
import { genAggressiveCacheGetResponsesPolicy } from '../utils/autorest.js';
import { UnexpectedTsError } from '../utils/errors.js';
import getTransactionSignerAddress from './transaction-signer.js';
import { getExecutionCostUsingNode } from './execution-cost.js';
const validators = [];
async function verifyTransactionInternal(tx, node, parentTxTypes) {
const address = getTransactionSignerAddress(buildTx(tx));
const [account, {
height
}, {
consensusProtocolVersion,
nodeNetworkId
}] = await Promise.all([node.getAccountByPubkey(address).catch(error => {
if (!isAccountNotFoundError(error)) throw error;
return {
id: address,
balance: 0n,
nonce: 0
};
})
// TODO: remove after fixing https://github.com/aeternity/aepp-sdk-js/issues/1537
.then(acc => ({
...acc,
id: acc.id
})), node.getCurrentKeyBlockHeight(),
// TODO: don't request height on each validation, use caching
node.getNodeInfo()]);
return (await Promise.all(validators.map(async v => v(tx, {
node,
account,
height,
consensusProtocolVersion,
nodeNetworkId,
parentTxTypes
})))).flat();
}
/**
* Transaction Validator
* This function validates some transaction properties,
* to make sure it can be posted it to the chain
* @category transaction builder
* @param transaction - Base64Check-encoded transaction
* @param nodeNotCached - Node to validate transaction against
* @returns Array with verification errors
* @example const errors = await verifyTransaction(transaction, node)
*/
export default async function verifyTransaction(transaction, nodeNotCached) {
const pipeline = nodeNotCached.pipeline.clone();
pipeline.removePolicy({
name: 'parse-big-int'
});
const node = new Node(nodeNotCached.$host, {
ignoreVersion: true,
pipeline,
additionalPolicies: [genAggressiveCacheGetResponsesPolicy()]
});
node._getCachedStatus = async () => nodeNotCached._getCachedStatus();
return verifyTransactionInternal(unpackTx(transaction), node, []);
}
validators.push((tx, {
account,
nodeNetworkId,
parentTxTypes
}) => {
if (tx.tag !== Tag.SignedTx) return [];
const {
encodedTx,
signatures
} = tx;
if ((encodedTx !== null && encodedTx !== void 0 ? encodedTx : signatures) == null) return [];
if (signatures.length !== 1) return []; // TODO: Support multisignature like in state channels
const prefix = _Buffer.from([nodeNetworkId, ...(parentTxTypes.includes(Tag.PayingForTx) ? ['inner_tx'] : [])].join('-'));
const txBinary = decode(buildTx(encodedTx));
const txWithNetworkId = concatBuffers([prefix, txBinary]);
const txHashWithNetworkId = concatBuffers([prefix, hash(txBinary)]);
if (verifySignature(txWithNetworkId, signatures[0], account.id) || verifySignature(txHashWithNetworkId, signatures[0], account.id)) return [];
return [{
message: 'Signature cannot be verified, please ensure that you transaction have' + ' the correct prefix and the correct private key for the sender address',
key: 'InvalidSignature',
checkedKeys: ['encodedTx', 'signatures']
}];
}, async (tx, {
node,
parentTxTypes
}) => {
let nestedTx;
if ('encodedTx' in tx) nestedTx = tx.encodedTx;
if ('tx' in tx) nestedTx = tx.tx;
if (nestedTx == null) return [];
return verifyTransactionInternal(nestedTx, node, [...parentTxTypes, tx.tag]);
}, (tx, {
height
}) => {
if (!('ttl' in tx)) return [];
if (tx.ttl === 0 || tx.ttl > height) return [];
return [{
message: `TTL ${tx.ttl} is already expired, current height is ${height}`,
key: 'ExpiredTTL',
checkedKeys: ['ttl']
}];
}, async (tx, {
account,
parentTxTypes,
node
}) => {
if (parentTxTypes.length !== 0) return [];
const cost = await getExecutionCostUsingNode(buildTx(tx), node).catch(() => 0n);
if (cost <= account.balance) return [];
return [{
message: `Account balance ${account.balance} is not enough to execute the transaction that costs ${cost}`,
key: 'InsufficientBalance',
checkedKeys: ['amount', 'fee', 'nameFee', 'gasLimit', 'gasPrice']
}];
}, async (tx, {
node
}) => {
if (tx.tag !== Tag.SpendTx || isEncoded(tx.recipientId, Encoding.Name)) return [];
const recipient = await node.getAccountByPubkey(tx.recipientId).catch(error => {
if (!isAccountNotFoundError(error)) throw error;
return null;
});
if (recipient == null || recipient.payable === true) return [];
return [{
message: 'Recipient account is not payable',
key: 'RecipientAccountNotPayable',
checkedKeys: ['recipientId']
}];
}, (tx, {
account
}) => {
let message;
if (tx.tag === Tag.SignedTx && account.kind === 'generalized' && tx.signatures.length !== 0) {
message = "Generalized account can't be used to generate SignedTx with signatures";
}
if (tx.tag === Tag.GaMetaTx && account.kind === 'basic') {
message = "Basic account can't be used to generate GaMetaTx";
}
if (message == null) return [];
return [{
message,
key: 'InvalidAccountType',
checkedKeys: ['tag']
}];
},
// TODO: revert nonce check
// TODO: ensure nonce valid when paying for own tx
(tx, {
consensusProtocolVersion
}) => {
var _ref, _ref2;
const oracleCall = Tag.OracleRegisterTx === tx.tag;
const contractCreate = Tag.ContractCreateTx === tx.tag || Tag.GaAttachTx === tx.tag;
const contractCall = Tag.ContractCallTx === tx.tag || Tag.GaMetaTx === tx.tag;
const type = (_ref = (_ref2 = oracleCall ? 'oracle-call' : null) !== null && _ref2 !== void 0 ? _ref2 : contractCreate ? 'contract-create' : null) !== null && _ref !== void 0 ? _ref : contractCall ? 'contract-call' : null;
if (type == null) return [];
const protocol = ProtocolToVmAbi[consensusProtocolVersion][type];
let ctVersion;
if ('abiVersion' in tx) ctVersion = {
abiVersion: tx.abiVersion
};
if ('ctVersion' in tx) ctVersion = tx.ctVersion;
if (ctVersion == null) throw new UnexpectedTsError();
if (!protocol.abiVersion.includes(ctVersion.abiVersion) || contractCreate && !protocol.vmVersion.includes(ctVersion.vmVersion)) {
return [{
message: `ABI/VM version ${JSON.stringify(ctVersion)} is wrong, supported is: ${JSON.stringify(protocol)}`,
key: 'VmAndAbiVersionMismatch',
checkedKeys: ['ctVersion', 'abiVersion']
}];
}
return [];
}, async (tx, {
node
}) => {
if (Tag.ContractCallTx !== tx.tag) return [];
// TODO: remove after solving https://github.com/aeternity/aeternity/issues/3669
if (tx.contractId.startsWith('nm_')) return [];
try {
const {
active
} = await node.getContract(tx.contractId);
if (active) return [];
return [{
message: `Contract ${tx.contractId} is not active`,
key: 'ContractNotActive',
checkedKeys: ['contractId']
}];
} catch (error) {
if (!(error instanceof RestError) || error.response?.bodyAsText == null) throw error;
return [{
message: JSON.parse(error.response.bodyAsText).reason,
// TODO: use parsedBody instead
key: 'ContractNotFound',
checkedKeys: ['contractId']
}];
}
});
//# sourceMappingURL=validator.js.map