crypttrace 0.6.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crypttrace-0.6.0/src/crypttrace.egg-info → crypttrace-0.8.0}/PKG-INFO +25 -10
- {crypttrace-0.6.0 → crypttrace-0.8.0}/README.md +24 -9
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/assess.py +1 -1
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/chains.py +12 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/cli.py +12 -9
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/freeze.py +13 -2
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/investigate.py +9 -7
- crypttrace-0.8.0/src/crypttrace/report.py +482 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/store.py +3 -1
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/web/index.html +70 -4
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/webapp.py +35 -1
- {crypttrace-0.6.0 → crypttrace-0.8.0/src/crypttrace.egg-info}/PKG-INFO +25 -10
- crypttrace-0.6.0/src/crypttrace/report.py +0 -175
- {crypttrace-0.6.0 → crypttrace-0.8.0}/LICENSE +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/pyproject.toml +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/setup.cfg +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/addresses.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/analysis.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/assets.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/bridges.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/config.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/etherscan.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/http.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/fetchers/tron.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/funder.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/labels/known.json +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/poisoning.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/prices.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/verify.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace/watch.py +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {crypttrace-0.6.0 → crypttrace-0.8.0}/src/crypttrace.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron and Solana
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -131,7 +131,11 @@ crypttrace serve # then open http://127.0.0.1:8000
|
|
|
131
131
|
Paste an address, pick a chain and direction, and get an interactive fund-flow
|
|
132
132
|
graph: nodes coloured by what they are, arrows showing where value went, click a
|
|
133
133
|
node for its full profile, click a transfer line to open it on the block
|
|
134
|
-
explorer. A side panel shows the profile, first-funder chain
|
|
134
|
+
explorer. A side panel shows the profile, first-funder chain, off-ramp check
|
|
135
|
+
and whether USDT/USDC at the address is frozen. Tabs add the assessment, the
|
|
136
|
+
timeline, the list of who sent funds here, and address poisoning (look-alikes
|
|
137
|
+
planted in the wallet's history, and payments lured by this address). **Case
|
|
138
|
+
file** downloads the same one-page HTML report `crypttrace report` writes.
|
|
135
139
|
Everything runs on your machine — nothing is uploaded anywhere.
|
|
136
140
|
|
|
137
141
|
This exists so non-technical victims can use the tool at all: a form and a
|
|
@@ -225,7 +229,7 @@ crypttrace crosschain 0xADDRESS --window 48
|
|
|
225
229
|
crypttrace watch add 0xADDRESS --note "my stolen ETH"
|
|
226
230
|
crypttrace watch run --interval 300 # or --once for cron / Task Scheduler
|
|
227
231
|
|
|
228
|
-
# Full investigation
|
|
232
|
+
# Full investigation as a case file to send: one HTML page (plus Markdown + JSON)
|
|
229
233
|
crypttrace report 0xADDRESS --depth 3
|
|
230
234
|
|
|
231
235
|
# Refresh label lists (OFAC sanctions, …)
|
|
@@ -460,10 +464,24 @@ are ignored, and every address is checked before it is stored.
|
|
|
460
464
|
|
|
461
465
|
### Reports
|
|
462
466
|
|
|
463
|
-
`report` runs the full analysis and writes
|
|
464
|
-
with `--out`)
|
|
465
|
-
|
|
466
|
-
the
|
|
467
|
+
`report` runs the full analysis and writes a **case file** to
|
|
468
|
+
`~/crypttrace-reports/` (override with `--out`); `investigate` saves the same
|
|
469
|
+
file with the victim's next steps in it. The case file is one HTML page, made
|
|
470
|
+
for whoever receives it — an exchange's compliance team, the police, a lawyer:
|
|
471
|
+
|
|
472
|
+
- the conclusion in plain words, and what to do now: stablecoins still at the
|
|
473
|
+
address and who can freeze them, the exchanges the money reached, address
|
|
474
|
+
poisoning;
|
|
475
|
+
- the fund-flow graph, each address linking to a block explorer;
|
|
476
|
+
- every transfer along the trace with its transaction hashes — the first thing
|
|
477
|
+
an exchange asks for;
|
|
478
|
+
- where each label comes from, how the tool checked its own arithmetic, and
|
|
479
|
+
what the method cannot tell you.
|
|
480
|
+
|
|
481
|
+
It has no scripts and loads nothing from the internet, so it opens in any
|
|
482
|
+
browser or mail client, prints (or saves as PDF) cleanly, and opening it tells
|
|
483
|
+
no one. The raw data is embedded for analysts and written next to it as JSON,
|
|
484
|
+
with its SHA-256 printed on the page; a Markdown version is written too.
|
|
467
485
|
|
|
468
486
|
### Self-verification
|
|
469
487
|
|
|
@@ -561,9 +579,6 @@ cases/ # worked investigations with their data
|
|
|
561
579
|
|
|
562
580
|
- More exchanges on Bitcoin, Tron and Solana (now: Binance, OKX, HTX, Bybit),
|
|
563
581
|
and refreshing these lists as the exchanges republish them
|
|
564
|
-
- `report --html`: one self-contained file with the interactive graph, to
|
|
565
|
-
send to an exchange or attach to a police report
|
|
566
|
-
- `report --pdf` for exchange and law-enforcement filings
|
|
567
582
|
- Internal transactions (completes `funder` and contract-mediated transfers)
|
|
568
583
|
- Per-mint filtering for Solana SPL tokens
|
|
569
584
|
- More label sources: Chainabuse, CryptoScamDB, exchange deposit-address sets
|
|
@@ -97,7 +97,11 @@ crypttrace serve # then open http://127.0.0.1:8000
|
|
|
97
97
|
Paste an address, pick a chain and direction, and get an interactive fund-flow
|
|
98
98
|
graph: nodes coloured by what they are, arrows showing where value went, click a
|
|
99
99
|
node for its full profile, click a transfer line to open it on the block
|
|
100
|
-
explorer. A side panel shows the profile, first-funder chain
|
|
100
|
+
explorer. A side panel shows the profile, first-funder chain, off-ramp check
|
|
101
|
+
and whether USDT/USDC at the address is frozen. Tabs add the assessment, the
|
|
102
|
+
timeline, the list of who sent funds here, and address poisoning (look-alikes
|
|
103
|
+
planted in the wallet's history, and payments lured by this address). **Case
|
|
104
|
+
file** downloads the same one-page HTML report `crypttrace report` writes.
|
|
101
105
|
Everything runs on your machine — nothing is uploaded anywhere.
|
|
102
106
|
|
|
103
107
|
This exists so non-technical victims can use the tool at all: a form and a
|
|
@@ -191,7 +195,7 @@ crypttrace crosschain 0xADDRESS --window 48
|
|
|
191
195
|
crypttrace watch add 0xADDRESS --note "my stolen ETH"
|
|
192
196
|
crypttrace watch run --interval 300 # or --once for cron / Task Scheduler
|
|
193
197
|
|
|
194
|
-
# Full investigation
|
|
198
|
+
# Full investigation as a case file to send: one HTML page (plus Markdown + JSON)
|
|
195
199
|
crypttrace report 0xADDRESS --depth 3
|
|
196
200
|
|
|
197
201
|
# Refresh label lists (OFAC sanctions, …)
|
|
@@ -426,10 +430,24 @@ are ignored, and every address is checked before it is stored.
|
|
|
426
430
|
|
|
427
431
|
### Reports
|
|
428
432
|
|
|
429
|
-
`report` runs the full analysis and writes
|
|
430
|
-
with `--out`)
|
|
431
|
-
|
|
432
|
-
the
|
|
433
|
+
`report` runs the full analysis and writes a **case file** to
|
|
434
|
+
`~/crypttrace-reports/` (override with `--out`); `investigate` saves the same
|
|
435
|
+
file with the victim's next steps in it. The case file is one HTML page, made
|
|
436
|
+
for whoever receives it — an exchange's compliance team, the police, a lawyer:
|
|
437
|
+
|
|
438
|
+
- the conclusion in plain words, and what to do now: stablecoins still at the
|
|
439
|
+
address and who can freeze them, the exchanges the money reached, address
|
|
440
|
+
poisoning;
|
|
441
|
+
- the fund-flow graph, each address linking to a block explorer;
|
|
442
|
+
- every transfer along the trace with its transaction hashes — the first thing
|
|
443
|
+
an exchange asks for;
|
|
444
|
+
- where each label comes from, how the tool checked its own arithmetic, and
|
|
445
|
+
what the method cannot tell you.
|
|
446
|
+
|
|
447
|
+
It has no scripts and loads nothing from the internet, so it opens in any
|
|
448
|
+
browser or mail client, prints (or saves as PDF) cleanly, and opening it tells
|
|
449
|
+
no one. The raw data is embedded for analysts and written next to it as JSON,
|
|
450
|
+
with its SHA-256 printed on the page; a Markdown version is written too.
|
|
433
451
|
|
|
434
452
|
### Self-verification
|
|
435
453
|
|
|
@@ -527,9 +545,6 @@ cases/ # worked investigations with their data
|
|
|
527
545
|
|
|
528
546
|
- More exchanges on Bitcoin, Tron and Solana (now: Binance, OKX, HTX, Bybit),
|
|
529
547
|
and refreshing these lists as the exchanges republish them
|
|
530
|
-
- `report --html`: one self-contained file with the interactive graph, to
|
|
531
|
-
send to an exchange or attach to a police report
|
|
532
|
-
- `report --pdf` for exchange and law-enforcement filings
|
|
533
548
|
- Internal transactions (completes `funder` and contract-mediated transfers)
|
|
534
549
|
- Per-mint filtering for Solana SPL tokens
|
|
535
550
|
- More label sources: Chainabuse, CryptoScamDB, exchange deposit-address sets
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crypttrace — OSINT crypto investigation CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.8.0"
|
|
@@ -240,7 +240,7 @@ def assess(address: str, chain: str = "eth", asset: Optional[dict] = None,
|
|
|
240
240
|
e = frozen[0]
|
|
241
241
|
signals.append(Signal(
|
|
242
242
|
name="frozen by issuer",
|
|
243
|
-
observed=
|
|
243
|
+
observed=freeze.describe_frozen(e),
|
|
244
244
|
implication="the issuer blocked it — usually at the request of law enforcement "
|
|
245
245
|
"or under sanctions",
|
|
246
246
|
confidence="high", weight=40, evidence={"freezes": frozen}))
|
|
@@ -30,6 +30,14 @@ EXPLORER = {
|
|
|
30
30
|
"sol": "https://solscan.io/account/{}",
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
TX_EXPLORER = {
|
|
34
|
+
"eth": "https://etherscan.io/tx/{}", "bsc": "https://bscscan.com/tx/{}",
|
|
35
|
+
"polygon": "https://polygonscan.com/tx/{}", "arbitrum": "https://arbiscan.io/tx/{}",
|
|
36
|
+
"optimism": "https://optimistic.etherscan.io/tx/{}", "base": "https://basescan.org/tx/{}",
|
|
37
|
+
"btc": "https://mempool.space/tx/{}", "tron": "https://tronscan.org/#/transaction/{}",
|
|
38
|
+
"sol": "https://solscan.io/tx/{}",
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
_UPSTREAM_ERRORS = (etherscan.EtherscanError, bitcoin.BitcoinError,
|
|
34
42
|
tron.TronError, solana.SolanaError)
|
|
35
43
|
|
|
@@ -59,6 +67,10 @@ def explorer_url(address: str, chain: str) -> str:
|
|
|
59
67
|
return EXPLORER.get(chain, EXPLORER["eth"]).format(address)
|
|
60
68
|
|
|
61
69
|
|
|
70
|
+
def tx_url(tx_hash: str, chain: str) -> str:
|
|
71
|
+
return TX_EXPLORER.get(chain, TX_EXPLORER["eth"]).format(tx_hash)
|
|
72
|
+
|
|
73
|
+
|
|
62
74
|
def check(chain: str) -> None:
|
|
63
75
|
if chain not in ALL_CHAINS:
|
|
64
76
|
raise ChainError(f"unsupported chain '{chain}'. Options: {ALL_CHAINS}")
|
|
@@ -224,7 +224,7 @@ def report(
|
|
|
224
224
|
help="Folder to save the report in",
|
|
225
225
|
),
|
|
226
226
|
):
|
|
227
|
-
"""Run a full investigation and save
|
|
227
|
+
"""Run a full investigation and save it as an HTML case file (plus Markdown and JSON)."""
|
|
228
228
|
try:
|
|
229
229
|
asset_desc = assets.resolve_asset(asset, chain)
|
|
230
230
|
except ValueError as e:
|
|
@@ -232,12 +232,14 @@ def report(
|
|
|
232
232
|
raise typer.Exit(1)
|
|
233
233
|
try:
|
|
234
234
|
with console.status("Gathering on-chain data and tracing funds…"):
|
|
235
|
-
|
|
236
|
-
except etherscan.EtherscanError as e:
|
|
235
|
+
paths = report_mod.generate(address, chain, depth, branching, out, asset_desc)
|
|
236
|
+
except (chains_mod.ChainError, etherscan.EtherscanError) as e:
|
|
237
237
|
console.print(f"[red]Error:[/red] {e}")
|
|
238
238
|
raise typer.Exit(1)
|
|
239
|
-
console.print(f"[green]✓
|
|
240
|
-
console.print(
|
|
239
|
+
console.print(f"[green]✓ Case file saved:[/green] {paths['html']}")
|
|
240
|
+
console.print("[dim] One self-contained page: open it in a browser, print it to PDF, or "
|
|
241
|
+
"send it as it is.[/dim]")
|
|
242
|
+
console.print("[dim] Markdown and raw JSON are saved next to it.[/dim]")
|
|
241
243
|
|
|
242
244
|
|
|
243
245
|
@app.command()
|
|
@@ -279,14 +281,15 @@ def freeze(
|
|
|
279
281
|
if e.get("error"):
|
|
280
282
|
console.print(f"{head}: [yellow]could not be read[/yellow] — {e['error']}")
|
|
281
283
|
elif e["frozen"]:
|
|
282
|
-
console.print(f"{head}: [bold green]FROZEN[/bold green] — {e
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
elif e["balance"]:
|
|
284
|
+
console.print(f"{head}: [bold green]FROZEN[/bold green] — {freeze_mod.describe_frozen(e)}"
|
|
285
|
+
+ (f"; {e['movable']:,.2f} still can move" if e["actionable"] else ""))
|
|
286
|
+
elif e["actionable"]:
|
|
286
287
|
console.print(f"{head}: [bold red]{e['balance']:,.2f} {e['token']} NOT frozen[/bold red]"
|
|
287
288
|
" — it can still be moved")
|
|
288
289
|
console.print(f" {e['how']}")
|
|
289
290
|
console.print(f" [dim]{e['url']}[/dim]")
|
|
291
|
+
elif e["balance"]:
|
|
292
|
+
console.print(f"{head}: only dust ({e['balance']:.6f}) — nothing to freeze")
|
|
290
293
|
else:
|
|
291
294
|
console.print(f"{head}: none at this address")
|
|
292
295
|
if labels.type_of(address) == "exchange":
|
|
@@ -48,6 +48,7 @@ ISSUERS = {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
SUPPORTED = ("eth", "tron", "sol")
|
|
51
|
+
MIN_ACTIONABLE = 1.0 # USDT/USDC below this is dust
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
class FreezeError(RuntimeError):
|
|
@@ -117,8 +118,18 @@ def _one(address: str, chain: str, symbol: str, contract: str) -> Dict:
|
|
|
117
118
|
balance = sum(a["amount"] for a in accts)
|
|
118
119
|
frozen_amount = sum(a["amount"] for a in accts if a["state"] == "frozen")
|
|
119
120
|
frozen = any(a["state"] == "frozen" for a in accts)
|
|
121
|
+
movable = balance - frozen_amount
|
|
120
122
|
return {"token": symbol, "balance": balance, "frozen": frozen,
|
|
121
|
-
"frozen_amount": frozen_amount, "movable":
|
|
123
|
+
"frozen_amount": frozen_amount, "movable": movable,
|
|
124
|
+
# leftover dust is not worth a freeze request, or a victim's panic
|
|
125
|
+
"actionable": movable >= MIN_ACTIONABLE}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def describe_frozen(e: Dict) -> str:
|
|
129
|
+
"""'4,021.97 USDT here is frozen by Tether', or the blacklisting alone when empty."""
|
|
130
|
+
if (e.get("frozen_amount") or 0) >= MIN_ACTIONABLE:
|
|
131
|
+
return f"{e['frozen_amount']:,.2f} {e['token']} here is frozen by {e['issuer']}"
|
|
132
|
+
return f"This address is blacklisted by {e['issuer']} (no {e['token']} left on it)"
|
|
122
133
|
|
|
123
134
|
|
|
124
135
|
def check(address: str, chain: str) -> List[Dict]:
|
|
@@ -144,7 +155,7 @@ def check(address: str, chain: str) -> List[Dict]:
|
|
|
144
155
|
entry.update(_one(address, chain, symbol, tok["contract"]))
|
|
145
156
|
except (FreezeError, KeyError, TypeError, ValueError) as e:
|
|
146
157
|
entry.update({"balance": None, "frozen": None, "frozen_amount": None,
|
|
147
|
-
"movable": None, "error": str(e)})
|
|
158
|
+
"movable": None, "actionable": None, "error": str(e)})
|
|
148
159
|
out.append(entry)
|
|
149
160
|
return out
|
|
150
161
|
|
|
@@ -32,7 +32,7 @@ def analyse(address: str, chain: str = "eth", asset: Optional[dict] = None,
|
|
|
32
32
|
"""Run every check and return a structured result (no printing)."""
|
|
33
33
|
result = {
|
|
34
34
|
"address": address, "chain": chain,
|
|
35
|
-
"asset": asset["symbol"] if asset else chains.symbol(chain),
|
|
35
|
+
"asset": asset["symbol"] if asset else chains.symbol(chain), "depth": depth,
|
|
36
36
|
"generated": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
|
|
37
37
|
"errors": [],
|
|
38
38
|
}
|
|
@@ -154,13 +154,13 @@ def build_guidance(r: dict) -> dict:
|
|
|
154
154
|
for e in r.get("freeze") or []:
|
|
155
155
|
if e.get("frozen"):
|
|
156
156
|
steps.insert(0, {
|
|
157
|
-
"title":
|
|
157
|
+
"title": freeze_mod.describe_frozen(e),
|
|
158
158
|
"body": ("It cannot be moved. Frozen funds can be returned to victims, but only "
|
|
159
159
|
f"through a legal process: tell the police and {e['issuer']} that you "
|
|
160
160
|
"are a victim of this address, and include this report."),
|
|
161
161
|
"urgent": True,
|
|
162
162
|
})
|
|
163
|
-
elif e.get("
|
|
163
|
+
elif e.get("actionable"):
|
|
164
164
|
steps.insert(0, {
|
|
165
165
|
"title": f"Ask for a freeze: {e['movable']:,.2f} {e['token']} is still here",
|
|
166
166
|
"body": (f"{e['how']} It can be moved at any moment, so this is the most "
|
|
@@ -226,11 +226,13 @@ def guidance_markdown(r: dict) -> str:
|
|
|
226
226
|
|
|
227
227
|
|
|
228
228
|
def save_case(r: dict, out_dir: Path, asset: Optional[dict] = None) -> Path:
|
|
229
|
-
"""Write the
|
|
230
|
-
|
|
229
|
+
"""Write the case file (HTML, plus Markdown and JSON) with the victim's next steps;
|
|
230
|
+
returns the HTML one — the file to send."""
|
|
231
|
+
paths = report_mod.generate(r["address"], r["chain"], r.get("depth", 3), 3, out_dir, asset,
|
|
232
|
+
guidance=r.get("guidance"))
|
|
231
233
|
try:
|
|
232
|
-
with open(
|
|
234
|
+
with open(paths["md"], "a", encoding="utf-8") as fh:
|
|
233
235
|
fh.write(guidance_markdown(r))
|
|
234
236
|
except OSError:
|
|
235
237
|
pass
|
|
236
|
-
return
|
|
238
|
+
return paths["html"]
|