Repository: rotki/rotki

Issue:
Allow "Notes" and "Link" Information to add to csv report
## Abstract

It would be great if you could make the notes and link field from transactions exportable in the CSV.

## Motivation

This field can contain important information for the person who recieves the CSV.

## Specification

Adding a Setting/Button to Enable/Disable this in the Accounting Settings field would be already enough.



Discussion:
Also make sure for swaps transactions the link is the autogenerated link to the transaction hash
Just checking by link we mean the etherscan link? And I'm not sure what notes means in this context?
The trades object has both a "link" and a "notes" field. This is to add them in the all_events csv report.
Okay gotcha thank you!

Resolving patch:
diff --git a/rotkehlchen/accounting/accountant.py b/rotkehlchen/accounting/accountant.py
--- a/rotkehlchen/accounting/accountant.py
+++ b/rotkehlchen/accounting/accountant.py
@@ -178,6 +178,7 @@ def add_asset_movement_to_events(self, movement: AssetMovement) -> None:
             fee=movement.fee,
             rate=fee_rate,
             timestamp=timestamp,
+            link=movement.link,
         )
 
     def account_for_gas_costs(
@@ -263,6 +264,8 @@ def trade_add_to_sell_events(self, trade: Trade, loan_settlement: bool) -> None:
                 trade_rate=trade.rate,
                 rate_in_profit_currency=selling_rate,
                 timestamp=trade.timestamp,
+                link=trade.link,
+                notes=trade.notes,
             )
         else:
             self.events.add_sell(
@@ -279,6 +282,8 @@ def trade_add_to_sell_events(self, trade: Trade, loan_settlement: bool) -> None:
                 timestamp=trade.timestamp,
                 loan_settlement=True,
                 is_virtual=False,
+                link=trade.link,
+                notes=trade.notes,
             )
 
     def process_history(
@@ -592,6 +597,8 @@ def _process_action(
                 fee_in_asset=action.fee,
                 open_time=action.open_time,
                 close_time=timestamp,
+                link=None,
+                notes=None,
             )
             return True, prev_time
         if action_type == 'asset_movement':
@@ -615,7 +622,19 @@ def _process_action(
             self.events.add_ledger_action(action)
             return True, prev_time
 
-        # else if we get here it's a trade
+        if isinstance(action, AMMTrade) and action.tx_hash:
+            link = f'{self.csvexporter.eth_explorer}{action.tx_hash}'
+        elif hasattr(action, 'link') and action.link:  # type: ignore
+            link = action.link  # type: ignore
+        else:
+            link = ''
+
+        if hasattr(action, 'note'):
+            notes = action.notes  # type: ignore
+        else:
+            notes = ''
+
+        # if we get here it's a trade
         trade = cast(Trade, action)
         # When you buy, you buy with the cost_currency and receive the other one
         # When you sell, you sell the amount in non-cost_currency and receive
@@ -641,6 +660,8 @@ def _process_action(
                 fee_currency=trade.fee_currency,
                 fee_amount=trade.fee,
                 timestamp=trade.timestamp,
+                link=link,
+                notes=notes,
             )
         elif trade.trade_type == TradeType.SELL:
             self.trade_add_to_sell_events(trade, False)
@@ -675,6 +696,8 @@ def _process_action(
                 rate_in_profit_currency=selling_asset_rate,
                 timestamp=trade.timestamp,
                 loan_settlement=True,
+                link=link,
+                notes=notes,
             )
         else:
             # Should never happen
diff --git a/rotkehlchen/accounting/events.py b/rotkehlchen/accounting/events.py
--- a/rotkehlchen/accounting/events.py
+++ b/rotkehlchen/accounting/events.py
@@ -108,6 +108,8 @@ def handle_prefork_asset_buys(
             fee_in_profit_currency: Fee,
             timestamp: Timestamp,
             is_from_prefork_virtual_buy: bool,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         if is_from_prefork_virtual_buy:
             # This way we avoid double counting. For example BTC before BCH/BSV fork
@@ -128,6 +130,8 @@ def handle_prefork_asset_buys(
                 timestamp=timestamp,
                 is_virtual=True,
                 is_from_prefork_virtual_buy=True,
+                link=link,
+                notes=notes,
             )
 
         if bought_asset == A_BTC and timestamp < BTC_BCH_FORK_TS:
@@ -144,6 +148,8 @@ def handle_prefork_asset_buys(
                 timestamp=timestamp,
                 is_virtual=True,
                 is_from_prefork_virtual_buy=True,
+                link=link,
+                notes=notes,
             )
             self.add_buy(
                 location=location,
@@ -157,6 +163,8 @@ def handle_prefork_asset_buys(
                 timestamp=timestamp,
                 is_virtual=True,
                 is_from_prefork_virtual_buy=True,
+                link=link,
+                notes=notes,
             )
 
         if bought_asset == A_BCH and timestamp < BCH_BSV_FORK_TS:
@@ -173,6 +181,8 @@ def handle_prefork_asset_buys(
                 timestamp=timestamp,
                 is_virtual=True,
                 is_from_prefork_virtual_buy=True,
+                link=link,
+                notes=notes,
             )
 
     def handle_prefork_asset_sells(
@@ -224,6 +234,8 @@ def add_buy_and_corresponding_sell(
             fee_currency: Optional[Asset],
             fee_amount: Optional[Fee],
             timestamp: Timestamp,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         """
         Account for the given buy and the corresponding sell if it's a crypto to crypto
@@ -246,6 +258,8 @@ def add_buy_and_corresponding_sell(
             fee_amount=fee_amount,
             timestamp=timestamp,
             is_virtual=False,
+            link=link,
+            notes=notes,
         )
 
         if paid_with_asset.is_fiat() or not self.include_crypto2crypto:
@@ -307,6 +321,8 @@ def add_buy_and_corresponding_sell(
             fee_amount=Fee(ZERO),
             timestamp=timestamp,
             is_virtual=True,
+            link=link,
+            notes=notes,
         )
 
     def add_buy(
@@ -322,6 +338,8 @@ def add_buy(
             timestamp: Timestamp,
             is_virtual: bool = False,
             is_from_prefork_virtual_buy: bool = False,
+            link: Optional[str] = '',
+            notes: Optional[str] = '',
     ) -> None:
         """
         Account for the given buy
@@ -358,6 +376,8 @@ def add_buy(
             fee_in_profit_currency=fee_in_profit_currency,
             timestamp=timestamp,
             is_from_prefork_virtual_buy=is_from_prefork_virtual_buy,
+            link=link,
+            notes=notes,
         )
 
         gross_cost = bought_amount * buy_rate
@@ -392,6 +412,8 @@ def add_buy(
                 paid_with_asset_amount=trade_rate * bought_amount,
                 timestamp=timestamp,
                 is_virtual=is_virtual,
+                link=link,
+                notes=notes,
             )
 
     def add_sell_and_corresponding_buy(
@@ -408,6 +430,8 @@ def add_sell_and_corresponding_buy(
             trade_rate: FVal,
             rate_in_profit_currency: FVal,
             timestamp: Timestamp,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         """
         Account for the given sell and the corresponding buy if it's a crypto to crypto
@@ -447,6 +471,8 @@ def add_sell_and_corresponding_buy(
             rate_in_profit_currency=rate_in_profit_currency,
             timestamp=timestamp,
             is_virtual=False,
+            link=link,
+            notes=notes,
         )
 
         if receiving_asset.is_fiat() or not self.include_crypto2crypto:
@@ -468,6 +494,8 @@ def add_sell_and_corresponding_buy(
             fee_amount=Fee(ZERO),
             timestamp=timestamp,
             is_virtual=True,
+            link=link,
+            notes=notes,
         )
 
     def add_sell(
@@ -485,6 +513,8 @@ def add_sell(
             timestamp: Timestamp,
             loan_settlement: bool = False,
             is_virtual: bool = False,
+            link: Optional[str] = '',
+            notes: Optional[str] = '',
     ) -> None:
         """Account for the given sell action
 
@@ -617,6 +647,8 @@ def add_sell(
                     total_fee_in_profit_currency=total_fee_in_profit_currency,
                     timestamp=timestamp,
                     cost_basis_info=cost_basis_info,
+                    link=link,
+                    notes=notes,
                 )
             else:
                 assert receiving_asset, 'Here receiving asset should have a value'
@@ -639,6 +671,8 @@ def add_sell(
                     cost_basis_info=cost_basis_info,
                     is_virtual=is_virtual,
                     total_bought_cost=total_bought_cost_in_profit_currency,
+                    link=link,
+                    notes=notes,
                 )
 
     def add_loan_gain(
@@ -650,6 +684,8 @@ def add_loan_gain(
             lent_amount: FVal,
             open_time: Timestamp,
             close_time: Timestamp,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         """Account for gains from the given loan
         May raise:
@@ -697,6 +733,8 @@ def add_loan_gain(
                 lent_amount=lent_amount,
                 open_time=open_time,
                 close_time=close_time,
+                link=link,
+                notes=notes,
             )
 
     def add_margin_position(self, margin: MarginPosition) -> None:
@@ -765,6 +803,8 @@ def add_margin_position(self, margin: MarginPosition) -> None:
                 gain_loss_amount=margin.profit_loss,
                 gain_loss_in_profit_currency=net_gain_loss_in_profit_currency,
                 timestamp=margin.close_time,
+                link=margin.link,
+                notes=margin.notes,
             )
 
     def add_defi_event(self, event: DefiEvent) -> None:
diff --git a/rotkehlchen/csv_exporter.py b/rotkehlchen/csv_exporter.py
--- a/rotkehlchen/csv_exporter.py
+++ b/rotkehlchen/csv_exporter.py
@@ -1,4 +1,5 @@
 import csv
+import json
 import logging
 from pathlib import Path
 from tempfile import mkdtemp
@@ -53,6 +54,7 @@
 FILENAME_DEFI_EVENTS_CSV = 'defi_events.csv'
 FILENAME_LEDGER_ACTIONS_CSV = 'ledger_actions.csv'
 FILENAME_ALL_CSV = 'all_events.csv'
+ETH_EXPLORER = 'https://etherscan.io/tx/'
 
 ACCOUNTING_SETTINGS = (
     'include_crypto2crypto',
@@ -103,6 +105,21 @@ def __init__(
         self.all_events: List[Dict[str, Any]] = []
         self.reset()
 
+        # get setting for prefered eth explorer
+        user_settings = self.database.get_settings()
+        try:
+            frontend_settings = json.loads(user_settings.frontend_settings)
+            if (
+                'explorers' in frontend_settings and
+                'ETH' in frontend_settings['explorers'] and
+                'transaction' in frontend_settings['explorers']['ETH']
+            ):
+                self.eth_explorer = frontend_settings['explorers']['ETH']['transaction']
+            else:
+                self.eth_explorer = ETH_EXPLORER
+        except (json.decoder.JSONDecodeError, KeyError):
+            self.eth_explorer = ETH_EXPLORER
+
     def reset(self) -> None:
         """Resets the CSVExporter and prepares it for a new profit/loss run"""
         # TODO: Further specify the types here in more detail. Get rid of "Any"
@@ -325,6 +342,8 @@ def add_to_allevents(
             taxable_bought_cost: FVal = ZERO,
             total_bought_cost: FVal = ZERO,
             cost_basis_info: Optional['CostBasisInfo'] = None,
+            link: Optional[str] = '',
+            notes: Optional[str] = '',
     ) -> None:
         row = len(self.all_events_csv) + 2
         if event_type == EV_BUY:
@@ -390,10 +409,15 @@ def add_to_allevents(
             'time': timestamp,
             'cost_basis': cost_basis_info.serialize() if cost_basis_info else None,
             'is_virtual': is_virtual,
+            'link': link,
+            'notes': notes,
         }
         log.debug('csv event', **entry)
         self.all_events.append(entry)
         new_entry = entry.copy()
+        # deleting and read link and notes for them to be at the end
+        del new_entry['link']
+        del new_entry['notes']
         new_entry['net_profit_or_loss'] = net_profit_or_loss_csv
         new_entry['time'] = self.timestamp_to_date(timestamp)
         new_entry[f'paid_in_{self.profit_currency.symbol}'] = paid_in_profit_currency
@@ -410,6 +434,8 @@ def add_to_allevents(
         del new_entry['paid_in_profit_currency']
         del new_entry['taxable_received_in_profit_currency']
         del new_entry['taxable_bought_cost_in_profit_currency']
+        new_entry['link'] = link
+        new_entry['notes'] = notes
         self.all_events_csv.append(new_entry)
 
     def add_buy(
@@ -425,6 +451,8 @@ def add_buy(
             paid_with_asset_rate: FVal,
             timestamp: Timestamp,
             is_virtual: bool,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         if not self.create_csv:
             return
@@ -448,6 +476,8 @@ def add_buy(
             'cost_basis': 'N/A',
             'is_virtual': is_virtual,
             f'total_bought_cost_in_{self.profit_currency.symbol}': ZERO,
+            'link': link,
+            'notes': notes,
         })
         self.add_to_allevents(
             event_type=EV_BUY,
@@ -461,6 +491,8 @@ def add_buy(
             total_received_in_profit_currency=ZERO,
             timestamp=timestamp,
             is_virtual=is_virtual,
+            link=link,
+            notes=notes,
         )
 
     def add_sell(
@@ -480,6 +512,8 @@ def add_sell(
             is_virtual: bool,
             cost_basis_info: 'CostBasisInfo',
             total_bought_cost: FVal,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         if not self.create_csv:
             return
@@ -521,6 +555,8 @@ def add_sell(
             'cost_basis': cost_basis_info.to_string(self.timestamp_to_date),
             'is_virtual': is_virtual,
             f'total_bought_cost_in_{self.profit_currency.symbol}': total_bought_cost,
+            'link': link,
+            'notes': notes,
         })
         paid_in_profit_currency = ZERO
         self.add_to_allevents(
@@ -539,6 +575,8 @@ def add_sell(
             taxable_bought_cost=taxable_bought_cost,
             total_bought_cost=total_bought_cost,
             cost_basis_info=cost_basis_info,
+            link=link,
+            notes=notes,
         )
 
     def add_loan_settlement(
@@ -550,6 +588,8 @@ def add_loan_settlement(
             total_fee_in_profit_currency: FVal,
             timestamp: Timestamp,
             cost_basis_info: 'CostBasisInfo',
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         if not self.create_csv:
             return
@@ -569,6 +609,8 @@ def add_loan_settlement(
             f'loss_in_{self.profit_currency.symbol}': loss_csv,
             'cost_basis': cost_basis_info.to_string(self.timestamp_to_date),
             'time': self.timestamp_to_date(timestamp),
+            'link': link,
+            'notes': notes,
         })
         self.add_to_allevents(
             event_type=EV_LOAN_SETTLE,
@@ -582,6 +624,8 @@ def add_loan_settlement(
             total_received_in_profit_currency=ZERO,
             timestamp=timestamp,
             cost_basis_info=cost_basis_info,
+            link=link,
+            notes=notes,
         )
 
     def add_loan_profit(
@@ -593,6 +637,8 @@ def add_loan_profit(
             lent_amount: FVal,
             open_time: Timestamp,
             close_time: Timestamp,
+            link: Optional[str],
+            notes: Optional[str],
     ) -> None:
         if not self.create_csv:
             return
@@ -605,6 +651,8 @@ def add_loan_profit(
             'gained_amount': gained_amount,
             'lent_amount': lent_amount,
             f'profit_in_{self.profit_currency.symbol}': gain_in_profit_currency,
+            'link': link,
+            'notes': notes,
         })
         self.add_to_allevents(
             location=location,
@@ -617,6 +665,8 @@ def add_loan_profit(
             taxable_received_in_profit_currency=gain_in_profit_currency,
             total_received_in_profit_currency=gain_in_profit_currency,
             timestamp=close_time,
+            link=link,
+            notes=notes,
         )
 
     def add_margin_position(
@@ -627,6 +677,8 @@ def add_margin_position(
             gain_loss_amount: FVal,
             gain_loss_in_profit_currency: FVal,
             timestamp: Timestamp,
+            link: str,
+            notes: str,
     ) -> None:
         if not self.create_csv:
             return
@@ -641,6 +693,8 @@ def add_margin_position(
             'gain_loss_asset': str(gain_loss_asset),
             'gain_loss_amount': gain_loss_amount,
             f'profit_loss_in_{self.profit_currency.symbol}': gain_loss_in_profit_currency,
+            'link': link,
+            'notes': notes,
         })
 
         paid_in_profit_currency = ZERO
@@ -665,6 +719,8 @@ def add_margin_position(
             taxable_received_in_profit_currency=received_in_profit_currency,
             total_received_in_profit_currency=received_in_profit_currency,
             timestamp=timestamp,
+            link=link,
+            notes=notes,
         )
 
     def add_asset_movement(
@@ -675,6 +731,7 @@ def add_asset_movement(
             fee: Fee,
             rate: FVal,
             timestamp: Timestamp,
+            link: str,
     ) -> None:
         if not self.create_csv:
             return
@@ -686,6 +743,8 @@ def add_asset_movement(
             'moving_asset': str(asset),
             'fee_in_asset': fee,
             f'fee_in_{self.profit_currency.symbol}': fee * rate,
+            'link': link,
+            'notes': '',
         })
         self.add_to_allevents(
             event_type=EV_ASSET_MOVE,
@@ -698,6 +757,8 @@ def add_asset_movement(
             taxable_received_in_profit_currency=ZERO,
             total_received_in_profit_currency=ZERO,
             timestamp=timestamp,
+            link=link,
+            notes='',
         )
 
     def add_tx_gas_cost(
@@ -727,6 +788,8 @@ def add_tx_gas_cost(
             taxable_received_in_profit_currency=ZERO,
             total_received_in_profit_currency=ZERO,
             timestamp=timestamp,
+            link='',
+            notes='',
         )
 
     def add_defi_event(
@@ -738,6 +801,10 @@ def add_defi_event(
             return
 
         profit_loss_sum = FVal(sum(profit_loss_in_profit_currency_list))
+        if event.tx_hash:
+            link = f'{self.eth_explorer}{event.tx_hash}'
+        else:
+            link = ''
         self.defi_events_csv.append({
             'time': self.timestamp_to_date(event.timestamp),
             'type': str(event.event_type),
@@ -748,6 +815,8 @@ def add_defi_event(
             f'profit_loss_in_{self.profit_currency.symbol}': profit_loss_sum,
             'tx_hash': event.tx_hash if event.tx_hash else '',
             'description': event.to_string(timestamp_converter=self.timestamp_to_date),
+            'link': link,
+            'notes': '',
         })
 
         paid_asset: Union[EmptyStr, Asset]
@@ -784,6 +853,8 @@ def add_defi_event(
                 taxable_received_in_profit_currency=received_in_profit_currency,
                 total_received_in_profit_currency=received_in_profit_currency,
                 timestamp=event.timestamp,
+                link=link,
+                notes='',
             )
 
     def add_ledger_action(
@@ -831,6 +902,8 @@ def add_ledger_action(
             taxable_received_in_profit_currency=received_in_profit_currency,
             total_received_in_profit_currency=received_in_profit_currency,
             timestamp=action.timestamp,
+            link=action.link,
+            notes=action.notes,
         )
 
     def create_files(self, dirpath: Path) -> Tuple[bool, str]:


Test patch:
diff --git a/rotkehlchen/tests/api/test_history.py b/rotkehlchen/tests/api/test_history.py
--- a/rotkehlchen/tests/api/test_history.py
+++ b/rotkehlchen/tests/api/test_history.py
@@ -413,7 +413,7 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
         count = 0
         for row in reader:
             assert_csv_formulas_trades(row, profit_currency)
-            assert len(row) == 17
+            assert len(row) == 19
             assert row['location'] in ('kraken', 'bittrex', 'binance', 'poloniex')
             assert row['type'] in ('buy', 'sell')
             assert row['asset'] is not None
@@ -434,6 +434,7 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
             assert row['cost_basis'] is not None
             assert row['is_virtual'] in ('True', 'False')
             assert row[f'total_bought_cost_in_{profit_currency.identifier}'] is not None
+            assert 'link' in row and 'notes' in row
 
             count += 1
     num_trades = 19
@@ -443,7 +444,7 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
         reader = csv.DictReader(csvfile)
         count = 0
         for row in reader:
-            assert len(row) == 7
+            assert len(row) == 9
             assert row['location'] == 'poloniex'
             assert create_timestamp(row['open_time'], '%d/%m/%Y %H:%M:%S') > 0
             assert create_timestamp(row['close_time'], '%d/%m/%Y %H:%M:%S') > 0
@@ -459,13 +460,15 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
         reader = csv.DictReader(csvfile)
         count = 0
         for row in reader:
-            assert len(row) == 6
+            assert len(row) == 8
             assert create_timestamp(row['time'], '%d/%m/%Y %H:%M:%S') > 0
             assert row['exchange'] in [str(x) for x in SUPPORTED_EXCHANGES]
             assert row['type'] in ('deposit', 'withdrawal')
             assert row['moving_asset'] is not None
             assert FVal(row['fee_in_asset']) >= ZERO
             assert FVal(row[f'fee_in_{profit_currency.identifier}']) >= ZERO
+            assert row['link'] != ''
+            assert row['notes'] == ''
             count += 1
     num_asset_movements = 11
     assert count == num_asset_movements, 'Incorrect amount of asset movement CSV entries found'
@@ -487,13 +490,14 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
         reader = csv.DictReader(csvfile)
         count = 0
         for row in reader:
-            assert len(row) == 6
+            assert len(row) == 8
             assert row['location'] == 'bitmex'
             assert row['name'] is not None
             assert create_timestamp(row['time'], '%d/%m/%Y %H:%M:%S') > 0
             assert row['gain_loss_asset'] is not None
             assert FVal(row['gain_loss_amount']) is not None
             assert FVal(row[f'profit_loss_in_{profit_currency.identifier}']) is not None
+            assert 'link' in row and 'notes' in row
             count += 1
     num_margins = 2
     assert count == num_margins, 'Incorrect amount of margin CSV entries found'
@@ -519,7 +523,7 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
         count = 0
         for row in reader:
             assert_csv_formulas_all_events(row, profit_currency)
-            assert len(row) == 16
+            assert len(row) == 18
             assert row['location'] in (
                 'kraken',
                 'bittrex',
@@ -550,6 +554,7 @@ def assert_csv_export_response(response, profit_currency, csv_dir):
             assert row['cost_basis'] is not None
             assert row[f'total_bought_cost_in_{profit_currency.identifier}'] is not None
             assert row[f'total_received_in_{profit_currency.identifier}'] is not None
+            assert 'link' in row and 'notes' in row
             count += 1
     assert count == (
         num_trades + num_loans + num_asset_movements + num_transactions + num_margins
diff --git a/rotkehlchen/tests/unit/test_accounting.py b/rotkehlchen/tests/unit/test_accounting.py
--- a/rotkehlchen/tests/unit/test_accounting.py
+++ b/rotkehlchen/tests/unit/test_accounting.py
@@ -797,6 +797,8 @@ def test_defi_event_zero_amount(accountant):
         'taxable_received_in_profit_currency': ZERO,
         'time': 1467279735,
         'type': 'defi_event',
+        'link': 'https://etherscan.io/tx/0x49c67445d26679623f9b7d56a8be260a275cb6744a1c1ae5a8d6883a5a5c03de',  # noqa: E501
+        'notes': '',
     }