Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,8 @@ jobs:
- name: Check anchors 🔗
run: pnpm run check:anchors

- name: Check app links 🔗
run: pnpm run check:app-links

- name: Build
run: pnpm run build
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"lint": "eslint .",
"check:images": "node scripts/check-images.mjs",
"check:anchors": "node scripts/check-anchors.mjs",
"check:app-links": "node scripts/check-app-links.mjs",
"lint-staged": "lint-staged",
"prepare": "husky"
},
Expand Down
Binary file modified public/images/_shared/create_account.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/_shared/login_screen.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/usage-guides/history/events/evm_event_form.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/usage-guides/history/events/swap_event_form.webp
Diff not rendered.
Diff not rendered.
Binary file modified public/images/usage-guides/utilities/help/support_menu.webp
8 changes: 7 additions & 1 deletion scripts/check-anchors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import process from 'node:process';
const ROOT = resolve(import.meta.dirname, '..');
const SKIP_DIRS = new Set(['node_modules', '.git', '.vitepress', 'dist', 'cache', 'scripts', 'public']);

/**
* Never published, so they are neither a link source nor an anchor target.
* Mirrors `srcExclude` in `.vitepress/config.mts` — keep the two in step.
*/
const SKIP_FILES = new Set(['README.md', 'LICENSE.md']);

/** A markdown link to an internal page with an anchor: `](/some/page#the-anchor)`. */
const ANCHORED_LINK = /]\(\/([\w/-]+)#([\w-]+)\)/g;

Expand Down Expand Up @@ -43,7 +49,7 @@ async function walk(dir) {
continue;
found.push(...await walk(join(dir, entry.name)));
}
else if (entry.name.endsWith('.md')) {
else if (!SKIP_FILES.has(entry.name) && entry.name.endsWith('.md')) {
found.push(join(dir, entry.name));
}
}
Expand Down
103 changes: 103 additions & 0 deletions scripts/check-app-links.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* The app deep-links into these docs. Renaming a heading or moving a page silently breaks a link
* inside rotki itself, and nothing in this repo would notice: `check-anchors.mjs` only validates
* links written in the docs, and VitePress never sees the app at all.
*
* The list mirrors `externalLinks` in `frontend/app/shared/external-links.ts`. It has to be
* maintained by hand, because the app is a different repository. When a link here fails, fix the
* docs rather than this list: the app is already released and cannot be changed retroactively.
*
* Only links into this site are listed. Third-party URLs and the old readthedocs changelog are the
* app's problem, not ours.
*/

import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import process from 'node:process';

const ROOT = resolve(import.meta.dirname, '..');
const SKIP_DIRS = new Set(['node_modules', '.git', '.vitepress', 'dist', 'cache', 'scripts', 'public', 'screenshots']);
const SKIP_FILES = new Set(['README.md', 'LICENSE.md']);

/** `page path` -> anchors the app links to. An empty array means the app only links to the page. */
const APP_LINKS = {
'contribution-guides/contribute-as-developer': [
'get-coingecko-asset-identifier',
'get-cryptocompare-asset-identifier',
'add-a-new-language-or-translation',
],
'faq': [],
'premium/devices': [],
'requirement-and-installation/docker': ['session-authentication'],
'usage-guides/data-management/address-book': ['importing-address-book-names-csv'],
'usage-guides/integrations/exchange-keys': ['exchanges-api-keys'],
'usage-guides/integrations/external-services': ['the-graph', 'gnosis-pay'],
'usage-guides/portfolio/accounts': ['import-and-export-blockchain-accounts-csv'],
'usage-guides/tax-accounting/accounting-rules': [
'count-cost-basis-pnl',
'accounting-treatment-swap',
'accounting-treatment-basis-transfer',
],
'usage-guides/tax-accounting/event-types': [],
};

const HEADING = /^#{1,6} +(.+?)\s*$/gm;

/** Matches VitePress: lowercase, collapse every run of non-alphanumerics into one hyphen. */
function slug(heading) {
return heading
.replace(/`/g, '')
.toLowerCase()
.replace(/[^\da-z]+/g, '-')
.replace(/^-|-$/g, '');
}

async function walk(dir) {
const found = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name))
found.push(...await walk(join(dir, entry.name)));
}
else if (entry.name.endsWith('.md') && !SKIP_FILES.has(entry.name)) {
found.push(join(dir, entry.name));
}
}
return found;
}

async function main() {
const files = await walk(ROOT);
const pages = new Map();
for (const file of files) {
const path = relative(ROOT, file).split('\\').join('/').replace(/\.md$/, '');
pages.set(path, await readFile(file, 'utf8'));
}

const errors = [];
for (const [path, anchors] of Object.entries(APP_LINKS)) {
const text = pages.get(path) ?? pages.get(`${path}/index`);
if (text === undefined) {
errors.push(`the app links to a page that does not exist: /${path}\n restore it, or add a redirect — the released app cannot be changed`);
continue;
}

const headings = new Set([...text.matchAll(HEADING)].map(match => slug(match[1])));
for (const anchor of anchors) {
if (!headings.has(anchor))
errors.push(`the app links to a heading that does not exist: /${path}#${anchor}\n a heading on that page must slug to "${anchor}"`);
}
}

if (errors.length > 0) {
console.error(`\n${errors.length} broken app link${errors.length === 1 ? '' : 's'} found:\n`);
for (const error of errors)
console.error(` ✖ ${error}\n`);
process.exit(1);
}

const total = Object.values(APP_LINKS).reduce((sum, anchors) => sum + Math.max(anchors.length, 1), 0);
console.log(`✔ all ${total} links the app makes into these docs resolve`);
}

await main();
42 changes: 41 additions & 1 deletion scripts/check-images.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const SHARED_DIRS = new Set(['_shared', '_external']);
*/
const SKIP_DIRS = new Set(['node_modules', '.git', '.vitepress', 'dist', 'cache', 'scripts']);

/**
* Never published, so their prose is not a page and its `/images/...` mentions are not references.
* Mirrors `srcExclude` in `.vitepress/config.mts` — keep the two in step.
*/
const SKIP_FILES = new Set(['README.md', 'LICENSE.md']);

/** Files that can carry an image reference. */
const TEXT_EXTENSIONS = new Set(['.md', '.vue', '.ts', '.mts', '.js', '.mjs', '.json']);

Expand All @@ -38,7 +44,7 @@ async function walk(dir, predicate) {
continue;
found.push(...await walk(join(dir, entry.name), predicate));
}
else if (predicate(entry.name)) {
else if (!SKIP_FILES.has(entry.name) && predicate(entry.name)) {
found.push(join(dir, entry.name));
}
}
Expand Down Expand Up @@ -119,6 +125,39 @@ function misplacedImages(imageFiles, docPages) {
.map(([image, orphanDir]) => `image is not under a docs page: public/images/${image}\n "${orphanDir}" is not a docs page — move it under its page's directory, or into _shared/ if several pages use it`);
}

/**
* The image sits under a real docs page, but a *different* page is the one referencing it. That
* passes the placement check above and still breaks the convention, because `ls <page dir>` no
* longer answers "which images does this page own".
*
* Only single-referrer images are judged. An image used by several pages belongs in `_shared/`,
* which is what the placement check already says, so flagging it here would just duplicate that.
*/
function strandedImages(references, docPages) {
const errors = [];
for (const [reference, sources] of references) {
const image = reference.replace(/^\/images\//, '');
const parts = image.split('/');
const dir = parts.slice(0, -1).join('/');

if (dir === '' || SHARED_DIRS.has(parts[0]) || !docPages.has(dir))
continue;

const pages = new Set([...sources].map(source =>
relative(ROOT, source).split('\\').join('/').replace(/\.md$/, ''),
));

if (pages.size !== 1)
continue;

const [only] = pages;
if (only !== dir) {
errors.push(`image sits under another page's directory: public/images/${image}\n only ${only}.md references it — move it to public/images/${only}/`);
}
}
return errors;
}

async function main() {
const imageFiles = (await walk(IMAGES_DIR, () => true))
.map(file => relative(IMAGES_DIR, file).split('\\').join('/'));
Expand All @@ -135,6 +174,7 @@ async function main() {
...brokenReferences(references, imageFiles),
...unreferencedImages(imageFiles, referenced),
...misplacedImages(imageFiles, docPages),
...strandedImages(references, docPages),
];

if (errors.length > 0) {
Expand Down
6 changes: 4 additions & 2 deletions usage-guides/data-management/address-book.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ rotki provides an address book for blockchains. This replaces addresses with nam

The address book uses the same filter bar as the rest of rotki, with a pill for `Address`, `Name` and
`Chain`. Chains are shown with their logo and name and picked from a list. There is also a
`Strict chain` pill: with it added, only entries recorded for that exact chain are shown, rather than
also including entries that apply to every chain.
`Strict blockchain filter` pill: with it added, only entries recorded for that exact chain are shown,
rather than also including entries that apply to every chain.

![Filtering the address book](/images/usage-guides/data-management/address-book/filter_bar.webp)

Both the chain selector and the strict-chain checkbox used to sit beside the bar; they are pills
inside it now, so there is a single place to filter from. See
Expand Down
2 changes: 1 addition & 1 deletion usage-guides/data-management/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ To mark an asset as spam, navigate to `Manage Assets → Assets`, find the asset

## Whitelisting of ignored assets

Spam assets are a plague in EVM chains. rotki has an automatic algorithm trying to match assets as spam to not bother the user with automatically ignoring them. You can see all ignored assets in `Manage Assets → Assets` by adding the `Ignored assets` pill to the filter bar and setting it to `Only show ignored assets`. A problem with automatic algorithms marking something as spam is that mistakes can be made and a legit token may be ignored.
Spam assets are a plague in EVM chains. rotki has an automatic algorithm trying to match assets as spam to not bother the user with automatically ignoring them. You can see all ignored assets in `Manage Assets → Assets` by adding the `Ignored assets` pill to the filter bar and setting it to `Only ignored`, which also shows how many there are. A problem with automatic algorithms marking something as spam is that mistakes can be made and a legit token may be ignored.

![Filter ignored assets](/images/usage-guides/data-management/assets/filter_ignored.webp)

Expand Down
11 changes: 7 additions & 4 deletions usage-guides/data-management/prices.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ The `Oracle Prices` page has two tabs:

![Oracle prices listing](/images/usage-guides/data-management/prices/oracle_list.webp)

The **Prices** tab uses the same filter bar as the rest of rotki, with a pill for the `From asset`,
the `To asset`, the `Source` oracle and the `Period`. Assets are shown with their icon and symbol,
and each oracle by the name the table itself uses rather than its raw identifier. The two separate
start and end date filters are now one period pill with a date picker for each end. See
The **Prices** tab uses the same filter bar as the rest of rotki, with a pill for the `Source asset`,
the `Target asset`, the `Source` oracle and the `Period`. Those first two filter the `From Asset` and
`To Asset` columns. Assets are shown with their icon and symbol, and each oracle by the name the
table itself uses rather than its raw identifier. The two separate start and end date filters are now
one period pill with a date picker for each end. See
[Filtering history events](/usage-guides/history/filtering) for the shared syntax and keyboard
handling.

![Filtering cached oracle prices](/images/usage-guides/data-management/prices/filter_bar.webp)

Each row in the **Prices** tab can be edited or deleted. Use this when you know the cached price is wrong (for example, an oracle returned a stale or incorrect value) and want to override it without adding a separate manual entry. Deleting an entry removes the cached value entirely; the next lookup will hit the oracle again.

Click the edit icon on a row to open the edit dialog:
Expand Down
11 changes: 9 additions & 2 deletions usage-guides/history/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ You can add multiple `spend` and `receive` assets.

== Bitcoin Event

![Bitcoin event form](/images/usage-guides/history/events/bitcoin_event_form.webp)

Use this entry type for a decoded Bitcoin or Bitcoin Cash transaction event. The form automatically uses `BTC` for Bitcoin and `BCH` for Bitcoin Cash, so the asset cannot be changed independently.

The key fields are `Transaction ID` (the transaction hash), `Location` (Bitcoin or Bitcoin Cash), `Timestamp`, `Amount`, `Action`, `Sequence Index`, and `Counterparty`. An amount is required. Location and transaction ID are fixed after creation.
Expand All @@ -268,24 +270,27 @@ Advanced options include `Group Identifier` and `Extra Data` for additional cont

== Solana Event

![Solana event form](/images/usage-guides/history/events/solana_event_form.webp)

Similar to an EVM Event but for the Solana blockchain. The location is fixed to Solana. Key fields include:

- `Signature`: The Solana transaction signature (equivalent to a transaction hash).
- `Action`: The same action picker as other event types.
- `Sequence Index`: Order of the event within the transaction.
- `Counterparty`: The protocol or address you interacted with.
- `Address`: The Solana address related to the event.

Advanced options include `Group Identifier` and `Extra Data` for additional context.

== Solana Swap Event

![Solana swap event form](/images/usage-guides/history/events/solana_swap_event_form.webp)

Similar to the EVM Swap Event but for the Solana blockchain. The location is fixed to Solana. You can add multiple `spend` and `receive` assets, along with optional fee entries. Key fields include:

- `Signature`: The Solana transaction signature.
- `Spend` / `Receive`: Lists of assets spent and received in the swap, each with an asset and amount.
- `Fee`: Optional fee entries that can be enabled via the fee checkbox.
- `Address`: The Solana program or contract address involved.
- `Contract Address`: The Solana program or contract address involved.
- `Sequence Index` / `Counterparty`: Same as other event types.
:::

Expand All @@ -297,6 +302,8 @@ Every event form has one `Action` field describing what the event does, written
as a raw type and subtype pair. Open it and you get a searchable list, so you can type `bridge` or
`airdrop` instead of working out which combination expresses it.

![The action picker](/images/usage-guides/history/events/action_picker.webp)

Actions are grouped by intent: `Trade`, `Transfer`, `DeFi deposit & withdraw`, `DeFi borrow & repay`,
`Staking`, `Income`, `Expense`, `Donation`, `NFT`, `Bridge`, `Centralized exchange`, `Validator`,
`Governance`, `Approval`, `Loss` and `Other`. Each row carries a one-line description of what it
Expand Down
Loading
Loading