1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const client_common_1 = require("@neo-one/client-common");
|
5 | const utils_1 = require("@neo-one/utils");
|
6 | const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
7 | const AssetType_1 = require("../AssetType");
|
8 | const constants_1 = require("../constants");
|
9 | const errors_1 = require("../errors");
|
10 | const ScriptContainer_1 = require("../ScriptContainer");
|
11 | const utils_2 = require("../utils");
|
12 | const Witness_1 = require("../Witness");
|
13 | const attribute_1 = require("./attribute");
|
14 | const common_1 = require("./common");
|
15 | const Input_1 = require("./Input");
|
16 | const Output_1 = require("./Output");
|
17 | const TransactionType_1 = require("./TransactionType");
|
18 | const getUtilityValue = ({ outputs, utilityToken, }) => outputs
|
19 | .filter((output) => client_common_1.common.uInt256Equal(output.asset, utilityToken.hash))
|
20 | .reduce((acc, output) => acc.add(output.value), utils_2.utils.ZERO);
|
21 | function TransactionBase(Base) {
|
22 | class TransactionBaseClass extends Base {
|
23 | constructor() {
|
24 | super(...arguments);
|
25 | this.equals = utils_2.utils.equals(this.constructor, this, (other) => client_common_1.common.uInt256Equal(this.hash, other.hash));
|
26 | this.toKeyString = utils_2.utils.toKeyString(TransactionBase, () => this.hashHex);
|
27 | this.getSortedScriptHashesForVerifying = utils_2.utils.lazyAsync(async (options) => {
|
28 | const hashes = await this.getScriptHashesForVerifying(options);
|
29 | return [...hashes].sort();
|
30 | });
|
31 | this.sizeInternal = utils_2.utils.lazy(() => client_common_1.IOHelper.sizeOfUInt8 +
|
32 | client_common_1.IOHelper.sizeOfArray(this.attributes, (attribute) => attribute.size) +
|
33 | client_common_1.IOHelper.sizeOfArray(this.inputs, (input) => input.size) +
|
34 | client_common_1.IOHelper.sizeOfArray(this.outputs, (output) => output.size) +
|
35 | client_common_1.IOHelper.sizeOfArray(this.scripts, (script) => script.size) +
|
36 | this.sizeExclusive());
|
37 | this.networkFee = utils_2.utils.lazyAsync(async (context) => {
|
38 | const { getOutput, utilityToken } = context;
|
39 | const outputsForInputs = await Promise.all(this.inputs.map(getOutput));
|
40 | const inputValue = getUtilityValue({
|
41 | outputs: outputsForInputs,
|
42 | utilityToken,
|
43 | });
|
44 | const outputValue = getUtilityValue({
|
45 | outputs: this.outputs,
|
46 | utilityToken,
|
47 | });
|
48 | const result = inputValue.sub(outputValue).sub(this.getSystemFee(context));
|
49 | return result.lt(utils_2.utils.ZERO) ? utils_2.utils.ZERO : result;
|
50 | });
|
51 | this.getReferencesInternal = utils_2.utils.lazyAsync(async ({ getOutput }) => Promise.all(this.inputs.map(async (input) => getOutput(input))));
|
52 | this.getTransactionResultsInternal = utils_2.utils.lazyAsync(async ({ getOutput }) => {
|
53 | const inputOutputs = await this.getReferences({ getOutput });
|
54 | const mutableResults = {};
|
55 | const addOutputs = (outputs, negative) => {
|
56 | outputs.forEach((output) => {
|
57 | const key = client_common_1.common.uInt256ToHex(output.asset);
|
58 | let result = mutableResults[key];
|
59 | if (result === undefined) {
|
60 | mutableResults[key] = result = utils_2.utils.ZERO;
|
61 | }
|
62 | mutableResults[key] = result.add(negative === true ? output.value.neg() : output.value);
|
63 | });
|
64 | };
|
65 | addOutputs(inputOutputs);
|
66 | addOutputs(this.outputs, true);
|
67 | return lodash_1.default.pickBy(mutableResults, (value) => value !== undefined && !value.eq(utils_2.utils.ZERO));
|
68 | });
|
69 | this.baseGetScriptHashesForVerifyingInternal = utils_2.utils.lazyAsync(async ({ getOutput, getAsset }) => {
|
70 | const [inputHashes, outputHashes] = await Promise.all([
|
71 | Promise.all(this.inputs.map(async (input) => {
|
72 | const output = await getOutput(input);
|
73 | return client_common_1.common.uInt160ToHex(output.address);
|
74 | })),
|
75 | Promise.all(this.outputs.map(async (output) => {
|
76 | const asset = await getAsset({ hash: output.asset });
|
77 | if (client_common_1.hasFlag(asset.type, AssetType_1.AssetType.DutyFlag)) {
|
78 | return client_common_1.common.uInt160ToHex(output.address);
|
79 | }
|
80 | return undefined;
|
81 | })).then((hashes) => hashes.filter(utils_1.utils.notNull)),
|
82 | ]);
|
83 | const attributeHashes = this.attributes
|
84 | .map((attribute) => attribute instanceof attribute_1.UInt160Attribute && attribute.usage === attribute_1.AttributeUsage.Script
|
85 | ? client_common_1.common.uInt160ToHex(attribute.value)
|
86 | : undefined)
|
87 | .filter(utils_1.utils.notNull);
|
88 | return new Set([...inputHashes, ...outputHashes, ...attributeHashes]);
|
89 | });
|
90 | this.sizeExclusive = () => 0;
|
91 | }
|
92 | static deserializeTransactionBaseStartWireBase({ reader, }) {
|
93 | const type = client_common_1.assertTransactionType(reader.readUInt8());
|
94 | const version = reader.readUInt8();
|
95 | return { type, version };
|
96 | }
|
97 | static deserializeTransactionBaseEndWireBase(options) {
|
98 | const { reader } = options;
|
99 | const attributes = reader.readArray(() => attribute_1.deserializeAttributeWireBase(options), client_common_1.MAX_TRANSACTION_ATTRIBUTES);
|
100 | const inputs = reader.readArray(() => Input_1.Input.deserializeWireBase(options));
|
101 | const outputs = reader.readArray(() => Output_1.Output.deserializeWireBase(options), utils_2.utils.USHORT_MAX_NUMBER + 1);
|
102 | const scripts = reader.readArray(() => Witness_1.Witness.deserializeWireBase(options));
|
103 | return { attributes, inputs, outputs, scripts };
|
104 | }
|
105 | static deserializeWireBase(_options) {
|
106 | throw new Error('Not Implemented');
|
107 | }
|
108 | static deserializeWire(options) {
|
109 | return this.deserializeWireBase({
|
110 | context: options.context,
|
111 | reader: new utils_2.BinaryReader(options.buffer),
|
112 | });
|
113 | }
|
114 | get size() {
|
115 | return this.sizeInternal();
|
116 | }
|
117 | async serializeTransactionBaseJSON(context) {
|
118 | const [networkFee, transactionData] = await Promise.all([
|
119 | this.getNetworkFee(context.feeContext),
|
120 | context.tryGetTransactionData(this),
|
121 | ]);
|
122 | return {
|
123 | txid: client_common_1.common.uInt256ToString(this.hashHex),
|
124 | size: this.size,
|
125 | version: this.version,
|
126 | attributes: this.attributes.map((attribute) => attribute.serializeJSON(context)),
|
127 | vin: this.inputs.map((input) => input.serializeJSON(context)),
|
128 | vout: this.outputs.map((output, index) => output.serializeJSON(context, index)),
|
129 | scripts: this.scripts.map((script) => script.serializeJSON(context)),
|
130 | sys_fee: client_common_1.JSONHelper.writeFixed8(this.getSystemFee(context.feeContext)),
|
131 | net_fee: client_common_1.JSONHelper.writeFixed8(networkFee),
|
132 | data: transactionData === undefined
|
133 | ? undefined
|
134 | : {
|
135 | blockHash: client_common_1.common.uInt256ToString(transactionData.blockHash),
|
136 | blockIndex: transactionData.startHeight,
|
137 | transactionIndex: transactionData.index,
|
138 | globalIndex: client_common_1.JSONHelper.writeUInt64(transactionData.globalIndex),
|
139 | },
|
140 | };
|
141 | }
|
142 | async serializeJSON(_context) {
|
143 | throw new Error('Not Implemented');
|
144 | }
|
145 | async getNetworkFee(context) {
|
146 | return this.networkFee(context);
|
147 | }
|
148 | getSystemFee({ fees }) {
|
149 | const fee = fees[this.type];
|
150 | return fee === undefined ? utils_2.utils.ZERO : fee;
|
151 | }
|
152 | async getReferences(options) {
|
153 | return this.getReferencesInternal(options);
|
154 | }
|
155 | async getTransactionResults(options) {
|
156 | return this.getTransactionResultsInternal(options);
|
157 | }
|
158 | async getScriptHashesForVerifying(options) {
|
159 | return this.baseGetScriptHashesForVerifyingInternal(options);
|
160 | }
|
161 | async verify(options) {
|
162 | if (this.size > constants_1.MAX_TRANSACTION_SIZE) {
|
163 | throw new errors_1.VerifyError('Transaction too large.');
|
164 | }
|
165 | const { memPool = [] } = options;
|
166 | if (common_1.hasDuplicateInputs(this.inputs)) {
|
167 | throw new errors_1.VerifyError('Duplicate inputs');
|
168 | }
|
169 | if (memPool.some((tx) => !tx.equals(this) && common_1.hasIntersectingInputs(tx.inputs, this.inputs))) {
|
170 | throw new errors_1.VerifyError('Input already exists in mempool');
|
171 | }
|
172 | if (this.attributes.filter((attribute) => attribute.usage === attribute_1.AttributeUsage.ECDH02 || attribute.usage === attribute_1.AttributeUsage.ECDH03).length > 1) {
|
173 | throw new errors_1.VerifyError('Too many ECDH attributes.');
|
174 | }
|
175 | const [results] = await Promise.all([
|
176 | this.verifyScripts(options),
|
177 | this.verifyDoubleSpend(options),
|
178 | this.verifyOutputs(options),
|
179 | this.verifyTransactionResults(options),
|
180 | ]);
|
181 | return results;
|
182 | }
|
183 | async verifyDoubleSpend({ isSpent }) {
|
184 | const isDoubleSpend = await Promise.all(this.inputs.map(isSpent));
|
185 | if (isDoubleSpend.some((value) => value)) {
|
186 | throw new errors_1.VerifyError('Transaction is a double spend');
|
187 | }
|
188 | }
|
189 | async verifyOutputs({ getAsset, currentHeight }) {
|
190 | const outputsGrouped = Object.entries(lodash_1.default.groupBy(this.outputs, (output) => client_common_1.common.uInt256ToHex(output.asset)));
|
191 | const hasInvalidOutputs = await Promise.all(outputsGrouped.map(async ([assetHex, outputs]) => {
|
192 | const asset = await getAsset({ hash: client_common_1.common.hexToUInt256(assetHex) });
|
193 | if (asset.expiration <= currentHeight + 1 &&
|
194 | asset.type !== AssetType_1.AssetType.GoverningToken &&
|
195 | asset.type !== AssetType_1.AssetType.UtilityToken) {
|
196 | return true;
|
197 | }
|
198 | return outputs.some((output) => !output.value.mod(utils_2.utils.TEN.pow(utils_2.utils.EIGHT.subn(asset.precision))).eq(utils_2.utils.ZERO));
|
199 | }));
|
200 | if (hasInvalidOutputs.some((value) => value)) {
|
201 | throw new errors_1.VerifyError('Transaction has invalid output');
|
202 | }
|
203 | }
|
204 | async verifyTransactionResults({ getOutput, utilityToken, governingToken, fees, registerValidatorFee, }) {
|
205 | const results = await this.getTransactionResults({ getOutput });
|
206 | const resultsDestroy = Object.entries(results).filter(([_key, value]) => value.gt(utils_2.utils.ZERO));
|
207 | if (resultsDestroy.length > 1 ||
|
208 | (resultsDestroy.length === 1 &&
|
209 | !client_common_1.common.uInt256Equal(client_common_1.common.hexToUInt256(resultsDestroy[0][0]), utilityToken.hash))) {
|
210 | throw new errors_1.VerifyError('Invalid destroyed output.');
|
211 | }
|
212 | const feeContext = {
|
213 | getOutput,
|
214 | governingToken,
|
215 | utilityToken,
|
216 | fees,
|
217 | registerValidatorFee,
|
218 | };
|
219 | const systemFee = this.getSystemFee(feeContext);
|
220 | if (systemFee.gt(utils_2.utils.ZERO) && (resultsDestroy.length === 0 || resultsDestroy[0][1].lt(systemFee))) {
|
221 | throw new errors_1.VerifyError('Not enough output value for system fee.');
|
222 | }
|
223 | const resultsIssue = Object.entries(results).filter(([__, value]) => value.lt(utils_2.utils.ZERO));
|
224 | switch (this.type) {
|
225 | case TransactionType_1.TransactionType.Miner:
|
226 | case TransactionType_1.TransactionType.Claim:
|
227 | if (resultsIssue.some(([assetHex]) => !client_common_1.common.uInt256Equal(client_common_1.common.hexToUInt256(assetHex), utilityToken.hash))) {
|
228 | throw new errors_1.VerifyError('Invalid miner/claim result');
|
229 | }
|
230 | break;
|
231 | case TransactionType_1.TransactionType.Issue:
|
232 | if (resultsIssue.some(([assetHex, __]) => client_common_1.common.uInt256Equal(client_common_1.common.hexToUInt256(assetHex), utilityToken.hash))) {
|
233 | throw new errors_1.VerifyError('Invalid issue result');
|
234 | }
|
235 | break;
|
236 | default:
|
237 | if (resultsIssue.length > 0) {
|
238 | throw new errors_1.VerifyError('Invalid results.');
|
239 | }
|
240 | }
|
241 | }
|
242 | async verifyScripts({ getAsset, getOutput, verifyScript, }) {
|
243 | const hashesArr = await this.getSortedScriptHashesForVerifying({
|
244 | getAsset,
|
245 | getOutput,
|
246 | });
|
247 | if (hashesArr.length !== this.scripts.length) {
|
248 | throw new errors_1.VerifyError(`Invalid witnesses. Found ${hashesArr.length} hashes and ${this.scripts.length} scripts.`);
|
249 | }
|
250 | const hashes = hashesArr.map((value) => client_common_1.common.hexToUInt160(value));
|
251 | return Promise.all(lodash_1.default.zip(hashes, this.scripts).map(async ([hash, witness]) => verifyScript({
|
252 | scriptContainer: {
|
253 | type: ScriptContainer_1.ScriptContainerType.Transaction,
|
254 | value: this,
|
255 | },
|
256 | hash: hash,
|
257 | witness: witness,
|
258 | })));
|
259 | }
|
260 | }
|
261 | TransactionBaseClass.WitnessConstructor = Witness_1.Witness;
|
262 | return TransactionBaseClass;
|
263 | }
|
264 | exports.TransactionBase = TransactionBase;
|
265 |
|
266 |
|