Files
actual-helpers/sync-crypto.js
T
Jacob Schooley b2a45903f9 Add sync-crypto.js for other cryptocurrencies besides just BTC (#31)
* add sync-crypto.js

* update crypto payee name
2025-11-17 09:44:16 -06:00

111 lines
4.4 KiB
JavaScript

const { closeBudget, openBudget, getTransactions, getAccountNote, getAccountBalance, getTagValue, ensurePayee } = require('./utils');
const api = require('@actual-app/api');
function getValueAtPath(obj, path) {
const keys = path.split('.').filter(Boolean);
return keys.reduce((acc, key) => {
const match = key.match(/^([^\[\]]+)(\[(\d+)\])?$/);
if (match) {
const property = match[1];
const index = match[3];
acc = acc[property];
if (index !== undefined) {
acc = acc[parseInt(index, 10)];
}
} else {
acc = acc[key];
}
return acc;
}, obj);
}
async function getCryptoPrice(crypto, krakenPath) {
const url = process.env[`CRYPTO_PRICE_URL_${crypto.toUpperCase()}`] || `https://api.kraken.com/0/public/Ticker?pair=${crypto}usd`;
if (!krakenPath && !process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`]) {
console.error(`No Kraken path provided for ${crypto}. Please set CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variable or provide krakenPath argument.`);
return undefined;
}
const path = process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`] || `result.${krakenPath}.c[0]`;
try {
const response = await fetch(url);
const json = await response.json();
return getValueAtPath(json, path);
} catch (error) {
console.error(`Error fetching price for ${crypto}:`, error);
return undefined;
}
}
(async () => {
await openBudget();
const payeeId = await ensurePayee(process.env.CRYPTO_PAYEE_NAME || 'Crypto Price Change');
const accounts = await api.getAccounts();
for (const account of accounts) {
if (account.closed) {
continue;
}
// Need to set cutoff date to tomorrow to ensure we get the latest transactions
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() + 1);
const note = await getAccountNote(account, cutoffDate);
// Get cryptos list from the note
let cryptos = getTagValue(note, 'CRYPTO', null);
if (!cryptos) continue;
cryptos = cryptos.split(',').map(c => c.trim()).filter(c => c.length > 0);
if (cryptos.length === 0) continue;
console.log(`Processing account: ${account.name}, Cryptos: ${cryptos.join(', ')}`);
let totalTargetBalance = 0;
for (const crypto of cryptos) {
const cryptoTag = getTagValue(note, crypto, 0.0);
if (!cryptoTag) continue;
cryptoTagSplit = cryptoTag.split(',');
// Get the amount from the first part of the tag
amount = parseFloat(cryptoTagSplit[0]);
if (isNaN(amount) || amount <= 0) continue;
console.log(`Crypto: ${crypto}, Amount: ${amount}`);
// Get Kraken path from the second part of the tag, if it exists
let krakenPath = cryptoTagSplit[1]
console.log(`Kraken Path: ${krakenPath}`);
// Get the crypto price
const cryptoPrice = await getCryptoPrice(crypto, krakenPath);
if (!cryptoPrice) {
console.error(`Unable to retrieve price for ${crypto}. Check your CRYPTO_PRICE_URL_${crypto.toUpperCase()} and CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variables`);
continue;
}
console.log(`Crypto: ${crypto}, Price: ${cryptoPrice}`);
const targetBalance = Math.round(cryptoPrice * amount * 100);
totalTargetBalance += targetBalance;
}
// Now we need to check if the total target balance is different from the current balance and update it
const currentBalance = await getAccountBalance(account, cutoffDate);
const diff = totalTargetBalance - currentBalance;
console.log(`Account: ${account.name}, Total Target Balance: ${totalTargetBalance}, Current Balance: ${currentBalance}, Diff: ${diff}`);
if (diff != 0) {
await api.importTransactions(account.id, [{
date: new Date(),
payee: payeeId,
amount: diff,
cleared: true,
reconciled: true,
notes: `Updated Crypto Price on ${new Date().toLocaleString()} (${cryptos.join(', ')})`,
}])
}
}
await closeBudget();
})();