Repository: rotki/rotki

Issue:
Add Sushiswap support
## Abstract

It seems that Sushiswap transactions are not currently picked up as trades in the profit/loss section of the application

## Motivation

Sushiswap is one of the primary decentralised exchanges and I would imagine that since it is a Uniswap fork, it should take less code to implement this feature than to implement tracking for an entirely different decentralised exchange

## Specification

N/A



Discussion:
Hey @JayWelsh thanks a lot for the feature request. We will see what can be done! In the meantime feel free to join our discord chat to keep tabs on our development and also chat with other users: https://discord.gg/aGCxHG7
Thanks a lot @LefterisJP, much appreciated! 🌟 
For this one there is a graph that is basically a fork of the uniswap one. Assuming that information in the graphs can be used a lot of code can be shared

Resolving patch:
diff --git a/rotkehlchen/chain/ethereum/interfaces/ammswap/typing.py b/rotkehlchen/chain/ethereum/interfaces/ammswap/typing.py
--- a/rotkehlchen/chain/ethereum/interfaces/ammswap/typing.py
+++ b/rotkehlchen/chain/ethereum/interfaces/ammswap/typing.py
@@ -80,7 +80,7 @@ class EventType(Enum):
     MINT_SUSHISWAP = 3
     BURN_SUSHISWAP = 4
 
-    def __str__(self) -> str:
+    def serialize_for_db(self) -> str:
         if self == EventType.MINT_UNISWAP:
             return 'mint'
         if self == EventType.BURN_UNISWAP:
@@ -92,6 +92,32 @@ def __str__(self) -> str:
         # else
         raise RuntimeError(f'Corrupt value {self} for EventType -- Should never happen')
 
+    @classmethod
+    def deserialize_from_db(
+            cls,
+            value: str,
+    ) -> 'EventType':
+        """May raise DeserializationError if anything is wrong"""
+        if value == 'mint':
+            return EventType.MINT_UNISWAP
+        if value == 'burn':
+            return EventType.BURN_UNISWAP
+        if value == 'mint sushiswap':
+            return EventType.MINT_SUSHISWAP
+        if value == 'burn sushiswap':
+            return EventType.BURN_SUSHISWAP
+
+        raise DeserializationError(f'Unexpected value {value} at AMM EventType deserialization')
+
+    def __str__(self) -> str:
+        if self in (EventType.MINT_UNISWAP, EventType.MINT_SUSHISWAP):
+            return 'mint'
+        if self in (EventType.BURN_UNISWAP, EventType.BURN_SUSHISWAP):
+            return 'burn'
+
+        # else
+        raise RuntimeError(f'Corrupt value {self} for EventType -- Should never happen')
+
 
 LiquidityPoolEventDBTuple = (
     Tuple[
@@ -147,23 +173,7 @@ def deserialize_from_db(
         10 - usd_price
         11 - lp_amount
         """
-        db_event_type = event_tuple[4]
-        if db_event_type not in {str(event_type) for event_type in EventType}:
-            raise DeserializationError(
-                f'Failed to deserialize event type. Unknown event: {db_event_type}.',
-            )
-
-        if db_event_type == str(EventType.MINT_UNISWAP):
-            event_type = EventType.MINT_UNISWAP
-        elif db_event_type == str(EventType.BURN_UNISWAP):
-            event_type = EventType.BURN_UNISWAP
-        elif db_event_type == str(EventType.MINT_SUSHISWAP):
-            event_type = EventType.MINT_SUSHISWAP
-        elif db_event_type == str(EventType.BURN_SUSHISWAP):
-            event_type = EventType.BURN_SUSHISWAP
-        else:
-            raise ValueError(f'Unexpected event type case: {db_event_type}.')
-
+        event_type = EventType.deserialize_from_db(event_tuple[4])
         token0 = deserialize_ethereum_token_from_db(identifier=event_tuple[6])
         token1 = deserialize_ethereum_token_from_db(identifier=event_tuple[7])
 
@@ -188,7 +198,7 @@ def to_db_tuple(self) -> LiquidityPoolEventDBTuple:
             self.log_index,
             str(self.address),
             int(self.timestamp),
-            str(self.event_type),
+            self.event_type.serialize_for_db(),
             str(self.pool_address),
             self.token0.identifier,
             self.token1.identifier,


Test patch:
diff --git a/frontend/app/tests/e2e/pages/defi-page.ts b/frontend/app/tests/e2e/pages/defi-page.ts
--- a/frontend/app/tests/e2e/pages/defi-page.ts
+++ b/frontend/app/tests/e2e/pages/defi-page.ts
@@ -1,4 +1,4 @@
-import { MODULE_AAVE, MODULES } from '@/services/session/consts';
+import { Module } from '@/services/session/consts';
 
 export class DefiPage {
   visit() {
@@ -12,9 +12,10 @@ export class DefiPage {
   }
 
   selectModules() {
-    for (let i = 0; i < MODULES.length; i++) {
-      const module = MODULES[i];
-      if (module === MODULE_AAVE) {
+    const values = Object.values(Module);
+    for (let i = 0; i < values.length; i++) {
+      const module = values[i];
+      if (module === Module.AAVE) {
         continue;
       }
 
@@ -22,7 +23,7 @@ export class DefiPage {
       cy.get(`#defi-module-${module}`).find('button').click();
       cy.get(`#defi-module-${module}`).should('not.be.visible');
     }
-    cy.get(`#defi-module-${MODULE_AAVE}`).should('be.visible');
+    cy.get(`#defi-module-${Module.AAVE}`).should('be.visible');
     cy.get('.defi-wizard__select-accounts').click();
   }
 
diff --git a/frontend/app/tests/e2e/tsconfig.json b/frontend/app/tests/e2e/tsconfig.json
--- a/frontend/app/tests/e2e/tsconfig.json
+++ b/frontend/app/tests/e2e/tsconfig.json
@@ -4,8 +4,8 @@
     "sourceMap": true,
     "baseUrl": "../../node_modules",
     "module": "commonjs",
-    "target": "es5",
-    "lib": ["dom", "es6"],
+    "target": "es2018",
+    "lib": ["dom", "es2018"],
     "types": ["cypress"],
     "paths": {
       "@/*": ["../src/*"]
diff --git a/frontend/app/tests/unit/components/defi/wizard/module-selector.spec.ts b/frontend/app/tests/unit/components/defi/wizard/module-selector.spec.ts
--- a/frontend/app/tests/unit/components/defi/wizard/module-selector.spec.ts
+++ b/frontend/app/tests/unit/components/defi/wizard/module-selector.spec.ts
@@ -3,6 +3,7 @@ import Vue from 'vue';
 import Vuetify from 'vuetify';
 import ModuleSelector from '@/components/defi/wizard/ModuleSelector.vue';
 import { api } from '@/services/rotkehlchen-api';
+import { Module } from '@/services/session/consts';
 import store from '@/store/store';
 import { GeneralSettings } from '@/typing/types';
 import '../../../i18n';
@@ -27,7 +28,7 @@ describe('ModuleSelector.vue', () => {
     document.body.setAttribute('data-app', 'true');
     const settings: GeneralSettings = {
       ...store.state.session!.generalSettings,
-      activeModules: ['aave']
+      activeModules: [Module.AAVE]
     };
     store.commit('session/generalSettings', settings);
 
diff --git a/frontend/app/tests/unit/store/session/actions.spec.ts b/frontend/app/tests/unit/store/session/actions.spec.ts
--- a/frontend/app/tests/unit/store/session/actions.spec.ts
+++ b/frontend/app/tests/unit/store/session/actions.spec.ts
@@ -1,4 +1,5 @@
 import { api } from '@/services/rotkehlchen-api';
+import { Module } from '@/services/session/consts';
 import {
   QueriedAddresses,
   QueriedAddressPayload
@@ -38,7 +39,7 @@ describe('session:actions', () => {
   test('addQueriedAddress', async () => {
     expect.assertions(2);
     const payload: QueriedAddressPayload = {
-      module: 'makerdao_dsr',
+      module: Module.MAKERDAO_DSR,
       address: '0x52bc44d5378309EE2abF1539BF71dE1b7d7bE3b5'
     };
     const response: QueriedAddresses = {
@@ -53,7 +54,7 @@ describe('session:actions', () => {
   test('addQueriedAddress fails', async () => {
     expect.assertions(3);
     const payload: QueriedAddressPayload = {
-      module: 'makerdao_dsr',
+      module: Module.MAKERDAO_DSR,
       address: '0x52bc44d5378309EE2abF1539BF71dE1b7d7bE3b5'
     };
 
@@ -73,7 +74,7 @@ describe('session:actions', () => {
     };
     store.commit('session/queriedAddresses', originalState);
     const payload: QueriedAddressPayload = {
-      module: 'makerdao_dsr',
+      module: Module.MAKERDAO_DSR,
       address: '0x52bc44d5378309EE2abF1539BF71dE1b7d7bE3b5'
     };
 
@@ -90,7 +91,7 @@ describe('session:actions', () => {
     };
     store.commit('session/queriedAddresses', originalState);
     const payload: QueriedAddressPayload = {
-      module: 'makerdao_dsr',
+      module: Module.MAKERDAO_DSR,
       address: '0x52bc44d5378309EE2abF1539BF71dE1b7d7bE3b5'
     };