This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Rotki is a privacy-focused crypto portfolio management and tax reporting application with:
- Python backend (Flask API, accounting engine, blockchain interactions)
- Vue.js/TypeScript frontend (Electron desktop app + web interface)
- Rust service (Colibri - performance-critical components)
# Install dependencies (requires Node.js 22+, Python 3.11+, pnpm 10+)
pnpm install
# Run full development environment (frontend + backend + colibri)
pnpm dev
# Run web-only development (no Electron)
pnpm dev:web# Run backend server
python -m rotkehlchen --api-port 4242 --websockets-port 4333
# Run all backend tests
uv run python pytestgeventwrapper.py
# Run specific test file
uv run python pytestgeventwrapper.py rotkehlchen/tests/api/test_assets.py
# Run specific test
uv run python pytestgeventwrapper.py rotkehlchen/tests/api/test_assets.py::test_add_user_asset
# Lint Python code
uv run make lint
# Format Python code
uv run make formatIMPORTANT: All frontend commands should be run from the frontend/ directory, NOT frontend/app/
# Navigate to frontend directory first
cd frontend
# Install dependencies
pnpm install --frozen-lockfile
# Lint and fix
pnpm run lint:fix
# Run the app
pnpm run dev
# Clean frontend modules
pnpm run clean:modules
# Build frontend
pnpm run build
# Run tests
pnpm run test:unit
# Type check
pnpm run typecheckcd colibri
cargo build
cargo run -- --database ../data/global.db --port 4343rotkehlchen/- Main Python packageapi/- REST API and WebSocket handlerschain/- Blockchain integrations (Ethereum, Bitcoin, L2s)exchanges/- Exchange integrations (Binance, Kraken, etc.)accounting/- Tax calculation and accounting logicdb/- Database layer with SQLiteexternalapis/- External service integrationsglobaldb/- Global assets database managementhistory/- Transaction and event history handling
frontend/app/- Vue.js Electron applicationsrc/- Application source codecomponents/- Reusable Vue componentspages/- Route pagescomposables/- Vue composition API utilitiesstore/- Pinia state management
electron/- Electron main process codetests/- Test suites
- API Communication: Frontend communicates with backend via REST API (port 4242) and WebSockets (port 4333)
- Database: SQLite for user data, separate global database for assets
- State Management: Pinia stores in frontend, coordinated with backend state
- Event System: History events are the core abstraction for all blockchain/exchange activities
- Plugin Architecture: Modular design for adding new blockchains and exchanges
- All frontend commands should be run from
frontend/directory, NOTfrontend/app/ - The main application code is in
frontend/app/src/
- Split complex logic: Break down large templates and script logic into smaller, focused composables
- Component decomposition: Split large components into smaller, reusable sub-components
- Logical separation: Each composable should have a single, well-defined responsibility
- Maintainability focus: Prioritize code readability and maintainability over brevity
- Use VueUse utilities for reactive state management
- IMPORTANT: Use
get()andset()from VueUse instead of.valuewhen working with refs
- Always use explicit types for refs:
ref<boolean>(false)instead ofref(false) - Always use explicit types for computed:
computed<boolean>(() => ...)instead ofcomputed(() => ...) - Always return explicit types from functions:
function getName(): string { ... } - Always type reactive variables:
const isLoading = ref<boolean>(false) - Always type computed properties:
const fullName = computed<string>(() => ...)
// ✅ Correct - Explicit typing with VueUse get/set
import { get, set } from '@vueuse/shared';
const isVisible = ref<boolean>(true);
const count = ref<number>(0);
const items = ref<string[]>([]);
const user = ref<User>();
const isEven = computed<boolean>(() => get(count) % 2 === 0);
const formattedName = computed<string>(() => `${get(firstName)} ${get(lastName)}`);
function getUserById(id: number): User | undefined {
return get(users).find(user => user.id === id) || undefined;
}
function updateCount(newValue: number): void {
set(count, newValue);
}
async function fetchData(): Promise<ApiResponse> {
return await $fetch('/api/data');
}// ❌ Incorrect - Missing explicit types
const isVisible = ref(true);
const count = ref(0);
const items = ref([]);
const user = ref();
const isEven = computed(() => count.value % 2 === 0);
const formattedName = computed(() => `${firstName.value} ${lastName.value}`);
function getUserById(id: number) {
return users.value.find(user => user.id === id) || undefined;
}
async function fetchData() {
return await $fetch('/api/data');
}- VueUse utilities like
get(),set(),toRefs(),computed()etc. are auto-imported - Use Pinia for state management - stores are in
frontend/app/src/store/ - TypeScript is strict - ensure proper typing
- Imports
- Definitions (
defineProps,defineEmits, etc.) - I18n & vue-router
- Reactive state variables
- Pinia stores
- Composables
- Computed properties
- Methods
- Watchers
- Lifecycle hooks
- Exposed methods
- Use
defineProps<{}>()instead ofdefineProps({}) - Simplified emit definitions:
const emit = defineEmits<{ 'update:msg': [msg: string]; }>();
- Use
$stylein templates instead ofuseCssModules - Use
$attrsin templates instead ofuseAttrs
- State definitions
- Computed getters
- Actions
- Optional watchers
- Transitioning from scoped SCSS with BEM to tailwind
- Follow existing patterns for consistency
- Run tests with
pnpm run test:unitfromfrontend/directory - Use Vitest for unit tests with Vue Test Utils
- Unit test file naming:
.spec.tsfiles should follow the naming of the tested file and be located in the same folder// Example structure: src/modules/balances/use-balances-store.ts src/modules/balances/use-balances-store.spec.ts src/composables/accounts/use-account-import-export.ts src/composables/accounts/use-account-import-export.spec.ts - Component tests should follow existing patterns in
frontend/app/tests/and*.spec.ts
The contribution guide can be seen here: https://docs.rotki.com/contribution-guides/contribute-as-developer.html
- To add an exchange you will need to add the new exchange under the
exchanges/directory. A nice example is bitfinxex.py - For each exchange you need to implement the basic method of the
ExchangeInterfacesuperclass:- Authentication for the api key/secret whatever the exchange API uses.
- Fetch balances from the exchange
- Fetch deposits/withdrawals (also called asset movements) and trades.
- You will need to create some tests with mocked data
As an example decoder, we can look at MakerDAO.
It needs to contain a class that inherits from the DecoderInterface and is named ModulenameDecoder.
Note: If your new decoder decodes an airdrop's claiming event and this airdrop is present in the data repo airdrop index with has_decoder as false, please update that also.
It needs to implement a method called counterparties() which returns a list of counterparties that can be associated with the transactions of this module. Most of the time these are protocol names like uniswap-v1, makerdao_dsr, etc.
These are defined in the constants.py file.
The addresses_to_decoders() method maps any contract addresses that are identified in the transaction with the specific decoding function that can decode it. This is optional.
The decoding_rules() define any functions that should simply be used for all decoding so long as this module is active. This is optional.
The enricher_rules() define any functions that would be used as long as this module is active to analyze already existing decoded events and enrich them with extra information we can decode thanks to this module. This is optional.
In very simple terms, the way the decoding works is that we go through all the transactions of the user and we apply all decoders to each transaction event that touches a tracked address. The first decoder that matches creates a decoded event.
The event creation consists of creating a HistoryBaseEntry. These are the most basic form of events in rotki and are used everywhere. The fields as far as decoded transactions are concerned are explained below:
event_identifieris always the transaction hash. This identifies history events in the same transaction.sequence_indexis the order of the event in the transaction. Many times this is the log index, but decoders tend to play with this to make events appear in a specific way.assetis the asset involved in the event.balanceis the balance of the involved asset.timestampis the Unix timestamp in milliseconds.locationis the location. Almost alwaysLocation.BLOCKCHAINunless we got a specific location for the protocol of the transaction.location_labelis the initiator of the transaction.notesis the human-readable description to be seen by the user for the transaction.event_typeis the main type of the event. (see next section)event_subtypeis the subtype of the event. (see next section)counterpartyis the counterparty/target of the transaction. For transactions that interact with protocols, we tend to use theCPT_XXXconstants here.
Each combination of event type and subtype and counterparty creates a new unique event type. This is important as they are all treated differently in many parts of rotki, including the accounting. But most importantly this is what determines how they appear in the UI!
The mapping of these HistoryEvents types, subtypes, and categories is done in rotkehlchen/accounting/constants.py.
- All byte signatures should be a constant byte literal. Like
SPARK_STAKE_SIGNATURE: Final = b'\xdc\xbc\x1c\x05$\x0f1\xff:\xd0g\xef\x1e\xe3\\\xe4\x99wbu.:\tR\x84uED\xf4\xc7\t\xd7' - Don't put assets as constants. If you need a constant just use the asset identifier as a string and compare against it.
- Uses pytest with gevent for async testing
- Extensive fixtures in
rotkehlchen/tests/fixtures/ - Mock external APIs for deterministic tests
- Database fixtures for integration testing
- Make sure that all EVM addresses constant literals you add in the code are properly checksummed. The output of to_checksum_address() is what they should be. Do not use string_to_evm_address(). This does not checksum the address.
- Do not VCR tests. Let the human developers do it. That means do not put
@pytest.mark.vcron any decoder tests you write. - For Etherscan use the api key from ETHERSCAN_API_KEY env variable and use etherscan v2. It's as v1 but using https://api.etherscan.io/v2/api?chainid=${chainid} . As described here: https://docs.etherscan.io/etherscan-v2. If there is no API key in the env variable then prompt the user for it when you need to query etherscan.
- Vitest for unit tests with Vue Test Utils
- Cypress for E2E testing
- Component testing with jsdom
# Build Electron app for current platform
pnpm electron:build
# Package for distribution (requires proper environment setup)
python package.pypyproject.toml- Python project configuration, linting rulesfrontend/app/package.json- Frontend dependencies and scriptsMakefile- Common development tasks.github/workflows/- CI/CD pipelines
- Always run through
pytestgeventwrapper.pyfor backend tests to ensure proper gevent patching - Frontend uses strict TypeScript - ensure types are properly defined
- Follow existing code patterns - the codebase has established conventions
- Use the existing test infrastructure - comprehensive fixtures are available
- WebSocket messages follow specific format - check
api/websockets/typedefs.py - For all python backend constants make sure to use the
Finaltype specifier.
- Commits should be just to the point, not too long and not too short.
- Commit titles should not exceed 50 characters.
- Give a description of what the commit does in a short title. If more information is needed, then add a blank line and afterward elaborate with as much information as needed.
- Commits should do one thing; if two commits both do the same thing, that's a good sign they should be combined.
- Do not add Co-Authored-By: Claude or similar anywhere in the commit.
- Do not add Co-Authored-By: Claude or similar anywhere.
- Frontend build fails: Run
pnpm run clean:modulesthenpnpm install --frozen-lockfile - Backend gevent errors: Always use
pytestgeventwrapper.py, never direct pytest - WebSocket connection issues: Check ports 4242 (API) and 4333 (WS) are free
- EVM addresses MUST be checksummed: ✅ CORRECT: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c' ❌ WRONG: '0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c'
- If you see "Invalid XXX account in DB" it's almost certain the address is not checksummed. Always checksum addresses you use with to_checksum_address
- string_to_evm_address() is just a no-op typing function. It will not checksum the literal argument to a checksummed evm address. That means you should make sure to only give checksummed EVM address literals to it