├── .gitignore ├── Kraken.lua ├── LICENSE ├── README.md └── screenshots └── balances.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | -------------------------------------------------------------------------------- /Kraken.lua: -------------------------------------------------------------------------------- 1 | -- Inofficial Kraken Extension (www.kraken.com) for MoneyMoney 2 | -- Fetches balances from Kraken API and returns them as securities 3 | -- 4 | -- Username: Kraken API Key 5 | -- Password: Kraken API Secret 6 | -- 7 | -- Copyright (c) 2024 aaronk6, zacczakk 8 | -- 9 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 10 | -- of this software and associated documentation files (the "Software"), to deal 11 | -- in the Software without restriction, including without limitation the rights 12 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | -- copies of the Software, and to permit persons to whom the Software is 14 | -- furnished to do so, subject to the following conditions: 15 | -- 16 | -- The above copyright notice and this permission notice shall be included in all 17 | -- copies or substantial portions of the Software. 18 | -- 19 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | -- SOFTWARE. 26 | 27 | WebBanking{ 28 | version = 1.12, 29 | url = "https://api.kraken.com", 30 | description = "Fetch balances from Kraken API and list them as securities", 31 | services= { "Kraken Account" }, 32 | } 33 | 34 | local apiKey 35 | local apiSecret 36 | local apiVersion = 0 37 | local currency = "EUR" -- fixme: Don't hardcode 38 | local currencyName = "ZEUR" -- fixme: Don't hardcode 39 | local stakeSuffix = '.S' 40 | local optInRewardsSuffix = '.M' 41 | local bitcoin = 'XXBT' 42 | local ethereum = 'XETH' 43 | local market = "Kraken" 44 | local accountName = "Balances" 45 | local accountNumber = "Main" 46 | local balances 47 | 48 | -- The currency names cannot be retrieved via the API, therefore hardcoding them (could use 49 | -- web scraping instead). 50 | 51 | -- Source: https://support.kraken.com/hc/en-us/articles/201893658-Currency-pairs-available-for-trading-on-Kraken 52 | -- Retrieved on: May 7, 2019 53 | 54 | -- Further currency names added on July 19, 2022, February 9, 2023, and April 11, 2024, but the list is still incomplete. 55 | 56 | local currencyNames = { 57 | 58 | -- crypto 59 | AAVE = "Aave", 60 | ADA = "Cardano", 61 | ALGO = "Algorand", 62 | APE = "ApeCoin", 63 | ARB = "Arbitrum", 64 | ASTR = "Astar", 65 | ATOM = "Cosmos", 66 | AVAX = "Avalanche", 67 | BCH = "Bitcoin Cash", 68 | COMP = "Compound", 69 | DAI = "Dai", 70 | DASH = "Dash", 71 | DOT = "Polkadot", 72 | DOT28 = "Polkadot Fixed 28", 73 | DYDX = "dYdX", 74 | DYM = "Dymension", 75 | EOS = "EOS", 76 | ETH2 = "Ethereum 2.0", 77 | ETHW = "Ethereum (PoW)", 78 | FET = "Fetch.ai", 79 | FIL = "Filecoin", 80 | FTM = "Fantom", 81 | GALA = "Gala Games", 82 | GNO = "Gnosis", 83 | INJ = "Injective", 84 | LINK = "Chainlink", 85 | LUNA = "Terra Classic", 86 | LUNA2 = "Terra 2.0", 87 | MATIC = "Polygon", 88 | MINA = "Mina", 89 | OCEAN = "OCEAN Token", 90 | PEPE = "Pepe", 91 | PERP = "Perpetual Protocol", 92 | QTUM = "QTUM", 93 | RLC = "iExec RLC", 94 | RUNE = "THORChain", 95 | SEI = "Sei", 96 | SHIB = "Shiba Inu", 97 | SOL = "Solana", 98 | SPELL = "Spell Token", 99 | STRK = "Starknet Token", 100 | STX = "Stacks", 101 | SUI = "Sui", 102 | TIA = "Celestia", 103 | TRX = "Tron", 104 | UNI = "Uniswap", 105 | USDC = "USD Coin", 106 | USDT = "Tether (Omni Layer)", 107 | WIF = "dogwifhat", 108 | WOO = "Woo Network", 109 | WBTC = "Wrapped Bitcoin", 110 | XETC = "Ethereum Classic", 111 | XETH = "Ethereum", 112 | XLTC = "Litecoin", 113 | XMLN = "Watermelon", 114 | XREP = "Augur", 115 | XTZ = "Tezos", 116 | XXBT = "Bitcoin", 117 | XBT = "Bitcoin", 118 | XXDG = "Dogecoin", 119 | XXLM = "Stellar Lumens", 120 | XXMR = "Monero", 121 | XXRP = "Ripple", 122 | XZEC = "Zcash", 123 | 124 | -- fiat 125 | ZCAD = "Canadian Dollar", 126 | ZEUR = "Euro", 127 | ZGBP = "Great British Pound", 128 | ZJPY = "Japanese Yen", 129 | ZUSD = "US Dollar" 130 | } 131 | 132 | function SupportsBank (protocol, bankCode) 133 | return protocol == ProtocolWebBanking and bankCode == "Kraken Account" 134 | end 135 | 136 | function InitializeSession (protocol, bankCode, username, username2, password, username3) 137 | apiKey = username 138 | apiSecret = password 139 | 140 | balances = queryPrivate("Balance") 141 | assetPairs = queryPublic("AssetPairs") 142 | prices = queryPublic("Ticker", { pair = table.concat(buildPairs(balances, assetPairs), ',') }) 143 | end 144 | 145 | function ListAccounts (knownAccounts) 146 | local account = { 147 | name = accountName, 148 | accountNumber = accountNumber, 149 | currency = currency, 150 | portfolio = true, 151 | type = "AccountTypePortfolio" 152 | } 153 | 154 | return {account} 155 | end 156 | 157 | function RefreshAccount (account, since) 158 | local name 159 | local pair, bitcoinPair, targetCurrency, price 160 | local s = {} 161 | 162 | for key, value in pairs(balances) do 163 | pair, targetCurrency = getPairInfo(key) 164 | name = resolveCurrencyName(key) 165 | if prices[pair] ~= nil or key == currencyName then 166 | price = prices[pair] ~= nil and prices[pair]["b"][1] or 1 167 | 168 | -- If this currency pair cannot be changed to fiat directly, we get the price 169 | -- in Bitcoin or Ethereum here and need to convert it to the correct fiat amount. 170 | if targetCurrency == bitcoin then 171 | price = price * prices[getPairInfo(bitcoin)]["b"][1] 172 | elseif targetCurrency == ethereum then 173 | price = price * prices[getPairInfo(ethereum)]["b"][1] 174 | end 175 | if tonumber(value) > 0 then 176 | s[#s+1] = { 177 | name = name, 178 | market = market, 179 | currency = nil, 180 | quantity = value, 181 | price = price 182 | } 183 | end 184 | end 185 | end 186 | 187 | return {securities = s} 188 | end 189 | 190 | function EndSession () 191 | end 192 | 193 | function resolveCurrencyName(key) 194 | 195 | local keyWithoutSuffix = removeSuffix(removeSuffix(key, stakeSuffix), optInRewardsSuffix) 196 | local isStaked = key ~= keyWithoutSuffix 197 | 198 | if isStaked and currencyNames[keyWithoutSuffix] ~=nil then 199 | return currencyNames[keyWithoutSuffix] .. ' (staked)' 200 | elseif currencyNames[key] then 201 | return currencyNames[key] 202 | end 203 | 204 | -- If we cannot resolve the key to a name, return the unmodified key. 205 | return key 206 | end 207 | 208 | function queryPrivate(method, request) 209 | if request == nil then 210 | request = {} 211 | end 212 | 213 | local path = string.format("/%s/private/%s", apiVersion, method) 214 | local nonce = string.format("%d", math.floor(MM.time() * 1000000)) 215 | request["nonce"] = nonce 216 | local postData = httpBuildQuery(request) 217 | local apiSign = MM.hmac512(MM.base64decode(apiSecret), path .. hex2str(MM.sha256(nonce .. postData))) 218 | local headers = {} 219 | 220 | headers["API-Key"] = apiKey 221 | headers["API-Sign"] = MM.base64(apiSign) 222 | 223 | connection = Connection() 224 | content = connection:request("POST", url .. path, postData, nil, headers) 225 | json = JSON(applyFillerWorkaround(content)) 226 | 227 | return json:dictionary()["result"] 228 | end 229 | 230 | function queryPublic(method, request) 231 | local path = string.format("/%s/public/%s", apiVersion, method) 232 | local queryParams = "" 233 | 234 | if request ~= nil and next(request) ~= nil then 235 | queryParams = "?" .. httpBuildQuery(request) 236 | end 237 | 238 | local urlWithParams = url .. path .. queryParams 239 | local content = connection:request("GET", urlWithParams, "") 240 | local json = JSON(applyFillerWorkaround(content)) 241 | 242 | return json:dictionary()["result"] 243 | end 244 | 245 | function applyFillerWorkaround(content) 246 | local fixVersion = '2.3.4' 247 | if versionCompare(MM.productVersion, fixVersion) == -1 then 248 | print("Adding filler to work around bug in product versions earlier than " .. fixVersion) 249 | return '{"filler":"' .. string.rep('x', 2048) .. '",' .. string.sub(content, 2) 250 | end 251 | return content 252 | end 253 | 254 | function hex2str(hex) 255 | return (hex:gsub("..", function (byte) 256 | return string.char(tonumber(byte, 16)) 257 | end)) 258 | end 259 | 260 | function httpBuildQuery(params) 261 | local str = '' 262 | for key, value in pairs(params) do 263 | str = str .. key .. "=" .. value .. "&" 264 | end 265 | return str.sub(str, 1, -2) 266 | end 267 | 268 | function buildPairs(balances, assetPairs) 269 | local pair = '' 270 | local defaultPair = bitcoin .. currencyName 271 | local t = {} 272 | 273 | -- Always add default pair (i.e. XXBTZEUR) 274 | -- If we don't add it, fiat price calculation for currencies that don't have a fiat 275 | -- trading pair (such as Dogecoin) will fail in accounts that don't own Bitcoin. 276 | table.insert(t, defaultPair) 277 | 278 | for key, value in pairs(assetPairs) do 279 | if balances[value["base"]] ~= nil or balances[value["quote"]] ~= nil then 280 | if (key ~= defaultPair) then 281 | table.insert(t, key) 282 | end 283 | end 284 | end 285 | 286 | return t 287 | end 288 | 289 | function getPairInfo(base) 290 | 291 | 292 | -- support for staked coins (cut off stakeSuffix so that the currency can be found in asset pairs) 293 | base = removeSuffix(base, stakeSuffix) 294 | 295 | -- support for Opt-In Rewards, e.g. Bitcoin "staking" (XBT.M) 296 | base = removeSuffix(base, optInRewardsSuffix) 297 | 298 | -- rename "staked" XBT to XXBT so it can be found in the asset pair list 299 | if base == 'XBT' then 300 | base = 'XXBT' 301 | end 302 | 303 | -- rename ETH2 to XETH so it can be found in the asset pair list 304 | if base == 'ETH2' then 305 | base = 'XETH' 306 | end 307 | 308 | local opt1 = base .. currency 309 | local opt2 = base .. currencyName 310 | local opt3 = base .. bitcoin 311 | 312 | if assetPairs[opt1] ~= nil then return opt1, currency 313 | elseif assetPairs[opt2] ~= nil then return opt2, currencyName 314 | -- opt3: currency cannot be changed to fiat directly, only to Bitcoin (applies to Lumen, Dogecoin) 315 | elseif assetPairs[opt3] ~= nil then return opt3, bitcoin 316 | end 317 | 318 | return nil 319 | end 320 | 321 | function removeSuffix(str, suffix) 322 | if ends_with(str, suffix) then 323 | return str:sub(1, -#suffix-1) 324 | end 325 | return str 326 | end 327 | 328 | function versionCompare(version1, version2) 329 | -- based on https://helloacm.com/how-to-compare-version-numbers-in-c/ 330 | 331 | local v1 = split(version1, '.') 332 | local v2 = split(version2, '.') 333 | 334 | if #v1 ~= #v2 then error("version1 and version2 need to have the same number of fields") end 335 | 336 | for i = 1, #v1 do 337 | local n1 = tonumber(v1[i]) 338 | local n2 = tonumber(v2[i]) 339 | if n1 > n2 then return 1 340 | elseif n1 < n2 then return -1 341 | end 342 | end 343 | 344 | return 0 345 | end 346 | 347 | function split(str, delimiter) 348 | -- from http://lua-users.org/wiki/SplitJoin 349 | local t, ll 350 | t = {} 351 | ll = 0 352 | if #str == 1 then return {str} end 353 | while true do 354 | l = string.find(str, delimiter, ll, true) 355 | if l ~= nil then 356 | table.insert(t, string.sub(str, ll, l-1)) 357 | ll = l + 1 358 | else 359 | table.insert(t, string.sub(str, ll)) 360 | break 361 | end 362 | end 363 | return t 364 | end 365 | 366 | function ends_with(str, ending) 367 | -- from http://lua-users.org/wiki/StringRecipes 368 | return ending == "" or str:sub(-#ending) == ending 369 | end 370 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kraken-MoneyMoney 2 | 3 | Fetches balances from Kraken API and returns them as securities 4 | 5 | ## Extension Setup 6 | 7 | You can get a signed version of this extension from 8 | 9 | * my [GitHub releases](https://github.com/aaronk6/Kraken-MoneyMoney/releases/latest) page, or 10 | * the [MoneyMoney Extensions](https://moneymoney-app.com/extensions/) page 11 | 12 | Once downloaded, move `Kraken.lua` to your MoneyMoney Extensions folder. 13 | 14 | **Note:** This extension requires MoneyMoney Version 2.2.17 (284) or newer. 15 | 16 | ## Account Setup 17 | 18 | ### Kraken 19 | 20 | 1. Log in to your Kraken account 21 | 2. Go to Settings → API 22 | 3. Click “Generate New Key” 23 | 4. Under “Key Permissions”, check “Query Funds” (the others aren’t needed) 24 | 5. Click “Generate Key” 25 | 26 | ### MoneyMoney 27 | 28 | Add a new account (type “Kraken Account”) and use your Kraken API key as username and your Kraken API secret as password. 29 | 30 | ## Screenshots 31 | 32 | ![MoneyMoney screenshot with Kraken balances](screenshots/balances.png) 33 | 34 | ## Known Issues and Limitations 35 | 36 | * Always assumes EUR as base currency 37 | -------------------------------------------------------------------------------- /screenshots/balances.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronk6/Kraken-MoneyMoney/25a2e22c8f29de2caeeb204e4a14747e4ceefd2b/screenshots/balances.png --------------------------------------------------------------------------------