-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathtracesPumlStreamer.js
396 lines (396 loc) · 17 KB
/
tracesPumlStreamer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeEvents = exports.genParams = exports.writeMessages = exports.writeParticipants = exports.singleTx2PumlStream = exports.multiTxTraces2PumlStream = exports.traces2PumlStream = void 0;
const stream_1 = require("stream");
const utils_1 = require("ethers/lib/utils");
const ethers_1 = require("ethers");
const tx2umlTypes_1 = require("./types/tx2umlTypes");
const formatters_1 = require("./utils/formatters");
const debug = require("debug")("tx2uml");
const DelegateLifelineColor = "#809ECB";
const DelegateMessageColor = "#3471CD";
const FailureFillColor = "#FFAAAA";
let networkCurrency = "ETH";
const traces2PumlStream = (transactions, traces, contracts, options) => {
networkCurrency = (0, tx2umlTypes_1.setNetworkCurrency)(options.chain);
const pumlStream = new stream_1.Readable({
read() { },
});
if (transactions.length > 1) {
(0, exports.multiTxTraces2PumlStream)(pumlStream, transactions, traces, contracts, options);
}
else {
(0, exports.singleTx2PumlStream)(pumlStream, transactions[0], traces[0], contracts, options);
}
return pumlStream;
};
exports.traces2PumlStream = traces2PumlStream;
const multiTxTraces2PumlStream = (pumlStream, transactions, traces, contracts, options = {}) => {
pumlStream.push(`@startuml\n`);
if (options.title) {
pumlStream.push(`title ${options.title}\n`);
}
if (options.hideFooter) {
pumlStream.push(`hide footbox\n`);
}
if (!options.hideCaption) {
pumlStream.push(genCaption(transactions));
}
(0, exports.writeParticipants)(pumlStream, contracts, options);
let i = 0;
for (const transaction of transactions) {
pumlStream.push(`\ngroup ${transaction.hash}`);
writeTransactionDetails(pumlStream, transaction, options);
(0, exports.writeMessages)(pumlStream, traces[i++], options);
(0, exports.writeEvents)(transaction.hash, pumlStream, contracts, options);
pumlStream.push("\nend");
}
pumlStream.push("\n@endumls");
pumlStream.push(null);
return pumlStream;
};
exports.multiTxTraces2PumlStream = multiTxTraces2PumlStream;
const singleTx2PumlStream = (pumlStream, transaction, traces, contracts, options) => {
pumlStream.push("@startuml\n");
pumlStream.push(`title ${options.title || transaction.hash}\n`);
if (options.hideFooter) {
pumlStream.push(`hide footbox\n`);
}
if (!options.hideCaption) {
pumlStream.push(genCaption(transaction));
}
(0, exports.writeParticipants)(pumlStream, contracts, options);
writeTransactionDetails(pumlStream, transaction, options);
(0, exports.writeMessages)(pumlStream, traces, options);
(0, exports.writeEvents)(transaction.hash, pumlStream, contracts, options);
pumlStream.push("\n@endumls");
pumlStream.push(null);
return pumlStream;
};
exports.singleTx2PumlStream = singleTx2PumlStream;
const writeParticipants = (plantUmlStream, contracts, options = {}) => {
plantUmlStream.push("\n");
// output remaining contracts as actors or participants
let participantType = "actor";
for (const [address, contract] of Object.entries(contracts)) {
// Do not write contract as a participant if min depth greater than trace depth
if (options.depth > 0 && contract.minDepth > options.depth)
continue;
let stereotypes = "";
if (contract.protocol)
stereotypes += `<<${contract.protocol}>>`;
if (contract.tokenName)
stereotypes += `<<${contract.tokenName}>>`;
if (contract.symbol)
stereotypes += `<<(${contract.symbol})>>`;
const contractName = getContractName(contract, options);
if (contractName)
stereotypes += `<<${contractName}>>`;
if (contract.ensName)
stereotypes += `<<(${contract.ensName})>>`;
debug(`Write lifeline ${(0, formatters_1.shortAddress)(address)} with stereotype ${stereotypes}`);
plantUmlStream.push(`${participantType} "${(0, formatters_1.shortAddress)(address)}" as ${(0, formatters_1.participantId)(address)} ${stereotypes}\n`);
participantType = "participant";
}
};
exports.writeParticipants = writeParticipants;
// Derives the contract name of a participant/contract.
// If contract is delegating calls to another contract and the noDelegates option is set,
// then use the contract name of the first delegated contract.
// Note there can be multiple delegated calls to different contracts.
// There can also be delegated calls to a library.
// Here's an example tx on mainnet 0x7210c306842d275044789b02ae64aff4513ed812682de7b1cbeb12a4a0dd07af
const getContractName = (contract, options) => {
return options.noDelegates
? contract.delegatedToContracts?.[0]?.contractName ||
contract.contractName
: contract.contractName;
};
const writeTransactionDetails = (plantUmlStream, transaction, options = {}) => {
if (options.noTxDetails) {
return;
}
plantUmlStream.push(`\nnote over ${(0, formatters_1.participantId)(transaction.from)}`);
if (transaction.error) {
plantUmlStream.push(` ${FailureFillColor}\nError: ${transaction.error} \n`);
}
else {
// no error so will use default colour of tx details note
plantUmlStream.push("\n");
}
plantUmlStream.push(`Nonce: ${transaction.nonce.toLocaleString()}\n`);
plantUmlStream.push(`Gas Price: ${(0, utils_1.formatUnits)(transaction.gasPrice, "gwei")} Gwei\n`);
if (transaction.maxFeePerGas) {
plantUmlStream.push(`Max Fee: ${(0, utils_1.formatUnits)(transaction.maxFeePerGas, "gwei")} Gwei\n`);
}
if (transaction.maxPriorityFeePerGas) {
plantUmlStream.push(`Max Priority: ${(0, utils_1.formatUnits)(transaction.maxPriorityFeePerGas, "gwei")} Gwei\n`);
}
plantUmlStream.push(`Gas Limit: ${(0, formatters_1.formatNumber)(transaction.gasLimit.toString())}\n`);
plantUmlStream.push(`Gas Used: ${(0, formatters_1.formatNumber)(transaction.gasUsed.toString())}\n`);
const txFeeInWei = transaction.gasUsed.mul(transaction.gasPrice);
const txFeeInEther = (0, utils_1.formatEther)(txFeeInWei);
const tFeeInEtherFormatted = Number(txFeeInEther).toLocaleString();
plantUmlStream.push(`Tx Fee: ${tFeeInEtherFormatted} ${networkCurrency}\n`);
plantUmlStream.push("end note\n");
};
const writeMessages = (plantUmlStream, traces, options = {}) => {
if (!traces?.length) {
return;
}
let contractCallStack = [];
let previousTrace;
plantUmlStream.push("\n");
// for each trace
for (const trace of traces) {
if (trace.depth > options.depth)
continue;
debug(`Write message ${trace.id} from ${trace.from} to ${trace.to}`);
// return from lifeline if processing has moved to a different contract
if (trace.delegatedFrom !== previousTrace?.to) {
// contractCallStack is mutated in the loop so make a copy
for (const callStack of [...contractCallStack]) {
// stop returns when the callstack is back to this trace's lifeline
if (trace.delegatedFrom === callStack.to) {
break;
}
plantUmlStream.push(genEndLifeline(callStack, options));
contractCallStack.shift();
}
}
if (trace.type === tx2umlTypes_1.MessageType.Selfdestruct) {
plantUmlStream.push(`${(0, formatters_1.participantId)(trace.from)} ${genArrow(trace)} ${(0, formatters_1.participantId)(trace.from)}: Self-Destruct\n`);
// TODO add ETH value transfer to refund address if there was a contract balance
}
else {
const beforeParams = `${(0, formatters_1.participantId)(trace.from)} ${genArrow(trace)} ${(0, formatters_1.participantId)(trace.to)}: `;
const afterParams = `${genGasUsage(trace.gasUsed, options.noGas)}${genEtherValue(trace, options.noEther)}\n`;
const rawParams = `${genFunctionText(trace, options)}`;
const maxParamLength = 2000 - beforeParams.length - afterParams.length;
const truncatedParams = rawParams.slice(0, maxParamLength);
if (maxParamLength < rawParams.length)
console.warn(`params were truncated by ${truncatedParams.length - maxParamLength} characters`);
plantUmlStream.push(beforeParams + truncatedParams + afterParams);
if (trace.type === tx2umlTypes_1.MessageType.DelegateCall) {
plantUmlStream.push(`activate ${(0, formatters_1.participantId)(trace.to)} ${DelegateLifelineColor}\n`);
}
else {
plantUmlStream.push(`activate ${(0, formatters_1.participantId)(trace.to)}\n`);
}
}
if (trace.type !== tx2umlTypes_1.MessageType.Selfdestruct) {
contractCallStack.unshift(trace);
previousTrace = trace;
}
}
contractCallStack.forEach(callStack => {
plantUmlStream.push(genEndLifeline(callStack, options));
});
};
exports.writeMessages = writeMessages;
const genEndLifeline = (trace, options = {}) => {
let plantUml = "";
if (!trace.error) {
if (options.noParams) {
plantUml += `return\n`;
}
else {
// remove the first carriage return
plantUml += `return ${(0, exports.genParams)(trace.outputParams, options.noParamValues).replace(/\\n/, "")}\n`;
}
if (!options.noGas && trace.childTraces.length > 0) {
const gasUsedLessChildCalls = calculateGasUsedLessChildTraces(trace);
if (gasUsedLessChildCalls?.gt(0)) {
plantUml += `note right of ${(0, formatters_1.participantId)(trace.to)}: ${genGasUsage(gasUsedLessChildCalls)}\n`;
}
}
}
else {
// a failed transaction so end the lifeline
plantUml += `destroy ${(0, formatters_1.participantId)(trace.to)}\nreturn\n`;
plantUml += `note right of ${(0, formatters_1.participantId)(trace.to)} ${FailureFillColor}: ${trace.error}\n`;
}
return plantUml;
};
const calculateGasUsedLessChildTraces = (trace) => {
// Sum gasUsed on all child traces of the parent
let gasUsedLessChildTraces = ethers_1.BigNumber.from(0);
for (const childTrace of trace.childTraces) {
if (!childTrace.gasUsed) {
return undefined;
}
gasUsedLessChildTraces = gasUsedLessChildTraces.add(childTrace.gasUsed);
}
return trace.gasUsed.sub(gasUsedLessChildTraces);
};
const genArrow = (trace) => {
const arrowColor = trace.parentTrace?.type === tx2umlTypes_1.MessageType.DelegateCall
? `[${DelegateMessageColor}]`
: "";
const line = trace.proxy ? "--" : "-";
if (trace.type === tx2umlTypes_1.MessageType.DelegateCall) {
return `${line}${arrowColor}>>`;
}
if (trace.type === tx2umlTypes_1.MessageType.Create) {
return `${line}${arrowColor}>o`;
}
if (trace.type === tx2umlTypes_1.MessageType.Selfdestruct) {
return `${line}${arrowColor}\\`;
}
// Call and Staticcall are the same
return `${line}${arrowColor}>`;
};
const genFunctionText = (trace, options) => {
const noParams = options.noParams || options.noParamValues;
if (!trace) {
return "";
}
if (trace.type === tx2umlTypes_1.MessageType.Create) {
if (noParams) {
return "constructor";
}
// If we have the contract ABI so the constructor params could be parsed
if (trace.parsedConstructorParams) {
return `${trace.funcName}(${(0, exports.genParams)(trace.inputParams, options.noParamValues, "", oneIndent)})`;
}
// we don't know if there was constructor params or not as the contract was not verified on Etherscan
// hence we don't have the constructor params or the contract ABI to parse them.
return "constructor(?)";
}
if (!trace.funcSelector) {
return options.noParams ? "fallback" : "fallback()";
}
if (!trace.funcName) {
return `${trace.funcSelector}`;
}
if (options.noParams)
return trace.funcName;
return `${trace.funcName}(${(0, exports.genParams)(trace.inputParams, options.noParamValues, "", oneIndent)})`;
};
const oneIndent = " ";
const genParams = (params, noValues, plantUml = "", indent = "") => {
if (!params) {
return "";
}
for (const param of params) {
// put each param on a new line.
// The \ needs to be escaped with \\
plantUml += "\\n" + indent;
if (param.name) {
if (noValues) {
plantUml += `${param.name},`;
continue;
}
plantUml += `${param.name}: `;
}
else if (noValues) {
// we don't know the param name and we aren't showing values
// so we'll break from showing any params
plantUml += "?,";
continue;
}
if (param.type === "address") {
plantUml += `${(0, formatters_1.shortAddress)(param.value)},`;
}
else if (param.components) {
if (Array.isArray(param.components)) {
plantUml += `[`;
plantUml = `${(0, exports.genParams)(param.components, noValues, plantUml, indent + oneIndent)}`;
plantUml += `],`;
}
else {
debug(`Unsupported components type ${JSON.stringify(param.components)}`);
}
}
else if (Array.isArray(param.value)) {
// not a component but an array of params
plantUml += `[`;
param.value.forEach((value, i) => {
plantUml = `${(0, exports.genParams)([
{
name: i.toString(),
value,
// remove the [] at the end of the type
type: param.type.slice(0, -2),
},
], noValues, plantUml, indent + oneIndent)}`;
});
plantUml += `],`;
}
else if (param.type.slice(0, 5) === "bytes") {
plantUml += `${(0, formatters_1.shortBytes)(param.value)},`;
}
else if (param.type.match("int")) {
plantUml += `"${(0, formatters_1.formatNumber)(param.value)}",`;
}
else {
// Need to escape \n with \\n
plantUml += `${(0, formatters_1.escapeCarriageReturns)(param.value)},`;
}
}
return plantUml.slice(0, -1);
};
exports.genParams = genParams;
const genGasUsage = (gasUsed, noGasUsage = false) => {
if (noGasUsage || !gasUsed) {
return "";
}
// Add thousand comma separators
const gasValueWithCommas = (0, formatters_1.formatNumber)(gasUsed.toString());
return `\\n${gasValueWithCommas} gas`;
};
const genEtherValue = (trace, noEtherValue = false) => {
if (noEtherValue || trace.value.eq(0)) {
return "";
}
// Convert wei value to Ether
const ether = (0, utils_1.formatEther)(trace.value);
// Add thousand commas. Can't use formatNumber for this as it doesn't handle decimal numbers.
// Assuming the amount of ether is not great than JS number limit.
const etherFormatted = Number(ether).toLocaleString();
return `\\n${etherFormatted} ${networkCurrency}`;
};
const genCaption = (details) => {
if (Array.isArray(details)) {
let caption = "footer\n";
details.forEach(detail => (caption += `${detail.network}, block ${detail.blockNumber}, ${detail.timestamp.toUTCString()}\n`));
caption += "\nendfooter";
return caption;
}
else {
const detail = details;
return `\ncaption ${detail.network}, block ${detail.blockNumber}, ${detail.timestamp.toUTCString()}`;
}
};
const writeEvents = (txHash, plantUmlStream, contracts, options = {}) => {
if (options.noLogDetails) {
return;
}
// For each contract
let firstEvent = true;
for (const contract of Object.values(contracts)) {
if (contract.ethersContract &&
contract.events?.length &&
(options.depth === undefined || contract.minDepth <= options.depth)) {
const txEvents = contract.events.filter(e => e.txHash === txHash);
if (txEvents.length === 0)
continue;
const align = firstEvent ? "" : "/ ";
firstEvent = false;
plantUmlStream.push(`\n${align}note over ${(0, formatters_1.participantId)(contract.address)} #aqua`);
for (const event of txEvents) {
if (options.noParams) {
plantUmlStream.push(`\n${event.name}`);
continue;
}
plantUmlStream.push(`\n${event.name}:`);
plantUmlStream.push(
// replace \\n with \n
`${(0, exports.genParams)(event.params, options.noParamValues, "", oneIndent).replace(/\\n/g, "\n")}`);
}
plantUmlStream.push("\nend note\n");
}
}
};
exports.writeEvents = writeEvents;
//# sourceMappingURL=tracesPumlStreamer.js.map