23 Commits
Author SHA1 Message Date
TZGyn 6d50566f37 Note the device build steps in the iOS readme 2026-09-19 13:44:11 +08:00
TZGyn 6b52b32e20 Document how the Monero iOS library is built 2026-09-19 13:43:08 +08:00
TZGyn a3af802166 Support real iOS devices with a device build of the Monero library 2026-09-19 13:42:04 +08:00
TZGyn 3e1a39d4d7 Replace the JavaScript mobile app with a native Swift iOS wallet 2026-09-19 13:33:11 +08:00
TZGyn d80b58b704 Point publish instructions at Gitea 2026-09-11 16:21:39 +08:00
TZGyn babd85e614 add vue lynx mobile wallet 2026-08-09 18:09:21 +08:00
TZGyn d15152a013 bump version to 0.0.12 2026-08-04 22:19:25 +08:00
TZGyn 3913889a22 add portfolio allocation donut chart on dashboard 2026-08-04 22:19:13 +08:00
TZGyn da0c56658b add QR image upload to send pages 2026-08-04 22:19:03 +08:00
TZGyn 743700b82c bump version to 0.0.11 2026-08-02 19:17:11 +08:00
TZGyn 8953596194 live sync state with status dot on dashboard 2026-08-02 19:17:02 +08:00
TZGyn 5df3ae7058 bump version to 0.0.10 2026-08-02 18:59:48 +08:00
TZGyn 7b4ba84fee global singleton wallet state with single bootstrap in layout 2026-08-02 18:59:31 +08:00
TZGyn 20f29faa80 bump version to 0.0.9 2026-08-02 18:44:53 +08:00
TZGyn c0f6417925 persist dashboard prices and balances in stores 2026-08-02 18:44:53 +08:00
TZGyn c61fea938a bump version to 0.0.8 2026-08-02 15:09:28 +08:00
TZGyn bdfee2a553 log monero sync state and refresh failures 2026-08-02 15:09:28 +08:00
TZGyn 1c5e5b617f bump version to 0.0.7 2026-08-02 14:54:49 +08:00
TZGyn 11e0bcdaf6 add timeouts so syncing wallet never hangs the UI 2026-08-02 14:54:39 +08:00
TZGyn 4270d380fb fix race calling open before wallet-rpc ready 2026-08-02 14:26:49 +08:00
TZGyn af6251669a add numbering system for pending lists 2026-08-02 14:23:17 +08:00
TZGyn 6eec1f998d bump version to 0.0.5 2026-08-02 14:15:39 +08:00
TZGyn d9ca8a0047 bump version to 0.0.4 2026-08-02 13:30:49 +08:00
86 changed files with 26857 additions and 245 deletions
+14 -3
View File
@@ -1,9 +1,20 @@
# Instructions for AI assistants # Instructions for AI assistants
- Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command. - Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command.
- "Apply the changes" / "apply it" / "do it" does NOT mean commit. It means make the file changes. Before any commit (main repo, tap repo, anywhere), stop and ask for explicit permission, and list which files will be committed. Wait for a clear "yes" / "commit it" before running `git commit`. - "Apply the changes" / "apply it" / "do it" does NOT mean commit. It means make the file changes. Before any commit (main repo, tap repo, anywhere), stop and ask for explicit permission, and list which files will be committed. Wait for a clear "yes" / "commit it" / "commit and push" before running `git commit`.
- Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location. - Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location.
- Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message. - Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message.
- When asked to publish, always rebuild the DMG first (`bun run build:stable` from `apps/desktop`), wait for notarization to complete, verify with `xcrun stapler validate`, then publish the fresh DMG. Never publish a stale build. - When asked to publish, always rebuild the DMG first (`bun run build:stable` from `apps/desktop`), wait for notarization to complete, verify with `xcrun stapler validate`, then publish the fresh DMG. Never publish a stale build.
- Never publish without explicit permission. Before any publish action — running `bun run build:stable`, `gh release upload`, updating the Homebrew cask, or pushing the tap repo — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed — the affirmative applies to the action you described, and you must still pause before each subsequent publish step (e.g. upload, cask update) to ask for permission again. - Never publish without explicit permission. Before any publish action — running `bun run build:stable`, uploading a release asset to Gitea (`https://gitea.tzgyn.dev/TZGyn/koins`), updating the Homebrew cask, or pushing the tap repo (`https://gitea.tzgyn.dev/TZGyn/homebrew-tap`) — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed through the whole pipeline without pausing between steps: build, notarize, verify, upload to the Gitea release, update the Homebrew cask, and push the Gitea tap repo. One permission covers the entire publish flow. You must still get a separate permission before starting each new publish cycle.
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending." - Permission to commit ("commit", "commit and push", or similar) authorizes `git commit` and also `git push` of the committed changes. Only push the branches/files described in the permission.
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending." List each pending item as a numbered entry (`1.`, `2.`, …) so the user can approve or act on them by number (e.g. "go ahead with 1, 2").
## iOS app (`apps/ios`)
Native SwiftUI wallet for Monero and XRP — no JavaScript runtime, no CocoaPods, no Swift packages.
- Regenerate the project after adding or removing files: `xcodegen generate` (from `apps/ios`).
- Simulator build: `xcodebuild -project Koins.xcodeproj -scheme Koins -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -derivedDataPath /tmp/koins-dd build`.
- Crypto/serialization self-test: launch with `-koinsSelfTest`, then read `log show --predicate 'subsystem == "com.koins.app"'`.
- Live network self-test: launch with `-koinsSelfTestNetwork`.
- See `apps/ios/README.md` for the full layout and verification workflow.
+1 -1
View File
@@ -4,7 +4,7 @@ export default {
app: { app: {
name: 'koins', name: 'koins',
identifier: 'koins.tzgyn.com', identifier: 'koins.tzgyn.com',
version: '0.0.3', version: '0.0.12',
}, },
build: { build: {
// Vite builds to dist/, we copy from there // Vite builds to dist/, we copy from there
+1
View File
@@ -20,6 +20,7 @@
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"electrobun": "^1.18.1", "electrobun": "^1.18.1",
"env-paths": "^4.0.0", "env-paths": "^4.0.0",
"jsqr": "^1.4.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"monero-ts": "^0.11.8", "monero-ts": "^0.11.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
+132 -48
View File
@@ -1,11 +1,7 @@
import { import { MoneroWalletRpc, MoneroRpcConnection, connectToWalletRpc, connectToDaemonRpc } from 'monero-ts'
MoneroWalletRpc,
MoneroRpcConnection,
connectToWalletRpc,
connectToDaemonRpc,
} from 'monero-ts'
import { getBinaryPath, getWalletDir } from './binary' import { getBinaryPath, getWalletDir } from './binary'
import { existsSync, readdirSync } from 'fs' import { existsSync, readdirSync } from 'fs'
import { log, logError } from '../logger'
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081' const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
@@ -45,13 +41,40 @@ function rpcConnection(
}) })
} }
function withTimeout<T>(
promise: Promise<T>,
ms: number,
label: string,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`${label} timed out after ${ms}ms`)),
ms,
)
promise.then(
(v) => {
clearTimeout(timer)
resolve(v)
},
(e) => {
clearTimeout(timer)
reject(e)
},
)
})
}
async function rawRpc( async function rawRpc(
state: MoneroWalletState, state: MoneroWalletState,
method: string, method: string,
params: Record<string, any> = {}, params: Record<string, any> = {},
): Promise<any> { ): Promise<any> {
const conn = rpcConnection(state) const conn = rpcConnection(state)
const res = await conn.sendJsonRequest(method, params) const res = await withTimeout(
conn.sendJsonRequest(method, params),
30000,
`wallet-rpc ${method}`,
)
if (res?.error) if (res?.error)
throw new Error(res.error.message || JSON.stringify(res.error)) throw new Error(res.error.message || JSON.stringify(res.error))
return res?.result return res?.result
@@ -65,20 +88,17 @@ async function waitForRpc(
let lastErr = '' let lastErr = ''
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
try { try {
await Bun.connect({ const conn = rpcConnection(state)
hostname: '127.0.0.1', const res = await withTimeout(
port: state.rpcPort, conn.sendJsonRequest('get_version', {}),
socket: { 5000,
open(s) { 'wallet-rpc probe',
s.end() )
}, if (res?.result?.version) {
close() {},
data() {},
error() {},
},
})
console.log(`[monero] RPC ready after ${Date.now() - start}ms`) console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
return return
}
lastErr = JSON.stringify(res)
} catch (e) { } catch (e) {
lastErr = String(e) lastErr = String(e)
} }
@@ -89,6 +109,28 @@ async function waitForRpc(
) )
} }
async function connectClient(
state: MoneroWalletState,
): Promise<MoneroWalletRpc> {
let lastErr = ''
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const client = (await connectToWalletRpc(
rpcUrl(state),
state.rpcUser,
state.rpcPassword,
)) as MoneroWalletRpc
console.log(`[monero] MoneroWalletRpc connected (attempt ${attempt})`)
return client
} catch (e) {
lastErr = String(e)
console.log(`[monero] connectToWalletRpc attempt ${attempt} failed:`, e)
if (attempt < 3) await new Promise((r) => setTimeout(r, 1000))
}
}
throw new Error(`Failed to connect to monero-wallet-rpc: ${lastErr}`)
}
export async function start( export async function start(
state: MoneroWalletState, state: MoneroWalletState,
daemonAddress: string = DEFAULT_DAEMON, daemonAddress: string = DEFAULT_DAEMON,
@@ -169,6 +211,9 @@ export async function start(
`[monero] wallet-rpc process exited with code ${exitCode}`, `[monero] wallet-rpc process exited with code ${exitCode}`,
error?.message ?? '', error?.message ?? '',
) )
log(
`[monero] wallet-rpc process exited with code ${exitCode} ${error?.message ?? ''}`,
)
if (state.saveTimer) { if (state.saveTimer) {
clearInterval(state.saveTimer) clearInterval(state.saveTimer)
state.saveTimer = null state.saveTimer = null
@@ -222,12 +267,9 @@ export async function start(
console.log(`[monero] RPC server is ready`) console.log(`[monero] RPC server is ready`)
console.log(`[monero] connecting MoneroWalletRpc client...`) console.log(`[monero] connecting MoneroWalletRpc client...`)
state.wallet = (await connectToWalletRpc( state.wallet = await connectClient(state)
rpcUrl(state),
state.rpcUser,
state.rpcPassword,
)) as MoneroWalletRpc
console.log(`[monero] MoneroWalletRpc connected successfully`) console.log(`[monero] MoneroWalletRpc connected successfully`)
log(`[monero] wallet-rpc started, RPC ready`)
} }
export async function stop(state: MoneroWalletState) { export async function stop(state: MoneroWalletState) {
@@ -241,21 +283,21 @@ export async function stop(state: MoneroWalletState) {
if (state.wallet) { if (state.wallet) {
await rawRpc(state, 'store') await rawRpc(state, 'store')
console.log(`[monero] wallet saved before stop`) console.log(`[monero] wallet saved before stop`)
log(`[monero] wallet saved before stop`)
} }
} catch (e) { } catch (e) {
console.log(`[monero] wallet save before stop failed:`, e) console.log(`[monero] wallet save before stop failed:`, e)
} }
if (state.process) { if (state.process) {
console.log( const proc = state.process
`[monero] sending SIGTERM to wallet-rpc (pid ${state.process.pid})`, console.log(`[monero] sending SIGTERM to wallet-rpc (pid ${proc.pid})`)
) proc.kill()
state.process.kill() await Promise.race([proc.exited, sleep(3000)])
await Promise.race([state.process.exited, sleep(3000)]) if (proc.exitCode === null) {
if (state.process.exitCode === null) {
console.log( console.log(
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`, `[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
) )
state.process.kill('SIGKILL') proc.kill('SIGKILL')
} }
state.process = null state.process = null
} }
@@ -279,7 +321,11 @@ export async function createWallet(
language: 'English', language: 'English',
}) })
if (!state.wallet) throw new Error('Wallet RPC not started') if (!state.wallet) throw new Error('Wallet RPC not started')
const address = await state.wallet.getAddress(0, 0) const address = (await withTimeout(
state.wallet.getAddress(0, 0),
30000,
'wallet get_address',
)) as string
console.log(`[monero] wallet created: ${name} -> ${address}`) console.log(`[monero] wallet created: ${name} -> ${address}`)
return { mnemonic: result.mnemonic, address } return { mnemonic: result.mnemonic, address }
} }
@@ -310,9 +356,20 @@ export async function openWallet(
password: string, password: string,
) { ) {
console.log(`[monero] opening wallet: ${name}`) console.log(`[monero] opening wallet: ${name}`)
if (!state.wallet) throw new Error('Wallet RPC not started') // Self-heal: the client connection may have been lost while the
await state.wallet.openWallet(name, password) // wallet-rpc process is still running (e.g. stale-process cleanup).
console.log(`[monero] wallet opened: ${name}`) if (!state.wallet) {
if (!state.process) throw new Error('Wallet RPC not started')
log(`[monero] reconnecting wallet client before open (${name})`)
state.wallet = await connectClient(state)
}
try {
await withTimeout(state.wallet.openWallet(name, password), 30000, `open_wallet ${name}`)
log(`[monero] wallet opened: ${name}`)
} catch (e) {
logError(`[monero] wallet open failed (${name})`, e)
throw e
}
} }
export async function closeWallet(state: MoneroWalletState) { export async function closeWallet(state: MoneroWalletState) {
@@ -326,8 +383,16 @@ export async function getBalance(
state: MoneroWalletState, state: MoneroWalletState,
): Promise<{ balance: bigint; unlocked: bigint }> { ): Promise<{ balance: bigint; unlocked: bigint }> {
if (!state.wallet) throw new Error('Wallet RPC not started') if (!state.wallet) throw new Error('Wallet RPC not started')
const balance = (await state.wallet.getBalance()) as bigint const balance = (await withTimeout(
const unlocked = (await state.wallet.getUnlockedBalance()) as bigint state.wallet.getBalance(),
30000,
'wallet get_balance',
)) as bigint
const unlocked = (await withTimeout(
state.wallet.getUnlockedBalance(),
30000,
'wallet get_unlocked_balance',
)) as bigint
console.log(`[monero] balance: ${balance} (unlocked: ${unlocked})`) console.log(`[monero] balance: ${balance} (unlocked: ${unlocked})`)
return { balance, unlocked } return { balance, unlocked }
} }
@@ -338,9 +403,10 @@ export async function getAddress(
subIdx = 0, subIdx = 0,
): Promise<string> { ): Promise<string> {
if (!state.wallet) throw new Error('Wallet RPC not started') if (!state.wallet) throw new Error('Wallet RPC not started')
const address = (await state.wallet.getAddress( const address = (await withTimeout(
accountIdx, state.wallet.getAddress(accountIdx, subIdx),
subIdx, 30000,
'wallet get_address',
)) as string )) as string
console.log(`[monero] address: ${address}`) console.log(`[monero] address: ${address}`)
return address return address
@@ -376,7 +442,11 @@ export async function getAccounts(
state: MoneroWalletState, state: MoneroWalletState,
): Promise<any[]> { ): Promise<any[]> {
if (!state.wallet) throw new Error('Wallet RPC not started') if (!state.wallet) throw new Error('Wallet RPC not started')
const accounts = (await state.wallet.getAccounts(true)) as any[] const accounts = (await withTimeout(
state.wallet.getAccounts(true),
30000,
'wallet get_accounts',
)) as any[]
console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`) console.log(`[monero] accounts: ${accounts?.length ?? 0} returned`)
if (accounts?.length) { if (accounts?.length) {
for (const acct of accounts) { for (const acct of accounts) {
@@ -521,7 +591,11 @@ export async function getHeight(
state: MoneroWalletState, state: MoneroWalletState,
): Promise<number> { ): Promise<number> {
if (!state.wallet) throw new Error('Wallet RPC not started') if (!state.wallet) throw new Error('Wallet RPC not started')
const height = (await state.wallet.getHeight()) as number const height = (await withTimeout(
state.wallet.getHeight(),
30000,
'wallet get_height',
)) as number
console.log(`[monero] wallet height: ${height}`) console.log(`[monero] wallet height: ${height}`)
return height return height
} }
@@ -565,7 +639,11 @@ export async function getDaemonHeight(
const daemon = await connectToDaemonRpc( const daemon = await connectToDaemonRpc(
`http://${state.daemonAddress}`, `http://${state.daemonAddress}`,
) )
const height = (await daemon.getHeight()) as number const height = (await withTimeout(
daemon.getHeight(),
15000,
'daemon get_height',
)) as number
console.log(`[monero] daemon height: ${height}`) console.log(`[monero] daemon height: ${height}`)
return height return height
} catch (e) { } catch (e) {
@@ -643,9 +721,13 @@ export async function isWalletOpen(
): Promise<boolean> { ): Promise<boolean> {
if (!state.wallet) return false if (!state.wallet) return false
try { try {
await state.wallet.getHeight() await withTimeout(state.wallet.getHeight(), 8000, 'wallet get_height')
return true return true
} catch { } catch (e) {
// A timeout usually means the wallet-rpc is busy syncing, not that
// the wallet is closed. Assume still-open so the UI doesn't flip to
// "closed" and try to re-open an already-open wallet.
if (e instanceof Error && e.message.includes('timed out')) return true
return false return false
} }
} }
@@ -655,10 +737,12 @@ export async function isConnected(
): Promise<boolean> { ): Promise<boolean> {
if (!state.wallet) return false if (!state.wallet) return false
try { try {
const daemon = await connectToDaemonRpc( const daemon = await withTimeout(
`http://${state.daemonAddress}`, connectToDaemonRpc(`http://${state.daemonAddress}`),
8000,
'daemon connect',
) )
await daemon.getHeight() await withTimeout(daemon.getHeight(), 8000, 'daemon get_height')
return true return true
} catch (e) { } catch (e) {
console.log(`[monero] connection check failed:`, e) console.log(`[monero] connection check failed:`, e)
+27 -1
View File
@@ -24,6 +24,7 @@ import {
isConnected, isConnected,
} from '../../lib/monero' } from '../../lib/monero'
import type { MoneroWalletState } from '../../lib/monero' import type { MoneroWalletState } from '../../lib/monero'
import { log, logError } from '../../lib/logger'
export function createMoneroHandlers(state: { export function createMoneroHandlers(state: {
manager: MoneroWalletState | null manager: MoneroWalletState | null
@@ -64,10 +65,12 @@ export function createMoneroHandlers(state: {
await walletStart(state.manager, daemonAddress) await walletStart(state.manager, daemonAddress)
} catch (e) { } catch (e) {
console.log('[rpc] moneroStart error:', e) console.log('[rpc] moneroStart error:', e)
logError('[rpc] moneroStart error', e)
return { running: false, walletOpen: false, connected: false } return { running: false, walletOpen: false, connected: false }
} }
const connected = await isConnected(state.manager) const connected = await isConnected(state.manager)
console.log('[rpc] moneroStart complete, connected:', connected) console.log('[rpc] moneroStart complete, connected:', connected)
log(`[rpc] moneroStart complete, connected: ${connected}`)
return { running: true, walletOpen: false, connected } return { running: true, walletOpen: false, connected }
}, },
moneroStop: async () => { moneroStop: async () => {
@@ -105,12 +108,19 @@ export function createMoneroHandlers(state: {
console.log('[rpc] moneroRestoreWallet:', name) console.log('[rpc] moneroRestoreWallet:', name)
if (!state.manager) if (!state.manager)
throw new Error('Monero wallet RPC not started') throw new Error('Monero wallet RPC not started')
// Default to ~2 days of history instead of a full genesis scan
// (which can take hours against a remote daemon).
let height = restoreHeight
if (!height || height === 0) {
const tip = await walletGetDaemonHeight(state.manager)
if (tip > 1440) height = tip - 1440
}
await walletRestore( await walletRestore(
state.manager, state.manager,
name, name,
password, password,
mnemonic, mnemonic,
restoreHeight, height,
) )
const address = await walletGetAddress(state.manager) const address = await walletGetAddress(state.manager)
console.log( console.log(
@@ -129,12 +139,19 @@ export function createMoneroHandlers(state: {
console.log('[rpc] moneroOpenWallet:', name) console.log('[rpc] moneroOpenWallet:', name)
if (!state.manager) if (!state.manager)
throw new Error('Monero wallet RPC not started') throw new Error('Monero wallet RPC not started')
try {
await walletOpen(state.manager, name, password) await walletOpen(state.manager, name, password)
log(`[rpc] moneroOpenWallet ok: ${name}`)
} catch (e) {
logError(`[rpc] moneroOpenWallet failed (${name})`, e)
throw e
}
}, },
moneroGetBalance: async () => { moneroGetBalance: async () => {
console.log('[rpc] moneroGetBalance') console.log('[rpc] moneroGetBalance')
if (!state.manager) if (!state.manager)
throw new Error('Monero wallet RPC not started') throw new Error('Monero wallet RPC not started')
try {
const { balance, unlocked } = await walletGetBalance( const { balance, unlocked } = await walletGetBalance(
state.manager, state.manager,
) )
@@ -155,6 +172,10 @@ export function createMoneroHandlers(state: {
height, height,
daemonHeight, daemonHeight,
} }
} catch (e) {
logError('[rpc] moneroGetBalance failed', e)
throw e
}
}, },
moneroGetTransactions: async ({ moneroGetTransactions: async ({
accountIndex, accountIndex,
@@ -164,12 +185,17 @@ export function createMoneroHandlers(state: {
console.log('[rpc] moneroGetTransactions', { accountIndex }) console.log('[rpc] moneroGetTransactions', { accountIndex })
if (!state.manager) if (!state.manager)
throw new Error('Monero wallet RPC not started') throw new Error('Monero wallet RPC not started')
try {
const txs = await walletGetTransactions( const txs = await walletGetTransactions(
state.manager, state.manager,
accountIndex, accountIndex,
) )
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`) console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
return txs return txs
} catch (e) {
logError('[rpc] moneroGetTransactions failed', e)
throw e
}
}, },
moneroWalletStatus: async () => { moneroWalletStatus: async () => {
if (!state.manager) { if (!state.manager) {
@@ -0,0 +1,62 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js'
import Loader from '$lib/components/loader.svelte'
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
import { decodeQrFromFile } from '$lib/qr.js'
let {
onDecode,
disabled = false,
}: {
onDecode: (text: string) => void
disabled?: boolean
} = $props()
let fileInput: HTMLInputElement
let busy = $state(false)
let error = $state('')
const handleFile = async (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
busy = true
error = ''
try {
const text = await decodeQrFromFile(file)
if (!text) throw new Error('No QR code found in the image')
onDecode(text)
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to read QR code'
} finally {
busy = false
if (fileInput) fileInput.value = ''
}
}
</script>
<div class="flex items-center gap-2">
<input
bind:this={fileInput}
type="file"
accept="image/*"
class="hidden"
onchange={handleFile}
disabled={disabled || busy} />
<Button
type="button"
variant="outline"
size="sm"
class="h-7 gap-1.5"
disabled={disabled || busy}
onclick={() => fileInput?.click()}>
{#if busy}
<Loader class="size-3.5" />
{:else}
<QrCodeIcon size={14} />
{/if}
Scan QR
</Button>
{#if error}
<span class="text-xs text-red-500">{error}</span>
{/if}
</div>
+75
View File
@@ -0,0 +1,75 @@
import jsQR from 'jsqr'
export const decodeQrFromFile = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const url = URL.createObjectURL(file)
const img = new Image()
img.onload = () => {
try {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Canvas is not supported')
ctx.drawImage(img, 0, 0)
const { data, width, height } = ctx.getImageData(
0,
0,
canvas.width,
canvas.height,
)
resolve(jsQR(data, width, height)?.data ?? '')
} catch (e) {
reject(e)
} finally {
URL.revokeObjectURL(url)
}
}
img.onerror = () => {
URL.revokeObjectURL(url)
reject(new Error('Could not load the image'))
}
img.src = url
})
export type QrParseResult = {
address: string
amount?: string
destinationTag?: string
}
// Handles payment URIs (monero:ADDRESS?tx_amount=..., xrp:rADDR?amount=..&dt=..)
// as well as bare addresses. Returns the last path-like segment before any
// query string so prefixes and /pay paths are skipped.
export const parseQrPayload = (
text: string,
kind: 'xrp' | 'monero',
): QrParseResult => {
let body = text.trim().replace(/\s+/g, '')
body = body.replace(/^(?:[a-z0-9+.-]+):\/?\//i, '')
const [beforeQuery, queryRaw] = body.split('?')
const segments = beforeQuery.split('/')
const address = segments[segments.length - 1]
const params: Record<string, string> = {}
if (queryRaw) {
for (const pair of queryRaw.split('&')) {
const eq = pair.indexOf('=')
const key = (eq >= 0 ? pair.slice(0, eq) : pair).toLowerCase()
const value = eq >= 0 ? decodeURIComponent(pair.slice(eq + 1)) : ''
if (key) params[key] = value
}
}
if (kind === 'xrp') {
return {
address,
amount: params['amount'] || undefined,
destinationTag: params['dt'] || undefined,
}
}
return {
address,
amount: params['tx_amount'] || undefined,
}
}
@@ -1,5 +1,7 @@
import { import {
electrobun, electrobun,
frontendLog,
frontendLogError,
type MoneroTxEntry, type MoneroTxEntry,
type MoneroAccountEntry, type MoneroAccountEntry,
type MoneroSendResult, type MoneroSendResult,
@@ -25,7 +27,7 @@ export const atomicToXmr = (atomic: string): string => {
return frac ? `${whole}.${frac}` : `${whole}` return frac ? `${whole}.${frac}` : `${whole}`
} }
export const MoneroWallet = () => { export const createMoneroWallet = () => {
let accountType = $state<'monero' | null>(null) let accountType = $state<'monero' | null>(null)
let ready = $state(false) let ready = $state(false)
let biometricAvailable = $state(false) let biometricAvailable = $state(false)
@@ -51,6 +53,30 @@ export const MoneroWallet = () => {
let passwordRequired = $state(false) let passwordRequired = $state(false)
let startInFlight: Promise<{ running: boolean; connected: boolean }> | null = null let startInFlight: Promise<{ running: boolean; connected: boolean }> | null = null
let loginInFlight: Promise<void> | null = null let loginInFlight: Promise<void> | null = null
let refreshInFlight: Promise<void> | null = null
let syncing = $state(false)
let syncRetryTimer: ReturnType<typeof setTimeout> | null = null
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
let price = $state<string | null>(null)
let priceInFlight = false
const rpcTimeout = <T>(promise: Promise<T>, ms: number, label: string): Promise<T> =>
new Promise<T>((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`${label} timed out after ${ms}ms`)),
ms,
)
promise.then(
(v) => {
clearTimeout(timer)
resolve(v)
},
(e) => {
clearTimeout(timer)
reject(e)
},
)
})
const beginOpening = () => { const beginOpening = () => {
openingDepth += 1 openingDepth += 1
@@ -80,7 +106,10 @@ export const MoneroWallet = () => {
return authed === true return authed === true
} }
let initPromise: Promise<void> | null = null
const init = async () => { const init = async () => {
if (initPromise) return initPromise
initPromise = (async () => {
await checkStatus() await checkStatus()
if (installed && !running && !downloading) { if (installed && !running && !downloading) {
await start() await start()
@@ -88,6 +117,8 @@ export const MoneroWallet = () => {
} }
await checkBiometric() await checkBiometric()
ready = true ready = true
})()
return initPromise
} }
const login = async () => { const login = async () => {
@@ -95,11 +126,21 @@ export const MoneroWallet = () => {
if (loginInFlight) return loginInFlight if (loginInFlight) return loginInFlight
loginInFlight = (async () => { loginInFlight = (async () => {
accountType = 'monero' accountType = 'monero'
error = ''
beginOpening() beginOpening()
try { try {
if (installed && !running) { if (installed && !running) {
await start() await start()
await checkStatus() await checkStatus()
} else if (startInFlight) {
// a start is already in flight (e.g. triggered by init);
// wait for it so we don't call open before the server is up
await startInFlight.catch(() => {})
await checkStatus()
}
if (!running) {
error = 'Wallet server failed to start. Please try again.'
return
} }
await listWallets() await listWallets()
if (wallets.length > 0) { if (wallets.length > 0) {
@@ -193,14 +234,22 @@ export const MoneroWallet = () => {
const checkStatus = async () => { const checkStatus = async () => {
if (!electrobun.rpc) return if (!electrobun.rpc) return
const [binStatus] = await tryCatch( const [binStatus] = await tryCatch(
rpcTimeout(
electrobun.rpc.request.moneroBinaryStatus({}), electrobun.rpc.request.moneroBinaryStatus({}),
15000,
'moneroBinaryStatus',
),
) )
if (binStatus) { if (binStatus) {
installed = binStatus.installed installed = binStatus.installed
downloading = binStatus.downloading downloading = binStatus.downloading
} }
const [status] = await tryCatch( const [status] = await tryCatch(
rpcTimeout(
electrobun.rpc.request.moneroWalletStatus({}), electrobun.rpc.request.moneroWalletStatus({}),
30000,
'moneroWalletStatus',
),
) )
if (status) { if (status) {
running = status.running running = status.running
@@ -352,13 +401,60 @@ export const MoneroWallet = () => {
if (remember) await moneroStorePassword(target, password) if (remember) await moneroStorePassword(target, password)
} }
const scheduleSyncRetry = () => {
if (syncRetryTimer) clearTimeout(syncRetryTimer)
syncRetryTimer = setTimeout(async () => {
syncRetryTimer = null
if (walletOpen && !loading) await refresh()
}, 15000)
}
const stopAutoRefresh = () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
}
// Refresh runs in the store so the dashboard's connection/sync state
// stays live regardless of which page is mounted.
const startAutoRefresh = () => {
if (autoRefreshTimer || !walletOpen) return
autoRefreshTimer = setInterval(() => {
if (walletOpen && !loading) refresh()
}, 30000)
}
// $effect needs a root outside a component; the root lives for the
// lifetime of the singleton store.
$effect.root(() => {
$effect(() => {
if (walletOpen && !loading) startAutoRefresh()
else if (!walletOpen) stopAutoRefresh()
})
})
const refresh = async () => { const refresh = async () => {
if (!electrobun.rpc) return const rpc = electrobun.rpc
if (!rpc) return
if (refreshInFlight) return refreshInFlight
refreshInFlight = (async () => {
loading = true loading = true
let failed = false
const wasSyncing = syncing
let lastError: unknown = null
try { try {
const [bal] = await tryCatch( const [bal, balErr] = await tryCatch(
electrobun.rpc.request.moneroGetBalance({}), rpcTimeout(
rpc.request.moneroGetBalance({}),
20000,
'moneroGetBalance',
),
) )
if (balErr) {
failed = true
lastError = balErr
}
if (bal) { if (bal) {
balAtomic = bal.balance balAtomic = bal.balance
unlockedAtomic = bal.unlocked unlockedAtomic = bal.unlocked
@@ -366,17 +462,64 @@ export const MoneroWallet = () => {
height = bal.height height = bal.height
daemonHeight = bal.daemonHeight daemonHeight = bal.daemonHeight
} }
const [result] = await tryCatch( const [result, txErr] = await tryCatch(
electrobun.rpc.request.moneroGetTransactions({ rpcTimeout(
rpc.request.moneroGetTransactions({
accountIndex: selectedAccountIndex, accountIndex: selectedAccountIndex,
}), }),
20000,
'moneroGetTransactions',
),
) )
if (txErr) {
failed = true
lastError = txErr
}
txs = result ?? [] txs = result ?? []
await fetchAccounts() const ok = await fetchAccounts()
if (!ok) {
failed = true
lastError = new Error('moneroGetAccounts failed')
}
if (failed) {
// The wallet-rpc is likely busy syncing; keep the stale
// balance and retry shortly instead of showing an error.
syncing = true
scheduleSyncRetry()
frontendLogError(
`monero refresh failed (height ${height}/${daemonHeight})`,
lastError,
)
} else {
if (wasSyncing) {
frontendLog(`monero sync complete at height ${height}`)
}
syncing = false
}
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Monero refresh failed' syncing = true
scheduleSyncRetry()
frontendLogError(
`monero refresh failed (height ${height}/${daemonHeight})`,
e,
)
} finally { } finally {
loading = false loading = false
refreshInFlight = null
}
})()
return refreshInFlight
}
const fetchPrice = async () => {
const rpc = electrobun.rpc
if (!rpc || priceInFlight) return
priceInFlight = true
try {
const [p] = await tryCatch(rpc.request.fetchMoneroPrice({}))
if (p) price = p.usd
} finally {
priceInFlight = false
} }
} }
@@ -496,6 +639,12 @@ export const MoneroWallet = () => {
get passwordRequired() { get passwordRequired() {
return passwordRequired return passwordRequired
}, },
get syncing() {
return syncing
},
get price() {
return price
},
get error() { get error() {
return error return error
}, },
@@ -545,6 +694,7 @@ export const MoneroWallet = () => {
autoUnlock, autoUnlock,
unlockWallet, unlockWallet,
refresh, refresh,
fetchPrice,
send, send,
sendAll, sendAll,
createAccount, createAccount,
@@ -552,4 +702,15 @@ export const MoneroWallet = () => {
} }
} }
// Singleton: calling MoneroWallet() always returns the same instance, so
// no component can ever create a second, disconnected wallet state.
type MoneroWalletStore = ReturnType<typeof createMoneroWallet>
let moneroWalletInstance: MoneroWalletStore | null = null
export const MoneroWallet = (): MoneroWalletStore => {
if (!moneroWalletInstance) moneroWalletInstance = createMoneroWallet()
return moneroWalletInstance
}
export const moneroWallet = MoneroWallet() export const moneroWallet = MoneroWallet()
@@ -13,7 +13,7 @@ export const xrpNetwork = {
explorerAddressUrl: 'https://xrpscan.com/account/', explorerAddressUrl: 'https://xrpscan.com/account/',
} as const } as const
export const XrpWallet = () => { export const createXrpWallet = () => {
let accountType = $state<'xrp' | null>(null) let accountType = $state<'xrp' | null>(null)
let ready = $state(false) let ready = $state(false)
let biometricAvailable = $state(false) let biometricAvailable = $state(false)
@@ -29,6 +29,9 @@ export const XrpWallet = () => {
let loading = $state(false) let loading = $state(false)
let error = $state('') let error = $state('')
let wallets = $state<XrpWalletInfo[]>([]) let wallets = $state<XrpWalletInfo[]>([])
let balances = $state<Record<string, { balance: number; funded: boolean }>>({})
let balancesLoading = $state(false)
let fetchBalancesInFlight = false
let currentWalletId = $state<string | null>(null) let currentWalletId = $state<string | null>(null)
let currentPasswordHash = $state<string | null>(null) let currentPasswordHash = $state<string | null>(null)
@@ -73,7 +76,10 @@ export const XrpWallet = () => {
biometricAvailable = ok === true biometricAvailable = ok === true
} }
let initPromise: Promise<void> | null = null
const init = async () => { const init = async () => {
if (initPromise) return initPromise
initPromise = (async () => {
if (!electrobun.rpc) return if (!electrobun.rpc) return
const [walletList] = await tryCatch( const [walletList] = await tryCatch(
electrobun.rpc.request.xrpListWallets({}), electrobun.rpc.request.xrpListWallets({}),
@@ -86,6 +92,8 @@ export const XrpWallet = () => {
} }
await checkBiometric() await checkBiometric()
ready = true ready = true
})()
return initPromise
} }
const login = async () => { const login = async () => {
@@ -319,6 +327,34 @@ export const XrpWallet = () => {
})() })()
} }
const fetchBalances = async () => {
const rpc = electrobun.rpc
if (!rpc || fetchBalancesInFlight) return
if (wallets.length === 0) return
fetchBalancesInFlight = true
balancesLoading = true
try {
const [p] = await tryCatch(rpc.request.fetchXrpPrice({}))
if (p) price = p.usd
await Promise.allSettled(
wallets.map(async (w) => {
const [res] = await tryCatch(
rpc.request.xrpGetBalance({ address: w.address }),
)
if (res) {
balances[w.address] = {
balance: parseFloat(res.balance),
funded: res.funded,
}
}
}),
)
} finally {
balancesLoading = false
fetchBalancesInFlight = false
}
}
const send = async ( const send = async (
to: string, to: string,
amount: string, amount: string,
@@ -399,6 +435,12 @@ export const XrpWallet = () => {
get wallets() { get wallets() {
return wallets return wallets
}, },
get balances() {
return balances
},
get balancesLoading() {
return balancesLoading
},
get currentWalletId() { get currentWalletId() {
return currentWalletId return currentWalletId
}, },
@@ -424,6 +466,7 @@ export const XrpWallet = () => {
login, login,
logout, logout,
refresh, refresh,
fetchBalances,
createWallet, createWallet,
importWallet, importWallet,
lock, lock,
@@ -442,4 +485,15 @@ export const XrpWallet = () => {
} }
} }
// Singleton: calling XrpWallet() always returns the same instance, so no
// component can ever create a second, disconnected wallet state.
type XrpWalletStore = ReturnType<typeof createXrpWallet>
let xrpWalletInstance: XrpWalletStore | null = null
export const XrpWallet = (): XrpWalletStore => {
if (!xrpWalletInstance) xrpWalletInstance = createXrpWallet()
return xrpWalletInstance
}
export const xrpWallet = XrpWallet() export const xrpWallet = XrpWallet()
+111 -77
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js' import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js' import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { electrobun } from '$lib/electrobun.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { import {
@@ -16,29 +15,23 @@
import WalletIcon from '@lucide/svelte/icons/wallet' import WalletIcon from '@lucide/svelte/icons/wallet'
import Loader from '$lib/components/loader.svelte' import Loader from '$lib/components/loader.svelte'
import { frontendLog, frontendLogError } from '$lib/electrobun.js' import { frontendLog, frontendLogError } from '$lib/electrobun.js'
import { arc } from 'd3-shape'
let xrpPrice = $state<string | null>(null) import { tweened } from 'svelte/motion'
let xmrPrice = $state<string | null>(null) import { cubicOut } from 'svelte/easing'
let xrpBalances = $state<Record<string, { balance: number; funded: boolean }>>({})
let loadingXrp = $state(true)
let loadingXmr = $state(true)
let feError = $state<string | null>(null) let feError = $state<string | null>(null)
const usd = (amount: string, price: string | null) =>
price ? parseFloat(amount) * parseFloat(price) : 0
const xrpTotalUsd = $derived.by(() => { const xrpTotalUsd = $derived.by(() => {
const price = xrpPrice const p = xrpWallet.price
return xrpWallet.wallets.reduce((sum, w) => { return xrpWallet.wallets.reduce((sum, w) => {
const b = xrpBalances[w.address]?.balance ?? 0 const b = xrpWallet.balances[w.address]?.balance ?? 0
return sum + (price ? b * parseFloat(price) : 0) return sum + (p ? b * parseFloat(p) : 0)
}, 0) }, 0)
}) })
const xrpTotalBalance = $derived.by(() => const xrpTotalBalance = $derived.by(() =>
xrpWallet.wallets.reduce((sum, w) => { xrpWallet.wallets.reduce((sum, w) => {
return sum + (xrpBalances[w.address]?.balance ?? 0) return sum + (xrpWallet.balances[w.address]?.balance ?? 0)
}, 0), }, 0),
) )
@@ -46,17 +39,54 @@
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0, moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
) )
const xmrTotalUsd = $derived( const xmrTotalUsd = $derived(
moneroWallet.walletOpen && xmrPrice moneroWallet.walletOpen && moneroWallet.price
? xmrBalance * parseFloat(xmrPrice) ? xmrBalance * parseFloat(moneroWallet.price)
: 0, : 0,
) )
const portfolioUsd = $derived(xrpTotalUsd + xmrTotalUsd) const portfolioUsd = $derived(xrpTotalUsd + xmrTotalUsd)
const xrpShare = $derived(portfolioUsd > 0 ? xrpTotalUsd / portfolioUsd : 0)
const xmrShare = $derived(
portfolioUsd > 0 ? xmrTotalUsd / portfolioUsd : 0,
)
const tweenedUsd = tweened(0, { duration: 600, easing: cubicOut })
const tweenedXrpShare = tweened(0, { duration: 600, easing: cubicOut })
$effect(() => {
tweenedUsd.set(portfolioUsd)
tweenedXrpShare.set(xrpShare)
})
const donut = $derived.by(() => {
if (portfolioUsd <= 0) return { xrp: '', xmr: '' }
const a = arc<{ startAngle: number; endAngle: number }>()
.innerRadius(62)
.outerRadius(80)
.padAngle(0.02)
const start = -Math.PI / 2
const endXrp = start + $tweenedXrpShare * 2 * Math.PI
return {
xrp: a({ startAngle: start, endAngle: endXrp }) ?? '',
xmr: a({ startAngle: endXrp, endAngle: start + 2 * Math.PI }) ?? '',
}
})
const hasXrp = $derived(xrpWallet.wallets.length > 0) const hasXrp = $derived(xrpWallet.wallets.length > 0)
const hasXmr = $derived(moneroWallet.wallets.length > 0) const hasXmr = $derived(moneroWallet.wallets.length > 0)
const noWallets = $derived(!hasXrp && !hasXmr) const noWallets = $derived(!hasXrp && !hasXmr)
const moneroDot = $derived(
!moneroWallet.running
? 'bg-muted'
: !moneroWallet.connected
? 'bg-red-500'
: moneroWallet.syncing || moneroWallet.height < moneroWallet.daemonHeight
? 'bg-yellow-500 animate-pulse'
: 'bg-emerald-500',
)
const fmtUsd = (v: number) => const fmtUsd = (v: number) =>
v.toLocaleString(undefined, { v.toLocaleString(undefined, {
minimumFractionDigits: 2, minimumFractionDigits: 2,
@@ -69,57 +99,23 @@
maximumFractionDigits: dp, maximumFractionDigits: dp,
}) })
async function fetchXrp() { const fmtPct = (v: number) => `${Math.round(v * 100)}%`
loadingXrp = true
const rpc = electrobun.rpc
if (!rpc) {
loadingXrp = false
return
}
try {
const p = await rpc.request.fetchXrpPrice({})
if (p) xrpPrice = p.usd
} catch { /* ignore */ }
await Promise.allSettled(
xrpWallet.wallets.map(async (w) => {
const res = await rpc.request.xrpGetBalance({
address: w.address,
})
xrpBalances[w.address] = {
balance: parseFloat(res.balance),
funded: res.funded,
}
}),
)
loadingXrp = false
}
async function fetchXmr() {
loadingXmr = true
const rpc = electrobun.rpc
if (!rpc) {
loadingXmr = false
return
}
try {
const p = await rpc.request.fetchMoneroPrice({})
xmrPrice = p?.usd ?? null
} catch { /* ignore */ }
loadingXmr = false
}
onMount(() => { onMount(() => {
frontendLog('dashboard mounted') frontendLog('dashboard mounted')
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
xrpWallet.init().then(async () => {
await fetchXrp()
}) })
fetchXmr()
// Balances live in the stores so they survive navigation; the effects
// only trigger background refetches.
$effect(() => {
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
xrpWallet.fetchBalances()
}
}) })
$effect(() => { $effect(() => {
if (moneroWallet.walletOpen) { if (moneroWallet.walletOpen) {
fetchXmr() moneroWallet.fetchPrice()
} }
}) })
@@ -179,9 +175,9 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-col items-center pt-4 pb-2"> <div class="flex flex-col items-center pt-4 pb-2">
<p class="text-xs text-muted-foreground">Total Portfolio Value</p> <p class="text-xs text-muted-foreground">Total Portfolio Value</p>
{#if loadingXrp || loadingXmr || moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen)} {#if moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen) || (xrpWallet.wallets.length > 0 && !xrpWallet.price) || (moneroWallet.walletOpen && !moneroWallet.price)}
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div> <div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
{:else} {:else}
<p class="mt-1 text-3xl font-semibold tabular-nums"> <p class="mt-1 text-3xl font-semibold tabular-nums">
@@ -190,6 +186,43 @@
{/if} {/if}
</div> </div>
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base">Portfolio Allocation</CardTitle>
<CardDescription class="text-xs">
Share of your balance by asset
</CardDescription>
</CardHeader>
<CardContent class="flex flex-col items-center gap-5 py-6">
<div class="relative">
<svg viewBox="-100 -100 200 200" class="size-48">
<path d={donut.xrp} fill="#0EA5E9" />
<path d={donut.xmr} fill="#F97316" />
</svg>
<div
class="absolute inset-0 flex flex-col items-center justify-center">
<p
class="text-lg font-semibold tabular-nums">$
{fmtUsd($tweenedUsd)}
</p>
<p class="text-xs text-muted-foreground">
Portfolio
</p>
</div>
</div>
<div class="flex gap-5 text-xs">
<span class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full bg-sky-500"></span>
XRP · {fmtPct($tweenedXrpShare)}
</span>
<span class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full bg-orange-500"></span>
XMR · {fmtPct(1 - $tweenedXrpShare)}
</span>
</div>
</CardContent>
</Card>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<button <button
onclick={() => navigate('/xrp')} onclick={() => navigate('/xrp')}
@@ -200,21 +233,19 @@
<img src="/icons/xrp.png" alt="" class="size-5 rounded-full" /> <img src="/icons/xrp.png" alt="" class="size-5 rounded-full" />
<CardTitle class="text-base">XRP</CardTitle> <CardTitle class="text-base">XRP</CardTitle>
</div> </div>
<CardDescription class="text-xs"> <CardDescription class="text-xs">XRP Ledger</CardDescription>
{xrpWallet.wallets.length} wallet{xrpWallet.wallets.length !== 1 ? 's' : ''}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="flex flex-1 flex-col justify-center"> <CardContent class="flex flex-1 flex-col justify-center">
{#if loadingXrp} {#if !xrpWallet.ready || (xrpWallet.wallets.length > 0 && Object.keys(xrpWallet.balances).length === 0)}
<div class="h-14 flex items-center"><Loader /></div> <div class="h-14 flex items-center"><Loader /></div>
{:else} {:else}
<p class="font-mono text-xl tabular-nums">{fmtBal(xrpTotalBalance)}</p> <p class="font-mono text-xl tabular-nums">{fmtBal(xrpTotalBalance)}</p>
{#if xrpPrice} {#if xrpWallet.price}
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums"> <p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
${fmtUsd(xrpTotalUsd)} ${fmtUsd(xrpTotalUsd)}
</p> </p>
<p class="mt-2 text-xs text-muted-foreground"> <p class="mt-2 text-xs text-muted-foreground">
${fmtUsd(parseFloat(xrpPrice) )} / XRP ${fmtUsd(parseFloat(xrpWallet.price) )} / XRP
</p> </p>
{/if} {/if}
{/if} {/if}
@@ -231,23 +262,28 @@
<img src="/icons/monero.png" alt="" class="size-5 rounded-full" /> <img src="/icons/monero.png" alt="" class="size-5 rounded-full" />
<CardTitle class="text-base">Monero</CardTitle> <CardTitle class="text-base">Monero</CardTitle>
</div> </div>
<CardDescription class="text-xs"> <CardDescription class="flex items-center gap-1.5 text-xs">
{moneroWallet.walletOpen <span class="size-2 shrink-0 rounded-full {moneroDot}"></span>
? moneroWallet.walletName || 'Wallet open' {#if !moneroWallet.running}
: moneroWallet.wallets.length > 0 Server offline
? `${moneroWallet.wallets.length} wallet${moneroWallet.wallets.length !== 1 ? 's' : ''}` {:else if !moneroWallet.connected}
: 'Not set up'} Connecting to daemon…
{:else}
{fmtBal(moneroWallet.height, 0)} / {fmtBal(moneroWallet.daemonHeight, 0)}
{/if}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent class="flex flex-1 flex-col justify-center"> <CardContent class="flex flex-1 flex-col justify-center">
{#if moneroWallet.walletOpen} {#if moneroWallet.walletOpen}
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p> <p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
{#if xmrPrice} {#if !moneroWallet.syncing && moneroWallet.price}
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums"> <p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
${fmtUsd(xmrTotalUsd)} ${fmtUsd(xmrTotalUsd)}
</p> </p>
{/if}
{#if moneroWallet.price}
<p class="mt-2 text-xs text-muted-foreground"> <p class="mt-2 text-xs text-muted-foreground">
${fmtUsd(parseFloat(xmrPrice) )} / XMR ${fmtUsd(parseFloat(moneroWallet.price) )} / XMR
</p> </p>
{/if} {/if}
{:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0} {:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0}
@@ -275,8 +311,6 @@
<Loader /> <Loader />
<p class="text-sm text-muted-foreground">Opening wallet...</p> <p class="text-sm text-muted-foreground">Opening wallet...</p>
</div> </div>
{:else if loadingXmr}
<div class="h-14 flex items-center"><Loader /></div>
{:else} {:else}
<Button <Button
onclick={() => navigate('/monero')} onclick={() => navigate('/monero')}
+1
View File
@@ -31,6 +31,7 @@
if (!xrpWallet.ready) await xrpWallet.init() if (!xrpWallet.ready) await xrpWallet.init()
if (!moneroWallet.ready) await moneroWallet.init() if (!moneroWallet.ready) await moneroWallet.init()
await Promise.allSettled([ await Promise.allSettled([
xrpWallet.login(),
xrpWallet.autoUnlock(), xrpWallet.autoUnlock(),
moneroWallet.login(), moneroWallet.login(),
]) ])
@@ -24,8 +24,6 @@
let unlocking = $state(false) let unlocking = $state(false)
let unlockError = $state('') let unlockError = $state('')
let initStarted = false
async function unlockWithPassword() { async function unlockWithPassword() {
if (unlocking || !walletPassword.trim()) return if (unlocking || !walletPassword.trim()) return
unlocking = true unlocking = true
@@ -48,16 +46,6 @@
} }
}) })
$effect(() => {
if (!initStarted) {
initStarted = true
w.init().then(async () => {
if (w.accountType === 'monero') return
await w.login()
})
}
})
$effect(() => { $effect(() => {
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) { if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
w.autoUnlock(w.wallets[0]) w.autoUnlock(w.wallets[0])
@@ -19,6 +19,8 @@
SelectTrigger, SelectTrigger,
} from '$lib/components/ui/select/index.js' } from '$lib/components/ui/select/index.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
import QrUpload from '$lib/components/qr-upload.svelte'
import { parseQrPayload } from '$lib/qr.js'
const w = moneroWallet const w = moneroWallet
@@ -134,7 +136,15 @@
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<div class="flex items-center justify-between">
<label class="text-xs font-medium">Recipient Address</label> <label class="text-xs font-medium">Recipient Address</label>
<QrUpload
onDecode={(text) => {
const parsed = parseQrPayload(text, 'monero')
recipient = parsed.address
if (parsed.amount && !sendAll) amountXmr = parsed.amount
}} />
</div>
<Input <Input
placeholder="Monero address starting with 4..." placeholder="Monero address starting with 4..."
bind:value={recipient} /> bind:value={recipient} />
@@ -15,10 +15,6 @@
const w = moneroWallet const w = moneroWallet
let moneroSettingsPw = $state('') let moneroSettingsPw = $state('')
$effect(() => {
w.init()
})
</script> </script>
<div class="mx-auto max-w-xl"> <div class="mx-auto max-w-xl">
@@ -154,7 +154,6 @@
$effect(() => { $effect(() => {
if (w.walletOpen && !autoRefreshTimer) { if (w.walletOpen && !autoRefreshTimer) {
autoRefreshTimer = setInterval(() => { autoRefreshTimer = setInterval(() => {
w.refresh()
electrobun.rpc?.request electrobun.rpc?.request
.fetchMoneroPrice({}) .fetchMoneroPrice({})
.then((p) => (moneroPrice = p)) .then((p) => (moneroPrice = p))
@@ -203,7 +202,7 @@
{:else} {:else}
<div class="space-y-6"> <div class="space-y-6">
<div class="flex flex-col items-center py-2"> <div class="flex flex-col items-center py-2">
{#if w.loading} {#if w.loading && !w.syncing}
<div class="h-12 w-40 rounded-lg bg-muted animate-pulse"></div> <div class="h-12 w-40 rounded-lg bg-muted animate-pulse"></div>
{:else} {:else}
<p class="text-sm text-muted-foreground">Balance</p> <p class="text-sm text-muted-foreground">Balance</p>
@@ -223,6 +222,14 @@
Unlocked: {w.unlocked} XMR Unlocked: {w.unlocked} XMR
</p> </p>
{/if} {/if}
{#if w.syncing}
<div class="mt-2 flex items-center gap-2">
<Loader />
<p class="text-xs text-muted-foreground">
Syncing wallet — balance updates when the scan completes
</p>
</div>
{/if}
<div class="mt-2 flex items-center gap-1.5"> <div class="mt-2 flex items-center gap-1.5">
<div class="size-2 rounded-full {w.connected ? 'bg-green-500' : 'bg-red-500'}"></div> <div class="size-2 rounded-full {w.connected ? 'bg-green-500' : 'bg-red-500'}"></div>
<span class="text-xs text-muted-foreground"> <span class="text-xs text-muted-foreground">
-12
View File
@@ -22,18 +22,6 @@
let inputUnlockPassword = $state('') let inputUnlockPassword = $state('')
let newSeed = $state<string | null>(null) let newSeed = $state<string | null>(null)
let initStarted = false
$effect(() => {
if (!initStarted) {
initStarted = true
w.init().then(async () => {
if (w.accountType === 'xrp') return
await w.login()
})
}
})
$effect(() => { $effect(() => {
if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) { if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) {
navigate('/xrp/wallet/:id', { navigate('/xrp/wallet/:id', {
+11
View File
@@ -10,6 +10,8 @@
CardDescription, CardDescription,
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
import QrUpload from '$lib/components/qr-upload.svelte'
import { parseQrPayload } from '$lib/qr.js'
const w = xrpWallet const w = xrpWallet
@@ -78,7 +80,16 @@
}} }}
class="space-y-4"> class="space-y-4">
<div class="space-y-1.5"> <div class="space-y-1.5">
<div class="flex items-center justify-between">
<label class="text-xs font-medium">Recipient Address</label> <label class="text-xs font-medium">Recipient Address</label>
<QrUpload
onDecode={(text) => {
const parsed = parseQrPayload(text, 'xrp')
recipient = parsed.address
if (parsed.destinationTag) destinationTag = parsed.destinationTag
if (parsed.amount) amountXrp = parsed.amount
}} />
</div>
<Input <Input
placeholder="XRP address starting with r..." placeholder="XRP address starting with r..."
bind:value={recipient} /> bind:value={recipient} />
+9
View File
@@ -0,0 +1,9 @@
# Generated by XcodeGen from project.yml
Koins.xcodeproj/
# Build output
build/
DerivedData/
*.xcworkspace
xcuserdata/
*.xcuserstate
+337
View File
@@ -0,0 +1,337 @@
import Foundation
import SwiftUI
@MainActor
final class AppModel: ObservableObject {
@Published var chain: Chain = .xrp
@Published var xrpWallet: WalletRecord?
@Published var xmrWallet: WalletRecord?
@Published var balance = Balance(total: "0", unlocked: "", funded: false)
@Published var transactions: [TransactionRecord] = []
@Published var syncStatus: SyncStatus?
@Published var isBusy = false
@Published var errorMessage: String?
@Published var statusMessage: String?
@Published var moneroReady = false
@Published var moneroAddress: String?
/// Rough time remaining for the Monero sync, derived from observed block rate.
@Published var syncEta: String?
private var moneroOpened = false
private var refreshTick = 0
private var refreshTask: Task<Void, Never>?
private var lastSample: (height: UInt64, date: Date)?
var wallet: WalletRecord? {
chain == .xrp ? xrpWallet : xmrWallet
}
var hasWallet: Bool { wallet != nil }
/// Address for the chain currently on screen. Kept chain-aware because the
/// Monero address stays cached once its wallet is open.
var displayAddress: String {
chain == .xrp ? (xrpWallet?.address ?? "") : (moneroAddress ?? xmrWallet?.address ?? "")
}
var symbol: String { chain.symbol }
// MARK: - Bootstrap
func bootstrap() async {
xrpWallet = WalletStore.wallet(.xrp)
xmrWallet = WalletStore.wallet(.xmr)
if xmrWallet == nil, let orphan = MoneroService.orphanedWalletNames().first {
// Wallet file on disk with no stored record (for example after the
// preferences were lost): re-attach it so funds are not stranded.
await adoptMoneroWallet(named: orphan)
}
if let xmrWallet {
await openMonero(afterRestore: false, wallet: xmrWallet)
}
if xrpWallet != nil || moneroReady {
chain = xrpWallet != nil ? .xrp : .xmr
}
await refresh(force: true)
startAutoRefresh()
}
private func adoptMoneroWallet(named name: String) async {
do {
let address = try await MoneroService.shared.openWallet(name: name)
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
name: name,
address: address,
createdAt: Date())
xmrWallet = record
WalletStore.saveWallet(record, chain: .xmr)
moneroOpened = true
moneroReady = true
moneroAddress = address
statusMessage = "Recovered the existing Monero wallet “\(name)” from this device."
} catch {
errorMessage = "Found a Monero wallet file but could not open it: \(error.localizedDescription)"
}
}
/// Polls the wallet while the app is in the foreground. Monero sync progress
/// changes constantly, so it is refreshed far more often than XRP.
func startAutoRefresh() {
guard refreshTask == nil else { return }
refreshTask = Task { [weak self] in
while !Task.isCancelled {
guard let self else { return }
let seconds: UInt64 = self.chain == .xmr ? 5 : 30
try? await Task.sleep(nanoseconds: seconds * 1_000_000_000)
if Task.isCancelled { return }
await self.refresh()
}
}
}
func select(_ newChain: Chain) async {
chain = newChain
errorMessage = nil
statusMessage = nil
balance = Balance(total: "0", unlocked: "", funded: false)
transactions = []
syncStatus = nil
if newChain == .xmr, let xmrWallet, !moneroOpened {
await openMonero(afterRestore: false, wallet: xmrWallet)
}
await refresh(force: true)
}
// MARK: - Monero lifecycle
private func openMonero(afterRestore: Bool, wallet: WalletRecord) async {
isBusy = true
defer { isBusy = false }
do {
let address = try await MoneroService.shared.openWallet(name: wallet.name)
moneroOpened = true
moneroReady = true
moneroAddress = address.isEmpty ? wallet.address : address
if let address = moneroAddress, address != wallet.address {
var updated = wallet
updated.address = address
xmrWallet = updated
WalletStore.saveWallet(updated, chain: .xmr)
}
// Always try to catch up, not only right after a restore.
_ = try? await MoneroService.shared.startSync()
} catch {
// Keep the stored wallet so a transient daemon failure is not fatal.
moneroOpened = false
moneroReady = false
moneroAddress = nil
errorMessage = "Monero wallet could not be opened: \(error.localizedDescription)"
}
}
// MARK: - Create / import
func createWallet(name: String) async throws -> String {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { throw XRPError.rpc("Enter a wallet name") }
isBusy = true
defer { isBusy = false }
if chain == .xrp {
let (record, seed) = try XRPWalletService.shared.createWallet(name: trimmed)
xrpWallet = record
await refresh()
return seed
}
let result = try await MoneroService.shared.createWallet(name: trimmed)
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
name: trimmed,
address: result.address,
createdAt: Date())
xmrWallet = record
WalletStore.saveWallet(record, chain: .xmr)
if !result.mnemonic.isEmpty {
WalletStore.saveSecret(result.mnemonic, chain: .xmr)
}
moneroOpened = true
moneroReady = true
moneroAddress = result.address
_ = try? await MoneroService.shared.startSync()
await refresh(force: true)
return result.mnemonic
}
func importWallet(name: String, secret: String, restoreHeight: UInt64?) async throws {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedSecret = secret.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty else { throw XRPError.rpc("Enter a wallet name") }
guard !trimmedSecret.isEmpty else { throw XRPError.rpc("Enter your seed or secret") }
isBusy = true
defer { isBusy = false }
if chain == .xrp {
xrpWallet = try XRPWalletService.shared.importWallet(name: trimmedName, secret: trimmedSecret)
await refresh(force: true)
return
}
let address = try await MoneroService.shared.restoreWallet(name: trimmedName,
seed: trimmedSecret,
restoreHeight: restoreHeight ?? 0)
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
name: trimmedName,
address: address,
createdAt: Date())
xmrWallet = record
WalletStore.saveWallet(record, chain: .xmr)
WalletStore.saveSecret(trimmedSecret, chain: .xmr)
moneroOpened = true
moneroReady = true
moneroAddress = address
_ = try? await MoneroService.shared.startSync()
await refresh(force: true)
}
// MARK: - Reads
func refresh(force: Bool = false) async {
guard hasWallet else { return }
errorMessage = nil
if chain == .xrp, let wallet = xrpWallet {
do {
async let balanceCall = XRPWalletService.shared.balance(address: wallet.address)
async let txCall = XRPWalletService.shared.transactions(address: wallet.address)
balance = try await balanceCall
transactions = try await txCall
} catch {
errorMessage = error.localizedDescription
}
// The Monero wallet keeps scanning while XRP is on screen, so keep its
// progress current here too.
if moneroReady, let status = try? await MoneroService.shared.syncStatus() {
updateSyncStatus(status)
}
return
}
guard moneroReady else { return }
do {
// Sync progress is cheap and polled every tick; the balance and transfer
// list touch the wallet's data files, so they are refreshed less often.
updateSyncStatus(try await MoneroService.shared.syncStatus())
refreshTick += 1
if force || refreshTick == 1 || refreshTick % 4 == 0 {
async let balanceCall = MoneroService.shared.balance()
async let txCall = MoneroService.shared.transactions()
balance = try await balanceCall
transactions = try await txCall
}
} catch {
errorMessage = error.localizedDescription
}
}
/// Tracks how fast blocks are being scanned so the UI can show an estimate.
private func updateSyncStatus(_ status: SyncStatus) {
syncStatus = status
let now = Date()
defer { lastSample = (status.height, now) }
guard status.syncing,
let sample = lastSample,
status.height > sample.height,
status.daemonHeight > status.height else {
if !status.syncing { syncEta = nil }
return
}
let elapsed = now.timeIntervalSince(sample.date)
guard elapsed > 1 else { return }
let blocksPerSecond = Double(status.height - sample.height) / elapsed
guard blocksPerSecond > 0 else { return }
let remaining = Double(status.daemonHeight - status.height) / blocksPerSecond
syncEta = Self.formatDuration(remaining)
}
private static func formatDuration(_ seconds: Double) -> String {
guard seconds.isFinite, seconds > 0 else { return "unknown" }
let total = Int(seconds.rounded())
let hours = total / 3600
let minutes = (total % 3600) / 60
if hours >= 24 { return "\(hours / 24)d \(hours % 24)h" }
if hours > 0 { return "\(hours)h \(minutes)m" }
if minutes > 0 { return "\(minutes)m" }
return "under a minute"
}
func startMoneroSync() async {
guard moneroReady else {
if let wallet = xmrWallet { await openMonero(afterRestore: true, wallet: wallet) }
return
}
_ = try? await MoneroService.shared.startSync()
await refresh(force: true)
}
func revealSeed() async throws -> String {
if chain == .xrp {
guard let seed = WalletStore.secret(.xrp) else {
throw XRPError.rpc("No stored secret for this wallet")
}
return seed
}
if let stored = WalletStore.secret(.xmr), !stored.isEmpty { return stored }
return try await MoneroService.shared.seed()
}
// MARK: - Sends
func send(to: String, amount: String, tag: String, priority: TransactionPriority) async throws {
let destinationTag = UInt32(tag.trimmingCharacters(in: .whitespaces))
if chain == .xrp {
guard let seed = WalletStore.secret(.xrp) else {
throw XRPError.rpc("Wallet secret is unavailable")
}
statusMessage = nil
_ = try await XRPWalletService.shared.send(secret: seed,
to: to,
amountXRP: amount,
destinationTag: destinationTag)
statusMessage = "Payment sent"
await refresh(force: true)
return
}
_ = try await MoneroService.shared.send(address: to, amountXMR: amount, priority: priority)
statusMessage = "Payment sent"
await refresh(force: true)
}
// MARK: - Delete
func deleteWallet() async {
if chain == .xrp {
WalletStore.removeWallet(.xrp)
xrpWallet = nil
} else {
if let wallet = xmrWallet { await MoneroService.shared.removeWallet(name: wallet.name) }
WalletStore.removeWallet(.xmr)
xmrWallet = nil
moneroOpened = false
moneroReady = false
moneroAddress = nil
syncStatus = nil
}
balance = Balance(total: "0", unlocked: "", funded: false)
transactions = []
errorMessage = nil
statusMessage = nil
}
func clearMessages() {
errorMessage = nil
statusMessage = nil
}
}
@@ -0,0 +1,2 @@
#import "KoinsMoneroWallet.h"
#import "KoinsSecp256k1.h"
+16
View File
@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
#import "KoinsSecp256k1.h"
NS_ASSUME_NONNULL_BEGIN
/// Objective-C++ bridge to the on-device monero-cpp wallet. A single JSON
/// in/out entry point keeps the C++ surface out of Swift; work is serialized
/// on a private queue because a monero_wallet_full is not thread safe.
@interface KoinsMoneroWallet : NSObject
+ (void)invoke:(NSString *)jsonRequest
completion:(void (^)(NSString *response))completion;
@end
NS_ASSUME_NONNULL_END
+276
View File
@@ -0,0 +1,276 @@
#import "KoinsMoneroWallet.h"
#import <os/log.h>
#include "monero_wallet_full.h"
static std::string walletPath(NSString *name) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = paths.firstObject ?: NSTemporaryDirectory();
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:dir]) {
[fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
}
return std::string([dir stringByAppendingPathComponent:name].UTF8String);
}
static monero_wallet_full *g_wallet = nullptr;
static NSString *g_daemonURL = @"xmr-node.cakewallet.com:18081";
static NSArray<NSString *> *g_fallbackDaemons = nil;
static std::atomic<bool> s_cachedConnected(false);
static std::atomic<unsigned long long> s_cachedDaemonHeight(0);
static std::atomic<long long> s_lastDaemonProbe(0);
static void connectDaemon(monero_wallet_full *w) {
if (!w) return;
@autoreleasepool {
if (!g_fallbackDaemons) {
g_fallbackDaemons = @[
@"xmr-node.cakewallet.com:18081",
@"node.monerodevs.org:18089",
@"monero.stackwallet.com:18081",
];
}
NSMutableArray<NSString *> *candidates = [NSMutableArray arrayWithObject:g_daemonURL];
for (NSString *url in g_fallbackDaemons) {
if (![url isEqualToString:g_daemonURL]) [candidates addObject:url];
}
for (NSString *url in candidates) {
try {
w->set_daemon_connection(std::string(url.UTF8String));
if (w->is_connected_to_daemon()) {
g_daemonURL = url;
return;
}
} catch (...) {
// Try the next daemon.
}
}
}
}
@implementation KoinsMoneroWallet
+ (void)invoke:(NSString *)jsonRequest
completion:(void (^)(NSString *response))completion {
static dispatch_queue_t invokeQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
invokeQueue = dispatch_queue_create("com.koins.monero.invoke", DISPATCH_QUEUE_SERIAL);
});
dispatch_async(invokeQueue, ^{
NSMutableDictionary *resp = [NSMutableDictionary new];
try {
NSData *data = [jsonRequest dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *req = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSString *op = req[@"op"] ?: @"";
resp[@"op"] = op;
if ([op isEqualToString:@"setDaemon"]) {
NSString *url = req[@"url"];
if (url.length > 0) g_daemonURL = url;
resp[@"url"] = g_daemonURL;
} else if ([op isEqualToString:@"createWallet"]) {
NSString *name = req[@"name"];
std::string path = walletPath(name);
std::string password = std::string([req[@"password"] UTF8String]);
bool exists = false;
try { exists = monero_wallet_full::wallet_exists(path); } catch (...) {}
if (exists) {
g_wallet = monero_wallet_full::open_wallet(path, password, monero_network_type::MAINNET, false);
connectDaemon(g_wallet);
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
try { resp[@"mnemonic"] = [NSString stringWithUTF8String:g_wallet->get_seed().c_str()]; } catch (...) {}
} else {
monero_wallet_config config;
config.m_path = path;
config.m_password = password;
config.m_network_type = monero_network_type::MAINNET;
config.m_language = std::string([req[@"language"] ?: @"English" UTF8String]);
config.m_save_current = false;
monero_wallet_full *w = monero_wallet_full::create_wallet(config, nullptr);
g_wallet = w;
connectDaemon(w);
resp[@"address"] = [NSString stringWithUTF8String:w->get_address(0, 0).c_str()];
resp[@"mnemonic"] = [NSString stringWithUTF8String:w->get_seed().c_str()];
}
} else if ([op isEqualToString:@"openWallet"] || [op isEqualToString:@"restoreWallet"]) {
if (g_wallet) {
try { g_wallet->close(true); } catch (...) {}
delete g_wallet;
g_wallet = nullptr;
}
if ([op isEqualToString:@"openWallet"]) {
g_wallet = monero_wallet_full::open_wallet(
walletPath(req[@"name"]),
std::string([req[@"password"] UTF8String]),
monero_network_type::MAINNET, false);
} else {
monero_wallet_config config;
config.m_path = walletPath(req[@"name"]);
config.m_password = std::string([req[@"password"] UTF8String]);
config.m_network_type = monero_network_type::MAINNET;
config.m_seed = std::string([req[@"seed"] UTF8String]);
config.m_restore_height = (uint64_t)[req[@"restoreHeight"] longLongValue];
config.m_language = "English";
config.m_save_current = false;
g_wallet = monero_wallet_full::create_wallet(config, nullptr);
}
connectDaemon(g_wallet);
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
} else if ([op isEqualToString:@"getSeed"]) {
if (g_wallet) {
resp[@"mnemonic"] = [NSString stringWithUTF8String:g_wallet->get_seed().c_str()];
} else {
resp[@"error"] = @"No wallet open";
}
} else if ([op isEqualToString:@"removeWallet"]) {
NSString *name = req[@"name"];
if (g_wallet) {
try { g_wallet->close(false); } catch (...) {}
delete g_wallet;
g_wallet = nullptr;
}
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = paths.firstObject;
NSFileManager *fm = [NSFileManager defaultManager];
for (NSString *ext in @[ @"", @".keys", @".address.txt", @".mms" ]) {
NSString *file = [dir stringByAppendingPathComponent:[name stringByAppendingString:ext]];
[fm removeItemAtPath:file error:nil];
}
} else if ([op isEqualToString:@"closeWallet"]) {
if (g_wallet) {
try { g_wallet->close(true); } catch (...) {}
delete g_wallet;
g_wallet = nullptr;
}
} else if ([op isEqualToString:@"getBalance"]) {
if (g_wallet) {
uint64_t balance = 0, unlocked = 0;
try {
balance = g_wallet->get_balance();
unlocked = g_wallet->get_unlocked_balance();
} catch (const std::exception &e) {
os_log_error(OS_LOG_DEFAULT, "[koins] getBalance: %{public}s", e.what());
}
resp[@"balance"] = @(balance).stringValue;
resp[@"unlocked"] = @(unlocked).stringValue;
} else {
resp[@"error"] = @"No wallet open";
}
} else if ([op isEqualToString:@"getAddress"]) {
if (g_wallet) {
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
} else {
resp[@"error"] = @"No wallet open";
}
} else if ([op isEqualToString:@"getTransactions"]) {
NSMutableArray *entries = [NSMutableArray new];
if (!g_wallet) {
resp[@"transactions"] = entries;
} else {
try {
monero_transfer_query query;
for (const auto &transfer : g_wallet->get_transfers(query)) {
const auto &tx = transfer->m_tx;
const auto isIn = transfer->is_incoming();
boost::optional<uint64_t> txHeight = tx ? tx->get_height() : boost::none;
[entries addObject:@{
@"hash" : [NSString stringWithUTF8String:((tx && tx->m_hash) ? *tx->m_hash : "").c_str()],
@"amount" : @((transfer->m_amount ? *transfer->m_amount : 0)).stringValue,
@"timestamp" : @((tx && tx->m_received_timestamp) ? *tx->m_received_timestamp : 0),
@"direction" : (isIn && *isIn) ? @"in" : @"out",
@"confirmed" : @((tx && tx->m_is_confirmed) ? *tx->m_is_confirmed : false),
@"height" : @(txHeight ? *txHeight : 0),
@"fee" : @((tx && tx->m_fee) ? *tx->m_fee : 0).stringValue,
}];
}
} catch (const std::exception &e) {
os_log_error(OS_LOG_DEFAULT, "[koins] getTransactions: %{public}s", e.what());
}
resp[@"transactions"] = entries;
}
} else if ([op isEqualToString:@"startSync"]) {
if (g_wallet) {
connectDaemon(g_wallet);
try {
if (g_wallet->is_connected_to_daemon()) {
g_wallet->start_syncing(5000);
resp[@"syncing"] = @YES;
} else {
resp[@"syncing"] = @NO;
}
} catch (const std::exception &e) {
resp[@"syncing"] = @NO;
}
} else {
resp[@"error"] = @"No wallet open";
}
} else if ([op isEqualToString:@"stopSync"]) {
try { if (g_wallet) g_wallet->stop_syncing(); } catch (...) {}
} else if ([op isEqualToString:@"getSyncStatus"]) {
uint64_t height = 0, daemonHeight = 0;
bool connected = false;
if (g_wallet) {
try {
height = g_wallet->get_height();
// is_connected_to_daemon performs a live round trip; public
// daemons rate limit aggressive polling, so cache for 10s.
long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000);
if (now - s_lastDaemonProbe.load() > 10000) {
s_lastDaemonProbe.store(now);
connected = g_wallet->is_connected_to_daemon();
s_cachedConnected = connected;
} else {
connected = s_cachedConnected;
}
daemonHeight = s_cachedDaemonHeight;
if (connected) {
daemonHeight = g_wallet->get_daemon_height();
s_cachedDaemonHeight = daemonHeight;
}
} catch (const std::exception &e) {
os_log_error(OS_LOG_DEFAULT, "[koins] getSyncStatus: %{public}s", e.what());
}
}
resp[@"height"] = @(height);
resp[@"daemonHeight"] = @(daemonHeight);
resp[@"syncing"] = @(connected && height < daemonHeight);
resp[@"connected"] = @(connected);
} else if ([op isEqualToString:@"transferTo"]) {
if (!g_wallet) {
resp[@"error"] = @"No wallet open";
} else {
monero_tx_config config;
auto destination = std::make_shared<monero_destination>();
destination->m_address = std::string([req[@"address"] UTF8String]);
destination->m_amount = (uint64_t)[req[@"amount"] longLongValue];
config.m_destinations = { destination };
config.m_priority = (monero_tx_priority)[req[@"priority"] integerValue];
config.m_relay = true;
auto txs = g_wallet->create_txs(config);
const auto &txSet = txs[0]->m_tx_set;
std::string signedHex = (txSet && txSet->m_signed_tx_hex) ? *txSet->m_signed_tx_hex : "";
g_wallet->submit_txs(signedHex);
resp[@"txHash"] = [NSString stringWithUTF8String:(txs[0]->m_hash ? *txs[0]->m_hash : "").c_str()];
resp[@"fee"] = @((txs[0]->m_fee ? *txs[0]->m_fee : 0)).stringValue;
}
} else {
resp[@"error"] = [NSString stringWithFormat:@"Unknown op: %@", op];
}
} catch (const std::exception &e) {
os_log_error(OS_LOG_DEFAULT, "[koins] monero %{public}@ failed: %{public}s",
resp[@"op"], e.what());
resp[@"error"] = [NSString stringWithUTF8String:e.what()];
} catch (...) {
resp[@"error"] = @"Unknown native exception";
}
NSData *out = [NSJSONSerialization dataWithJSONObject:resp options:0 error:nil];
NSString *outStr = [[NSString alloc] initWithData:out encoding:NSUTF8StringEncoding];
completion(outStr ?: @"{\"error\":\"encode failed\"}");
});
}
@end
+25
View File
@@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// secp256k1 helpers used by the XRP wallet. Backed by the OpenSSL build that
/// already ships inside the on-device monero static library, so no extra
/// dependency is linked.
@interface KoinsSecp256k1 : NSObject
/// XRP account private scalar derived from a 16-byte family seed entropy.
+ (nullable NSData *)privateKeyFromEntropy:(NSData *)entropy;
/// 33-byte compressed public key for a 32-byte private scalar.
+ (nullable NSData *)publicKeyFromPrivateKey:(NSData *)privateKey;
/// DER-encoded, low-S ECDSA signature over a 32-byte digest.
+ (nullable NSData *)signDigest:(NSData *)digest
withPrivateKey:(NSData *)privateKey;
/// (left + right) mod n, used by BIP-32 hardened child derivation.
+ (nullable NSData *)addScalar:(NSData *)left toScalar:(NSData *)right;
@end
NS_ASSUME_NONNULL_END
+187
View File
@@ -0,0 +1,187 @@
#import "KoinsSecp256k1.h"
#import <openssl/bn.h>
#import <openssl/ec.h>
#import <openssl/ecdsa.h>
#import <openssl/obj_mac.h>
#import <openssl/sha.h>
// secp256k1 group order.
static const char *kCurveOrderHex =
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141";
static BIGNUM *KoinsCurveOrder(void) {
BIGNUM *order = NULL;
BN_hex2bn(&order, kCurveOrderHex);
return order;
}
static NSData *KoinsSHA512Half(NSData *input) {
unsigned char digest[SHA512_DIGEST_LENGTH];
SHA512((const unsigned char *)input.bytes, (size_t)input.length, digest);
return [NSData dataWithBytes:digest length:32];
}
// Mirror of ripple-keypairs deriveScalar: hash the bytes (optionally with a
// 32-bit discriminator) until the result is a valid non-zero curve scalar.
static BIGNUM *KoinsDeriveScalar(NSData *bytes, BOOL hasDiscriminator,
uint32_t discriminator, const BIGNUM *order) {
for (uint32_t i = 0; i <= 4294967295u; i++) {
NSMutableData *input = [NSMutableData dataWithData:bytes];
if (hasDiscriminator) {
uint32_t d = htonl(discriminator);
[input appendBytes:&d length:4];
}
uint32_t idx = htonl(i);
[input appendBytes:&idx length:4];
NSData *half = KoinsSHA512Half(input);
BIGNUM *scalar = BN_bin2bn((const unsigned char *)half.bytes, (int)half.length, NULL);
if (scalar && !BN_is_zero(scalar) && BN_cmp(scalar, order) < 0) {
return scalar;
}
if (scalar) BN_free(scalar);
}
return NULL;
}
@implementation KoinsSecp256k1
+ (nullable NSData *)privateKeyFromEntropy:(NSData *)entropy {
if (entropy.length != 16) return nil;
BN_CTX *ctx = BN_CTX_new();
BIGNUM *order = KoinsCurveOrder();
EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_secp256k1);
EC_POINT *publicGen = EC_POINT_new(group);
NSData *result = nil;
BIGNUM *privateGen = KoinsDeriveScalar(entropy, NO, 0, order);
if (privateGen && EC_POINT_mul(group, publicGen, privateGen, NULL, NULL, ctx)) {
unsigned char compressed[33];
size_t written = EC_POINT_point2oct(group, publicGen,
POINT_CONVERSION_COMPRESSED,
compressed, sizeof(compressed), ctx);
if (written == sizeof(compressed)) {
NSData *publicGenBytes = [NSData dataWithBytes:compressed length:written];
BIGNUM *offset = KoinsDeriveScalar(publicGenBytes, YES, 0, order);
if (offset) {
BIGNUM *derived = BN_new();
if (BN_mod_add(derived, offset, privateGen, order, ctx)) {
unsigned char out[32] = {0};
BN_bn2binpad(derived, out, sizeof(out));
result = [NSData dataWithBytes:out length:sizeof(out)];
}
BN_free(derived);
BN_free(offset);
}
}
}
if (privateGen) BN_free(privateGen);
if (publicGen) EC_POINT_free(publicGen);
if (group) EC_GROUP_free(group);
if (order) BN_free(order);
if (ctx) BN_CTX_free(ctx);
return result;
}
+ (nullable NSData *)publicKeyFromPrivateKey:(NSData *)privateKey {
if (privateKey.length != 32) return nil;
BN_CTX *ctx = BN_CTX_new();
EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_secp256k1);
EC_POINT *point = EC_POINT_new(group);
BIGNUM *scalar = BN_bin2bn((const unsigned char *)privateKey.bytes, (int)privateKey.length, NULL);
NSData *result = nil;
if (scalar && EC_POINT_mul(group, point, scalar, NULL, NULL, ctx)) {
unsigned char compressed[33];
size_t written = EC_POINT_point2oct(group, point,
POINT_CONVERSION_COMPRESSED,
compressed, sizeof(compressed), ctx);
if (written == sizeof(compressed)) {
result = [NSData dataWithBytes:compressed length:written];
}
}
if (scalar) BN_free(scalar);
if (point) EC_POINT_free(point);
if (group) EC_GROUP_free(group);
if (ctx) BN_CTX_free(ctx);
return result;
}
+ (nullable NSData *)addScalar:(NSData *)left toScalar:(NSData *)right {
if (left.length != 32 || right.length != 32) return nil;
BN_CTX *ctx = BN_CTX_new();
BIGNUM *order = KoinsCurveOrder();
BIGNUM *a = BN_bin2bn((const unsigned char *)left.bytes, (int)left.length, NULL);
BIGNUM *b = BN_bin2bn((const unsigned char *)right.bytes, (int)right.length, NULL);
BIGNUM *sum = BN_new();
NSData *result = nil;
if (a && b && sum && BN_mod_add(sum, a, b, order, ctx)) {
unsigned char out[32] = {0};
BN_bn2binpad(sum, out, sizeof(out));
result = [NSData dataWithBytes:out length:sizeof(out)];
}
if (sum) BN_free(sum);
if (b) BN_free(b);
if (a) BN_free(a);
if (order) BN_free(order);
if (ctx) BN_CTX_free(ctx);
return result;
}
+ (nullable NSData *)signDigest:(NSData *)digest
withPrivateKey:(NSData *)privateKey {
if (digest.length != 32 || privateKey.length != 32) return nil;
BN_CTX *ctx = BN_CTX_new();
EC_KEY *key = EC_KEY_new_by_curve_name(NID_secp256k1);
BIGNUM *scalar = BN_bin2bn((const unsigned char *)privateKey.bytes, (int)privateKey.length, NULL);
NSData *result = nil;
if (key && scalar && EC_KEY_set_private_key(key, scalar)) {
const EC_GROUP *group = EC_KEY_get0_group(key);
EC_POINT *pub = EC_POINT_new(group);
if (pub && EC_POINT_mul(group, pub, scalar, NULL, NULL, ctx) &&
EC_KEY_set_public_key(key, pub)) {
ECDSA_SIG *sig = ECDSA_do_sign((const unsigned char *)digest.bytes, (int)digest.length, key);
if (sig) {
// Ripple only accepts canonical (low-S) signatures.
BIGNUM *order = KoinsCurveOrder();
BIGNUM *halfOrder = BN_dup(order);
BN_rshift1(halfOrder, halfOrder);
const BIGNUM *r = NULL;
const BIGNUM *s = NULL;
ECDSA_SIG_get0(sig, &r, &s);
if (BN_cmp(s, halfOrder) > 0) {
BIGNUM *lowS = BN_new();
BN_sub(lowS, order, s);
ECDSA_SIG_set0(sig, BN_dup(r), lowS);
}
unsigned char *der = NULL;
int derLen = i2d_ECDSA_SIG(sig, &der);
if (derLen > 0) {
result = [NSData dataWithBytes:der length:(NSUInteger)derLen];
OPENSSL_free(der);
}
BN_free(halfOrder);
BN_free(order);
ECDSA_SIG_free(sig);
}
}
if (pub) EC_POINT_free(pub);
}
if (scalar) BN_free(scalar);
if (key) EC_KEY_free(key);
if (ctx) BN_CTX_free(ctx);
return result;
}
@end
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
</dict>
</plist>
+30
View File
@@ -0,0 +1,30 @@
import SwiftUI
@main
struct KoinsApp: App {
@StateObject private var model: AppModel
init() {
Secp256k1Backend.derivePrivateKey = { entropy in
KoinsSecp256k1.privateKey(fromEntropy: entropy)
}
Secp256k1Backend.publicKey = { privateKey in
KoinsSecp256k1.publicKey(fromPrivateKey: privateKey)
}
Secp256k1Backend.sign = { digest, privateKey in
KoinsSecp256k1.signDigest(digest, withPrivateKey: privateKey)
}
Secp256k1Backend.addScalar = { left, right in
KoinsSecp256k1.addScalar(left, toScalar: right)
}
_model = StateObject(wrappedValue: AppModel())
SelfTest.runIfRequested()
}
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(model)
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import Foundation
enum Chain: String, Codable, CaseIterable {
case xrp
case xmr
var symbol: String { self == .xrp ? "XRP" : "XMR" }
var title: String { self == .xrp ? "XRP Ledger" : "Monero" }
var subtitle: String {
self == .xrp ? "Fast, low-fee payments" : "Private, untraceable payments"
}
}
struct WalletRecord: Codable, Identifiable, Equatable {
let id: String
var name: String
var address: String
var createdAt: Date
}
enum Direction {
case incoming
case outgoing
}
struct TransactionRecord: Identifiable {
let id: String
let amount: String
let timestamp: Date?
let direction: Direction
let confirmed: Bool
var fee: String?
var tag: Int?
var counterparty: String?
}
struct Balance {
var total: String
var unlocked: String
var funded: Bool
}
struct SyncStatus {
var height: UInt64
var daemonHeight: UInt64
var syncing: Bool
var connected: Bool
var progress: Double {
guard daemonHeight > 0 else { return 0 }
return min(1, Double(height) / Double(daemonHeight))
}
}
enum TransactionPriority: Int {
case defaultPriority = 0
case low = 1
case medium = 2
case high = 3
var label: String {
switch self {
case .defaultPriority: return "Default"
case .low: return "Low"
case .medium: return "Medium"
case .high: return "High"
}
}
}
@@ -0,0 +1,107 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <exception>
#include <string>
/**
* Public interface for libmonero-cpp library.
*/
namespace monero {
/**
* Exception when interacting with a Monero wallet or daemon.
*/
class monero_error : public std::exception {
public:
std::string message;
monero_error() {}
/**
* Construct the exception.
*
* @param msg is a human-readable description of the error
*/
monero_error(const std::string& msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
};
/**
* Exception when interacting with the Monero daemon or wallet RPC API.
*/
class monero_rpc_error : public monero_error {
public:
int code;
/**
* Construct the exception.
*
* @param error_code is the error code (optional)
* @param msg is a human-readable description of the error
*/
monero_rpc_error(int error_code, const std::string& msg) : code(error_code) { message = msg; }
/**
* Construct the exception.
*
* @param msg is a human-readable description of the error
*/
monero_rpc_error(const std::string& msg) : code(-1) { message = msg; }
};
}
@@ -0,0 +1,439 @@
#include "monero_rpc_connection.h"
#include "utils/gen_utils.h"
#include "utils/monero_utils.h"
#include "daemon/monero_daemon_rpc_model.h"
#include "monero_error.h"
static const uint32_t DEFAULT_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
namespace monero {
// --------------------------- MONERO REQUEST PARAMS ---------------------------
rapidjson::Value monero_request_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
rapidjson::Value root(rapidjson::kObjectType);
return root;
}
// --------------------------- MONERO RPC REQUEST ---------------------------
monero_rpc_request::monero_rpc_request(const std::string& method, const std::shared_ptr<serializable_struct>& params, bool json_rpc): m_method(method), m_params(params) {
if (params == nullptr) m_params = std::make_shared<monero_request_params>();
if (json_rpc) {
m_id = "0";
m_version = "2.0";
}
}
rapidjson::Value monero_rpc_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
if (!is_json_rpc()) {
if (m_params == nullptr) throw monero_error("No params provided");
return m_params->to_rapidjson_val(allocator);
}
// create root
rapidjson::Value root(rapidjson::kObjectType);
// set string values
rapidjson::Value value_str(rapidjson::kStringType);
if (m_version != boost::none) monero_utils::add_json_member("jsonrpc", m_version.get(), allocator, root, value_str);
if (m_id != boost::none) monero_utils::add_json_member("id", m_id.get(), allocator, root, value_str);
if (m_method != boost::none) monero_utils::add_json_member("method", m_method.get(), allocator, root, value_str);
if (m_params != nullptr) root.AddMember("params", m_params->to_rapidjson_val(allocator), allocator);
// return root
return root;
}
// --------------------------- MONERO RPC RESPONSE ---------------------------
void raise_rpc_error(const boost::property_tree::ptree& error_node, const boost::string_ref uri, const std::string& method) {
std::string err_message = "Unknown error";
int err_code = -1;
for (auto it = error_node.begin(); it != error_node.end(); ++it) {
std::string key_err = it->first;
if (key_err == std::string("message")) {
if (it->second.data().empty()) {
err_message = "Received error response from RPC request with method '" + method + "' to " + uri.to_string();
}
else err_message = it->second.data();
} else if (key_err == std::string("code")) {
try {
err_code = it->second.get_value<int>();
} catch (...) {
MERROR("non-numeric error code from a malformed/unusual RPC response");
err_code = -1;
}
}
}
throw monero_rpc_error(err_code, err_message);
}
// --------------------------- MONERO RPC CONNECTION ---------------------------
bool monero_rpc_connection::compare(int p1, int p2) {
if (p1 == p2) return false;
// 0 alway first
if (p1 == 0) return true;
if (p2 == 0) return false;
return p1 > p2;
}
monero_rpc_connection::monero_rpc_connection(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, int priority, const boost::optional<uint32_t>& timeout_ms) {
if (!uri.empty()) m_uri = uri;
else m_uri = boost::none;
if (!proxy_uri.empty()) m_proxy_uri = proxy_uri;
else m_proxy_uri = boost::none;
if (!zmq_uri.empty()) m_zmq_uri = zmq_uri;
else m_zmq_uri = boost::none;
m_priority = priority;
m_timeout_ms = timeout_ms;
set_credentials(username, password);
}
monero_rpc_connection::monero_rpc_connection(const monero_rpc_connection& rpc) {
boost::lock_guard<boost::recursive_mutex> src_lock(rpc.m_mutex);
m_uri = rpc.m_uri;
m_proxy_uri = rpc.m_proxy_uri;
m_zmq_uri = rpc.m_zmq_uri;
m_priority = rpc.m_priority;
m_timeout_ms = rpc.m_timeout_ms;
set_credentials(rpc.m_username.value_or(""), rpc.m_password.value_or(""));
m_is_online = rpc.m_is_online;
m_is_authenticated = rpc.m_is_authenticated;
m_response_time = rpc.m_response_time;
m_attributes = rpc.m_attributes;
}
rapidjson::Value monero_rpc_connection::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
// create root
rapidjson::Value root(rapidjson::kObjectType);
// set string values
rapidjson::Value value_str(rapidjson::kStringType);
if (m_uri != boost::none) monero_utils::add_json_member("uri", m_uri.get(), allocator, root, value_str);
if (m_username != boost::none) monero_utils::add_json_member("username", m_username.get(), allocator, root, value_str);
if (m_password != boost::none) monero_utils::add_json_member("password", m_password.get(), allocator, root, value_str);
if (m_proxy_uri != boost::none) monero_utils::add_json_member("proxyUri", m_proxy_uri.get(), allocator, root, value_str);
if (m_zmq_uri != boost::none) monero_utils::add_json_member("zmqUri", m_zmq_uri.get(), allocator, root, value_str);
// set num values
rapidjson::Value value_num(rapidjson::kNumberType);
monero_utils::add_json_member("priority", m_priority, allocator, root, value_num);
if (m_timeout_ms != boost::none) monero_utils::add_json_member("timeoutMs", m_timeout_ms.get(), allocator, root, value_num);
if (m_response_time != boost::none) monero_utils::add_json_member("responseTime", m_response_time.get(), allocator, root, value_num);
// set bool values
if (m_is_online != boost::none) monero_utils::add_json_member("isOnline", m_is_online.get(), allocator, root);
if (m_is_authenticated != boost::none) monero_utils::add_json_member("isAuthenticated", m_is_authenticated.get(), allocator, root);
return root;
}
bool monero_rpc_connection::is_onion() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
epee::net_utils::http::url_content parsed{};
if (!epee::net_utils::parse_url(m_uri.value_or(""), parsed)) return false;
return parsed.host.size() >= 6 && parsed.host.compare(parsed.host.size() - 6, 6, ".onion") == 0;
}
bool monero_rpc_connection::is_i2p() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
epee::net_utils::http::url_content parsed{};
if (!epee::net_utils::parse_url(m_uri.value_or(""), parsed)) return false;
return parsed.host.size() >= 8 && parsed.host.compare(parsed.host.size() - 8, 8, ".b32.i2p") == 0;
}
void monero_rpc_connection::set_credentials(const std::string& username, const std::string& password) {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
// disconnect http client if connected
if (m_http_client != nullptr && m_http_client->is_connected()) m_http_client->disconnect();
bool username_empty = username.empty();
bool password_empty = password.empty();
// check username and password consistency
if (!username_empty || !password_empty) {
if (password_empty) {
throw monero_error("password cannot be empty because username is not empty");
}
if (username_empty) {
throw monero_error("username cannot be empty because password is not empty");
}
}
// check username and password changes
bool username_equals = (m_username == boost::none && username_empty) || (m_username != boost::none && *m_username == username);
bool password_equals = (m_password == boost::none && password_empty) || (m_password != boost::none && *m_password == password);
// connection reset values
if (!username_equals || !password_equals) {
m_is_online = boost::none;
m_is_authenticated = boost::none;
}
// setup username and password
if (!username_empty && !password_empty) {
m_username = username;
m_password = password;
} else {
m_username = boost::none;
m_password = boost::none;
}
}
void monero_rpc_connection::set_attribute(const std::string& key, const std::string& val) {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
m_attributes[key] = val;
}
std::string monero_rpc_connection::get_attribute(const std::string& key) const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
std::unordered_map<std::string, std::string>::const_iterator i = m_attributes.find(key);
if (i == m_attributes.end()) {
return std::string(""); // attribute not found
}
return i->second;
}
boost::optional<bool> monero_rpc_connection::is_online() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
return m_is_online;
}
boost::optional<bool> monero_rpc_connection::is_authenticated() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
return m_is_authenticated;
}
boost::optional<bool> monero_rpc_connection::is_connected() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
if (m_is_online == boost::none) return boost::none;
return m_is_online.get() && (m_is_authenticated == boost::none || m_is_authenticated.get());
}
bool monero_rpc_connection::check_connection(const boost::optional<uint32_t>& timeout_ms) {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
boost::optional<bool> is_online_before = m_is_online;
boost::optional<bool> is_authenticated_before = m_is_authenticated;
boost::optional<std::chrono::high_resolution_clock::time_point> start_time = boost::none;
try {
// assume daemon connection
monero_get_blocks_by_height_request request(100);
start_time = std::chrono::high_resolution_clock::now();
send_binary_request(request, timeout_ms);
// set response time
m_response_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time.get()).count();
m_is_online = true;
m_is_authenticated = true;
}
catch (const monero_rpc_error& ex) {
m_is_online = false;
m_is_authenticated = boost::none;
m_response_time = boost::none;
if (ex.code == 401 || ex.code == 404) {
// fallback to latency check
m_is_online = true;
m_is_authenticated = ex.code == 404;
}
}
catch (...) {
m_is_online = false;
m_is_authenticated = boost::none;
m_response_time = boost::none;
}
if (gen_utils::bool_equals(true, m_is_online) && start_time != boost::none) {
m_response_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time.get()).count();
}
return is_online_before != m_is_online || is_authenticated_before != m_is_authenticated;
}
boost::property_tree::ptree monero_rpc_connection::send_json_request(const std::string& path, const std::shared_ptr<serializable_struct>& params, const boost::optional<uint32_t>& timeout_ms) const {
// send JSON-RPC request
monero_rpc_request request(path, params);
auto response = send_json_request(request, timeout_ms);
// assert JSON-RPC response is defined
if (response.m_result == boost::none) throw monero_error("Invalid Monero JSONRPC response");
return response.m_result.get();
}
monero_rpc_response monero_rpc_connection::send_json_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
// invoke JSON-RPC method
monero_rpc_response response;
send_rpc_request("/json_rpc", request, response, timeout_ms);
// return JSON-RPC response
return response;
}
boost::property_tree::ptree monero_rpc_connection::send_path_request(const std::string& path, const std::shared_ptr<serializable_struct>& params, const boost::optional<uint32_t>& timeout_ms) const {
// send RPC request
monero_rpc_request request(path, params, false);
auto response = send_path_request(request, timeout_ms);
// assert RPC response is defined
if (response.m_response == boost::none) throw monero_error("Invalid Monero RPC response");
return response.m_response.get();
}
monero_rpc_response monero_rpc_connection::send_path_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
// validate parameters
if (request.m_method == boost::none || request.m_method->empty()) throw monero_error("No RPC method set in path request");
// invoke RPC method
monero_rpc_response response;
send_rpc_request(std::string("/") + request.m_method.get(), request, response, timeout_ms);
// return RPC response
return response;
}
monero_rpc_response monero_rpc_connection::send_binary_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
// validate parameters
if (request.m_method == boost::none || request.m_method->empty()) throw monero_error("No RPC method set in binary request");
// invoke binary RPC method
monero_rpc_response response;
send_rpc_request(std::string("/") + request.m_method.get(), request, response, timeout_ms, true);
// return binary response
return response;
}
void monero_rpc_connection::ensure_configured() const {
// create http client on first use, so constructing a connection does not require network support
if (m_http_client == nullptr) {
auto factory = std::make_unique<net::http::client_factory>();
m_http_client = factory->create();
m_http_client->set_auto_connect(true);
}
// only reconfigure when uri/creds/proxy changed since last apply
std::string uri = m_uri.value_or("");
std::string username = m_username.value_or("");
std::string password = m_password.value_or("");
std::string proxy_uri = m_proxy_uri.value_or("");
auto key = std::make_tuple(uri, username, password, proxy_uri);
if (m_applied == key) return; // unchanged, so reuse live connection
// setup connection credentials
boost::optional<epee::net_utils::http::login> login;
if(!username.empty() && !password.empty()) {
login = epee::net_utils::http::login();
login->username = username;
login->password = password;
}
// detect ssl
epee::net_utils::ssl_support_t ssl = uri.rfind("https", 0) == 0 ? epee::net_utils::ssl_support_t::e_ssl_support_enabled : epee::net_utils::ssl_support_t::e_ssl_support_disabled;
if (!m_http_client->set_proxy(proxy_uri)) throw monero_error("Could not set proxy uri: " + proxy_uri);
if (!m_http_client->set_server(uri, login, std::move(ssl))) throw monero_error("Could not set uri: " + uri);
if (!m_http_client->connect(std::chrono::milliseconds(DEFAULT_TIMEOUT_MS))) MWARNING("Could not connect to server: " + uri);
m_applied = key;
}
std::string monero_rpc_connection::invoke_post(const boost::string_ref uri, const std::string& body, const boost::optional<uint32_t>& timeout_ms) const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
// request/response timeout: user-configurable, defaults to 3 minutes when not set.
uint32_t timeout = DEFAULT_TIMEOUT_MS;
if (timeout_ms != boost::none) timeout = timeout_ms.get();
else if (m_timeout_ms != boost::none) timeout = m_timeout_ms.get();
if (timeout == 0) timeout = std::numeric_limits<uint32_t>::max(); // timeout of 0 is treated as indefinite
// ensure_configured() bounds only the first connect; epee reconnects with this request timeout
ensure_configured();
const epee::net_utils::http::http_response_info* pri = NULL;
// invoke http json
const uint64_t received_before = m_http_client->get_bytes_received();
if (!m_http_client->invoke_post(uri, body, std::chrono::milliseconds(timeout), std::addressof(pri))) {
// epee never surfaces the 401 status, but a rejected login means the server answered;
// a dead or idle-closed socket yields no response bytes at all
bool received = m_http_client->get_bytes_received() > received_before;
if (received && m_http_client->is_connected()) {
throw monero_rpc_error(401, "Unauthorized");
}
// drop the socket: epee leaves EOF'd sockets marked connected, so without this the next
// call reuses the poisoned socket and hits the same misdiagnosis again
m_http_client->disconnect();
if (m_uri == boost::none || m_uri->empty()) throw monero_error("Cannot send RPC request: uri not set.");
throw monero_error("Network error: " + std::string(received ? "connection dropped mid-response" : "no response") + " from " + m_uri.get() + uri.to_string() + " (timeout: " + std::to_string(timeout) + "ms)");
}
if (!pri) throw monero_error("Could not get response info");
if (pri->m_response_code < 200 || pri->m_response_code > 299) {
std::string err_msg = std::to_string(pri->m_response_code) + " " + pri->m_response_comment + (pri->m_body.empty() ? "" : ": " + pri->m_body);
throw monero_rpc_error(pri->m_response_code, err_msg);
}
// return response info body
return pri->m_body;
}
void deserialize_rpc_response(const std::string& json, monero_rpc_response& response, const boost::string_ref uri, const std::string& method) {
// parse json to property node
boost::property_tree::ptree node;
gen_utils::deserialize(json, node);
// parse JSON-RPC response
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
std::string key = it->first;
if (key == std::string("error")) raise_rpc_error(it->second, uri, method);
else if (key == std::string("jsonrpc")) response.m_jsonrpc = it->second.data();
else if (key == std::string("result")) response.m_result = it->second;
}
// if not JSON-RPC, set RPC response
if (response.m_jsonrpc == boost::none) response.m_response = node;
}
void monero_rpc_connection::send_rpc_request(const boost::string_ref uri, const monero_rpc_request& request, monero_rpc_response& response, const boost::optional<uint32_t>& timeout_ms, bool binary) const {
std::string body;
if (request.m_method == boost::none) throw monero_error("Must provide a valid request method");
if (binary) monero_utils::json_to_binary(request.serialize(), body);
else body = request.serialize();
std::string result = invoke_post(uri, body, timeout_ms);
if (binary) response.m_binary = result;
else deserialize_rpc_response(result, response, uri, request.m_method.get());
}
std::shared_ptr<monero_rpc_connection> monero_rpc_connection::from_property_tree(const boost::property_tree::ptree& node) {
std::shared_ptr<monero_rpc_connection> connection = std::make_shared<monero_rpc_connection>();
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
std::string key = it->first;
if (key == std::string("uri")) connection->m_uri = it->second.data();
else if (key == std::string("username")) connection->m_username = it->second.data();
else if (key == std::string("password")) connection->m_password = it->second.data();
else if (key == std::string("proxyUri") || key == std::string("proxy_uri")) connection->m_proxy_uri = it->second.data();
else if (key == std::string("zmqUri")) connection->m_zmq_uri = it->second.data();
}
return connection;
}
}
@@ -0,0 +1,281 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/condition_variable.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
#include "net/http.h"
#include "daemon/monero_daemon_model.h"
/**
* Public interface for libmonero-cpp library.
*/
namespace monero {
/**
* Models parameters for a request to a RPC server.
*/
struct monero_request_params : public serializable_struct {
monero_request_params() { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a request to a RPC server.
*/
struct monero_rpc_request : public serializable_struct {
boost::optional<std::string> m_id;
boost::optional<std::string> m_version;
boost::optional<std::string> m_method;
std::shared_ptr<serializable_struct> m_params;
monero_rpc_request() { }
monero_rpc_request(const std::string& method, const std::shared_ptr<serializable_struct>& params, bool json_rpc = true);
bool is_json_rpc() const { return m_id != boost::none && m_version != boost::none; }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a response from a RPC server.
*/
struct monero_rpc_response {
boost::optional<std::string> m_jsonrpc;
boost::optional<boost::property_tree::ptree> m_result;
boost::optional<boost::property_tree::ptree> m_response;
boost::optional<std::string> m_binary;
};
/**
* Maintains a connection and sends requests to a Monero RPC API.
*/
class monero_rpc_connection : public serializable_struct {
public:
boost::optional<std::string> m_uri;
boost::optional<std::string> m_username;
boost::optional<std::string> m_password;
boost::optional<std::string> m_proxy_uri;
boost::optional<std::string> m_zmq_uri; // TODO: implement zmq listener
boost::optional<uint32_t> m_timeout_ms; // RPC request timeout in milliseconds
boost::optional<uint64_t> m_response_time; // automatically set by calling check_connection()
int m_priority; // priority relative to other connections where 1 is highest, then 2, etc, and 0 is lowest (default)
static std::shared_ptr<monero_rpc_connection> from_property_tree(const boost::property_tree::ptree& node);
/**
* Checks RPC connection priority order.
*
* @param c1 first priority to compare
* @param c2 second priority to compare
*/
static bool compare(int p1, int p2);
/**
* Initialize a new RPC connection.
*
* @param uri RPC connection uri
* @param username RPC connection authentication username
* @param password RPC connection authentication password
* @param proxy_uri RPC connection proxy uri
* @param zmq_uri RPC connection zmq uri
* @param priority RPC connection priority
* @param timeout_ms RPC connection timeout in milliseconds
*/
monero_rpc_connection(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", int priority = 0, const boost::optional<uint32_t>& timeout_ms = boost::none);
/**
* Copy a RPC connection.
*
* @param rpc RPC connection to copy
*/
monero_rpc_connection(const monero_rpc_connection& rpc);
/**
* Indicates if the connection uri is a TOR server.
*
* @return true or false to indicate if connection uri is a TOR server
*/
bool is_onion() const;
/**
* Indicates if the connection uri is a I2P server.
*
* @return true or false to indicate if connection uri is a I2P server
*/
bool is_i2p() const;
/**
* Set connection credentials.
*
* @param username username to use in RPC authentication
* @param password password to use in RPC authentication
*/
void set_credentials(const std::string& username, const std::string& password);
/**
* Set connection attribute.
*
* @param key is the attribute key
* @param val is the attribute value
*/
void set_attribute(const std::string& key, const std::string& val);
/**
* Get connection attribute.
*
* @param key is the attribute to get the value of
* @return key's value if set
*/
std::string get_attribute(const std::string& key) const;
/**
* Indicates if the connection is online, which is set automatically by calling check_connection().
*
* @return true or false to indicate if online, or null if check_connection() has not been called
*/
boost::optional<bool> is_online() const;
/**
* Indicates if the connection is authenticated, which is set automatically by calling check_connection().
*
* @return true if authenticated or no authentication, false if not authenticated, or null if not set
*/
boost::optional<bool> is_authenticated() const;
/**
* Indicates if the connection is connected, which is set automatically by calling check_connection().
*
* @return true or false to indicate if connected, or null if check_connection() has not been called
*/
boost::optional<bool> is_connected() const;
/**
* Check the connection and update online, authentication, and response time status.
*
* @param timeout_ms the maximum response time before considered offline
* @return
*/
bool check_connection(const boost::optional<uint32_t>& timeout_ms = boost::none);
/**
* Send a request to the RPC API.
*
* @param path specifies the method to request
* @param params are the request's input parameters
* @return the RPC API response as a map
*/
boost::property_tree::ptree send_json_request(const std::string& path, const std::shared_ptr<serializable_struct>& params = nullptr, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a request to the RPC API.
*
* @param request specifies the method to request with parameters
* @param timeout request timeout in milliseconds
* @return the RPC API response as a map
*/
monero_rpc_response send_json_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a RPC request to the given path and with the given paramters.
*
* E.g. "/get_transactions" with params
*
* @param path is the url path of the request to invoke
* @param params are request parameters sent in the body
* @return the RPC API response as a map
*/
boost::property_tree::ptree send_path_request(const std::string& path, const std::shared_ptr<serializable_struct>& params = nullptr, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a RPC request to the given path and with the given paramters.
*
* @param request specifies the method to request with parameters
* @param timeout request timeout in milliseconds
* @return the request's deserialized response
*/
monero_rpc_response send_path_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a binary RPC request.
*
* @param request specifies the method to request with paramesters
* @param timeout request timeout in milliseconds
* @return the request's deserialized response
*/
monero_rpc_response send_binary_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
protected:
// instance variables
mutable boost::recursive_mutex m_mutex;
boost::optional<bool> m_is_online;
boost::optional<bool> m_is_authenticated;
mutable std::unique_ptr<epee::net_utils::http::abstract_http_client> m_http_client;
std::unordered_map<std::string, std::string> m_attributes;
mutable std::tuple<std::string, std::string, std::string, std::string> m_applied;
void ensure_configured() const;
std::string invoke_post(const boost::string_ref uri, const std::string& body, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
void send_rpc_request(const boost::string_ref uri, const monero_rpc_request& request, monero_rpc_response& response, const boost::optional<uint32_t>& timeout_ms = boost::none, bool binary = false) const;
};
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_daemon.h"
/**
* Public library intereface.
*/
namespace monero {
}
@@ -0,0 +1,805 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "common/monero_rpc_connection.h"
#include "monero_daemon_model.h"
/**
* Public library interface.
*/
namespace monero {
/**
* Receives notifications as a daemon is updated.
*/
class monero_daemon_listener {
public:
std::shared_ptr<monero_block_header> m_last_header;
/**
* Called when a new block is added to the chain.
*
* @param header is the header of the block added to the chain
*/
virtual void on_block_header(const std::shared_ptr<monero_block_header>& header) {
m_last_header = header;
}
};
/**
* Monero daemon interface.
*/
class monero_daemon {
public:
/**
* Virtual destructor.
*/
virtual ~monero_daemon() {}
/**
* Register a listener receive daemon notifications.
*
* @param listener is the listener to receive daemon notifications
*/
virtual void add_listener(monero_daemon_listener &listener) {
throw std::runtime_error("monero_daemon::add_listener(): not supported");
}
/**
* Unregister a listener to receive daemon notifications.
*
* @param listener is the listener to unregister
*/
virtual void remove_listener(monero_daemon_listener &listener) {
throw std::runtime_error("monero_daemon::remove_listener(): not supported");
}
/**
* Get the listeners registered with the daemon.
*/
virtual std::set<monero_daemon_listener*> get_listeners() {
throw std::runtime_error("monero_daemon::get_listeners(): not supported");
}
/**
* Remove all listeners registered with the daemon.
*/
virtual void remove_listeners() {
throw std::runtime_error("monero_daemon::remove_listeners(): not supported");
}
/**
* Get the daemon's version.
*
* @return the daemon's version
*/
virtual monero_version get_version() {
throw std::runtime_error("monero_daemon::get_version(): not supported");
}
/**
* Indicates if the daemon is trusted or untrusted.
*
* @return true if the daemon is trusted, false otherwise
*/
virtual bool is_trusted() {
throw std::runtime_error("monero_daemon::is_trusted(): not supported");
}
/**
* Get the number of blocks in the longest chain known to the node.
*
* @return the number of blocks
*/
virtual uint64_t get_height() {
throw std::runtime_error("monero_daemon::get_height(): not supported");
}
/**
* Get a block's hash by its height.
*
* @param height is the height of the block hash to get
* @return tthe block's hash at the given height
*/
virtual std::string get_block_hash(uint64_t height) {
throw std::runtime_error("monero_daemon::get_block_hash(): not supported");
}
/**
* Get a block template for mining a new block.
*
* @param wallet_address is the address of the wallet to receive miner transactions if block is successfully mined
* @param reserve_size is the reserve size (optional)
* @return a block template for mining a new block
*/
virtual std::shared_ptr<monero_block_template> get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size = boost::none) {
throw std::runtime_error("monero_daemon::get_block_template(): not supported");
}
/**
* Get the last block's header.
*
* @return the last block's header
*/
virtual std::shared_ptr<monero_block_header> get_last_block_header() {
throw std::runtime_error("monero_daemon::get_last_block_header(): not supported");
}
/**
* Get a block header by its hash.
*
* @param block_hash is the hash of the block to get the header of
* @return the block's header
*/
virtual std::shared_ptr<monero_block_header> get_block_header_by_hash(const std::string& block_hash) {
throw std::runtime_error("monero_daemon::get_block_header_by_hash(): not supported");
}
/**
* Get a block header by its height.
*
* @param height is the height of the block to get the header of
* @return the block's header
*/
virtual std::shared_ptr<monero_block_header> get_block_header_by_height(uint64_t height) {
throw std::runtime_error("monero_daemon::get_block_header_by_height(): not supported");
}
/**
* Get block headers for the given range.
*
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return block headers in the given range
*/
virtual std::vector<std::shared_ptr<monero_block_header>> get_block_headers_by_range(uint64_t start_height, uint64_t end_height) {
throw std::runtime_error("monero_daemon::get_block_headers_by_range(): not supported");
}
/**
* Get a block by hash.
*
* @param block_hash is the hash of the block to get
* @return the block with the given hash
*/
virtual std::shared_ptr<monero_block> get_block_by_hash(const std::string& block_hash) {
throw std::runtime_error("monero_daemon::get_block_by_hash(): not supported");
}
/**
* Get blocks by hash.
*
* @param block_hashes are array of hashes; first 10 blocks hash goes sequential,
* next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64 and so on,
* and the last one is always genesis block
* @param start_height is the start height to get blocks by hash
* @param prune specifies if returned blocks should be pruned (defaults to false) // TODO: test default
* @return the retrieved blocks
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) {
throw std::runtime_error("monero_daemon::get_blocks_by_hash(): not supported");
}
/**
* Get a block by height.
*
* @param height is the height of the block to get
* @return the block at the given height
*/
virtual std::shared_ptr<monero_block> get_block_by_height(uint64_t height) {
throw std::runtime_error("monero_daemon::get_block_by_height(): not supported");
}
/**
* Get blocks at the given heights.
*
* @param heights are the heights of the blocks to get
* @return blocks at the given heights
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_height(const std::vector<uint64_t>& heights) {
throw std::runtime_error("monero_daemon::get_blocks_by_height(): not supported");
}
/**
* Get blocks in the given height range.
*
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return blocks in the given height range
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) {
throw std::runtime_error("monero_daemon::get_blocks_by_range(): not supported");
}
/**
* Get blocks in the given height range as chunked requests so that each request is
* not too big.
*
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return blocks in the given height range
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) {
throw std::runtime_error("monero_daemon::get_blocks_by_range_chunked(): not supported");
}
/**
* Get block hashes as a binary request to the daemon.
*
* @param block_hashes specify block hashes to fetch; first 10 blocks hash goes
* sequential, next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64
* and so on, and the last one is always genesis block
* @param start_height is the starting height of block hashes to return
* @return the requested block hashes
*/
virtual std::vector<std::string> get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) {
throw std::runtime_error("monero_daemon::get_block_hashes(): not supported");
}
/**
* Get a transaction by hash.
*
* @param tx_hash is the hash of the transaction to get
* @param prune specifies if the returned tx should be pruned (defaults to false)
* @return the transaction with the given hash or null if not found
*/
virtual std::shared_ptr<monero_tx> get_tx(const std::string& tx_hash, bool prune = false) {
std::vector<std::string> hashes;
hashes.push_back(tx_hash);
auto txs = get_txs(hashes, prune);
std::shared_ptr<monero_tx> tx;
if (txs.size() > 0) {
tx = txs[0];
}
return tx;
}
/**
* Get transactions by hashes.
*
* @param tx_hashes are hashes of transactions to get
* @param prune specifies if the returned txs should be pruned (defaults to false)
* @return found transactions with the given hashes
*/
virtual std::vector<std::shared_ptr<monero_tx>> get_txs(const std::vector<std::string>& tx_hashes, bool prune = false) {
throw std::runtime_error("monero_daemon::get_txs(): not supported");
}
/**
* Get a transaction hex by hash.
*
* @param tx_hash is the hash of the transaction to get hex from
* @param prune specifies if the returned tx hex should be pruned (defaults to false)
* @return the tx hex with the given hash
*/
virtual boost::optional<std::string> get_tx_hex(const std::string& tx_hash, bool prune = false) {
std::vector<std::string> hashes;
hashes.push_back(tx_hash);
auto hexes = get_tx_hexes(hashes, prune);
boost::optional<std::string> hex;
if (hexes.size() > 0) {
hex = hexes[0];
}
return hex;
}
/**
* Get transaction hexes by hashes.
*
* @param tx_hashes are hashes of transactions to get hexes from
* @param prune specifies if the returned tx hexes should be pruned (defaults to false)
* @return are the tx hexes
*/
virtual std::vector<std::string> get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune = false) {
throw std::runtime_error("monero_daemon::get_tx_hexes(): not supported");
}
/**
* Gets the total emissions and fees from the genesis block to the current height.
*
* @param height is the height to start computing the miner sum
* @param num_blocks are the number of blocks to include in the sum
* @return the sum emission and fees since the geneis block
*/
virtual std::shared_ptr<monero_miner_tx_sum> get_miner_tx_sum(uint64_t height, uint64_t num_blocks) {
throw std::runtime_error("monero_daemon::get_miner_tx_sum(): not supported");
}
/**
* Get mining fee estimates per kB.
*
* @param grace_blocks TODO
* @return mining fee estimates per kB
*/
virtual std::shared_ptr<monero_fee_estimate> get_fee_estimate(uint64_t grace_blocks = 0) {
throw std::runtime_error("monero_daemon::get_fee_estimate(): not supported");
}
/**
* Submits a transaction to the daemon's pool.
*
* @param tx_hex is the raw transaction hex to submit
* @param do_not_relay specifies if the tx should be relayed (optional)
* @return the submission results
*/
virtual std::shared_ptr<monero_submit_tx_result> submit_tx_hex(const std::string& tx_hex, bool do_not_relay = false) {
throw std::runtime_error("monero_daemon::submit_tx_hex(): not supported");
}
/**
* Relays a transaction by hash.
*
* @param tx_hash identifies the transaction to relay
*/
virtual void relay_tx_by_hash(const std::string& tx_hash) {
std::vector<std::string> tx_hashes;
tx_hashes.push_back(tx_hash);
relay_txs_by_hash(tx_hashes);
}
/**
* Relays transactions by hash.
*
* @param tx_hashes identify the transactions to relay
*/
virtual void relay_txs_by_hash(const std::vector<std::string>& tx_hashes) {
throw std::runtime_error("monero_daemon::relay_txs_by_hash(): not supported");
}
/**
* Get valid transactions seen by the node but not yet mined into a block, as well
* as spent key image information for the tx pool.
*
* @return transactions in the transaction pool
*/
virtual std::vector<std::shared_ptr<monero_tx>> get_tx_pool() {
throw std::runtime_error("monero_daemon::get_tx_pool(): not supported");
}
/**
* Get hashes of transactions in the transaction pool.
*
* @return hashes of transactions in the transaction pool
*/
virtual std::vector<std::string> get_tx_pool_hashes() {
throw std::runtime_error("monero_daemon::get_tx_pool_hashes(): not supported");
}
/**
* Get all transaction pool backlog.
*
* @return transaction pool backlog entries
*/
virtual std::vector<monero_tx_backlog_entry> get_tx_pool_backlog() {
throw std::runtime_error("monero_daemon::get_tx_pool_backlog(): not supported");
}
/**
* Get transaction pool statistics.
*
* @return statistics about the transaction pool
*/
virtual std::shared_ptr<monero_tx_pool_stats> get_tx_pool_stats() {
throw std::runtime_error("monero_daemon::get_tx_pool_stats(): not supported");
}
/**
* Flushes all transactions from the tx pool.
*/
virtual void flush_tx_pool() {
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
}
/**
* Flush transactions from the tx pool.
*
* @param hashes are hashes of transactions to flush
*/
virtual void flush_tx_pool(const std::vector<std::string> &hashes) {
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
}
/**
* Flush a single transaction from the tx pool.
*
* @param hash is the hash of transaction to flush
*/
virtual void flush_tx_pool(const std::string &hash) {
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
}
/**
* Get the spent status of the given key image.
*
* @param key_image is key image hex to get the status of
* @return the status of the key image
*/
virtual monero_key_image_spent_status get_key_image_spent_status(const std::string& key_image) {
std::vector<std::string> key_images;
key_images.push_back(key_image);
auto statuses = get_key_image_spent_statuses(key_images);
if (statuses.empty()) throw std::runtime_error("Could not get key image spent status");
return statuses[0];
}
/**
* Get the spent status of each given key image.
*
* @param key_image are hex key images to get the statuses of
* @return the spent status for each key image
*/
virtual std::vector<monero_key_image_spent_status> get_key_image_spent_statuses(const std::vector<std::string>& key_images) {
throw std::runtime_error("monero_daemon::get_key_image_spent_statuses(): not supported");
}
/**
* Get outputs identified by a list of output amounts and indices as a binary
* request.
*
* @param outputs identify each output by amount and index
* @return the identified outputs
*/
virtual std::vector<std::shared_ptr<monero_output>> get_outputs(const std::vector<monero_output>& outputs) {
throw std::runtime_error("monero_daemon::get_outputs(): not supported");
}
/**
* Get a histogram of output amounts. For all amounts (possibly filtered by
* parameters), gives the number of outputs on the chain for that amount.
* RingCT outputs counts as 0 amount.
*
* @param amounts are amounts of outputs to make the histogram with
* @param min_count TODO
* @param max_count TODO
* @param is_unlocked makes a histogram with outputs with the specified lock state
* @param recent_cutoff TODO
* @return output histogram entries meeting the parameters
*/
virtual std::vector<std::shared_ptr<monero_output_histogram_entry>> get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) {
throw std::runtime_error("monero_daemon::get_output_histogram(): not supported");
}
/**
* Creates an output distribution.
*
* @param amounts are amounts of outputs to make the distribution with
* @param is_cumulative specifies if the results should be cumulative (defaults to TODO)
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return output distribution entries meeting the parameters
*/
virtual std::vector<std::shared_ptr<monero_output_distribution_entry>> get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative = boost::none, const boost::optional<uint64_t>& start_height = boost::none, const boost::optional<uint64_t>& end_height = boost::none) {
throw std::runtime_error("monero_daemon::get_output_distribution(): not supported");
}
/**
* Get general information about the state of the node and the network.
*
* @return general information about the node and network
*/
virtual std::shared_ptr<monero_daemon_info> get_info() {
throw std::runtime_error("monero_daemon::get_info(): not supported");
}
/**
* Get synchronization information.
*
* @return contains sync information
*/
virtual std::shared_ptr<monero_daemon_sync_info> get_sync_info() {
throw std::runtime_error("monero_daemon::get_sync_info(): not supported");
}
/**
* Look up information regarding hard fork voting and readiness.
*
* @return hard fork information
*/
virtual std::shared_ptr<monero_hard_fork_info> get_hard_fork_info() {
throw std::runtime_error("monero_daemon::get_hard_fork_info(): not supported");
}
/**
* Get alternative chains seen by the node.
*
* @return alternative chains seen by the node
*/
virtual std::vector<std::shared_ptr<monero_alt_chain>> get_alt_chains() {
throw std::runtime_error("monero_daemon::get_alt_chains(): not supported");
}
/**
* Get known block hashes which are not on the main chain.
*
* @return known block hashes which are not on the main chain
*/
virtual std::vector<std::string> get_alt_block_hashes() {
throw std::runtime_error("monero_daemon::get_alt_block_hashes(): not supported");
}
/**
* Get the download bandwidth limit.
*
* @return is the download bandwidth limit
*/
virtual int get_download_limit() {
throw std::runtime_error("monero_daemon::get_download_limit(): not supported");
}
/**
* Set the download bandwidth limit.
*
* @param limit is the download limit to set (-1 to reset to default)
* @return int is the new download limit after setting
*/
virtual int set_download_limit(int limit) {
throw std::runtime_error("monero_daemon::set_download_limit(): not supported");
}
/**
* Reset the download bandwidth limit.
*
* @return the download bandwidth limit after resetting
*/
virtual int reset_download_limit() {
throw std::runtime_error("monero_daemon::reset_download_limit(): not supported");
}
/**
* Get the upload bandwidth limit.
*
* @return is the upload bandwidth limit
*/
virtual int get_upload_limit() {
throw std::runtime_error("monero_daemon::get_upload_limit(): not supported");
}
/**
* Set the upload bandwidth limit.
*
* @param limit is the upload limit to set (-1 to reset to default)
* @return int is the new upload limit after setting
*/
virtual int set_upload_limit(int limit) {
throw std::runtime_error("monero_daemon::set_upload_limit(): not supported");
}
/**
* Reset the upload bandwidth limit.
*
* @return the upload bandwidth limit after resetting
*/
virtual int reset_upload_limit() {
throw std::runtime_error("monero_daemon::reset_upload_limit(): not supported");
}
/**
* Get peers with active incoming or outgoing connections to the node.
*
* @return the daemon's peers
*/
virtual std::vector<std::shared_ptr<monero_peer>> get_peers() {
throw std::runtime_error("monero_daemon::get_peers(): not supported");
}
/**
* Get all known peers including their last known online status.
*
* @return the daemon's known peers
*/
virtual std::vector<std::shared_ptr<monero_peer>> get_known_peers() {
throw std::runtime_error("monero_daemon::get_known_peers(): not supported");
}
/**
* Limit number of outgoing peers.
*
* @param limit is the maximum number of outgoing peers
*/
virtual void set_outgoing_peer_limit(int limit) {
throw std::runtime_error("monero_daemon::set_outgoing_peer_limit(): not supported");
}
/**
* Limit number of incoming peers.
*
* @param limit is the maximum number of incoming peers
*/
virtual void set_incoming_peer_limit(int limit) {
throw std::runtime_error("monero_daemon::set_incoming_peer_limit(): not supported");
}
/**
* Get peer bans.
*
* @return entries about banned peers
*/
virtual std::vector<std::shared_ptr<monero_ban>> get_peer_bans() {
throw std::runtime_error("monero_daemon::get_peer_bans(): not supported");
}
/**
* Ban peers nodes.
*
* @param bans are bans to apply against peer nodes
*/
virtual void set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) {
throw std::runtime_error("monero_daemon::set_peer_bans(): not supported");
}
/**
* Ban a peer node.
*
* @param ban contains information about a node to ban
*/
virtual void set_peer_ban(const std::shared_ptr<monero_ban>& ban) {
if (ban == nullptr) throw std::runtime_error("Ban is none");
std::vector<std::shared_ptr<monero_ban>> bans;
bans.push_back(ban);
set_peer_bans(bans);
}
/**
* Start mining.
*
* @param address is the address given miner rewards if the daemon mines a block
* @param num_threads is the number of mining threads to run
* @param is_background specifies if the miner should run in the background or not
* @param ignore_battery specifies if the battery state (e.g. on laptop) should be ignored or not
*/
virtual void start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> is_background, boost::optional<bool> ignore_battery) {
throw std::runtime_error("monero_daemon::start_mining(): not supported");
}
/**
* Stop mining.
*/
virtual void stop_mining() {
throw std::runtime_error("monero_daemon::stop_mining(): not supported");
}
/**
* Get the daemon's mining status.
*
* @return the daemon's mining status
*/
virtual std::shared_ptr<monero_mining_status> get_mining_status() {
throw std::runtime_error("monero_daemon::get_mining_status(): not supported");
}
/**
* Generate blocks to a wallet address (regtest only).
*
* @param wallet_address is the address of the wallet to receive miner transactions if block is successfully mined
* @param num_blocks is the number of blocks to generate
* @param prev_block_hash is the hash of the previous block to build on top of (optional, builds on the current tip if not given)
* @param starting_nonce is the starting nonce to use (optional)
* @return the result of generating blocks; height is the height of the last block generated
*/
virtual std::shared_ptr<monero_generate_blocks_result> generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash = boost::none, const boost::optional<uint32_t>& starting_nonce = boost::none) {
throw std::runtime_error("monero_daemon::generate_blocks(): not supported");
}
/**
* Submit a mined block to the network.
*
* @param block_blob is the mined block to submit
*/
virtual void submit_block(const std::string& block_blob) {
std::vector<std::string> block_blobs;
block_blobs.push_back(block_blob);
return submit_blocks(block_blobs);
}
/**
* Submit mined blocks to the network.
*
* @param block_blobs are the mined blocks to submit
*/
virtual void submit_blocks(const std::vector<std::string>& block_blobs) {
throw std::runtime_error("monero_daemon::submit_blocks(): not supported");
}
/**
* Prune the blockchain.
*
* @param check specifies to check the pruning (default false)
* @return the prune result
*/
virtual std::shared_ptr<monero_prune_result> prune_blockchain(bool check) {
throw std::runtime_error("monero_daemon::prune_blockchain(): not supported");
}
/**
* Check for update.
*
* @return the result of the update check
*/
virtual std::shared_ptr<monero_daemon_update_check_result> check_for_update() {
throw std::runtime_error("monero_daemon::check_for_update(): not supported");
}
/**
* Download an update.
*
* @param path is the path to download the update (optional)
* @return the result of the update download
*/
virtual std::shared_ptr<monero_daemon_update_download_result> download_update(const std::string& path = "") {
throw std::runtime_error("monero_daemon::download_update(): not supported");
}
/**
* Safely disconnect and shut down the daemon.
*/
virtual void stop() {
throw std::runtime_error("monero_daemon::stop(): not supported");
}
/**
* Get the header of the next block added to the chain.
*
* @return the header of the next block added to the chain
*/
virtual std::shared_ptr<monero_block_header> wait_for_next_block_header() {
throw std::runtime_error("monero_daemon::wait_for_next_block_header(): not supported");
}
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,639 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <map>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
/**
* Public interface for libmonero-cpp library.
*/
namespace monero {
/**
* Base struct which can be serialized.
*/
struct serializable_struct {
virtual ~serializable_struct() = default;
/**
* Serializes the struct to a json std::string.
*
* @return the struct serialized to a json std::string
*/
std::string serialize() const;
/**
* Converts the struct to a rapidjson Value.
*
* @param allocator is the rapidjson document allocator
* @return the struct as a rapidjson Value
*/
virtual rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const = 0;
};
/**
* Models connection ssl options.
*/
struct ssl_options : public serializable_struct {
boost::optional<std::string> m_ssl_private_key_path;
boost::optional<std::string> m_ssl_certificate_path;
boost::optional<std::string> m_ssl_ca_file;
std::vector<std::string> m_ssl_allowed_fingerprints;
boost::optional<bool> m_ssl_allow_any_cert;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Enumerates Monero connection types.
*/
enum monero_connection_type : uint8_t {
INVALID = 0,
IPV4,
IPV6,
TOR,
I2P
};
/**
* Enumerates Monero network types.
*/
enum monero_network_type : uint8_t {
MAINNET = 0,
TESTNET,
STAGENET
};
/**
* Models connection bandwidth limits.
*/
struct monero_bandwidth_limits : public serializable_struct {
boost::optional<int> m_up;
boost::optional<int> m_down;
monero_bandwidth_limits() { }
monero_bandwidth_limits(int up, int down): m_up(up), m_down(down) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_bandwidth_limits>& limits);
};
/**
* Models a Monero version.
*/
struct monero_version : public serializable_struct {
boost::optional<uint32_t> m_number;
boost::optional<bool> m_is_release;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_version>& version);
};
// forward declarations
struct monero_tx;
struct monero_output;
/**
* Models a Monero block header which contains information about the block.
*
* TODO: a header that is transmitted may have fewer fields like cryptonote::block_header; separate?
*/
struct monero_block_header : public serializable_struct {
boost::optional<std::string> m_hash;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_timestamp;
boost::optional<uint64_t> m_size;
boost::optional<uint64_t> m_weight;
boost::optional<uint64_t> m_long_term_weight;
boost::optional<uint64_t> m_depth;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_cumulative_difficulty_low;
boost::optional<uint64_t> m_cumulative_difficulty_high;
boost::optional<uint32_t> m_major_version;
boost::optional<uint32_t> m_minor_version;
boost::optional<uint32_t> m_nonce;
boost::optional<std::string> m_miner_tx_hash;
boost::optional<uint32_t> m_num_txs;
boost::optional<bool> m_orphan_status;
boost::optional<std::string> m_prev_hash;
boost::optional<uint64_t> m_reward;
boost::optional<std::string> m_pow_hash;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_block_header> copy(const std::shared_ptr<monero_block_header>& src, const std::shared_ptr<monero_block_header>& tgt) const;
virtual void merge(const std::shared_ptr<monero_block_header>& self, const std::shared_ptr<monero_block_header>& other);
};
/**
* Models a Monero block in the blockchain.
*/
struct monero_block : public monero_block_header {
boost::optional<std::string> m_hex;
std::shared_ptr<monero_tx> m_miner_tx;
std::vector<std::shared_ptr<monero_tx>> m_txs;
std::vector<std::string> m_tx_hashes;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_block> copy(const std::shared_ptr<monero_block>& src, const std::shared_ptr<monero_block>& tgt) const;
void merge(const std::shared_ptr<monero_block_header>& self, const std::shared_ptr<monero_block_header>& other);
void merge(const std::shared_ptr<monero_block>& self, const std::shared_ptr<monero_block>& other);
};
/**
* Models a Monero transaction on the blockchain.
*/
struct monero_tx : public serializable_struct {
static const std::string DEFAULT_PAYMENT_ID; // default payment id "0000000000000000"
static const std::string DEFAULT_ID;
std::shared_ptr<monero_block> m_block;
boost::optional<std::string> m_hash;
boost::optional<uint32_t> m_version;
boost::optional<bool> m_is_miner_tx;
boost::optional<std::string> m_payment_id;
boost::optional<uint64_t> m_fee;
boost::optional<uint32_t> m_ring_size;
boost::optional<bool> m_relay;
boost::optional<bool> m_is_relayed;
boost::optional<bool> m_is_confirmed;
boost::optional<bool> m_in_tx_pool;
boost::optional<uint64_t> m_num_confirmations;
boost::optional<uint64_t> m_unlock_time;
boost::optional<uint64_t> m_last_relayed_timestamp;
boost::optional<uint64_t> m_received_timestamp;
boost::optional<bool> m_is_double_spend_seen;
boost::optional<std::string> m_key;
boost::optional<std::string> m_full_hex;
boost::optional<std::string> m_pruned_hex;
boost::optional<std::string> m_prunable_hex;
boost::optional<std::string> m_prunable_hash;
boost::optional<uint64_t> m_size;
boost::optional<uint64_t> m_weight;
std::vector<std::shared_ptr<monero_output>> m_inputs;
std::vector<std::shared_ptr<monero_output>> m_outputs;
std::vector<uint64_t> m_output_indices;
boost::optional<std::string> m_metadata;
boost::optional<std::string> m_common_tx_sets;
std::vector<uint8_t> m_extra;
boost::optional<std::string> m_rct_signatures; // TODO: implement
boost::optional<std::string> m_rct_sig_prunable; // TODO: implement
boost::optional<bool> m_is_kept_by_block;
boost::optional<bool> m_is_failed;
boost::optional<uint64_t> m_last_failed_height;
boost::optional<std::string> m_last_failed_hash;
boost::optional<uint64_t> m_max_used_block_height;
boost::optional<std::string> m_max_used_block_hash;
std::vector<std::string> m_signatures;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, std::shared_ptr<monero_tx> tx);
std::shared_ptr<monero_tx> copy(const std::shared_ptr<monero_tx>& src, const std::shared_ptr<monero_tx>& tgt) const;
virtual void merge(const std::shared_ptr<monero_tx>& self, const std::shared_ptr<monero_tx>& other);
boost::optional<uint64_t> get_height() const;
};
/**
* Models a Monero key image.
*/
struct monero_key_image : public serializable_struct {
boost::optional<std::string> m_hex;
boost::optional<std::string> m_signature;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image>& key_image);
static std::vector<std::shared_ptr<monero_key_image>> deserialize_key_images(const std::string& key_images_json); // TODO: remove this specialty util used once
std::shared_ptr<monero_key_image> copy(const std::shared_ptr<monero_key_image>& src, const std::shared_ptr<monero_key_image>& tgt) const;
void merge(const std::shared_ptr<monero_key_image>& self, const std::shared_ptr<monero_key_image>& other);
};
/**
* Models a Monero transaction output.
*/
struct monero_output : public serializable_struct {
std::shared_ptr<monero_tx> m_tx;
std::shared_ptr<monero_key_image> m_key_image;
boost::optional<uint64_t> m_amount;
boost::optional<uint64_t> m_index;
std::vector<uint64_t> m_ring_output_indices;
boost::optional<std::string> m_stealth_public_key;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output>& output);
std::shared_ptr<monero_output> copy(const std::shared_ptr<monero_output>& src, const std::shared_ptr<monero_output>& tgt) const;
virtual void merge(const std::shared_ptr<monero_output>& self, const std::shared_ptr<monero_output>& other);
};
/**
* Models the status of a Monero key image.
*/
enum monero_key_image_spent_status : uint8_t {
NOT_SPENT = 0,
CONFIRMED,
TX_POOL
};
/**
* Models a Monero RPC payment information.
*/
struct monero_rpc_payment_info : public serializable_struct {
boost::optional<uint64_t> m_credits;
boost::optional<std::string> m_top_block_hash;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_rpc_payment_info>& rpc_payment_info);
};
/**
* Models an alternative chain seen by the node.
*/
struct monero_alt_chain : public serializable_struct {
std::vector<std::string> m_block_hashes;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_length;
boost::optional<std::string> m_main_chain_parent_block_hash;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_alt_chain>& alt_chain);
};
/**
* Monero banhammer.
*/
struct monero_ban : public serializable_struct {
boost::optional<std::string> m_host;
boost::optional<uint32_t> m_ip;
boost::optional<bool> m_is_banned;
boost::optional<uint64_t> m_seconds;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_ban>& ban);
};
/**
* Result of pruning the blockchain.
*/
struct monero_prune_result : public serializable_struct {
boost::optional<bool> m_is_pruned;
boost::optional<int> m_pruning_seed;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_prune_result>& result);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero daemon mining status.
*/
struct monero_mining_status : public serializable_struct {
boost::optional<bool> m_is_active;
boost::optional<bool> m_is_background;
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_speed;
boost::optional<int> m_num_threads;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_mining_status>& status);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Model for the summation of miner emissions and fees.
*/
struct monero_miner_tx_sum : public serializable_struct {
boost::optional<uint64_t> m_emission_sum_low;
boost::optional<uint64_t> m_emission_sum_high;
boost::optional<uint64_t> m_fee_sum_low;
boost::optional<uint64_t> m_fee_sum_high;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_miner_tx_sum>& sum);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero block template to mine.
*/
struct monero_block_template : public serializable_struct {
boost::optional<std::string> m_block_template_blob;
boost::optional<std::string> m_block_hashing_blob;
boost::optional<std::string> m_prev_hash;
boost::optional<std::string> m_seed_hash;
boost::optional<std::string> m_next_seed_hash;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_expected_reward;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_reserved_offset;
boost::optional<uint64_t> m_seed_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block_template>& tmplt);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero daemon connection span.
*/
struct monero_connection_span : public serializable_struct {
boost::optional<std::string> m_connection_id;
boost::optional<std::string> m_remote_address;
boost::optional<uint64_t> m_num_blocks;
boost::optional<uint64_t> m_rate;
boost::optional<uint64_t> m_speed;
boost::optional<uint64_t> m_size;
boost::optional<uint64_t> m_start_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_connection_span>& span);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a peer to the daemon.
*/
struct monero_peer : public serializable_struct {
boost::optional<std::string> m_id;
boost::optional<std::string> m_address;
boost::optional<std::string> m_host;
boost::optional<int> m_port;
boost::optional<bool> m_is_online;
boost::optional<uint64_t> m_last_seen_timestamp;
boost::optional<int> m_pruning_seed;
boost::optional<int> m_rpc_port;
boost::optional<uint64_t> m_rpc_credits_per_hash;
boost::optional<std::string> m_hash;
boost::optional<uint64_t> m_avg_download;
boost::optional<uint64_t> m_avg_upload;
boost::optional<uint64_t> m_current_download;
boost::optional<uint64_t> m_current_upload;
boost::optional<uint64_t> m_height;
boost::optional<bool> m_is_incoming;
boost::optional<uint64_t> m_live_time;
boost::optional<bool> m_is_local_ip;
boost::optional<bool> m_is_local_host;
boost::optional<uint64_t> m_num_receives;
boost::optional<uint64_t> m_num_sends;
boost::optional<uint64_t> m_receive_idle_time;
boost::optional<uint64_t> m_send_idle_time;
boost::optional<std::string> m_state;
boost::optional<int> m_num_support_flags;
boost::optional<monero_connection_type> m_connection_type;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_peer>& peer);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models the result from submitting a tx to a daemon.
*/
struct monero_submit_tx_result : public monero_rpc_payment_info {
boost::optional<bool> m_has_invalid_input;
boost::optional<bool> m_has_invalid_output;
boost::optional<bool> m_has_too_few_outputs;
boost::optional<bool> m_is_good;
boost::optional<bool> m_is_relayed;
boost::optional<bool> m_is_double_spend;
boost::optional<bool> m_is_fee_too_low;
boost::optional<bool> m_is_mixin_too_low;
boost::optional<bool> m_is_overspend;
boost::optional<bool> m_is_too_big;
boost::optional<bool> m_sanity_check_failed;
boost::optional<bool> m_is_tx_extra_too_big;
boost::optional<bool> m_is_nonzero_unlock_time;
boost::optional<std::string> m_reason;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_submit_tx_result>& result);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* TODO.
*/
struct monero_tx_backlog_entry {
// TODO
};
/**
* Monero output distribution entry.
*/
struct monero_output_distribution_entry : public serializable_struct {
boost::optional<uint64_t> m_amount;
boost::optional<uint64_t> m_base;
std::vector<uint64_t> m_distribution;
boost::optional<uint64_t> m_start_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_distribution_entry>& entry);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Entry in a Monero output histogram (see get_output_histogram of Daemon RPC documentation).
*/
struct monero_output_histogram_entry : public serializable_struct {
boost::optional<uint64_t> m_amount;
boost::optional<uint64_t> m_num_instances;
boost::optional<uint64_t> m_unlocked_instances;
boost::optional<uint64_t> m_recent_instances;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_histogram_entry>& entry);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models transaction pool statistics.
*/
struct monero_tx_pool_stats : public serializable_struct {
boost::optional<int> m_num_txs;
boost::optional<int> m_num_not_relayed;
boost::optional<int> m_num_failing;
boost::optional<int> m_num_double_spends;
boost::optional<int> m_num10m;
boost::optional<uint64_t> m_fee_total;
boost::optional<uint64_t> m_bytes_max;
boost::optional<uint64_t> m_bytes_med;
boost::optional<uint64_t> m_bytes_min;
boost::optional<uint64_t> m_bytes_total;
std::map<uint64_t, uint64_t> m_histo;
boost::optional<uint64_t> m_histo98pc;
boost::optional<uint64_t> m_oldest_timestamp;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_pool_stats>& stats);
};
/**
* Models the result of checking for a daemon update.
*/
struct monero_daemon_update_check_result : public serializable_struct {
boost::optional<bool> m_is_update_available;
boost::optional<std::string> m_version;
boost::optional<std::string> m_hash;
boost::optional<std::string> m_auto_uri;
boost::optional<std::string> m_user_uri;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_check_result>& check);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models the result of downloading an update.
*/
struct monero_daemon_update_download_result : public monero_daemon_update_check_result {
boost::optional<std::string> m_download_path;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_download_result>& check);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a Monero fee estimate.
*/
struct monero_fee_estimate : public serializable_struct {
boost::optional<uint64_t> m_quantization_mask;
boost::optional<uint64_t> m_fee;
std::vector<uint64_t> m_fees;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_fee_estimate>& estimate);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero daemon info.
*/
struct monero_daemon_info : public monero_rpc_payment_info {
boost::optional<std::string> m_version;
boost::optional<uint64_t> m_num_alt_blocks;
boost::optional<uint64_t> m_block_size_limit;
boost::optional<uint64_t> m_block_size_median;
boost::optional<uint64_t> m_block_weight_limit;
boost::optional<uint64_t> m_block_weight_median;
boost::optional<std::string> m_bootstrap_daemon_address;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_cumulative_difficulty_low;
boost::optional<uint64_t> m_cumulative_difficulty_high;
boost::optional<uint64_t> m_free_space;
boost::optional<int> m_num_offline_peers;
boost::optional<int> m_num_online_peers;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_height_without_bootstrap;
boost::optional<monero_network_type> m_network_type;
boost::optional<bool> m_is_offline;
boost::optional<int> m_num_incoming_connections;
boost::optional<int> m_num_outgoing_connections;
boost::optional<int> m_num_rpc_connections;
boost::optional<uint64_t> m_start_timestamp;
boost::optional<uint64_t> m_adjusted_timestamp;
boost::optional<uint64_t> m_target;
boost::optional<uint64_t> m_target_height;
boost::optional<int> m_num_txs;
boost::optional<int> m_num_txs_pool;
boost::optional<bool> m_was_bootstrap_ever_used;
boost::optional<uint64_t> m_database_size;
boost::optional<bool> m_update_available;
boost::optional<bool> m_is_busy_syncing;
boost::optional<bool> m_is_synchronized;
boost::optional<bool> m_is_restricted;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_info>& info);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models daemon synchronization information.
*/
struct monero_daemon_sync_info : public monero_rpc_payment_info {
boost::optional<std::string> m_overview;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_target_height;
boost::optional<int> m_next_needed_pruning_seed;
std::vector<std::shared_ptr<monero_peer>> m_peers;
std::vector<std::shared_ptr<monero_connection_span>> m_spans;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_sync_info>& info);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero hard fork info.
*/
struct monero_hard_fork_info : public monero_rpc_payment_info {
boost::optional<bool> m_is_enabled;
boost::optional<uint64_t> m_earliest_height;
boost::optional<int> m_state;
boost::optional<int> m_threshold;
boost::optional<int> m_version;
boost::optional<int> m_num_votes;
boost::optional<int> m_window;
boost::optional<int> m_voting;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_hard_fork_info>& info);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models the result of generating blocks.
*/
struct monero_generate_blocks_result : public serializable_struct {
std::vector<std::string> m_block_hashes;
boost::optional<uint64_t> m_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_generate_blocks_result>& result);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
}
@@ -0,0 +1,892 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_daemon_rpc.h"
#include "monero_daemon_rpc_model.h"
#include "common/monero_error.h"
#include "utils/gen_utils.h"
#include "utils/monero_utils.h"
static const uint64_t MAX_REQ_SIZE = 3000000;
static const uint64_t NUM_HEADERS_PER_REQ = 750;
namespace monero {
/**
* Polls daemon and sends notifications in order to notify external daemon listeners.
*/
class monero_daemon_poller : public gen_utils::thread_poller {
public:
explicit monero_daemon_poller(monero_daemon* daemon, uint64_t poll_period_ms = 10000): m_daemon(daemon) {
init_common("monero_daemon_rpc");
m_poll_period_ms = poll_period_ms;
}
~monero_daemon_poller() override {
set_is_polling(false);
}
void poll() override {
if (!m_last_header) {
m_last_header = m_daemon->get_last_block_header();
return;
}
auto header = m_daemon->get_last_block_header();
if (header->m_hash != m_last_header->m_hash) {
m_last_header = header;
announce_block_header(header);
}
}
private:
monero_daemon* m_daemon;
std::shared_ptr<monero_block_header> m_last_header;
void announce_block_header(const std::shared_ptr<monero_block_header>& header) {
gen_utils::thread_poller::announce_scope scope(*this); // see wait_for_callbacks_idle()
auto listeners = m_daemon->get_listeners();
for (auto& listener : listeners) {
try {
listener->on_block_header(header);
} catch (const std::exception& e) {
MERROR("Error calling listener on new block header: " << e.what());
}
}
}
};
/**
* Sends a notification on a condition variable when a block is added to blockchain.
*/
class block_notifier : public monero_daemon_listener {
public:
block_notifier(boost::mutex* temp, boost::condition_variable* cv, bool* ready) { this->temp = temp; this->cv = cv; this->ready = ready; }
void on_block_header(const std::shared_ptr<monero_block_header>& header) override {
boost::mutex::scoped_lock lock(*temp);
m_last_header = header;
*ready = true;
cv->notify_one();
}
private:
boost::mutex* temp;
boost::condition_variable* cv;
bool* ready;
};
void monero_daemon_rpc::set_poll_period_in_ms(uint64_t period_ms) {
boost::lock_guard<boost::mutex> poller_lock(m_poller_mutex);
if (!m_poller) m_poller = std::make_unique<monero_daemon_poller>(this);
m_poller->set_period_in_ms(period_ms);
}
/**
* Validate RPC response status.
*/
void check_response_status(const boost::property_tree::ptree& node) {
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
std::string key = it->first;
if (key == std::string("status")) {
std::string status = it->second.data();
// TODO monero-project empty string status is returned for download update response when an update is available
if (status == std::string("OK") || status == std::string("")) {
return;
}
else throw monero_rpc_error(status);
}
}
throw monero_error("Could not get JSON RPC response status");
}
monero_daemon_rpc::monero_daemon_rpc(const std::shared_ptr<monero_rpc_connection>& rpc): m_rpc(rpc) {
if (rpc == nullptr) throw monero_error("Connection cannot be null");
if (!rpc->is_online().value_or(false) && rpc->m_uri != boost::none) rpc->check_connection();
}
monero_daemon_rpc::monero_daemon_rpc(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, const boost::optional<uint32_t>& timeout):
m_rpc(std::make_shared<monero_rpc_connection>(uri, username, password, proxy_uri, zmq_uri, 0, timeout)) {
if (!uri.empty()) m_rpc->check_connection();
}
std::set<monero_daemon_listener*> monero_daemon_rpc::get_listeners() {
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
return m_listeners;
}
void monero_daemon_rpc::add_listener(monero_daemon_listener &listener) {
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
m_listeners.insert(&listener);
}
refresh_listening();
}
void monero_daemon_rpc::remove_listener(monero_daemon_listener &listener) {
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
m_listeners.erase(&listener);
}
refresh_listening();
wait_for_listener_callbacks_idle();
}
void monero_daemon_rpc::remove_listeners() {
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
m_listeners.clear();
}
refresh_listening();
wait_for_listener_callbacks_idle();
}
void monero_daemon_rpc::wait_for_listener_callbacks_idle() {
// ensures no in-flight callback dispatch can still reference a just-removed listener
monero_daemon_poller* poller;
{
boost::lock_guard<boost::mutex> poller_lock(m_poller_mutex);
poller = m_poller.get();
}
if (poller != nullptr) poller->wait_for_callbacks_idle();
}
std::shared_ptr<monero_rpc_connection> monero_daemon_rpc::get_rpc_connection() const {
MTRACE("monero_daemon_rpc::get_rpc_connection()");
return m_rpc;
}
bool monero_daemon_rpc::is_connected() {
try {
get_version();
return true;
}
catch (...) {
return false;
}
}
monero_version monero_daemon_rpc::get_version() {
auto result = m_rpc->send_json_request("get_version");
check_response_status(result);
monero_version version;
deserialize_version(result, version);
return version;
}
bool monero_daemon_rpc::is_trusted() {
auto result = m_rpc->send_path_request("get_height");
check_response_status(result);
auto get_height_response = std::make_shared<monero_get_block_result>();
monero_get_block_result::from_property_tree(result, get_height_response);
return gen_utils::bool_equals(false, get_height_response->m_untrusted);
}
uint64_t monero_daemon_rpc::get_height() {
auto result = m_rpc->send_json_request("get_block_count");
check_response_status(result);
std::shared_ptr<monero_get_block_result> block_result = std::make_shared<monero_get_block_result>();
monero_get_block_result::from_property_tree(result, block_result);
if (block_result->m_count == boost::none) throw monero_error("Could not get height");
return block_result->m_count.get();
}
std::string monero_daemon_rpc::get_block_hash(uint64_t height) {
std::shared_ptr<monero_get_block_hash_params> params = std::make_shared<monero_get_block_hash_params>(height);
auto result = m_rpc->send_json_request("on_get_block_hash", params);
return result.data();
}
std::shared_ptr<monero_block_template> monero_daemon_rpc::get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size) {
MTRACE("monero_daemon_rpc::get_block_template()");
auto params = std::make_shared<monero_get_block_params>(wallet_address, reserve_size);
auto result = m_rpc->send_json_request("get_block_template", params);
check_response_status(result);
std::shared_ptr<monero_block_template> tmplt = std::make_shared<monero_block_template>();
deserialize_block_template(result, tmplt);
return tmplt;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_last_block_header() {
auto result = m_rpc->send_json_request("get_last_block_header");
check_response_status(result);
std::shared_ptr<monero_block_header> header = std::make_shared<monero_block_header>();
deserialize_block_header(result, header);
return header;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_block_header_by_hash(const std::string& hash) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(hash);
auto result = m_rpc->send_json_request("get_block_header_by_hash", params);
check_response_status(result);
std::shared_ptr<monero_block_header> header = std::make_shared<monero_block_header>();
deserialize_block_header(result, header);
return header;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_block_header_by_height(uint64_t height) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(height);
auto result = m_rpc->send_json_request("get_block_header_by_height", params);
check_response_status(result);
std::shared_ptr<monero_block_header> header = std::make_shared<monero_block_header>();
deserialize_block_header(result, header);
return header;
}
std::vector<std::shared_ptr<monero_block_header>> monero_daemon_rpc::get_block_headers_by_range(uint64_t start_height, uint64_t end_height) {
auto params = std::make_shared<monero_get_block_params>(start_height, end_height);
auto result = m_rpc->send_json_request("get_block_headers_range", params);
check_response_status(result);
std::vector<std::shared_ptr<monero_block_header>> headers;
deserialize_block_headers(result, headers);
return headers;
}
std::shared_ptr<monero_block> monero_daemon_rpc::get_block_by_hash(const std::string& hash) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(hash);
auto result = m_rpc->send_json_request("get_block", params);
check_response_status(result);
auto block = std::make_shared<monero_block>();
deserialize_block(result, block);
return block;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_block_header_by_height_cached(uint64_t height, uint64_t max_height, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache) {
// get header from cache
auto found = header_cache.find(height);
if (found != header_cache.end()) return found->second;
// fetch and cache headers if not in cache
uint64_t end_height = std::min(max_height, height + NUM_HEADERS_PER_REQ - 1);
auto headers = get_block_headers_by_range(height, end_height);
for(const auto& header : headers) {
if (header->m_height == boost::none) throw monero_error("Malformed block header: expected height to be defined");
header_cache[header->m_height.get()] = header;
}
// look up rather than default-construct via operator[]: a daemon response missing the
// requested height must not silently cache (and return) a null header
found = header_cache.find(height);
if (found == header_cache.end()) throw monero_error("Daemon did not return a header for height " + std::to_string(height));
return found->second;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) {
throw monero_error("monero_daemon_rpc::get_blocks_by_hash(): not implemented");
}
std::shared_ptr<monero_block> monero_daemon_rpc::get_block_by_height(uint64_t height) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(height);
auto result = m_rpc->send_json_request("get_block", params);
check_response_status(result);
auto block = std::make_shared<monero_block>();
deserialize_block(result, block);
return block;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_height(const std::vector<uint64_t>& heights) {
// fetch blocks in binary
monero_get_blocks_by_height_request request(heights);
auto response = m_rpc->send_binary_request(request);
if (response.m_binary == boost::none) throw monero_error("Invalid Monero Binary response");
boost::property_tree::ptree node;
monero_utils::binary_blocks_to_property_tree(response.m_binary.get(), node);
check_response_status(node);
std::vector<std::shared_ptr<monero_block>> blocks;
deserialize_blocks(node, heights, blocks);
return blocks;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) {
if (start_height == boost::none) start_height = 0;
if (end_height == boost::none) {
uint64_t block_height = get_height();
end_height = block_height == 0 ? 0 : block_height - 1;
}
std::vector<uint64_t> heights;
for (uint64_t height = start_height.get(); height <= end_height.get(); height++) heights.push_back(height);
return get_blocks_by_height(heights);
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) {
if (start_height == boost::none) start_height = 0;
if (end_height == boost::none) {
uint64_t block_height = get_height();
end_height = block_height == 0 ? 0 : block_height - 1;
}
boost::optional<uint64_t> last_height = boost::none;
std::vector<std::shared_ptr<monero_block>> blocks;
if (start_height.get() > end_height.get()) return blocks;
// scoped to this call: see the comment on get_max_blocks()/get_block_header_by_height_cached() in the header
std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>> header_cache;
while (last_height == boost::none || last_height.get() < end_height.get()) {
uint64_t height_to_get = (last_height == boost::none) ? start_height.get() : last_height.get() + 1;
auto max_blocks = get_max_blocks(height_to_get, end_height, max_chunk_size, header_cache);
if (max_blocks.empty()) throw monero_error("Daemon returned an empty block chunk for a non-empty range at height " + std::to_string(height_to_get));
blocks.insert(blocks.end(), max_blocks.begin(), max_blocks.end());
last_height = blocks[blocks.size() - 1]->m_height.get();
}
return blocks;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_max_blocks(boost::optional<uint64_t> start_height, boost::optional<uint64_t> max_height, boost::optional<uint64_t> chunk_size, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache) {
if (start_height == boost::none) start_height = 0;
if (max_height == boost::none) {
uint64_t block_height = get_height();
max_height = block_height == 0 ? 0 : block_height - 1;
}
if (chunk_size == boost::none) chunk_size = MAX_REQ_SIZE;
// determine end height to fetch
uint64_t req_size = 0;
boost::optional<uint64_t> end_height = boost::none; // none = no block accepted yet
uint64_t height_to_get = start_height.get();
while (req_size < chunk_size.get() && height_to_get <= max_height.get()) {
// get header of next block
auto header = get_block_header_by_height_cached(height_to_get, max_height.get(), header_cache);
if (header->m_size == boost::none) throw monero_error("Malformed block header: expected size to be defined");
uint64_t header_size = header->m_size.get();
// block cannot be bigger than max request size
if (header_size > chunk_size.get()) throw monero_error("Block exceeds maximum request size: " + std::to_string(header_size));
// done iterating if fetching block would exceed max request size
if (req_size + header_size > chunk_size.get()) break;
// otherwise block is included
req_size += header_size;
end_height = height_to_get;
height_to_get++;
}
if (end_height != boost::none) {
return get_blocks_by_range(start_height, end_height.get());
}
return std::vector<std::shared_ptr<monero_block>>();
}
std::vector<std::string> monero_daemon_rpc::get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) {
throw monero_error("monero_daemon_rpc::get_block_hashes(): not implemented");
}
std::vector<std::shared_ptr<monero_tx>> monero_daemon_rpc::get_txs(const std::vector<std::string>& tx_hashes, bool prune) {
MTRACE("monero_daemon_rpc::get_txs()");
if (tx_hashes.empty()) throw monero_error("Must provide an array of transaction hashes");
auto params = std::make_shared<monero_get_txs_params>(tx_hashes, prune);
auto result = m_rpc->send_path_request("get_transactions", params);
try { check_response_status(result); }
catch (const std::exception& ex) {
if (std::string(ex.what()).find("Failed to parse hex representation of transaction hash") != std::string::npos) {
throw monero_error("Invalid transaction hash");
}
throw;
}
std::vector<std::shared_ptr<monero_tx>> txs;
deserialize_txs(result, txs);
return txs;
}
std::vector<std::string> monero_daemon_rpc::get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune) {
MTRACE("monero_daemon_rpc::get_tx_hexes()");
std::vector<std::string> hexes;
for(const auto& tx : get_txs(tx_hashes, prune)) {
// tx may be pruned regardless of configuration
if (tx->m_pruned_hex == boost::none) {
if (tx->m_full_hex == boost::none) throw monero_error("Tx has no hex");
hexes.push_back(tx->m_full_hex.get());
} else {
hexes.push_back(tx->m_pruned_hex.get());
}
}
return hexes;
}
std::shared_ptr<monero_miner_tx_sum> monero_daemon_rpc::get_miner_tx_sum(uint64_t height, uint64_t num_blocks) {
auto params = std::make_shared<monero_get_miner_tx_sum_params>(height, num_blocks);
auto result = m_rpc->send_json_request("get_coinbase_tx_sum", params);
check_response_status(result);
auto sum = std::make_shared<monero_miner_tx_sum>();
deserialize_miner_tx_sum(result, sum);
return sum;
}
std::shared_ptr<monero_fee_estimate> monero_daemon_rpc::get_fee_estimate(uint64_t grace_blocks) {
auto params = std::make_shared<monero_get_fee_estimate_params>(grace_blocks);
auto result = m_rpc->send_json_request("get_fee_estimate", params);
check_response_status(result);
auto estimate = std::make_shared<monero_fee_estimate>();
deserialize_fee_estimate(result, estimate);
return estimate;
}
std::shared_ptr<monero_submit_tx_result> monero_daemon_rpc::submit_tx_hex(const std::string& tx_hex, bool do_not_relay) {
MTRACE("monero_daemon_rpc::submit_tx_hex()");
auto params = std::make_shared<monero_submit_tx_params>(tx_hex, do_not_relay);
auto result = m_rpc->send_path_request("send_raw_transaction", params);
auto sum = std::make_shared<monero_submit_tx_result>();
deserialize_submit_tx_result(result, sum);
// set m_is_good based on status
try {
check_response_status(result);
sum->m_is_good = true;
} catch (...) {
sum->m_is_good = false;
}
return sum;
}
void monero_daemon_rpc::relay_txs_by_hash(const std::vector<std::string>& tx_hashes) {
MTRACE("monero_daemon_rpc::relay_txs_by_hash()");
auto params = std::make_shared<monero_submit_tx_params>(tx_hashes);
auto result = m_rpc->send_json_request("relay_tx", params);
check_response_status(result);
}
std::shared_ptr<monero_tx_pool_stats> monero_daemon_rpc::get_tx_pool_stats() {
auto result = m_rpc->send_path_request("get_transaction_pool_stats");
check_response_status(result);
auto stats = std::make_shared<monero_tx_pool_stats>();
deserialize_tx_pool_stats(result, stats);
return stats;
}
std::vector<std::shared_ptr<monero_tx>> monero_daemon_rpc::get_tx_pool() {
auto result = m_rpc->send_path_request("get_transaction_pool");
check_response_status(result);
std::vector<std::shared_ptr<monero_tx>> pool;
deserialize_txs(result, pool);
return pool;
}
std::vector<std::string> monero_daemon_rpc::get_tx_pool_hashes() {
auto result = m_rpc->send_path_request("get_transaction_pool_hashes");
check_response_status(result);
std::vector<std::string> tx_hashes;
deserialize_tx_hashes(result, tx_hashes);
return tx_hashes;
}
void monero_daemon_rpc::flush_tx_pool(const std::vector<std::string> &hashes) {
MTRACE("monero_daemon_rpc::flush_tx_pool()");
auto params = std::make_shared<monero_submit_tx_params>(hashes);
auto result = m_rpc->send_json_request("flush_txpool", params);
check_response_status(result);
}
void monero_daemon_rpc::flush_tx_pool() {
std::vector<std::string> hashes;
flush_tx_pool(hashes);
}
void monero_daemon_rpc::flush_tx_pool(const std::string &hash) {
std::vector<std::string> hashes;
hashes.push_back(hash);
flush_tx_pool(hashes);
}
std::vector<monero_key_image_spent_status> monero_daemon_rpc::get_key_image_spent_statuses(const std::vector<std::string>& key_images) {
if (key_images.empty()) throw monero_error("Must provide key images to check the status of");
auto params = std::make_shared<monero_is_key_image_spent_params>(key_images);
auto result = m_rpc->send_path_request("is_key_image_spent", params);
check_response_status(result);
std::vector<monero_key_image_spent_status> statuses;
deserialize_key_image_spent_status(result, statuses);
return statuses;
}
std::vector<std::shared_ptr<monero_output>> monero_daemon_rpc::get_outputs(const std::vector<monero_output>& outputs) {
throw monero_error("monero_daemon_rpc::get_outputs(): not implemented");
}
std::vector<std::shared_ptr<monero_output_histogram_entry>> monero_daemon_rpc::get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) {
MTRACE("monero_daemon_rpc::get_output_histogram()");
auto params = std::make_shared<monero_get_output_histogram_params>(amounts, min_count, max_count, is_unlocked, recent_cutoff);
auto result = m_rpc->send_json_request("get_output_histogram", params);
check_response_status(result);
std::vector<std::shared_ptr<monero_output_histogram_entry>> entries;
deserialize_output_histogram_entries(result, entries);
return entries;
}
std::vector<std::shared_ptr<monero_output_distribution_entry>> monero_daemon_rpc::get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative, const boost::optional<uint64_t>& start_height, const boost::optional<uint64_t>& end_height) {
MTRACE("monero_daemon_rpc::get_output_distribution()");
auto params = std::make_shared<monero_get_output_distribution_params>(amounts, is_cumulative, start_height, end_height);
auto result = m_rpc->send_json_request("get_output_distribution", params);
check_response_status(result);
std::vector<std::shared_ptr<monero_output_distribution_entry>> entries;
deserialize_output_distribution_entries(result, entries);
return entries;
}
std::shared_ptr<monero_daemon_info> monero_daemon_rpc::get_info() {
auto result = m_rpc->send_json_request("get_info");
check_response_status(result);
std::shared_ptr<monero_daemon_info> info = std::make_shared<monero_daemon_info>();
deserialize_daemon_info(result, info);
return info;
}
std::shared_ptr<monero_daemon_sync_info> monero_daemon_rpc::get_sync_info() {
auto result = m_rpc->send_json_request("sync_info");
check_response_status(result);
std::shared_ptr<monero_daemon_sync_info> info = std::make_shared<monero_daemon_sync_info>();
deserialize_daemon_sync_info(result, info);
return info;
}
std::shared_ptr<monero_hard_fork_info> monero_daemon_rpc::get_hard_fork_info() {
auto result = m_rpc->send_json_request("hard_fork_info");
check_response_status(result);
std::shared_ptr<monero_hard_fork_info> info = std::make_shared<monero_hard_fork_info>();
deserialize_hard_fork_info(result, info);
return info;
}
std::vector<std::shared_ptr<monero_alt_chain>> monero_daemon_rpc::get_alt_chains() {
auto result = m_rpc->send_json_request("get_alternate_chains");
check_response_status(result);
std::vector<std::shared_ptr<monero_alt_chain>> alt_chains;
deserialize_alt_chains(result, alt_chains);
return alt_chains;
}
std::vector<std::string> monero_daemon_rpc::get_alt_block_hashes() {
auto result = m_rpc->send_path_request("get_alt_blocks_hashes");
check_response_status(result);
std::vector<std::string> hashes;
deserialize_alt_block_hashes(result, hashes);
return hashes;
}
int monero_daemon_rpc::get_download_limit() {
MTRACE("monero_daemon_rpc::get_download_limit()");
auto limits = get_bandwidth_limits();
if (limits->m_down != boost::none) return limits->m_down.get();
throw monero_error("Could not get download limit");
}
int monero_daemon_rpc::set_download_limit(int limit) {
MTRACE("monero_daemon_rpc::set_download_limit()");
if (limit == -1) return reset_download_limit();
if (limit <= 0) throw monero_error("Download limit must be an integer greater than 0");
auto result = set_bandwidth_limits(0, limit);
if (result->m_down != boost::none) return result->m_down.get();
throw monero_error("Could not set download limit");
}
int monero_daemon_rpc::reset_download_limit() {
MTRACE("monero_daemon_rpc::reset_download_limit()");
auto result = set_bandwidth_limits(0, -1);
if (result->m_down != boost::none) return result->m_down.get();
throw monero_error("Could not set download limit");
}
int monero_daemon_rpc::get_upload_limit() {
MTRACE("monero_daemon_rpc::get_upload_limit()");
auto limits = get_bandwidth_limits();
if (limits->m_up != boost::none) return limits->m_up.get();
throw monero_error("Could not get upload limit");
}
int monero_daemon_rpc::set_upload_limit(int limit) {
MTRACE("monero_daemon_rpc::set_upload_limit()");
if (limit == -1) return reset_upload_limit();
if (limit <= 0) throw monero_error("Upload limit must be an integer greater than 0");
auto result = set_bandwidth_limits(limit, 0);
if (result->m_up != boost::none) return result->m_up.get();
throw monero_error("Could not set download limit");
}
int monero_daemon_rpc::reset_upload_limit() {
MTRACE("monero_daemon_rpc::reset_upload_limit()");
auto result = set_bandwidth_limits(-1, 0);
if (result->m_up != boost::none) return result->m_up.get();
throw monero_error("Could not set download limit");
}
std::vector<std::shared_ptr<monero_peer>> monero_daemon_rpc::get_peers() {
auto result = m_rpc->send_json_request("get_connections");
check_response_status(result);
std::vector<std::shared_ptr<monero_peer>> peers;
deserialize_peers(result, peers);
return peers;
}
std::vector<std::shared_ptr<monero_peer>> monero_daemon_rpc::get_known_peers() {
auto result = m_rpc->send_path_request("get_peer_list");
check_response_status(result);
std::vector<std::shared_ptr<monero_peer>> peers;
deserialize_peers(result, peers);
return peers;
}
void monero_daemon_rpc::set_outgoing_peer_limit(int limit) {
MTRACE("monero_daemon_rpc::set_outgoing_peer_limit()");
if (limit < 0) throw monero_error("Outgoing peer limit must be >= 0");
auto params = std::make_shared<monero_peer_limits_params>();
params->m_out_peers = limit;
auto result = m_rpc->send_path_request("out_peers", params);
check_response_status(result);
}
void monero_daemon_rpc::set_incoming_peer_limit(int limit) {
MTRACE("monero_daemon_rpc::set_incoming_peer_limit()");
if (limit < 0) throw monero_error("Incoming peer limit must be >= 0");
auto params = std::make_shared<monero_peer_limits_params>();
params->m_in_peers = limit;
auto result = m_rpc->send_path_request("in_peers", params);
check_response_status(result);
}
std::vector<std::shared_ptr<monero_ban>> monero_daemon_rpc::get_peer_bans() {
MTRACE("monero_daemon_rpc::get_peer_bans()");
auto result = m_rpc->send_json_request("get_bans");
check_response_status(result);
std::vector<std::shared_ptr<monero_ban>> bans;
deserialize_bans(result, bans);
return bans;
}
void monero_daemon_rpc::set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) {
MTRACE("monero_daemon_rpc::set_peer_bans()");
auto params = std::make_shared<monero_set_bans_params>(bans);
auto result = m_rpc->send_json_request("set_bans", params);
check_response_status(result);
}
void monero_daemon_rpc::start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) {
MTRACE("monero_daemon_rpc::start_mining()");
if (address.empty()) throw monero_error("Must provide address to mine to");
if (num_threads != boost::none && num_threads.get() == 0) throw monero_error("Number of threads must be an integer greater than 0");
auto params = std::make_shared<monero_start_mining_params>(address, num_threads, background_mining, ignore_battery);
auto result = m_rpc->send_path_request("start_mining", params);
check_response_status(result);
}
void monero_daemon_rpc::stop_mining() {
MTRACE("monero_daemon_rpc::stop_mining()");
auto result = m_rpc->send_path_request("stop_mining");
check_response_status(result);
}
std::shared_ptr<monero_mining_status> monero_daemon_rpc::get_mining_status() {
MTRACE("monero_daemon_rpc::get_mining_status()");
auto result = m_rpc->send_path_request("mining_status");
check_response_status(result);
auto status = std::make_shared<monero_mining_status>();
deserialize_mining_status(result, status);
return status;
}
std::shared_ptr<monero_generate_blocks_result> monero_daemon_rpc::generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash, const boost::optional<uint32_t>& starting_nonce) {
MTRACE("monero_daemon_rpc::generate_blocks()");
if (wallet_address.empty()) throw monero_error("Must provide an address to mine to");
if (num_blocks == 0) throw monero_error("Must provide a number of blocks to generate greater than 0");
auto params = std::make_shared<monero_generate_blocks_params>(wallet_address, num_blocks, prev_block_hash, starting_nonce);
auto result = m_rpc->send_json_request("generateblocks", params);
check_response_status(result);
auto generate_result = std::make_shared<monero_generate_blocks_result>();
deserialize_generate_blocks_result(result, generate_result);
return generate_result;
}
void monero_daemon_rpc::submit_blocks(const std::vector<std::string>& block_blobs) {
MTRACE("monero_daemon_rpc::submit_blocks()");
if (block_blobs.empty()) throw monero_error("Must provide an array of mined block blobs to submit");
auto params = std::make_shared<monero_submit_blocks_params>(block_blobs);
auto result = m_rpc->send_json_request("submit_block", params);
check_response_status(result);
}
std::shared_ptr<monero_prune_result> monero_daemon_rpc::prune_blockchain(bool check) {
MTRACE("monero_daemon_rpc::prune_blockchain()");
auto params = std::make_shared<monero_prune_blockchain_params>(check);
auto result = m_rpc->send_json_request("prune_blockchain", params);
check_response_status(result);
std::shared_ptr<monero_prune_result> prune_result = std::make_shared<monero_prune_result>();
deserialize_prune_result(result, prune_result);
return prune_result;
}
std::shared_ptr<monero_daemon_update_check_result> monero_daemon_rpc::check_for_update() {
MTRACE("monero_daemon_rpc::check_for_update()");
auto params = std::make_shared<monero_download_update_params>("check");
auto result = m_rpc->send_path_request("update", params);
check_response_status(result);
auto check_result = std::make_shared<monero_daemon_update_check_result>();
deserialize_update_check_result(result, check_result);
return check_result;
}
std::shared_ptr<monero_daemon_update_download_result> monero_daemon_rpc::download_update(const std::string& path) {
MTRACE("monero_daemon_rpc::download_update()");
auto params = std::make_shared<monero_download_update_params>("download", path);
auto result = m_rpc->send_path_request("update", params);
check_response_status(result);
auto download_result = std::make_shared<monero_daemon_update_download_result>();
deserialize_update_download_result(result, download_result);
return download_result;
}
void monero_daemon_rpc::stop() {
MTRACE("monero_daemon_rpc::stop()");
auto result = m_rpc->send_path_request("stop_daemon");
check_response_status(result);
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::wait_for_next_block_header() {
MTRACE("monero_daemon_rpc::wait_for_next_block_header()");
// use mutex and condition variable with predicate to wait for block
boost::mutex temp;
boost::condition_variable cv;
bool ready = false;
// create listener which notifies condition variable when block is added
block_notifier block_listener(&temp, &cv, &ready);
// register the listener, and unregister it unconditionally on the way out
// otherwise m_listeners keeps a pointer into this stack
// frame after it's gone, and the next block dispatch calls through it (UAF)
struct listener_guard {
monero_daemon_rpc* daemon;
monero_daemon_listener& listener;
~listener_guard() {
// never let remove_listener() escape a destructor since if we're
// already unwinding from another exception, that would call std::terminate
try { daemon->remove_listener(listener); } catch (...) {}
}
} unregister_guard{this, block_listener};
add_listener(block_listener);
// guard against an offline/unresponsive daemon (no connection, stalled node, etc.):
// such a daemon will never announce a new block, so bound the wait instead of hanging
// this call forever.
// TODO monero-java waits indefinitely.
bool timed_out;
std::shared_ptr<monero_block_header> header;
{
boost::mutex::scoped_lock lock(temp);
cv.timed_wait(lock, boost::posix_time::minutes(30), [&]() { return ready; });
// check ready directly rather than trust timed_wait's return value, still under the
// lock: avoids any discrepancy if a header arrives right at the timeout boundary
timed_out = !ready;
// copy out while still holding the lock: block_listener stays registered until the
// unregister_guard destructor runs (at function exit, after this point), so
// on_block_header() could still write m_last_header under temp concurrently with an
// unlocked read here
if (!timed_out) header = block_listener.m_last_header;
}
if (timed_out) throw monero_error("Timed out waiting for next block header");
// return last height
if (header == nullptr) throw monero_error("Could not get last block header.");
return header;
}
std::shared_ptr<monero_bandwidth_limits> monero_daemon_rpc::get_bandwidth_limits() {
MTRACE("monero_daemon_rpc::get_bandwidth_limits()");
auto result = m_rpc->send_path_request("get_limit");
check_response_status(result);
auto limits = std::make_shared<monero_bandwidth_limits>();
monero_bandwidth_limits::from_property_tree(result, limits);
return limits;
}
std::shared_ptr<monero_bandwidth_limits> monero_daemon_rpc::set_bandwidth_limits(int up, int down) {
MTRACE("monero_daemon_rpc::set_bandwidth_limits()");
auto limits = std::make_shared<monero_bandwidth_limits>(up, down);
auto result = m_rpc->send_path_request("set_limit", limits);
check_response_status(result);
monero_bandwidth_limits::from_property_tree(result, limits);
return limits;
}
void monero_daemon_rpc::refresh_listening() {
boost::lock_guard<boost::mutex> poller_lock(m_poller_mutex);
bool should_poll;
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
should_poll = m_listeners.size() > 0;
}
if (!m_poller && should_poll) {
m_poller = std::make_unique<monero_daemon_poller>(this);
}
if (m_poller) m_poller->request_is_polling(should_poll);
}
monero_daemon_rpc::~monero_daemon_rpc() {
MTRACE("~monero_daemon_rpc()");
remove_listeners();
}
}
@@ -0,0 +1,189 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "monero_daemon.h"
/**
* Implements a monero_daemon.h using monero-project's monerod.
*/
namespace monero {
// forward declaration of internal daemon poller
class monero_daemon_poller;
/**
* Implements a Monero daemon using monerod-rpc.
*/
class monero_daemon_rpc : public monero_daemon {
public:
/**
* Destruct the daemon.
*/
~monero_daemon_rpc() override;
monero_daemon_rpc(const std::shared_ptr<monero_rpc_connection>& rpc);
monero_daemon_rpc(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", const boost::optional<uint32_t>& timeout = boost::none);
/**
* Get the daemon's RPC connection.
*
* @return the daemon's rpc connection
*/
std::shared_ptr<monero_rpc_connection> get_rpc_connection() const;
/**
* Indicates if the client is connected to the daemon via RPC.
*
* @return true if the client is connected to the daemon, false otherwise
*/
bool is_connected();
/**
* Set the daemon poll period.
*
* @param period_ms poll period in milliseconds.
*/
void set_poll_period_in_ms(uint64_t period_ms);
/**
* Supported daemon methods.
*/
std::set<monero_daemon_listener*> get_listeners() override;
void add_listener(monero_daemon_listener &listener) override;
void remove_listener(monero_daemon_listener &listener) override;
void remove_listeners() override;
monero_version get_version() override;
bool is_trusted() override;
uint64_t get_height() override;
std::string get_block_hash(uint64_t height) override;
std::shared_ptr<monero_block_template> get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size = boost::none) override;
std::shared_ptr<monero_block_header> get_last_block_header() override;
std::shared_ptr<monero_block_header> get_block_header_by_hash(const std::string& hash) override;
std::shared_ptr<monero_block_header> get_block_header_by_height(uint64_t height) override;
std::vector<std::shared_ptr<monero_block_header>> get_block_headers_by_range(uint64_t start_height, uint64_t end_height) override;
std::shared_ptr<monero_block> get_block_by_hash(const std::string& hash) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) override;
std::shared_ptr<monero_block> get_block_by_height(uint64_t height) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_height(const std::vector<uint64_t>& heights) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) override;
std::vector<std::string> get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) override;
std::vector<std::shared_ptr<monero_tx>> get_txs(const std::vector<std::string>& tx_hashes, bool prune = false) override;
std::vector<std::string> get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune = false) override;
std::shared_ptr<monero_miner_tx_sum> get_miner_tx_sum(uint64_t height, uint64_t num_blocks) override;
std::shared_ptr<monero_fee_estimate> get_fee_estimate(uint64_t grace_blocks = 0) override;
std::shared_ptr<monero_submit_tx_result> submit_tx_hex(const std::string& tx_hex, bool do_not_relay = false) override;
void relay_txs_by_hash(const std::vector<std::string>& tx_hashes) override;
std::shared_ptr<monero_tx_pool_stats> get_tx_pool_stats() override;
std::vector<std::shared_ptr<monero_tx>> get_tx_pool() override;
std::vector<std::string> get_tx_pool_hashes() override;
void flush_tx_pool(const std::vector<std::string> &hashes) override;
void flush_tx_pool() override;
void flush_tx_pool(const std::string &hash) override;
std::vector<monero_key_image_spent_status> get_key_image_spent_statuses(const std::vector<std::string>& key_images) override;
std::vector<std::shared_ptr<monero_output>> get_outputs(const std::vector<monero_output>& outputs) override;
std::vector<std::shared_ptr<monero_output_histogram_entry>> get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) override;
std::vector<std::shared_ptr<monero_output_distribution_entry>> get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative = boost::none, const boost::optional<uint64_t>& start_height = boost::none, const boost::optional<uint64_t>& end_height = boost::none) override;
std::shared_ptr<monero_daemon_info> get_info() override;
std::shared_ptr<monero_daemon_sync_info> get_sync_info() override;
std::shared_ptr<monero_hard_fork_info> get_hard_fork_info() override;
std::vector<std::shared_ptr<monero_alt_chain>> get_alt_chains() override;
std::vector<std::string> get_alt_block_hashes() override;
int get_download_limit() override;
int set_download_limit(int limit) override;
int reset_download_limit() override;
int get_upload_limit() override;
int set_upload_limit(int limit) override;
int reset_upload_limit() override;
std::vector<std::shared_ptr<monero_peer>> get_peers() override;
std::vector<std::shared_ptr<monero_peer>> get_known_peers() override;
void set_outgoing_peer_limit(int limit) override;
void set_incoming_peer_limit(int limit) override;
std::vector<std::shared_ptr<monero_ban>> get_peer_bans() override;
void set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) override;
void start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) override;
void stop_mining() override;
std::shared_ptr<monero_mining_status> get_mining_status() override;
std::shared_ptr<monero_generate_blocks_result> generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash = boost::none, const boost::optional<uint32_t>& starting_nonce = boost::none) override;
void submit_blocks(const std::vector<std::string>& block_blobs) override;
std::shared_ptr<monero_prune_result> prune_blockchain(bool check) override;
std::shared_ptr<monero_daemon_update_check_result> check_for_update() override;
std::shared_ptr<monero_daemon_update_download_result> download_update(const std::string& path = "") override;
void stop() override;
std::shared_ptr<monero_block_header> wait_for_next_block_header() override;
// --------------------------------- PRIVATE --------------------------------
private:
friend class monero_daemon_poller;
mutable boost::recursive_mutex m_listeners_mutex;
std::set<monero_daemon_listener*> m_listeners;
std::shared_ptr<monero_rpc_connection> m_rpc;
boost::mutex m_poller_mutex;
std::unique_ptr<monero_daemon_poller> m_poller;
// header_cache is scoped to a single get_blocks_by_range_chunked() call (see below) rather than
// stored as a member: it only helps avoid re-fetching a header already seen earlier in that one
// chunking pass, and keeping it as instance state would let it grow unbounded over the connection's
// lifetime and serve stale headers across reorgs on later calls
std::vector<std::shared_ptr<monero_block>> get_max_blocks(boost::optional<uint64_t> start_height, boost::optional<uint64_t> max_height, boost::optional<uint64_t> chunk_size, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache);
std::shared_ptr<monero_block_header> get_block_header_by_height_cached(uint64_t height, uint64_t max_height, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache);
std::shared_ptr<monero_bandwidth_limits> get_bandwidth_limits();
std::shared_ptr<monero_bandwidth_limits> set_bandwidth_limits(int up, int down);
void refresh_listening();
void wait_for_listener_callbacks_idle();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,288 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "common/monero_rpc_connection.h"
/**
* Internal data model for monero_daemon_rpc.h.
*/
namespace monero {
/**
* Models a Monero RPC banhammer.
*/
struct monero_rpc_ban : public monero_ban {
monero_rpc_ban(const std::shared_ptr<monero_ban> &ban);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ Binary Request ---------------------------------
/**
* Models paramaters for monerod-rpc `/get_blocks_by_height.bin` method.
*/
struct monero_get_blocks_by_height_request : public monero_rpc_request {
std::vector<uint64_t> m_heights;
monero_get_blocks_by_height_request(uint64_t num_blocks);
monero_get_blocks_by_height_request(const std::vector<uint64_t>& heights): m_heights(heights) { m_method = "get_blocks_by_height.bin"; }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ RPC Params ---------------------------------
struct monero_download_update_params : public serializable_struct {
boost::optional<std::string> m_command;
boost::optional<std::string> m_path;
monero_download_update_params(const std::string& command = "download", const std::string& path = "");
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_submit_tx_params : public serializable_struct {
boost::optional<std::string> m_tx_hex;
boost::optional<bool> m_do_not_relay;
std::vector<std::string> m_tx_hashes;
monero_submit_tx_params(const std::vector<std::string>& tx_hashes): m_tx_hashes(tx_hashes) { }
monero_submit_tx_params(const std::string& tx_hex, bool do_not_relay): m_tx_hex(tx_hex), m_do_not_relay(do_not_relay) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_peer_limits_params : public serializable_struct {
boost::optional<int> m_in_peers;
boost::optional<int> m_out_peers;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_txs_params : public serializable_struct {
std::vector<std::string> m_tx_hashes;
boost::optional<bool> m_decode_as_json;
boost::optional<bool> m_prune;
monero_get_txs_params(const std::vector<std::string> &tx_hashes, bool prune, bool decode_as_json = true): m_tx_hashes(tx_hashes), m_prune(prune), m_decode_as_json(decode_as_json) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_is_key_image_spent_params : public serializable_struct {
std::vector<std::string> m_key_images;
monero_is_key_image_spent_params(const std::vector<std::string>& key_images): m_key_images(key_images) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Params ---------------------------------
struct monero_start_mining_params : public serializable_struct {
boost::optional<std::string> m_miner_address;
boost::optional<uint64_t> m_num_threads;
boost::optional<bool> m_is_background;
boost::optional<bool> m_ignore_battery;
monero_start_mining_params(const boost::optional<std::string>& address, const boost::optional<uint64_t>& num_threads, const boost::optional<bool>& is_background, const boost::optional<bool>& ignore_battery): m_miner_address(address), m_num_threads(num_threads), m_is_background(is_background), m_ignore_battery(ignore_battery) { }
monero_start_mining_params(const boost::optional<uint64_t>& num_threads, const boost::optional<bool>& is_background, const boost::optional<bool>& ignore_battery): m_num_threads(num_threads), m_is_background(is_background), m_ignore_battery(ignore_battery) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_generate_blocks_params : public serializable_struct {
boost::optional<std::string> m_wallet_address;
boost::optional<uint64_t> m_num_blocks;
boost::optional<std::string> m_prev_block_hash;
boost::optional<uint32_t> m_starting_nonce;
monero_generate_blocks_params(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash, const boost::optional<uint32_t>& starting_nonce): m_wallet_address(wallet_address), m_num_blocks(num_blocks), m_prev_block_hash(prev_block_hash), m_starting_nonce(starting_nonce) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_prune_blockchain_params : public serializable_struct {
boost::optional<bool> m_check;
monero_prune_blockchain_params(bool check = true): m_check(check) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_submit_blocks_params : public serializable_struct {
std::vector<std::string> m_block_blobs;
monero_submit_blocks_params(const std::vector<std::string>& block_blobs): m_block_blobs(block_blobs) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_block_params : public serializable_struct {
boost::optional<uint64_t> m_height;
boost::optional<std::string> m_hash;
boost::optional<bool> m_fill_pow_hash;
boost::optional<uint64_t> m_start_height;
boost::optional<uint64_t> m_end_height;
boost::optional<std::string> m_wallet_address;
boost::optional<int> m_reserve_size;
monero_get_block_params(uint64_t height, bool fill_pow_hash = false): m_height(height), m_fill_pow_hash(fill_pow_hash) { }
monero_get_block_params(const std::string& hash, bool fill_pow_hash = false): m_hash(hash), m_fill_pow_hash(fill_pow_hash) { }
monero_get_block_params(uint64_t start_height, uint64_t end_height): m_start_height(start_height), m_end_height(end_height) { }
monero_get_block_params(const std::string& wallet_address, const boost::optional<int>& reserve_size): m_wallet_address(wallet_address), m_reserve_size(reserve_size) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_block_hash_params : public serializable_struct {
boost::optional<uint64_t> m_height;
monero_get_block_hash_params(uint64_t height): m_height(height) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_miner_tx_sum_params : public serializable_struct {
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_count;
monero_get_miner_tx_sum_params(uint64_t height, uint64_t count): m_height(height), m_count(count) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_fee_estimate_params : public serializable_struct {
boost::optional<uint64_t> m_grace_blocks;
monero_get_fee_estimate_params(uint64_t grace_blocks = 0): m_grace_blocks(grace_blocks) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_set_bans_params : public serializable_struct {
std::vector<std::shared_ptr<monero_rpc_ban>> m_bans;
monero_set_bans_params(const std::vector<std::shared_ptr<monero_ban>>& bans);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_output_histogram_params : public serializable_struct {
std::vector<uint64_t> m_amounts;
boost::optional<int> m_min_count;
boost::optional<int> m_max_count;
boost::optional<bool> m_is_unlocked;
boost::optional<int> m_recent_cutoff;
monero_get_output_histogram_params(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) : m_amounts(amounts), m_min_count(min_count), m_max_count(max_count), m_is_unlocked(is_unlocked), m_recent_cutoff(recent_cutoff) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_output_distribution_params : public serializable_struct {
std::vector<uint64_t> m_amounts;
boost::optional<bool> m_cumulative;
boost::optional<bool> m_binary;
boost::optional<uint64_t> m_from_height;
boost::optional<uint64_t> m_to_height;
monero_get_output_distribution_params(const std::vector<uint64_t>& amounts, const boost::optional<bool>& cumulative, const boost::optional<uint64_t>& from_height, const boost::optional<uint64_t>& to_height) : m_amounts(amounts), m_cumulative(cumulative), m_from_height(from_height), m_to_height(to_height), m_binary(false) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Response ---------------------------------
struct monero_get_block_result {
boost::optional<uint64_t> m_count;
boost::optional<uint64_t> m_height;
boost::optional<bool> m_untrusted;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_get_block_result>& result);
};
// ------------------------------ RPC Deserialization ---------------------------------
void deserialize_version(const boost::property_tree::ptree& node, monero_version& version);
void deserialize_block_header(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block_header>& header);
void deserialize_block_headers(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_block_header>>& headers);
void deserialize_block(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block>& block, bool is_nested = false);
void deserialize_blocks(const boost::property_tree::ptree& node, const std::vector<uint64_t>& heights, std::vector<std::shared_ptr<monero_block>>& blocks);
void deserialize_alt_block_hashes(const boost::property_tree::ptree& node, std::vector<std::string>& block_hashes);
void deserialize_txs(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_tx>>& txs);
void deserialize_tx_hashes(const boost::property_tree::ptree& node, std::vector<std::string>& tx_hashes);
void deserialize_key_image_spent_status(const boost::property_tree::ptree& node, std::vector<monero_key_image_spent_status>& statuses);
void deserialize_alt_chains(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_alt_chain>>& alt_chains);
void deserialize_bans(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_ban>>& bans);
void deserialize_prune_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_prune_result>& result);
void deserialize_mining_status(const boost::property_tree::ptree& node, const std::shared_ptr<monero_mining_status>& status);
void deserialize_generate_blocks_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_generate_blocks_result>& result);
void deserialize_miner_tx_sum(const boost::property_tree::ptree& node, const std::shared_ptr<monero_miner_tx_sum>& sum);
void deserialize_block_template(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block_template>& tmplt);
void deserialize_peers(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_peer>>& peers);
void deserialize_submit_tx_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_submit_tx_result>& result);
void deserialize_output_distribution_entries(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_output_distribution_entry>>& entries);
void deserialize_output_histogram_entries(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_output_histogram_entry>>& entries);
void deserialize_tx_pool_stats(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_pool_stats>& stats);
void deserialize_update_check_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_check_result>& check);
void deserialize_update_download_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_download_result>& check);
void deserialize_fee_estimate(const boost::property_tree::ptree& node, const std::shared_ptr<monero_fee_estimate>& estimate);
void deserialize_daemon_info(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_info>& info);
void deserialize_daemon_sync_info(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_sync_info>& info);
void deserialize_hard_fork_info(const boost::property_tree::ptree& node, const std::shared_ptr<monero_hard_fork_info>& info);
}
@@ -0,0 +1,220 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "gen_utils.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
using namespace gen_utils;
// ------------------------- SERIALIZATION ---------------------------
std::string gen_utils::serialize(const rapidjson::Document& doc) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
return buffer.GetString();
}
std::string gen_utils::serialize(const boost::property_tree::ptree& node) {
std::stringstream ss;
boost::property_tree::write_json(ss, node, false);
std::string str = ss.str();
return str.substr(0, str.size() - 1); // strip newline
}
void gen_utils::deserialize(const std::string& json, boost::property_tree::ptree& root) {
std::istringstream iss = json.empty() ? std::istringstream() : std::istringstream(json);
try {
boost::property_tree::read_json(iss, root);
} catch (std::exception const& e) {
throw std::runtime_error("Invalid JSON");
}
}
// --------------------------- THREAD POLLER ---------------------------
thread_poller::thread_poller(): m_is_polling(false), m_poll_loop_running(false), m_poll_period_ms(20000) {
}
thread_poller::~thread_poller() {
// safety net only: derived classes must stop polling in their own destructor
// (before their members are torn down) since by the time this base destructor
// runs, the derived part of the object is already gone and a concurrently
// running poll() (a virtual call) would hit a partially-destructed object
set_is_polling(false);
}
namespace {
// stack of poller instances whose announce_scope is currently active on the current
// thread, innermost last. Per-instance so that a callback of poller A calling into
// a *different* poller B's wait_for_callbacks_idle() still waits normally.
thread_local std::vector<const thread_poller*> t_active_dispatch_stack;
}
thread_poller::announce_scope::announce_scope(thread_poller& poller): m_poller(poller) {
t_active_dispatch_stack.push_back(&poller);
boost::lock_guard<boost::mutex> lock(m_poller.m_announce_mutex);
++m_poller.m_announce_count;
}
thread_poller::announce_scope::~announce_scope() {
{
boost::lock_guard<boost::mutex> lock(m_poller.m_announce_mutex);
if (--m_poller.m_announce_count == 0) m_poller.m_announce_cv.notify_all();
}
t_active_dispatch_stack.pop_back();
}
void thread_poller::wait_for_callbacks_idle() {
// skip the wait only if this poller's own dispatch is active on the current thread;
// waiting would deadlock in that case. A different poller's active dispatch must not
// short-circuit this wait.
for (const thread_poller* active : t_active_dispatch_stack) {
if (active == this) return;
}
boost::unique_lock<boost::mutex> lock(m_announce_mutex);
m_announce_cv.wait(lock, [&]() { return m_announce_count == 0; });
}
void thread_poller::init_common(const std::string& name) {
m_name = name;
}
void thread_poller::request_is_polling(bool is_polling) {
boost::lock_guard<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
if (is_polling) {
if (m_is_polling) return;
m_is_polling = true;
run_poll_loop();
return;
}
bool was_polling = m_is_polling.exchange(false);
if (was_polling) {
boost::lock_guard<boost::mutex> polling_lock(m_polling_mutex);
m_poll_cv.notify_one();
}
}
void thread_poller::set_is_polling(bool is_polling) {
boost::unique_lock<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
if (is_polling) {
if (m_is_polling) return;
m_is_polling = true;
run_poll_loop();
return;
}
bool was_polling = m_is_polling.exchange(false);
if (was_polling) {
boost::lock_guard<boost::mutex> polling_lock(m_polling_mutex);
m_poll_cv.notify_one();
}
if (!m_poll_loop_running) return; // loop already fully exited; nothing to wait for
if (boost::this_thread::get_id() == m_poll_thread_id) {
// called from the poll thread itself (e.g. a listener removed itself from within a
// callback invoked by poll()): waiting for our own loop to exit would deadlock. Just
// return -- the loop re-checks m_is_polling itself and will exit and clear
// m_poll_loop_running/m_poll_thread_id on its own (see run_poll_loop).
return;
}
// wait for the loop to actually finish
m_lifecycle_cv.wait(lifecycle_lock, [&]() { return !m_poll_loop_running; });
}
void thread_poller::run_poll_loop() {
// called with m_lifecycle_mutex held
if (m_poll_loop_running) return; // only run one loop at a time
m_poll_loop_running = true;
// TODO: use global threadpool, background sync wasm wallet in c++ thread
try {
boost::thread([this]() {
{
boost::lock_guard<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
m_poll_thread_id = boost::this_thread::get_id();
}
while (true) {
while (m_is_polling) {
try { poll(); }
catch (const std::exception& e) { MERROR(m_name << " failed to background poll: " << e.what()); }
catch (...) { MERROR(m_name << " failed to background poll"); }
// only wait if polling still enabled
if (m_is_polling) {
boost::mutex::scoped_lock lock(m_polling_mutex);
boost::posix_time::milliseconds wait_for_ms(m_poll_period_ms.load());
m_poll_cv.timed_wait(lock, wait_for_ms, [&]() { return !m_is_polling; });
}
}
boost::unique_lock<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
if (m_is_polling) continue;
m_poll_loop_running = false;
m_poll_thread_id = boost::thread::id();
m_lifecycle_cv.notify_all();
return;
}
}).detach();
} catch (...) {
m_poll_loop_running = false;
m_is_polling = false;
throw;
}
}
@@ -0,0 +1,220 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#ifndef gen_utils_h
#define gen_utils_h
#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "rapidjson/document.h"
#include <boost/thread/recursive_mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/condition_variable.hpp>
#include <chrono>
#include <thread>
#include <atomic>
#include "include_base_utils.h"
#include "common/util.h"
/**
* Collection of generic utilities.
*/
namespace gen_utils
{
/**
* Return a unique identifier.
*
* @return a unique id
*/
static std::string get_uuid() {
boost::uuids::random_generator generator;
boost::uuids::uuid uuid = generator();
return boost::uuids::to_string(uuid);
}
/**
* Wait for the given duration.
*
* @param duration_ms the duration to wait in milliseconds
*/
static void wait_for(uint64_t duration_ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(duration_ms));
}
static bool bool_equals(bool val, const boost::optional<bool>& opt_val) {
return opt_val == boost::none ? false : val == *opt_val;
}
// ------------------------- SERIALIZATION ---------------------------
// TODO: fully switch from property trees to rapidjson
std::string serialize(const rapidjson::Document& doc);
std::string serialize(const boost::property_tree::ptree& node);
void deserialize(const std::string& json, boost::property_tree::ptree& root);
// ------------------------- VALUE RECONCILATION ----------------------------
// TODO: refactor common template code
template <class T, typename std::enable_if<std::is_same<T, std::string>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, boost::optional<bool> resolve_defined, boost::optional<bool> resolve_true, boost::optional<bool> resolve_max, const std::string& err_msg = "") {
// check for equality
if (val1 == val2) return val1;
// resolve one value none
if (val1 == boost::none || val2 == boost::none) {
if (resolve_defined != boost::none && *resolve_defined == false) return boost::none;
else return val1 == boost::none ? val2 : val1;
}
throw std::runtime_error(std::string("Cannot reconcile strings: ") + boost::lexical_cast<std::string>(val1) + std::string(" vs ") + boost::lexical_cast<std::string>(val2) + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}
template <class T, typename std::enable_if<std::is_same<T, std::string>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, const std::string& err_msg = "") {
return reconcile(val1, val2, boost::none, boost::none, boost::none, err_msg);
}
template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, boost::optional<bool> resolve_defined, boost::optional<bool> resolve_true, boost::optional<bool> resolve_max, const std::string& err_msg = "") {
// check for equality
if (val1 == val2) return val1;
// resolve one value none
if (val1 == boost::none || val2 == boost::none) {
if (resolve_defined != boost::none && *resolve_defined == false) return boost::none;
else return val1 == boost::none ? val2 : val1;
}
// resolve different booleans
if (resolve_true != boost::none) return (bool) val1 == *resolve_true ? val1 : val2; // if resolve true, return true, else return false
// resolve different numbers
if (resolve_max != boost::none) return *resolve_max ? std::max(*val1, *val2) : std::min(*val1, *val2);
// cannot reconcile
throw std::runtime_error(std::string("Cannot reconcile integrals: ") + boost::lexical_cast<std::string>(val1) + std::string(" vs ") + boost::lexical_cast<std::string>(val2) + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}
template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, const std::string& err_msg = "") {
return reconcile(val1, val2, boost::none, boost::none, boost::none, err_msg);
}
template <class T>
std::vector<T> reconcile(const std::vector<T>& v1, const std::vector<T>& v2, const std::string& err_msg = "") {
// check for equality
if (v1 == v2) return v1;
// resolve one vector empty
if (v1.empty()) return v2;
if (v2.empty()) return v1;
// otherwise cannot reconcile
throw std::runtime_error("Cannot reconcile vectors" + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}
// ------------------------- THREAD POLLER ----------------------------
// WARNING: Only destroy a thread_poller from a thread other than its own pool thread.
// (e.g. a listener callback running on the poll thread, drops the last owning reference/unique_ptr
// to the poller.)
class thread_poller {
public:
thread_poller();
virtual ~thread_poller();
bool is_polling() const { return m_is_polling; }
void set_is_polling(bool is_polling);
void request_is_polling(bool is_polling);
void set_period_in_ms(uint64_t period_ms) { m_poll_period_ms = period_ms; }
virtual void poll() = 0;
void wait_for_callbacks_idle();
class announce_scope {
public:
explicit announce_scope(thread_poller& poller);
~announce_scope();
announce_scope(const announce_scope&) = delete;
announce_scope& operator=(const announce_scope&) = delete;
private:
thread_poller& m_poller;
};
protected:
std::string m_name;
boost::recursive_mutex m_mutex;
boost::mutex m_polling_mutex; // guards the interruptible periodic sleep (m_poll_cv)
boost::mutex m_lifecycle_mutex; // guards m_poll_loop_running/m_poll_thread_id and start/stop coordination
boost::thread::id m_poll_thread_id; // id of the currently running loop thread; guarded by m_lifecycle_mutex
std::atomic<bool> m_is_polling;
bool m_poll_loop_running; // guarded by m_lifecycle_mutex
std::atomic<uint64_t> m_poll_period_ms;
boost::condition_variable m_poll_cv; // periodic-sleep interrupt
boost::condition_variable m_lifecycle_cv; // notified (under m_lifecycle_mutex) when the loop actually exits
boost::mutex m_announce_mutex; // guards m_announce_count
boost::condition_variable m_announce_cv; // notified (under m_announce_mutex) when m_announce_count reaches 0
int m_announce_count = 0; // number of announce_scope instances currently alive, across all threads
void init_common(const std::string& name);
void run_poll_loop();
};
}
#endif /* gen_utils_h */
@@ -0,0 +1,624 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_utils.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "storages/portable_storage_template_helper.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "mnemonics/electrum-words.h"
#include "mnemonics/english.h"
#include "string_tools.h"
#include "byte_stream.h"
#include "gen_utils.h"
using namespace cryptonote;
using namespace monero_utils;
void monero_utils::set_log_level(int level) {
mlog_set_log_level(level);
}
void monero_utils::set_log_categories(const std::string& categories) {
mlog_set_categories(categories.c_str());
}
void monero_utils::configure_logging(const std::string& path, bool console) {
mlog_configure(path, console);
}
// --------------------------- VALIDATION UTILS -------------------------------
monero_integrated_address monero_utils::get_integrated_address(monero_network_type network_type, const std::string& standard_address, const std::string& payment_id) {
// parse and validate address
cryptonote::address_parse_info address_info;
if (!get_account_address_from_str(address_info, static_cast<cryptonote::network_type>(network_type), standard_address)) throw std::runtime_error("Invalid address");
if (address_info.has_payment_id) throw std::runtime_error("The given address already has a payment id");
// randomly generate payment id if not given, else validate
crypto::hash8 payment_id_h8;
if (payment_id.empty()) {
payment_id_h8 = crypto::rand<crypto::hash8>();
} else {
cryptonote::blobdata payment_id_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(payment_id, payment_id_data) || sizeof(crypto::hash8) != payment_id_data.size()) throw std::runtime_error("Invalid payment id");
payment_id_h8 = *reinterpret_cast<const crypto::hash8*>(payment_id_data.data());
}
// build integrated address
monero_integrated_address integrated_address;
integrated_address.m_integrated_address = cryptonote::get_account_integrated_address_as_str(static_cast<cryptonote::network_type>(network_type), address_info.address, payment_id_h8);
integrated_address.m_standard_address = standard_address;
integrated_address.m_payment_id = epee::string_tools::pod_to_hex(payment_id_h8);
return integrated_address;
}
bool monero_utils::is_valid_address(const std::string& address, monero_network_type network_type) {
try {
validate_address(address, network_type);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_private_view_key(const std::string& private_view_key) {
try {
validate_private_view_key(private_view_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_private_spend_key(const std::string& private_spend_key) {
try {
validate_private_spend_key(private_spend_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_public_view_key(const std::string& public_view_key) {
try {
validate_public_view_key(public_view_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_public_spend_key(const std::string& public_spend_key) {
try {
validate_public_spend_key(public_spend_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_payment_id(const std::string& payment_id) {
try {
validate_payment_id(payment_id);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_mnemonic(const std::string& mnemonic, const std::string& language) {
try {
validate_mnemonic(mnemonic, language);
return true;
}
catch (...) {
return false;
}
}
void monero_utils::validate_address(const std::string& address, monero_network_type network_type) {
cryptonote::address_parse_info info;
if (!get_account_address_from_str(info, static_cast<cryptonote::network_type>(network_type), address)) throw std::runtime_error("Invalid address");
}
void monero_utils::validate_private_view_key(const std::string& private_view_key) {
if (private_view_key.length() != 64) throw std::runtime_error("private view key expected to be 64 hex characters");
cryptonote::blobdata private_view_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(private_view_key, private_view_key_data) || private_view_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("private view key expected to be 64 hex characters");
}
}
void monero_utils::validate_private_spend_key(const std::string& private_spend_key) {
if (private_spend_key.length() != 64) throw std::runtime_error("private spend key expected to be 64 hex characters");
cryptonote::blobdata private_spend_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(private_spend_key, private_spend_key_data) || private_spend_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("private spend key expected to be 64 hex characters");
}
}
void monero_utils::validate_public_view_key(const std::string& public_view_key) {
if (public_view_key.length() != 64) throw std::runtime_error("public view key expected to be 64 hex characters");
cryptonote::blobdata public_view_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(public_view_key, public_view_key_data) || public_view_key_data.size() != sizeof(crypto::public_key)) {
throw std::runtime_error("public view key expected to be 64 hex characters");
}
}
void monero_utils::validate_public_spend_key(const std::string& public_spend_key) {
if (public_spend_key.length() != 64) throw std::runtime_error("public spend key expected to be 64 hex characters");
cryptonote::blobdata public_spend_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(public_spend_key, public_spend_key_data) || public_spend_key_data.size() != sizeof(crypto::public_key)) {
throw std::runtime_error("public spend key expected to be 64 hex characters");
}
}
void monero_utils::validate_payment_id(const std::string& payment_id) {
if (payment_id.length() != 64 && payment_id.length() != 16) throw std::runtime_error("payment id expected to be 64 or 16 hex characters");
cryptonote::blobdata payment_id_data;
if(!epee::string_tools::parse_hexstr_to_binbuff(payment_id, payment_id_data) || (payment_id_data.size() != sizeof(crypto::hash8) && payment_id_data.size() != sizeof(crypto::hash))) {
throw std::runtime_error("payment id expected to be 64 or 16 hex characters");
}
}
void monero_utils::validate_mnemonic(const std::string& mnemonic, const std::string& language) {
if (mnemonic.empty()) throw std::runtime_error("Mnemonic phrase is empty");
std::string seed_language;
crypto::secret_key recovery_key;
if (crypto::ElectrumWords::get_is_old_style_seed(mnemonic)) throw std::runtime_error("Mnemonic phrased words must be 25");
if(!crypto::ElectrumWords::words_to_bytes(mnemonic, recovery_key, seed_language)) throw std::runtime_error("Invalid mnemonic");
if (seed_language == crypto::ElectrumWords::old_language_name) seed_language = "English";
// validate language
if (!language.empty()) {
if (!crypto::ElectrumWords::is_valid_language(language)) throw std::runtime_error("Invalid language: " + language);
if (language != seed_language) throw std::runtime_error("Seed language mismatch");
}
}
// -------------------------- BINARY SERIALIZATION ----------------------------
void monero_utils::json_to_binary(const std::string &json, std::string &bin) {
epee::serialization::portable_storage ps;
ps.load_from_json(json);
epee::byte_stream bs;
ps.store_to_binary(bs);
bin = std::string((char*) bs.data(), bs.size());
}
void monero_utils::binary_to_json(const std::string &bin, std::string &json) {
epee::serialization::portable_storage ps;
ps.load_from_binary(bin);
ps.dump_as_json(json);
}
void monero_utils::binary_blocks_to_json(const std::string &bin, std::string &json) {
// load binary rpc response to struct
cryptonote::COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::response resp_struct;
epee::serialization::load_t_from_binary(resp_struct, bin);
// build property tree from deserialized blocks and transactions
boost::property_tree::ptree root;
boost::property_tree::ptree blocksNode; // array of block strings
boost::property_tree::ptree txsNodes; // array of txs per block (array of array)
for (int blockIdx = 0; blockIdx < resp_struct.blocks.size(); blockIdx++) {
// parse and validate block
cryptonote::block block;
if (cryptonote::parse_and_validate_block_from_blob(resp_struct.blocks[blockIdx].block, block)) {
// add block node to blocks node
boost::property_tree::ptree blockNode;
blockNode.put("", cryptonote::obj_to_json_str(block)); // TODO: no pretty print
blocksNode.push_back(std::make_pair("", blockNode));
} else {
throw std::runtime_error("failed to parse block blob at index " + std::to_string(blockIdx));
}
// parse and validate txs
boost::property_tree::ptree txs_node;
for (int txIdx = 0; txIdx < resp_struct.blocks[blockIdx].txs.size(); txIdx++) {
cryptonote::transaction tx;
if (cryptonote::parse_and_validate_tx_from_blob(resp_struct.blocks[blockIdx].txs[txIdx].blob, tx)) {
// add tx node to txs node
boost::property_tree::ptree txNode;
//MTRACE("PRUNED:\n" << monero_utils::get_pruned_tx_json(tx));
txNode.put("", monero_utils::get_pruned_tx_json(tx)); // TODO: no pretty print
txs_node.push_back(std::make_pair("", txNode));
} else {
throw std::runtime_error("failed to parse tx blob at index " + std::to_string(txIdx));
}
}
txsNodes.push_back(std::make_pair("", txs_node)); // array of array of transactions, one array per block
}
root.add_child("blocks", blocksNode);
root.add_child("txs", txsNodes);
root.put("status", resp_struct.status);
root.put("untrusted", resp_struct.untrusted); // TODO: loss of ints and bools
// convert root to string // TODO: common utility with serial_bridge
std::stringstream ss;
boost::property_tree::write_json(ss, root, false/*pretty*/);
json = ss.str();
}
// ------------------------------- RAPIDJSON ----------------------------------
void monero_utils::add_json_member(std::string key, std::string val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root, rapidjson::Value& field) {
rapidjson::Value field_key(key.c_str(), key.size(), allocator);
field.SetString(val.c_str(), val.size(), allocator);
root.AddMember(field_key, field, allocator);
}
void monero_utils::add_json_member(std::string key, bool val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root) {
rapidjson::Value field_key(key.c_str(), key.size(), allocator);
if (val) {
rapidjson::Value field_val(rapidjson::kTrueType);
root.AddMember(field_key, field_val, allocator);
} else {
rapidjson::Value field_val(rapidjson::kFalseType);
root.AddMember(field_key, field_val, allocator);
}
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<std::string>& strs) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_str(rapidjson::kStringType);
for (const std::string& str : strs) {
value_str.SetString(str.c_str(), str.size(), allocator);
value_arr.PushBack(value_str, allocator);
}
return value_arr;
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<int>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetInt(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint8_t>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetInt(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
// TODO: remove these redundant implementations for different sizes?
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint32_t>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetUint64(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint64_t>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetUint64(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
// ----------------------------------------------------------------------------
bool monero_utils::is_valid_language(const std::string& language) {
std::vector<std::string> languages;
crypto::ElectrumWords::get_language_list(languages, false);
std::vector<std::string>::iterator it = std::find(languages.begin(), languages.end(), language);
if (it == languages.end()) {
crypto::ElectrumWords::get_language_list(languages, true);
it = std::find(languages.begin(), languages.end(), language);
}
if (it == languages.end()) return false;
return true;
}
// TODO: this is unused
std::shared_ptr<monero_block> monero_utils::cn_block_to_block(const cryptonote::block& cn_block) {
cryptonote::block temp = cn_block;
std::cout << cryptonote::obj_to_json_str(temp) << std::endl;
std::shared_ptr<monero_block> block = std::make_shared<monero_block>();
block->m_major_version = cn_block.major_version;
block->m_minor_version = cn_block.minor_version;
block->m_timestamp = cn_block.timestamp;
block->m_prev_hash = epee::string_tools::pod_to_hex(cn_block.prev_id);
block->m_nonce = cn_block.nonce;
block->m_miner_tx = monero_utils::cn_tx_to_tx(cn_block.miner_tx);
for (const crypto::hash& tx_hash : cn_block.tx_hashes) {
block->m_tx_hashes.push_back(epee::string_tools::pod_to_hex(tx_hash));
}
return block;
}
std::shared_ptr<monero_tx> monero_utils::cn_tx_to_tx(const cryptonote::transaction& cn_tx, bool init_as_tx_wallet) {
std::shared_ptr<monero_tx> tx = init_as_tx_wallet ? std::make_shared<monero_tx_wallet>() : std::make_shared<monero_tx>();
tx->m_version = cn_tx.version;
tx->m_unlock_time = cn_tx.unlock_time;
tx->m_hash = epee::string_tools::pod_to_hex(cn_tx.hash);
tx->m_extra = cn_tx.extra;
// init inputs
for (const txin_v& cnVin : cn_tx.vin) {
if (cnVin.which() != 0 && cnVin.which() != 3) throw std::runtime_error("Unsupported variant type");
if (tx->m_is_miner_tx == boost::none) tx->m_is_miner_tx = cnVin.which() == 0;
if (cnVin.which() != 3) continue; // only process txin_to_key of variant TODO: support other types, like 0 "gen" which is miner tx?
std::shared_ptr<monero_output> input = init_as_tx_wallet ? std::make_shared<monero_output_wallet>() : std::make_shared<monero_output>();
input->m_tx = tx;
tx->m_inputs.push_back(input);
const txin_to_key& txin = boost::get<txin_to_key>(cnVin);
input->m_amount = txin.amount;
input->m_ring_output_indices = txin.key_offsets;
crypto::key_image cnKeyImage = txin.k_image;
input->m_key_image = std::make_shared<monero_key_image>();
input->m_key_image->m_hex = epee::string_tools::pod_to_hex(cnKeyImage);
}
// init outputs
for (const tx_out& cnVout : cn_tx.vout) {
std::shared_ptr<monero_output> output = init_as_tx_wallet ? std::make_shared<monero_output_wallet>() : std::make_shared<monero_output>();
output->m_tx = tx;
tx->m_outputs.push_back(output);
output->m_amount = cnVout.amount;
// before HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_key
// after HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_tagged_key
crypto::public_key cnStealthPublicKey;
if (cnVout.target.type() == typeid(txout_to_key))
cnStealthPublicKey = boost::get<txout_to_key>(cnVout.target).key;
else if (cnVout.target.type() == typeid(txout_to_tagged_key))
cnStealthPublicKey = boost::get<txout_to_tagged_key>(cnVout.target).key;
else
throw std::runtime_error(std::string("Unexpected output target type found: ") + std::string(cnVout.target.type().name()));
output->m_stealth_public_key = epee::string_tools::pod_to_hex(cnStealthPublicKey);
}
return tx;
// TODO: finish this, cryptonote::transaction has:
// std::vector<std::vector<crypto::signature> > m_signatures;
// rct::rctSig m_rct_signatures;
// mutable size_t blob_size;
}
void monero_utils::binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node) {
std::string response_json;
monero_utils::binary_blocks_to_json(bin, response_json);
std::istringstream iss(response_json);
boost::property_tree::read_json(iss, node);
auto blocks = node.get_child("blocks");
boost::property_tree::ptree parsed_blocks;
for (auto &entry : blocks) {
const std::string &block_str = entry.second.get_value<std::string>();
boost::property_tree::ptree block_node;
gen_utils::deserialize(block_str, block_node);
parsed_blocks.push_back(std::make_pair("", block_node));
}
node.put_child("blocks", parsed_blocks);
auto txs = node.get_child("txs");
boost::property_tree::ptree all_txs;
for (auto &rpc_txs_entry : txs) {
boost::property_tree::ptree txs_for_block;
const auto &rpc_txs = rpc_txs_entry.second;
if (!rpc_txs.empty() || !rpc_txs.data().empty()) {
for (auto &tx_entry : rpc_txs) {
std::string tx_str = tx_entry.second.get_value<std::string>();
auto pos = tx_str.find(',');
if (pos != std::string::npos) {
tx_str.replace(pos, 1, "{");
tx_str += "}";
}
boost::property_tree::ptree tx_node;
gen_utils::deserialize(tx_str, tx_node);
txs_for_block.push_back(std::make_pair("", tx_node));
}
}
all_txs.push_back(std::make_pair("", txs_for_block));
}
node.put_child("txs", all_txs);
}
void monero_utils::merge_tx(const std::shared_ptr<monero_tx_wallet>& tx, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map) {
if (tx->m_hash == boost::none) throw std::runtime_error("Tx hash is not initialized");
// merge tx
std::map<std::string, std::shared_ptr<monero_tx_wallet>>::const_iterator tx_iter = tx_map.find(*tx->m_hash);
if (tx_iter == tx_map.end()) {
tx_map[*tx->m_hash] = tx; // cache new tx
} else {
std::shared_ptr<monero_tx_wallet>& a_tx = tx_map[*tx->m_hash];
a_tx->merge(a_tx, tx); // merge with existing tx
}
// merge tx's block if confirmed
if (tx->get_height() != boost::none) {
std::map<uint64_t, std::shared_ptr<monero_block>>::const_iterator block_iter = block_map.find(tx->get_height().get());
if (block_iter == block_map.end()) {
block_map[tx->get_height().get()] = tx->m_block; // cache new block
} else {
std::shared_ptr<monero_block>& a_block = block_map[tx->get_height().get()];
a_block->merge(a_block, tx->m_block); // merge with existing block
}
}
}
bool monero_utils::tx_height_less_than(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2) {
if (tx1->m_block != nullptr && tx2->m_block != nullptr) return tx1->get_height() < tx2->get_height();
else if (tx1->m_block == nullptr) return false;
else return true;
}
bool monero_utils::incoming_transfer_before(const std::shared_ptr<monero_incoming_transfer>& transfer1, const std::shared_ptr<monero_incoming_transfer>& transfer2) {
// compare by height
if (tx_height_less_than(transfer1->m_tx, transfer2->m_tx)) return true;
// compare by account and subaddress index
if (transfer1->m_account_index.get() < transfer2->m_account_index.get()) return true;
else if (transfer1->m_account_index.get() == transfer2->m_account_index.get()) return transfer1->m_subaddress_index.get() < transfer2->m_subaddress_index.get();
else return false;
}
bool monero_utils::vout_before(const std::shared_ptr<monero_output>& o1, const std::shared_ptr<monero_output>& o2) {
if (o1 == o2) return false; // ignore equal references
std::shared_ptr<monero_output_wallet> ow1 = std::static_pointer_cast<monero_output_wallet>(o1);
std::shared_ptr<monero_output_wallet> ow2 = std::static_pointer_cast<monero_output_wallet>(o2);
// compare by height
if (tx_height_less_than(ow1->m_tx, ow2->m_tx)) return true;
// compare by account index, subaddress index, output index, then key image hex
if (ow1->m_account_index.get() < ow2->m_account_index.get()) return true;
if (ow1->m_account_index.get() == ow2->m_account_index.get()) {
if (ow1->m_subaddress_index.get() < ow2->m_subaddress_index.get()) return true;
if (ow1->m_subaddress_index.get() == ow2->m_subaddress_index.get()) {
if (ow1->m_index.get() < ow2->m_index.get()) return true;
if (ow1->m_index.get() == ow2->m_index.get()) throw std::runtime_error("Should never sort outputs with duplicate indices");
}
}
return false;
}
static std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, monero_network_type network_type) {
cryptonote::address_parse_info info;
if(!get_account_address_from_str(info, static_cast<cryptonote::network_type>(network_type), address)) {
throw std::runtime_error(std::string("Invalid address: ") + address);
}
if (!payment_id.empty()) {
throw std::runtime_error("Standalone payment id deprecated, use integrated address instead");
}
std::string uri = "monero:" + address;
unsigned int n_fields = 0;
if (amount > 0) {
// URI encoded amount is in decimal units, not atomic units
uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount);
}
if (!recipient_name.empty()) {
uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name);
}
if (!tx_description.empty()) {
uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description);
}
return uri;
}
std::string monero_utils::get_payment_uri(const monero_tx_config& config, monero_network_type network_type) {
// validate config
std::vector<std::shared_ptr<monero_destination>> destinations = config.get_normalized_destinations();
if (destinations.size() != 1) throw std::runtime_error("Cannot make URI from supplied parameters: must provide exactly one destination to send funds");
if (destinations.at(0)->m_address == boost::none) throw std::runtime_error("Cannot make URI from supplied parameters: must provide destination address");
if (destinations.at(0)->m_amount == boost::none) throw std::runtime_error("Cannot make URI from supplied parameters: must provide destination amount");
// prepare wallet2 params
std::string address = destinations.at(0)->m_address.get();
std::string payment_id = config.m_payment_id == boost::none ? "" : config.m_payment_id.get();
uint64_t amount = destinations.at(0)->m_amount.get();
std::string note = config.m_note == boost::none ? "" : config.m_note.get();
std::string m_recipient_name = config.m_recipient_name == boost::none ? "" : config.m_recipient_name.get();
// make uri
std::string uri = make_uri(address, payment_id, amount, note, m_recipient_name, network_type);
if (uri.empty()) throw std::runtime_error("Cannot make URI from supplied parameters");
return uri;
}
uint64_t monero_utils::xmr_to_atomic_units(double amount_xmr) {
if (!std::isfinite(amount_xmr) || amount_xmr < 0) throw std::invalid_argument("amount must be a finite, non-negative number");
long double atomic = std::round(static_cast<long double>(amount_xmr) * static_cast<long double>(XMR_AU_MULTIPLIER));
// validate the actual rounded value about to be cast, not a precomputed "amount_xmr" threshold:
// a threshold like UINT64_MAX / XMR_AU_MULTIPLIER carries its own division rounding error and can
// let a value that overflows uint64_t slip through, and casting an out-of-range floating point
// value to uint64_t is undefined behavior (not a wraparound). 2^64 is exactly representable in any
// binary floating point type with enough exponent range (it's a pure power of two), so this bound
// check is exact regardless of long double's precision on a given platform.
static const long double MAX_ATOMIC_UNITS_EXCLUSIVE = std::ldexp(1.0L, 64); // 2^64
if (atomic < 0.0L || atomic >= MAX_ATOMIC_UNITS_EXCLUSIVE) throw std::invalid_argument("amount exceeds maximum representable atomic units");
return static_cast<uint64_t>(atomic);
}
double monero_utils::atomic_units_to_xmr(uint64_t amount_atomic_units) {
return static_cast<double>(amount_atomic_units) / static_cast<double>(XMR_AU_MULTIPLIER);
}
@@ -0,0 +1,322 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#ifndef monero_utils_h
#define monero_utils_h
#include "wallet/monero_wallet_model.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "serialization/keyvalue_serialization.h" // TODO: consolidate with other binary deps?
#include "storages/portable_storage.h"
/**
* Collection of utilities for the Monero library.
*/
namespace monero_utils
{
using namespace cryptonote;
// ------------------------------ CONSTANTS ---------------------------------
static const int RING_SIZE = 12; // network-enforced ring size
static const uint64_t XMR_AU_MULTIPLIER = 1000000000000ULL;
// -------------------------------- UTILS -----------------------------------
void set_log_level(int level);
void set_log_categories(const std::string& categories);
void configure_logging(const std::string& path, bool console);
monero_integrated_address get_integrated_address(monero_network_type network_type, const std::string& standard_address, const std::string& payment_id);
std::string get_payment_uri(const monero_tx_config& config, monero_network_type network_type);
bool is_valid_address(const std::string& address, monero_network_type network_type);
bool is_valid_private_view_key(const std::string& private_view_key);
bool is_valid_private_spend_key(const std::string& private_spend_key);
bool is_valid_public_view_key(const std::string& public_view_key);
bool is_valid_public_spend_key(const std::string& public_spend_key);
bool is_valid_payment_id(const std::string& payment_id);
bool is_valid_mnemonic(const std::string& mnemonic, const std::string& language = "");
void validate_address(const std::string& address, monero_network_type network_type);
void validate_private_view_key(const std::string& private_view_key);
void validate_private_spend_key(const std::string& private_spend_key);
void validate_public_view_key(const std::string& public_view_key);
void validate_public_spend_key(const std::string& public_spend_key);
void validate_payment_id(const std::string& payment_id);
void validate_mnemonic(const std::string& mnemonic, const std::string& language = "");
void json_to_binary(const std::string &json, std::string &bin);
void binary_to_json(const std::string &bin, std::string &json);
void binary_blocks_to_json(const std::string &bin, std::string &json);
uint64_t xmr_to_atomic_units(double amount_xmr);
double atomic_units_to_xmr(uint64_t amount_atomic_units);
// ------------------------------ RAPIDJSON ---------------------------------
/**
* Add number, string, and boolean json members using template specialization.
*
* TODO: add_json_member("key", "val", ...) treated as integer instead of string literal
*/
template <class T>
void add_json_member(std::string key, T val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root, rapidjson::Value& field) {
rapidjson::Value field_key(key.c_str(), key.size(), allocator);
if (std::is_signed<T>::value) field.SetInt64((int64_t) val);
else field.SetUint64((uint64_t) val);
root.AddMember(field_key, field, allocator);
}
void add_json_member(std::string key, std::string val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root, rapidjson::Value& field);
void add_json_member(std::string key, bool val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root);
// TODO: template implementation here, could move to monero_utils.hpp per https://stackoverflow.com/questions/3040480/c-template-function-compiles-in-header-but-not-implementation
template <class T> rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<std::shared_ptr<T>>& vals) {
rapidjson::Value value_arr(rapidjson::kArrayType);
for (const auto& val : vals) {
value_arr.PushBack(val->to_rapidjson_val(allocator), allocator);
}
return value_arr;
}
// TODO: template implementation here, could move to monero_utils.hpp per https://stackoverflow.com/questions/3040480/c-template-function-compiles-in-header-but-not-implementation
template <class T> rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<T>& vals) {
rapidjson::Value value_arr(rapidjson::kArrayType);
for (const auto& val : vals) {
value_arr.PushBack(val.to_rapidjson_val(allocator), allocator);
}
return value_arr;
}
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<std::string>& strs);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<int>& nums);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint8_t>& nums);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint32_t>& nums);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint64_t>& nums);
// --------------------------------------------------------------------------
/**
* Indicates if the given language is valid.
*
* @param language is the language to validate
* @return true if the language is valid, false otherwise
*/
bool is_valid_language(const std::string& language);
/**
* Convert a cryptonote::block to a block in this library's native model.
*
* @param cn_block is the block to convert
* @return a block in this library's native model
*/
std::shared_ptr<monero_block> cn_block_to_block(const cryptonote::block& cn_block);
/**
* Convert a cryptonote::transaction to a transaction in this library's
* native model.
*
* @param cn_tx is the transaction to convert
* @param init_as_tx_wallet specifies if a monero_tx xor monero_tx_wallet should be initialized
*/
std::shared_ptr<monero_tx> cn_tx_to_tx(const cryptonote::transaction& cn_tx, bool init_as_tx_wallet = false);
/**
* Modified from core_rpc_server.cpp to return a std::string.
*
* TODO: remove this duplicate, use core_rpc_server instead
*/
static std::string get_pruned_tx_json(cryptonote::transaction &tx)
{
std::stringstream ss;
json_archive<true> ar(ss);
bool r = tx.serialize_base(ar);
CHECK_AND_ASSERT_MES(r, std::string(), "Failed to serialize rct signatures base");
return ss.str();
}
void binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node);
/**
* Merges a transaction into a unique set of transactions.
*
* @param tx is the transaction to merge into the existing txs
* @param tx_map maps tx hashes to txs
* @param block_map maps block heights to blocks
*/
void merge_tx(const std::shared_ptr<monero_tx_wallet>& tx, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map);
/**
* Returns true iff tx1's height is known to be less than tx2's height for sorting.
*/
bool tx_height_less_than(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2);
/**
* Returns true iff transfer1 is ordered before transfer2 by ascending account and subaddress indices.
*/
bool incoming_transfer_before(const std::shared_ptr<monero_incoming_transfer>& transfer1, const std::shared_ptr<monero_incoming_transfer>& transfer2);
/**
* Returns true iff wallet vout1 is ordered before vout2 by ascending account and subaddress indices then index.
*/
bool vout_before(const std::shared_ptr<monero_output>& o1, const std::shared_ptr<monero_output>& o2);
// ----------------------------- GATHER BLOCKS ------------------------------
static std::vector<std::shared_ptr<monero_block>> get_blocks_from_txs(std::vector<std::shared_ptr<monero_tx_wallet>> txs) {
std::shared_ptr<monero_block> unconfirmed_block = nullptr; // placeholder for unconfirmed txs
std::vector<std::shared_ptr<monero_block>> blocks;
std::unordered_set<std::shared_ptr<monero_block>> seen_block_ptrs;
for (const std::shared_ptr<monero_tx_wallet>& tx : txs) {
if (tx->m_block == nullptr) {
if (unconfirmed_block == nullptr) unconfirmed_block = std::make_shared<monero_block>();
tx->m_block = unconfirmed_block;
unconfirmed_block->m_txs.push_back(tx);
}
std::unordered_set<std::shared_ptr<monero_block>>::const_iterator got = seen_block_ptrs.find(tx->m_block);
if (got == seen_block_ptrs.end()) {
seen_block_ptrs.insert(tx->m_block);
blocks.push_back(tx->m_block);
}
}
return blocks;
}
static std::vector<std::shared_ptr<monero_block>> get_blocks_from_transfers(std::vector<std::shared_ptr<monero_transfer>> transfers) {
std::shared_ptr<monero_block> unconfirmed_block = nullptr; // placeholder for unconfirmed txs in return json
std::vector<std::shared_ptr<monero_block>> blocks;
std::unordered_set<std::shared_ptr<monero_block>> seen_block_ptrs;
for (auto const& transfer : transfers) {
std::shared_ptr<monero_tx_wallet> tx = transfer->m_tx;
if (tx->m_block == nullptr) {
if (unconfirmed_block == nullptr) unconfirmed_block = std::make_shared<monero_block>();
tx->m_block = unconfirmed_block;
unconfirmed_block->m_txs.push_back(tx);
}
std::unordered_set<std::shared_ptr<monero_block>>::const_iterator got = seen_block_ptrs.find(tx->m_block);
if (got == seen_block_ptrs.end()) {
seen_block_ptrs.insert(tx->m_block);
blocks.push_back(tx->m_block);
}
}
return blocks;
}
static std::vector<std::shared_ptr<monero_block>> get_blocks_from_outputs(std::vector<std::shared_ptr<monero_output_wallet>> outputs) {
std::vector<std::shared_ptr<monero_block>> blocks;
std::unordered_set<std::shared_ptr<monero_block>> seen_block_ptrs;
for (auto const& output : outputs) {
std::shared_ptr<monero_tx_wallet> tx = std::static_pointer_cast<monero_tx_wallet>(output->m_tx);
if (tx->m_block == nullptr) throw std::runtime_error("Need to handle unconfirmed output");
std::unordered_set<std::shared_ptr<monero_block>>::const_iterator got = seen_block_ptrs.find(tx->m_block);
if (got == seen_block_ptrs.end()) {
seen_block_ptrs.insert(tx->m_block);
blocks.push_back(tx->m_block);
}
}
return blocks;
}
// ------------------------------ FREE MEMORY -------------------------------
static void free(std::shared_ptr<monero_block> block) {
for (std::shared_ptr<monero_tx>& tx : block->m_txs) {
tx->m_block.reset();
monero_tx_wallet* tx_wallet = dynamic_cast<monero_tx_wallet*>(tx.get());
if (tx_wallet != nullptr) {
if (tx_wallet->m_tx_set != nullptr) tx_wallet->m_tx_set.reset();
if (tx_wallet->m_outgoing_transfer != nullptr) tx_wallet->m_outgoing_transfer->m_tx.reset();
for (std::shared_ptr<monero_transfer> transfer : tx_wallet->m_incoming_transfers) transfer->m_tx.reset();
for (std::shared_ptr<monero_output> output : tx_wallet->m_outputs) output->m_tx.reset();
for (std::shared_ptr<monero_output> input : tx_wallet->m_inputs) {
input->m_key_image.reset();
input->m_tx.reset();
}
}
monero_tx_query* tx_query = dynamic_cast<monero_tx_query*>(tx.get());
if (tx_query != nullptr) {
if (tx_query->m_transfer_query != nullptr) {
tx_query->m_transfer_query->m_tx_query.reset();
tx_query->m_transfer_query.reset();
}
if (tx_query->m_output_query != nullptr) {
tx_query->m_output_query->m_tx_query.reset();
tx_query->m_output_query.reset();
}
}
}
block.reset();
}
static void free(std::vector<std::shared_ptr<monero_block>> blocks) {
for (std::shared_ptr<monero_block>& block : blocks) monero_utils::free(block);
}
static void free(std::shared_ptr<monero_tx> tx) {
if (tx->m_block == nullptr) {
std::shared_ptr<monero_block> block = std::make_shared<monero_block>();
tx->m_block = block;
block->m_txs.push_back(tx);
}
monero_utils::free(tx->m_block);
}
static void free(std::vector<std::shared_ptr<monero_tx_wallet>> txs) {
return monero_utils::free(monero_utils::get_blocks_from_txs(txs));
}
static void free(std::vector<std::shared_ptr<monero_transfer>> transfers) {
return monero_utils::free(monero_utils::get_blocks_from_transfers(transfers));
}
static void free(std::vector<std::shared_ptr<monero_output_wallet>> outputs) {
return monero_utils::free(monero_utils::get_blocks_from_outputs(outputs));
}
}
#endif /* monero_utils_h */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,311 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <boost/optional.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/condition_variable.hpp>
#include <atomic>
#include "monero_wallet.h"
#include "wallet/wallet2.h"
/**
* Implements a monero_wallet.h by wrapping monero-project's wallet2.
*/
namespace monero {
// -------------------------------- LISTENERS -------------------------------
// forward declaration of internal wallet2 listener
struct wallet2_listener;
// --------------------------- STATIC WALLET UTILS --------------------------
/**
* Monero wallet implementation which uses monero-project's wallet2.
*/
class monero_wallet_full : public monero_wallet {
public:
/**
* Indicates if a wallet exists at the given path.
*
* @param path is the path to check for a wallet
* @return true if a wallet exists at the given path, false otherwise
*/
static bool wallet_exists(const std::string& path);
/**
* Open an existing wallet from disk.
*
* @param path is the path to the wallet file to open
* @param password is the password of the wallet file to open
* @param network_type is the wallet's network type
* @param regtest enable regtest
* @return a pointer to the wallet instance
*/
static monero_wallet_full* open_wallet(const std::string& path, const std::string& password, const monero_network_type network_type, bool regtest = false);
/**
* Open an in-memory wallet from existing data buffers.
*
* @param password is the password of the wallet file to open
* @param network_type is the wallet's network type
* @param keys_data contains the contents of the ".keys" file
* @param cache_data contains the contents of the wallet cache file (no extension)
* @param daemon_connection is connection information to a daemon (default = an unconnected wallet)
* @param http_client_factory allows use of custom http clients
* @param regtest enable regtest
* @return a pointer to the wallet instance
*/
static monero_wallet_full* open_wallet_data(const std::string& password, const monero_network_type, const std::string& keys_data, const std::string& cache_data, const std::shared_ptr<monero_rpc_connection>& daemon_connection = std::make_shared<monero_rpc_connection>(), std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory = nullptr, bool regtest = false);
/**
* Create a new wallet with the given configuration.
*
* @param config is the wallet configuration
* @param http_client_factory allows use of custom http clients
* @return a pointer to the wallet instance
*/
static monero_wallet_full* create_wallet(const monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory = nullptr);
/**
* Get a list of available languages for the wallet's seed.
*
* @return the available languages for the wallet's seed
*/
static std::vector<std::string> get_seed_languages();
// ----------------------------- WALLET METHODS -----------------------------
/**
* Destruct the wallet.
*/
~monero_wallet_full();
/**
* Supported wallet methods.
*/
bool is_view_only() const override { return m_w2->watch_only(); }
void set_daemon_connection(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const boost::optional<bool>& is_trusted = boost::none) override;
void set_daemon_connection(const std::shared_ptr<monero_rpc_connection>& connection, const boost::optional<bool>& is_trusted = boost::none) override;
std::shared_ptr<monero_rpc_connection> get_daemon_connection() const override;
bool is_connected_to_daemon() const override;
bool is_daemon_synced() const override;
bool is_daemon_trusted() const override;
bool is_synced() const override;
monero_version get_version() const override;
std::string get_path() const override;
monero_network_type get_network_type() const override;
std::string get_seed() const override;
std::string get_seed_language() const override;
std::string get_public_view_key() const override;
std::string get_private_view_key() const override;
std::string get_public_spend_key() const override;
std::string get_private_spend_key() const override;
std::string get_address(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_subaddress get_address_index(const std::string& address) const override;
monero_integrated_address get_integrated_address(const std::string& standard_address = "", const std::string& payment_id = "") const override;
monero_integrated_address decode_integrated_address(const std::string& integrated_address) const override;
uint64_t get_height() const override;
uint64_t get_restore_height() const override;
void set_restore_height(uint64_t restore_height) override;
uint64_t get_daemon_height() const override;
uint64_t get_daemon_max_peer_height() const override;
uint64_t get_height_by_date(uint16_t year, uint8_t month, uint8_t day) const override;
void add_listener(monero_wallet_listener& listener) override;
void remove_listener(monero_wallet_listener& listener) override;
std::set<monero_wallet_listener*> get_listeners() override;
monero_sync_result sync() override;
monero_sync_result sync(monero_wallet_listener& listener) override;
monero_sync_result sync(uint64_t start_height) override;
monero_sync_result sync(uint64_t start_height, monero_wallet_listener& listener) override;
void start_syncing(uint64_t sync_period_in_ms) override;
void stop_syncing() override;
void scan_txs(const std::vector<std::string>& tx_hashes) override;
void rescan_spent() override;
void rescan_blockchain() override;
uint64_t get_balance() const override;
uint64_t get_balance(uint32_t account_idx) const override;
uint64_t get_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
uint64_t get_unlocked_balance() const override;
uint64_t get_unlocked_balance(uint32_t account_idx) const override;
uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
std::vector<monero_account> get_accounts(bool include_subaddresses, const std::string& tag) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override;
monero_account create_account(const std::string& label = "") override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const override;
monero_subaddress create_subaddress(uint32_t account_idx, const std::string& label = "") override;
void set_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label = "") override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs(const monero_tx_query& query) const override;
std::vector<std::shared_ptr<monero_transfer>> get_transfers(const monero_transfer_query& query) const override;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs(const monero_output_query& query) const override;
std::string export_outputs(bool all = false) const override;
int import_outputs(const std::string& outputs_hex) override;
std::shared_ptr<monero_key_image_export_result> export_key_images(bool all = false) const override;
std::shared_ptr<monero_key_image_import_result> import_key_images(const std::vector<std::shared_ptr<monero_key_image>>& key_images, uint64_t offset = 0) override;
void freeze_output(const std::string& key_image) override;
void thaw_output(const std::string& key_image) override;
bool is_output_frozen(const std::string& key_image) override;
monero_tx_priority get_default_fee_priority() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> create_txs(const monero_tx_config& config) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_unlocked(const monero_tx_config& config) override;
std::shared_ptr<monero_tx_wallet> sweep_output(const monero_tx_config& config) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_dust(bool relay = false) override;
std::vector<std::string> relay_txs(const std::vector<std::string>& tx_metadatas) override;
monero_tx_set describe_tx_set(const monero_tx_set& tx_set) override;
monero_tx_set sign_txs(const std::string& unsigned_tx_hex) override;
std::vector<std::string> submit_txs(const std::string& signed_tx_hex) override;
std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override;
monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override;
std::string get_tx_key(const std::string& tx_hash) const override;
std::shared_ptr<monero_check_tx> check_tx_key(const std::string& tx_hash, const std::string& txKey, const std::string& address) const override;
std::string get_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message) const override;
std::shared_ptr<monero_check_tx> check_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_spend_proof(const std::string& tx_hash, const std::string& message) const override;
bool check_spend_proof(const std::string& tx_hash, const std::string& message, const std::string& signature) const override;
std::string get_reserve_proof_wallet(const std::string& message) const override;
std::string get_reserve_proof_account(uint32_t account_idx, uint64_t amount, const std::string& message) const override;
std::shared_ptr<monero_check_reserve> check_reserve_proof(const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_tx_note(const std::string& tx_hash) const override;
std::vector<std::string> get_tx_notes(const std::vector<std::string>& tx_hashes) const override;
void set_tx_note(const std::string& tx_hash, const std::string& note) override;
void set_tx_notes(const std::vector<std::string>& tx_hashes, const std::vector<std::string>& notes) override;
std::vector<monero_address_book_entry> get_address_book_entries(const std::vector<uint64_t>& indices) const override;
uint64_t add_address_book_entry(const std::string& address, const std::string& description) override;
void edit_address_book_entry(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description) override;
void delete_address_book_entry(uint64_t index) override;
std::string get_payment_uri(const monero_tx_config& config) const override;
std::shared_ptr<monero_tx_config> parse_payment_uri(const std::string& uri) const override;
bool get_attribute(const std::string& key, std::string& value) const override;
void set_attribute(const std::string& key, const std::string& val) override;
void start_mining(boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) override;
void stop_mining() override;
uint64_t wait_for_next_block() override;
bool is_multisig_import_needed() const override;
monero_multisig_info get_multisig_info() const override;
std::string prepare_multisig() override;
std::string make_multisig(const std::vector<std::string>& multisig_hexes, int threshold, const std::string& password) override;
monero_multisig_init_result exchange_multisig_keys(const std::vector<std::string>& multisig_hexes, const std::string& password) override;
std::string export_multisig_hex() override;
int import_multisig_hex(const std::vector<std::string>& multisig_hexes, const bool refresh_after_import = true) override;
monero_multisig_sign_result sign_multisig_tx_hex(const std::string& multisig_tx_hex) override;
std::vector<std::string> submit_multisig_tx_hex(const std::string& signed_multisig_tx_hex) override;
void change_password(const std::string& old_password, const std::string& new_password) override;
void move_to(const std::string& path, const std::string& password) override;
void save() override;
void close(bool save = false) override;
bool is_closed() const override { return m_is_closed; }
/**
* Wallet import and export using buffers and not the file system.
*/
std::string get_keys_file_buffer(const epee::wipeable_string& password, bool view_only) const;
std::string get_cache_file_buffer() const;
// --------------------------------- PROTECTED --------------------------------
protected:
std::unique_ptr<tools::wallet2> m_w2; // internal wallet implementation
void init_common();
// ---------------------------------- PRIVATE ---------------------------------
private:
friend struct wallet2_listener;
std::unique_ptr<wallet2_listener> m_w2_listener; // internal wallet implementation listener
std::set<monero_wallet_listener*> m_listeners; // external wallet listeners
std::atomic<bool> m_is_closed;
static monero_wallet_full* create_wallet_from_seed(monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory);
static monero_wallet_full* create_wallet_from_keys(monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory);
static monero_wallet_full* create_wallet_random(monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory);
std::vector<monero_subaddress> get_subaddresses_aux(uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices, const std::vector<tools::wallet2::transfer_details>& transfers) const;
std::vector<std::shared_ptr<monero_transfer>> get_transfers_aux(const monero_transfer_query& query) const;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_aux(const monero_output_query& query) const;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_account(const monero_tx_config& config); // sweeps unlocked funds within an account; private helper to sweep_unlocked()
void assert_not_closed() const;
// serializes a wallet operation with background sync: pauses the sync loop, interrupts background refresh, and locks m_sync_mutex
struct sync_op_lock {
sync_op_lock(const monero_wallet_full& wallet);
sync_op_lock(const sync_op_lock&) = delete;
~sync_op_lock();
const monero_wallet_full& m_wallet;
};
// blockchain sync management
mutable std::atomic<bool> m_is_synced; // whether or not wallet is synced
mutable std::atomic<bool> m_is_connected; // cache connection status to avoid unecessary RPC calls
boost::condition_variable m_sync_cv; // to make sync threads woke
mutable boost::mutex m_sync_mutex; // synchronize sync and other wallet operations
mutable std::atomic<uint32_t> m_num_sync_pauses; // number of operations pausing background sync
std::atomic<bool> m_background_syncing; // whether or not a background sync pass is in progress
std::atomic<bool> m_rescan_on_sync; // whether or not to rescan on sync
std::atomic<bool> m_syncing_enabled; // whether or not auto sync is enabled
std::atomic<bool> m_sync_loop_running; // whether or not the syncing thread is shut down
std::atomic<int> m_syncing_interval; // auto sync loop interval in milliseconds
boost::thread m_syncing_thread; // thread for auto sync loop
boost::mutex m_syncing_mutex; // synchronize auto sync loop
void run_sync_loop(); // run the sync loop in a thread
monero_sync_result lock_and_sync(boost::optional<uint64_t> start_height = boost::none, bool background = false); // internal function to synchronize request to sync and rescan
monero_sync_result sync_aux(boost::optional<uint64_t> start_height = boost::none); // internal function to immediately block, sync, and report progress
};
}
@@ -0,0 +1,320 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_wallet_keys.h"
#include "utils/monero_utils.h"
#include <chrono>
#include <iostream>
#include "mnemonics/electrum-words.h"
#include "mnemonics/english.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/cryptonote_basic_impl.h"
#include "string_tools.h"
#include "device/device.hpp"
using namespace epee;
using namespace tools;
using namespace crypto;
/**
* Public library interface.
*/
namespace monero {
// ---------------------------- WALLET MANAGEMENT ---------------------------
monero_wallet_keys* monero_wallet_keys::create_wallet_random(const monero_wallet_config& config) {
// validate and normalize config
monero_wallet_config config_normalized = config.copy();
if (config_normalized.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type");
if (config_normalized.m_language == boost::none || config_normalized.m_language.get().empty()) config_normalized.m_language = "English";
if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get());
// initialize random wallet account
monero_wallet_keys* wallet = new monero_wallet_keys();
crypto::secret_key spend_key_sk = wallet->m_account.generate();
// initialize remaining wallet
wallet->m_network_type = config_normalized.m_network_type.get();
wallet->m_language = config_normalized.m_language.get();
epee::wipeable_string wipeable_mnemonic;
if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) {
throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language));
}
wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size());
wallet->init_common();
return wallet;
}
monero_wallet_keys* monero_wallet_keys::create_wallet_from_seed(const monero_wallet_config& config) {
// validate config
if (config.m_is_multisig != boost::none && config.m_is_multisig.get()) throw std::runtime_error("Restoring from multisig seed not supported");
if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type");
if (config.m_seed == boost::none || config.m_seed.get().empty()) throw std::runtime_error("Must provide wallet seed");
// validate mnemonic and get recovery key and language
crypto::secret_key spend_key_sk;
std::string language;
bool is_valid = crypto::ElectrumWords::words_to_bytes(config.m_seed.get(), spend_key_sk, language);
if (!is_valid) throw std::runtime_error("Invalid mnemonic");
if (language == crypto::ElectrumWords::old_language_name) language = Language::English().get_language_name();
// apply offset if given
if (config.m_seed_offset != boost::none && !config.m_seed_offset.get().empty()) spend_key_sk = cryptonote::decrypt_key(spend_key_sk, config.m_seed_offset.get());
// initialize wallet account
monero_wallet_keys* wallet = new monero_wallet_keys();
wallet->m_account = cryptonote::account_base{};
wallet->m_account.generate(spend_key_sk, true, false);
// initialize remaining wallet
wallet->m_network_type = config.m_network_type.get();
wallet->m_language = language;
epee::wipeable_string wipeable_mnemonic;
if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) {
throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language));
}
wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size());
wallet->init_common();
return wallet;
}
monero_wallet_keys* monero_wallet_keys::create_wallet_from_keys(const monero_wallet_config& config) {
// validate and normalize config
monero_wallet_config config_normalized = config.copy();
if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type");
if (config.m_language == boost::none || config_normalized.m_language.get().empty()) config_normalized.m_language = "English";
if (config.m_private_spend_key == boost::none) config_normalized.m_private_spend_key = std::string("");
if (config.m_private_view_key == boost::none) config_normalized.m_private_view_key = std::string("");
if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get());
// parse and validate private spend key
crypto::secret_key spend_key_sk;
bool has_spend_key = false;
if (!config_normalized.m_private_spend_key.get().empty()) {
cryptonote::blobdata spend_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(config.m_private_spend_key.get(), spend_key_data) || spend_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("failed to parse secret spend key");
}
has_spend_key = true;
spend_key_sk = *reinterpret_cast<const crypto::secret_key*>(spend_key_data.data());
}
// parse and validate private view key
bool has_view_key = true;
crypto::secret_key view_key_sk;
if (config_normalized.m_private_view_key.get().empty()) {
if (has_spend_key) has_view_key = false;
else throw std::runtime_error("Neither spend key nor view key supplied");
}
if (has_view_key) {
cryptonote::blobdata view_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(config_normalized.m_private_view_key.get(), view_key_data) || view_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("failed to parse secret view key");
}
view_key_sk = *reinterpret_cast<const crypto::secret_key*>(view_key_data.data());
}
// parse and validate address
cryptonote::address_parse_info address_info;
if (config_normalized.m_primary_address.get().empty()) {
if (has_view_key) throw std::runtime_error("must provide address if providing private view key");
} else {
if (!get_account_address_from_str(address_info, static_cast<cryptonote::network_type>(config_normalized.m_network_type.get()), config_normalized.m_primary_address.get())) throw std::runtime_error("failed to parse address");
// check the spend and view keys match the given address
crypto::public_key pkey;
if (has_spend_key) {
if (!crypto::secret_key_to_public_key(spend_key_sk, pkey)) throw std::runtime_error("failed to verify secret spend key");
if (address_info.address.m_spend_public_key != pkey) throw std::runtime_error("spend key does not match address");
}
if (has_view_key) {
if (!crypto::secret_key_to_public_key(view_key_sk, pkey)) throw std::runtime_error("failed to verify secret view key");
if (address_info.address.m_view_public_key != pkey) throw std::runtime_error("view key does not match address");
}
}
// initialize wallet account
monero_wallet_keys* wallet = new monero_wallet_keys();
if (has_spend_key && has_view_key) {
wallet->m_account.create_from_keys(address_info.address, spend_key_sk, view_key_sk);
} else if (has_spend_key) {
wallet->m_account.generate(spend_key_sk, true, false);
} else {
wallet->m_account.create_from_viewkey(address_info.address, view_key_sk);
}
// initialize remaining wallet
wallet->m_is_view_only = !has_spend_key;
wallet->m_network_type = config_normalized.m_network_type.get();
if (!config_normalized.m_private_spend_key.get().empty()) {
wallet->m_language = config_normalized.m_language.get();
epee::wipeable_string wipeable_mnemonic;
if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) {
throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language));
}
wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size());
}
wallet->init_common();
return wallet;
}
std::vector<std::string> monero_wallet_keys::get_seed_languages() {
std::vector<std::string> languages;
crypto::ElectrumWords::get_language_list(languages, true); // TODO: support getting names in language
return languages;
}
// ----------------------------- WALLET METHODS -----------------------------
monero_wallet_keys::~monero_wallet_keys() {
MTRACE("~monero_wallet_keys()");
close();
}
monero_version monero_wallet_keys::get_version() const {
monero_version version;
version.m_number = 65552; // same as monero-wallet-rpc v0.15.0.1 release
version.m_is_release = false; // TODO: could pull from MONERO_VERSION_IS_RELEASE in version.cpp
return version;
}
std::string monero_wallet_keys::get_address(uint32_t account_idx, uint32_t subaddress_idx) const {
hw::device &hwdev = m_account.get_device();
cryptonote::subaddress_index index{account_idx, subaddress_idx};
cryptonote::account_public_address address = hwdev.get_subaddress(m_account.get_keys(), index);
return cryptonote::get_account_address_as_str(static_cast<cryptonote::network_type>(m_network_type), !index.is_zero(), address);
}
monero_integrated_address monero_wallet_keys::get_integrated_address(const std::string& standard_address, const std::string& payment_id) const {
MTRACE("monero_wallet_keys::get_integrated_address()");
throw std::runtime_error("monero_wallet_keys::get_integrated_address() not implemented");
}
monero_integrated_address monero_wallet_keys::decode_integrated_address(const std::string& integrated_address) const {
MTRACE("monero_wallet_keys::decode_integrated_address()");
throw std::runtime_error("monero_wallet_keys::decode_integrated_address() not implemented");
}
monero_account monero_wallet_keys::get_account(uint32_t account_idx, bool include_subaddresses) const {
MTRACE("monero_wallet_keys::get_account()");
if (include_subaddresses) {
std::string err = "monero_wallet_keys::get_account(account_idx, include_subaddresses) include_subaddresses must be false";
MERROR(err);
throw std::runtime_error(err);
}
// build and return account
monero_account account;
account.m_index = account_idx;
account.m_primary_address = get_address(account_idx, 0);
return account;
}
std::vector<monero_subaddress> monero_wallet_keys::get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const {
// must provide subaddress indices
if (subaddress_indices.empty()) {
std::string err = "Keys-only wallet does not have enumerable set of subaddresses; specific specific subaddresses";
MERROR(err);
throw std::runtime_error(err);
}
// initialize subaddresses at indices
std::vector<monero_subaddress> subaddresses;
for (uint32_t subaddressIndicesIdx = 0; subaddressIndicesIdx < subaddress_indices.size(); subaddressIndicesIdx++) {
monero_subaddress subaddress;
subaddress.m_account_index = account_idx;
uint32_t subaddress_idx = subaddress_indices.at(subaddressIndicesIdx);
subaddress.m_index = subaddress_idx;
subaddress.m_address = get_address(account_idx, subaddress_idx);
subaddresses.push_back(subaddress);
}
return subaddresses;
}
std::string monero_wallet_keys::sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx, uint32_t subaddress_idx) const {
MTRACE("monero_wallet_keys::sign_message()");
throw std::runtime_error("monero_wallet_keys::sign_message() not implemented");
}
monero_message_signature_result monero_wallet_keys::verify_message(const std::string& msg, const std::string& address, const std::string& signature) const {
MTRACE("monero_wallet_keys::verify_message()");
throw std::runtime_error("monero_wallet_keys::verify_message() not implemented");
}
void monero_wallet_keys::close(bool save) {
if (save) throw std::runtime_error("MoneroWalletKeys does not support saving");
// no pointers to destroy
}
// ------------------------------- PRIVATE HELPERS ----------------------------
void monero_wallet_keys::init_common() {
m_primary_address = m_account.get_public_address_str(static_cast<cryptonote::network_type>(m_network_type));
const cryptonote::account_keys& keys = m_account.get_keys();
m_pub_view_key = epee::string_tools::pod_to_hex(keys.m_account_address.m_view_public_key);
m_prv_view_key = epee::string_tools::pod_to_hex(unwrap(unwrap(keys.m_view_secret_key)));
m_pub_spend_key = epee::string_tools::pod_to_hex(keys.m_account_address.m_spend_public_key);
m_prv_spend_key = epee::string_tools::pod_to_hex(unwrap(unwrap(keys.m_spend_secret_key)));
if (m_prv_spend_key == "0000000000000000000000000000000000000000000000000000000000000000") m_prv_spend_key = "";
}
}
@@ -0,0 +1,147 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "monero_wallet.h"
#include "cryptonote_basic/account.h"
using namespace monero;
/**
* Public library interface.
*/
namespace monero {
/**
* Implements a Monero wallet to provide basic key management.
*/
class monero_wallet_keys : public monero_wallet {
public:
// --------------------------- STATIC WALLET UTILS --------------------------
/**
* Create a new wallet with a randomly generated seed.
*
* @param config is the wallet configuration (network type and language)
*/
static monero_wallet_keys* create_wallet_random(const monero_wallet_config& config);
/**
* Create a wallet from an existing mnemonic phrase or seed.
*
* @param config is the wallet configuration (network type, seed, seed offset, isMultisig)
*/
static monero_wallet_keys* create_wallet_from_seed(const monero_wallet_config& config);
/**
* Create a wallet from an address, view key, and spend key.
*
* @param config is the wallet configuration (network type, address, view key, spend key, language)
*/
static monero_wallet_keys* create_wallet_from_keys(const monero_wallet_config& config);
/**
* Get a list of available languages for the wallet's seed.
*
* @return the available languages for the wallet's seed
*/
static std::vector<std::string> get_seed_languages();
// ----------------------------- WALLET METHODS -----------------------------
/**
* Destruct the wallet.
*/
~monero_wallet_keys();
/**
* Supported wallet methods.
*/
bool is_view_only() const override { return m_is_view_only; }
monero_version get_version() const override;
monero_network_type get_network_type() const override { return m_network_type; }
std::string get_seed() const override { return m_seed; }
std::string get_seed_language() const override { return m_language; }
std::string get_private_view_key() const override { return m_prv_view_key; }
std::string get_private_spend_key() const override { return m_prv_spend_key; }
std::string get_public_view_key() const override { return m_pub_view_key; }
std::string get_public_spend_key() const override { return m_pub_spend_key; }
std::string get_primary_address() const override { return m_primary_address; }
std::string get_address(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_integrated_address get_integrated_address(const std::string& standard_address = "", const std::string& payment_id = "") const override;
monero_integrated_address decode_integrated_address(const std::string& integrated_address) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const override;
std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override;
monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override;
void close(bool save = false) override;
// --------------------------------- PRIVATE --------------------------------
private:
bool m_is_view_only;
monero_network_type m_network_type;
cryptonote::account_base m_account;
std::string m_seed;
std::string m_language;
std::string m_pub_view_key;
std::string m_prv_view_key;
std::string m_pub_spend_key;
std::string m_prv_spend_key;
std::string m_primary_address;
void init_common();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,609 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "common/monero_rpc_connection.h"
#include "daemon/monero_daemon_model.h"
using namespace monero;
/**
* Public library interface.
*/
namespace monero {
/**
* Configures a wallet to create.
*/
struct monero_wallet_config : public serializable_struct {
boost::optional<std::string> m_path;
boost::optional<std::string> m_password;
boost::optional<monero_network_type> m_network_type;
std::shared_ptr<monero_rpc_connection> m_server;
boost::optional<bool> m_is_trusted_daemon;
boost::optional<std::string> m_seed;
boost::optional<std::string> m_seed_offset;
boost::optional<std::string> m_primary_address;
boost::optional<std::string> m_private_view_key;
boost::optional<std::string> m_private_spend_key;
boost::optional<uint64_t> m_restore_height;
boost::optional<std::string> m_language;
boost::optional<bool> m_save_current;
boost::optional<uint64_t> m_account_lookahead;
boost::optional<uint64_t> m_subaddress_lookahead;
boost::optional<bool> m_is_multisig;
boost::optional<bool> m_regtest;
monero_wallet_config() {}
monero_wallet_config(const monero_wallet_config& config);
monero_wallet_config copy() const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static std::shared_ptr<monero_wallet_config> deserialize(const std::string& config_json);
};
/**
* Models a result of syncing a wallet.
*/
struct monero_sync_result : public serializable_struct {
uint64_t m_num_blocks_fetched;
bool m_received_money;
monero_sync_result() {}
monero_sync_result(const uint64_t num_blocks_fetched, const bool received_money) : m_num_blocks_fetched(num_blocks_fetched), m_received_money(received_money) {}
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
};
/**
* Models a Monero subaddress.
*/
struct monero_subaddress : public serializable_struct {
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_index;
boost::optional<std::string> m_address;
boost::optional<std::string> m_label;
boost::optional<uint64_t> m_balance;
boost::optional<uint64_t> m_unlocked_balance;
boost::optional<uint64_t> m_num_unspent_outputs;
boost::optional<bool> m_is_used;
boost::optional<uint64_t> m_num_blocks_to_unlock;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_subaddress>& subaddress);
};
/**
* Models a Monero account.
*/
struct monero_account : public serializable_struct {
boost::optional<uint32_t> m_index;
boost::optional<std::string> m_primary_address;
boost::optional<uint64_t> m_balance;
boost::optional<uint64_t> m_unlocked_balance;
boost::optional<std::string> m_tag;
std::vector<monero_subaddress> m_subaddresses;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_account>& account);
};
/**
* Models an outgoing transfer destination.
*/
struct monero_destination : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_amount;
monero_destination(boost::optional<std::string> address = boost::none, boost::optional<uint64_t> amount = boost::none) : m_address(address), m_amount(amount) {}
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_destination>& destination);
std::shared_ptr<monero_destination> copy(const std::shared_ptr<monero_destination>& src, const std::shared_ptr<monero_destination>& tgt) const;
};
// forward declarations
struct monero_tx_config;
struct monero_tx_wallet;
struct monero_tx_query;
struct monero_tx_set;
/**
* Models a base transfer of funds to or from the wallet.
*
* TODO: m_is_incoming for api consistency
*/
struct monero_transfer : public serializable_struct {
std::shared_ptr<monero_tx_wallet> m_tx;
boost::optional<uint64_t> m_amount;
boost::optional<uint32_t> m_account_index;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_transfer>& transfer);
virtual boost::optional<bool> is_incoming() const = 0; // derived class must implement
std::shared_ptr<monero_transfer> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
boost::optional<bool> is_outgoing() const {
if (is_incoming() == boost::none) return boost::none;
return !(*is_incoming());
}
void merge(const std::shared_ptr<monero_transfer>& self, const std::shared_ptr<monero_transfer>& other);
};
/**
* Models an incoming transfer of funds to the wallet.
*/
struct monero_incoming_transfer : public monero_transfer {
boost::optional<uint32_t> m_subaddress_index;
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_num_suggested_confirmations;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_incoming_transfer> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
std::shared_ptr<monero_incoming_transfer> copy(const std::shared_ptr<monero_incoming_transfer>& src, const std::shared_ptr<monero_incoming_transfer>& tgt) const;
boost::optional<bool> is_incoming() const;
void merge(const std::shared_ptr<monero_transfer>& self, const std::shared_ptr<monero_transfer>& other);
void merge(const std::shared_ptr<monero_incoming_transfer>& self, const std::shared_ptr<monero_incoming_transfer>& other);
};
/**
* Models an outgoing transfer of funds from the wallet.
*/
struct monero_outgoing_transfer : public monero_transfer {
std::vector<uint32_t> m_subaddress_indices;
std::vector<std::string> m_addresses;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_outgoing_transfer> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
std::shared_ptr<monero_outgoing_transfer> copy(const std::shared_ptr<monero_outgoing_transfer>& src, const std::shared_ptr<monero_outgoing_transfer>& tgt) const;
boost::optional<bool> is_incoming() const;
void merge(const std::shared_ptr<monero_transfer>& self, const std::shared_ptr<monero_transfer>& other);
void merge(const std::shared_ptr<monero_outgoing_transfer>& self, const std::shared_ptr<monero_outgoing_transfer>& other);
};
/**
* Configures a query to retrieve transfers.
*
* All transfers are returned except those that do not meet the criteria defined in this query.
*/
struct monero_transfer_query : public monero_transfer {
boost::optional<bool> m_is_incoming;
boost::optional<std::string> m_address;
std::vector<std::string> m_addresses;
boost::optional<uint32_t> m_subaddress_index;
std::vector<uint32_t> m_subaddress_indices;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
boost::optional<bool> m_has_destinations;
std::shared_ptr<monero_tx_query> m_tx_query;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_transfer_query>& transfer_query);
static std::shared_ptr<monero_transfer_query> deserialize_from_block(const std::string& transfer_query_json);
static bool is_contextual(const monero_transfer_query& query);
std::shared_ptr<monero_transfer_query> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
std::shared_ptr<monero_transfer_query> copy(const std::shared_ptr<monero_transfer_query>& src, const std::shared_ptr<monero_transfer_query>& tgt) const;
boost::optional<bool> is_incoming() const;
bool meets_criteria(monero_transfer* transfer, bool query_parent = true) const;
};
/**
* Models a Monero output with wallet extensions.
*/
struct monero_output_wallet : public monero_output {
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_subaddress_index;
boost::optional<bool> m_is_spent;
boost::optional<bool> m_is_frozen;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_wallet>& output_wallet);
std::shared_ptr<monero_output_wallet> copy(const std::shared_ptr<monero_output>& src, const std::shared_ptr<monero_output>& tgt) const;
std::shared_ptr<monero_output_wallet> copy(const std::shared_ptr<monero_output_wallet>& src, const std::shared_ptr<monero_output_wallet>& tgt) const;
void merge(const std::shared_ptr<monero_output>& self, const std::shared_ptr<monero_output>& other);
void merge(const std::shared_ptr<monero_output_wallet>& self, const std::shared_ptr<monero_output_wallet>& other);
};
/**
* Configures a query to retrieve wallet outputs (i.e. outputs that the wallet has or had the
* ability to spend).
*
* All outputs are returned except those that do not meet the criteria defined in this query.
*/
struct monero_output_query : public monero_output_wallet {
std::vector<uint32_t> m_subaddress_indices;
boost::optional<uint64_t> m_min_amount;
boost::optional<uint64_t> m_max_amount;
std::shared_ptr<monero_tx_query> m_tx_query;
//boost::property_tree::ptree to_property_tree() const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_query>& output_query);
static std::shared_ptr<monero_output_query> deserialize_from_block(const std::string& output_query_json);
static bool is_contextual(const monero_output_query& query);
std::shared_ptr<monero_output_query> copy(const std::shared_ptr<monero_output>& src, const std::shared_ptr<monero_output>& tgt) const;
std::shared_ptr<monero_output_query> copy(const std::shared_ptr<monero_output_wallet>& src, const std::shared_ptr<monero_output_wallet>& tgt) const; // TODO: necessary to override all super classes?
std::shared_ptr<monero_output_query> copy(const std::shared_ptr<monero_output_query>& src, const std::shared_ptr<monero_output_query>& tgt) const;
bool meets_criteria(monero_output_wallet* output, bool query_parent = true) const;
};
/**
* Models a Monero transaction in the context of a wallet.
*/
struct monero_tx_wallet : public monero_tx {
std::shared_ptr<monero_tx_set> m_tx_set;
boost::optional<bool> m_is_incoming;
boost::optional<bool> m_is_outgoing;
std::vector<std::shared_ptr<monero_incoming_transfer>> m_incoming_transfers;
std::shared_ptr<monero_outgoing_transfer> m_outgoing_transfer;
boost::optional<std::string> m_note;
boost::optional<bool> m_is_locked;
boost::optional<uint64_t> m_input_sum;
boost::optional<uint64_t> m_output_sum;
boost::optional<std::string> m_change_address;
boost::optional<uint64_t> m_change_amount;
boost::optional<uint32_t> m_num_dummy_outputs;
boost::optional<std::string> m_extra_hex;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_wallet>& tx_wallet);
std::shared_ptr<monero_tx_wallet> copy(const std::shared_ptr<monero_tx>& src, const std::shared_ptr<monero_tx>& tgt) const;
std::shared_ptr<monero_tx_wallet> copy(const std::shared_ptr<monero_tx_wallet>& src, const std::shared_ptr<monero_tx_wallet>& tgt) const;
void merge(const std::shared_ptr<monero_tx>& self, const std::shared_ptr<monero_tx>& other);
void merge(const std::shared_ptr<monero_tx_wallet>& self, const std::shared_ptr<monero_tx_wallet>& other);
std::vector<std::shared_ptr<monero_transfer>> get_transfers() const;
std::vector<std::shared_ptr<monero_transfer>> get_transfers(const monero_transfer_query& query) const;
std::vector<std::shared_ptr<monero_transfer>> filter_transfers(const monero_transfer_query& query);
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_wallet() const;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_wallet(const monero_output_query& query) const;
std::vector<std::shared_ptr<monero_output_wallet>> filter_outputs_wallet(const monero_output_query& query);
};
/**
* Configures a query to retrieve transactions.
*
* All transactions are returned except those that do not meet the criteria defined in this query.
*/
struct monero_tx_query : public monero_tx_wallet {
boost::optional<bool> m_is_outgoing;
boost::optional<bool> m_is_incoming;
std::vector<std::string> m_hashes;
boost::optional<bool> m_has_payment_id;
std::vector<std::string> m_payment_ids;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_min_height;
boost::optional<uint64_t> m_max_height;
boost::optional<uint64_t> m_include_outputs;
std::shared_ptr<monero_transfer_query> m_transfer_query;
std::shared_ptr<monero_output_query> m_input_query;
std::shared_ptr<monero_output_query> m_output_query;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_query>& tx_query);
static std::shared_ptr<monero_tx_query> deserialize_from_block(const std::string& tx_query_json);
/**
* Remove query criteria which require looking up other transfers/outputs to
* fulfill query.
*
* @param query the query to decontextualize
* @return a reference to the query for convenience
*/
static std::shared_ptr<monero_tx_query> decontextualize(std::shared_ptr<monero_tx_query> query);
std::shared_ptr<monero_tx_query> copy(const std::shared_ptr<monero_tx>& src, const std::shared_ptr<monero_tx>& tgt) const;
std::shared_ptr<monero_tx_query> copy(const std::shared_ptr<monero_tx_wallet>& src, const std::shared_ptr<monero_tx_wallet>& tgt) const; // TODO: necessary to override all super classes?
std::shared_ptr<monero_tx_query> copy(const std::shared_ptr<monero_tx_query>& src, const std::shared_ptr<monero_tx_query>& tgt) const;
bool meets_criteria(monero_tx_wallet* tx, bool query_children = true) const;
};
/**
* Groups transactions who share common hex data which is needed in order to
* sign and submit the transactions.
*
* For example, multisig transactions created from create_txs() share a common
* hex string which is needed in order to sign and submit the multisig
* transactions.
*/
struct monero_tx_set : public serializable_struct {
std::vector<std::shared_ptr<monero_tx_wallet>> m_txs;
boost::optional<std::string> m_signed_tx_hex;
boost::optional<std::string> m_unsigned_tx_hex;
boost::optional<std::string> m_multisig_tx_hex;
//boost::property_tree::ptree to_property_tree() const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static monero_tx_set deserialize(const std::string& tx_set_json);
};
/**
* Monero integrated address model.
*/
struct monero_integrated_address : public serializable_struct {
std::string m_standard_address;
std::string m_payment_id;
std::string m_integrated_address;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_integrated_address>& subaddress);
};
/**
* Enumerates Monero network types.
*/
enum monero_tx_priority : uint8_t {
DEFAULT = 0,
UNIMPORTANT,
NORMAL,
ELEVATED
};
/**
* Configures a transaction to send, sweep, or create a payment URI.
*/
struct monero_tx_config : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_amount;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
std::vector<uint32_t> m_subtract_fee_from;
boost::optional<std::string> m_payment_id;
boost::optional<monero_tx_priority> m_priority;
boost::optional<uint32_t> m_ring_size;
boost::optional<uint64_t> m_fee;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddress_indices;
boost::optional<bool> m_can_split;
boost::optional<bool> m_relay;
boost::optional<std::string> m_note;
boost::optional<std::string> m_recipient_name;
boost::optional<uint64_t> m_below_amount;
boost::optional<bool> m_sweep_each_subaddress;
boost::optional<std::string> m_key_image;
monero_tx_config() {}
monero_tx_config(const monero_tx_config& config);
monero_tx_config copy() const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static std::shared_ptr<monero_tx_config> deserialize(const std::string& config_json);
std::vector<std::shared_ptr<monero_destination>> get_normalized_destinations() const;
};
/**
* Models results from exporting signed key images.
*/
struct monero_key_image_export_result : public serializable_struct {
boost::optional<uint64_t> m_offset;
std::vector<std::shared_ptr<monero_key_image>> m_key_images;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static std::shared_ptr<monero_key_image_export_result> deserialize(const std::string& result_json);
};
/**
* Models results from importing key images.
*/
struct monero_key_image_import_result : public serializable_struct {
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_spent_amount;
boost::optional<uint64_t> m_unspent_amount;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image_import_result>& result);
};
/**
* Enumerates message verification results.
*/
enum monero_message_signature_type : uint8_t {
SIGN_WITH_SPEND_KEY = 0,
SIGN_WITH_VIEW_KEY
};
/**
* Models results from message verification.
*/
struct monero_message_signature_result : public serializable_struct {
bool m_is_good;
uint32_t m_version;
bool m_is_old;
monero_message_signature_type m_signature_type;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_message_signature_result> result);
};
/**
* Base class for results from checking a transaction or reserve proof.
*/
struct monero_check : public serializable_struct {
bool m_is_good;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
};
/**
* Results from checking a transaction key.
*/
struct monero_check_tx : public monero_check {
boost::optional<bool> m_in_tx_pool;
boost::optional<uint64_t> m_num_confirmations;
boost::optional<uint64_t> m_received_amount;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_tx>& check);
};
/**
* Results from checking a reserve proof.
*/
struct monero_check_reserve : public monero_check {
boost::optional<uint64_t> m_total_amount;
boost::optional<uint64_t> m_unconfirmed_spent_amount;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_reserve>& check);
};
/**
* Models information about a multisig wallet.
*/
struct monero_multisig_info : public serializable_struct {
bool m_is_multisig;
bool m_is_ready;
uint32_t m_threshold;
uint32_t m_num_participants;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_info>& info);
};
/**
* Models the result of initializing a multisig wallet which results in the
* multisig wallet's address xor another multisig hex to share with
* participants to create the wallet.
*/
struct monero_multisig_init_result : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<std::string> m_multisig_hex;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_init_result>& res);
};
/**
* Models the result of signing multisig tx hex.
*/
struct monero_multisig_sign_result : public serializable_struct {
boost::optional<std::string> m_signed_multisig_tx_hex;
std::vector<std::string> m_tx_hashes;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_sign_result>& res);
};
/**
* Monero address book entry model.
*/
struct monero_address_book_entry : public serializable_struct {
boost::optional<uint64_t> m_index; // TODO: not boost::optional
boost::optional<std::string> m_address;
boost::optional<std::string> m_description;
boost::optional<std::string> m_payment_id;
monero_address_book_entry() {}
monero_address_book_entry(uint64_t index, const std::string& address, const std::string& description) : m_index(index), m_address(address), m_description(description) {}
monero_address_book_entry(uint64_t index, const std::string& address, const std::string& description, const std::string& payment_id) : m_index(index), m_address(address), m_description(description), m_payment_id(payment_id) {}
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_address_book_entry>& entry);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
};
/**
* Models a Monero address type.
*/
enum monero_address_type : uint8_t {
PRIMARY_ADDRESS = 0,
INTEGRATED_ADDRESS,
SUBADDRESS
};
/**
* Compares two transactions by their height.
*/
struct monero_tx_height_comparator {
bool operator()(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2) const;
};
/**
* Compares two transfers by ascending account and subaddress indices.
*/
struct monero_incoming_transfer_comparator {
bool operator()(const std::shared_ptr<monero_incoming_transfer>& t1, const std::shared_ptr<monero_incoming_transfer>& t2) const;
bool operator()(const monero_incoming_transfer& t1, const monero_incoming_transfer& t2) const;
};
/**
* Compares two outputs by ascending account and subaddress indices.
*/
struct monero_output_comparator {
bool operator()(const monero_output_wallet& o1, const monero_output_wallet& o2) const;
};
/**
* Maintains metadata for a decoded address.
*/
struct monero_decoded_address : public serializable_struct {
std::string m_address;
monero_address_type m_address_type;
monero_network_type m_network_type;
monero_decoded_address(const std::string& address, monero_address_type address_type, monero_network_type network_type);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Represents an account tag.
*/
struct monero_account_tag : public serializable_struct {
boost::optional<std::string> m_tag;
boost::optional<std::string> m_label;
std::vector<uint32_t> m_account_indices;
monero_account_tag() { }
monero_account_tag(const std::string& tag, const std::string& label): m_tag(tag), m_label(label) { }
monero_account_tag(const std::string& tag, const std::string& label, const std::vector<uint32_t>& account_indices): m_tag(tag), m_label(label), m_account_indices(account_indices) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_account_tag>& account_tag);
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "monero_wallet.h"
/**
* Implements a monero_wallet.h by wrapping monero-project's monero-wallet-rpc.
*/
namespace monero {
// forward declaration of internal wallet poller
class monero_wallet_poller;
/**
* Implements a Monero wallet using monero-wallet-rpc.
*/
class monero_wallet_rpc : public monero_wallet {
public:
/**
* Destruct the wallet.
*/
~monero_wallet_rpc();
monero_wallet_rpc(const std::shared_ptr<monero_rpc_connection>& rpc_connection);
monero_wallet_rpc(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", const boost::optional<uint32_t>& timeout = boost::none);
/**
* Set the wallet poll period.
*
* @param period_ms poll period in milliseconds.
*/
void set_poll_period_in_ms(uint64_t period_ms);
/**
* Open an existing wallet from rpc server.
*
* @param config is the wallet configuration
* @return a pointer to the wallet instance
*/
monero_wallet_rpc* open_wallet(const std::shared_ptr<monero_wallet_config> &config);
/**
* Open an existing wallet from rpc server.
*
* @param name is the wallet's name to open
* @param password is the password of the wallet file to open
* @return a pointer to the wallet instance
*/
monero_wallet_rpc* open_wallet(const std::string& name, const std::string& password);
/**
* Create a new wallet with the given configuration.
*
* @param config is the wallet configuration
* @return a pointer to the wallet instance
*/
monero_wallet_rpc* create_wallet(const std::shared_ptr<monero_wallet_config> &config);
/**
* Get the wallet's RPC connection.
*
* @return the wallet's rpc connection
*/
std::shared_ptr<monero_rpc_connection> get_rpc_connection() const { return m_rpc; }
/**
* Get a list of available languages for the wallet's seed.
*
* @return the available languages for the wallet's seed
*/
std::vector<std::string> get_seed_languages() const;
/**
* Save and close the current wallet and stop the RPC server.
*/
void stop();
/**
* Supported wallet methods.
*/
void add_listener(monero_wallet_listener& listener) override;
void remove_listener(monero_wallet_listener& listener) override;
std::set<monero_wallet_listener*> get_listeners() override;
bool is_view_only() const override;
std::shared_ptr<monero_rpc_connection> get_daemon_connection() const override;
void set_daemon_connection(const std::shared_ptr<monero_rpc_connection>& connection, bool is_trusted, const boost::optional<ssl_options>& ssl_options);
void set_daemon_connection(const std::shared_ptr<monero_rpc_connection>& connection, const boost::optional<bool>& is_trusted = boost::none) override;
void set_daemon_connection(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const boost::optional<bool>& is_trusted = boost::none) override;
bool is_connected_to_daemon() const override;
monero_version get_version() const override;
std::string get_path() const override;
std::string get_seed() const override;
std::string get_seed_language() const override;
std::string get_public_view_key() const override;
std::string get_private_view_key() const override;
std::string get_public_spend_key() const override;
std::string get_private_spend_key() const override;
std::string get_address(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_subaddress get_address_index(const std::string& address) const override;
monero_integrated_address get_integrated_address(const std::string& standard_address = "", const std::string& payment_id = "") const override;
monero_integrated_address decode_integrated_address(const std::string& integrated_address) const override;
uint64_t get_height() const override;
uint64_t get_daemon_height() const override;
uint64_t get_height_by_date(uint16_t year, uint8_t month, uint8_t day) const override;
monero_sync_result sync() override;
monero_sync_result sync(monero_wallet_listener& listener) override;
monero_sync_result sync(uint64_t start_height, monero_wallet_listener& listener) override;
monero_sync_result sync(uint64_t start_height) override;
void start_syncing(uint64_t sync_period_in_ms = 10000) override;
void stop_syncing() override;
void scan_txs(const std::vector<std::string>& tx_hashes) override;
void rescan_spent() override;
void rescan_blockchain() override;
uint64_t get_balance() const override;
uint64_t get_balance(uint32_t account_index) const override;
uint64_t get_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
uint64_t get_unlocked_balance() const override;
uint64_t get_unlocked_balance(uint32_t account_index) const override;
uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses, bool skip_balances) const;
std::vector<monero_account> get_accounts(bool include_subaddresses, const std::string& tag) const override;
std::vector<monero_account> get_accounts(bool include_subaddresses, const std::string& tag, bool skip_balances) const;
monero_account create_account(const std::string& label = "") override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices, bool skip_balances) const;
std::vector<monero_subaddress> get_subaddresses(uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx) const override;
monero_subaddress get_subaddress(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_subaddress create_subaddress(uint32_t account_idx, const std::string& label = "") override;
void set_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label = "") override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs(const monero_tx_query& query) const override;
std::vector<std::shared_ptr<monero_transfer>> get_transfers(const monero_transfer_query& query) const override;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs(const monero_output_query& query) const override;
std::string export_outputs(bool all = false) const override;
int import_outputs(const std::string& outputs_hex) override;
std::shared_ptr<monero_key_image_export_result> export_key_images(bool all = false) const override;
std::shared_ptr<monero_key_image_import_result> import_key_images(const std::vector<std::shared_ptr<monero_key_image>>& key_images, uint64_t offset = 0) override;
void freeze_output(const std::string& key_image) override;
void thaw_output(const std::string& key_image) override;
bool is_output_frozen(const std::string& key_image) override;
monero_tx_priority get_default_fee_priority() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> create_txs(const monero_tx_config& conf) override;
std::shared_ptr<monero_tx_wallet> sweep_output(const monero_tx_config& config) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_dust(bool relay = false) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_unlocked(const monero_tx_config& config) override;
std::vector<std::string> relay_txs(const std::vector<std::string>& tx_metadatas) override;
monero_tx_set describe_tx_set(const monero_tx_set& tx_set) override;
monero_tx_set sign_txs(const std::string& unsigned_tx_hex) override;
std::vector<std::string> submit_txs(const std::string& signed_tx_hex) override;
std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override;
monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override;
std::string get_tx_key(const std::string& tx_hash) const override;
std::shared_ptr<monero_check_tx> check_tx_key(const std::string& tx_hash, const std::string& tx_key, const std::string& address) const override;
std::string get_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message) const override;
std::shared_ptr<monero_check_tx> check_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_spend_proof(const std::string& tx_hash, const std::string& message) const override;
bool check_spend_proof(const std::string& tx_hash, const std::string& message, const std::string& signature) const override;
std::string get_reserve_proof_wallet(const std::string& message) const override;
std::string get_reserve_proof_account(uint32_t account_idx, uint64_t amount, const std::string& message) const override;
std::shared_ptr<monero_check_reserve> check_reserve_proof(const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_tx_note(const std::string& tx_hash) const override;
std::vector<std::string> get_tx_notes(const std::vector<std::string>& tx_hashes) const override;
void set_tx_note(const std::string& tx_hashes, const std::string& notes) override;
void set_tx_notes(const std::vector<std::string>& tx_hashes, const std::vector<std::string>& notes) override;
std::vector<monero_address_book_entry> get_address_book_entries(const std::vector<uint64_t>& indices) const override;
uint64_t add_address_book_entry(const std::string& address, const std::string& description) override;
void edit_address_book_entry(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description) override;
void delete_address_book_entry(uint64_t index) override;
void tag_accounts(const std::string& tag, const std::vector<uint32_t>& account_indices) override;
void untag_accounts(const std::vector<uint32_t>& account_indices) override;
std::vector<std::shared_ptr<monero_account_tag>> get_account_tags() const override;
void set_account_tag_label(const std::string& tag, const std::string& label) override;
std::string get_payment_uri(const monero_tx_config& config) const override;
std::shared_ptr<monero_tx_config> parse_payment_uri(const std::string& uri) const override;
void set_attribute(const std::string& key, const std::string& val) override;
bool get_attribute(const std::string& key, std::string& value) const override;
void start_mining(boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) override;
void stop_mining() override;
bool is_multisig_import_needed() const override;
monero_multisig_info get_multisig_info() const override;
std::string prepare_multisig() override;
std::string make_multisig(const std::vector<std::string>& multisig_hexes, int threshold, const std::string& password) override;
monero_multisig_init_result exchange_multisig_keys(const std::vector<std::string>& multisig_hexes, const std::string& password) override;
std::string export_multisig_hex() override;
int import_multisig_hex(const std::vector<std::string>& multisig_hexes, const bool refresh_after_import = true) override;
monero_multisig_sign_result sign_multisig_tx_hex(const std::string& multisig_tx_hex) override;
std::vector<std::string> submit_multisig_tx_hex(const std::string& signed_multisig_tx_hex) override;
void change_password(const std::string& old_password, const std::string& new_password) override;
void save() override;
bool is_closed() const override;
void close(bool save = false) override;
std::shared_ptr<monero_subaddress> get_balances(const boost::optional<uint32_t>& account_idx, const boost::optional<uint32_t>& subaddress_idx) const;
// --------------------------------- PRIVATE --------------------------------
private:
friend class monero_wallet_poller;
boost::optional<uint64_t> m_sync_period_in_ms;
std::string m_path = "";
std::shared_ptr<monero_rpc_connection> m_rpc;
std::shared_ptr<monero_rpc_connection> m_daemon_connection;
mutable boost::recursive_mutex m_listeners_mutex;
std::set<monero_wallet_listener*> m_listeners;
mutable boost::recursive_mutex m_sync_mutex;
mutable boost::mutex m_address_cache_mutex;
mutable std::unordered_map<uint32_t, std::unordered_map<uint32_t, std::string>> m_address_cache;
boost::mutex m_poller_mutex;
std::unique_ptr<monero_wallet_poller> m_poller;
monero_wallet_rpc* create_wallet_random(const std::shared_ptr<monero_wallet_config> &config);
monero_wallet_rpc* create_wallet_from_seed(const std::shared_ptr<monero_wallet_config> &config);
monero_wallet_rpc* create_wallet_from_keys(const std::shared_ptr<monero_wallet_config> &config);
monero_sync_result refresh(const std::shared_ptr<serializable_struct>& params);
std::map<uint32_t, std::vector<uint32_t>> get_account_indices(bool get_subaddress_indices) const;
std::vector<uint32_t> get_subaddress_indices(uint32_t account_idx) const;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_aux(const monero_output_query& query) const;
std::vector<std::shared_ptr<monero_transfer>> get_transfers_aux(const monero_transfer_query& query) const;
std::string query_key(const std::string& key_type) const;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_account(const monero_tx_config &conf);
void clear_address_cache();
void refresh_listening();
void wait_for_listener_callbacks_idle();
void poll();
void clear();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,500 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "daemon/monero_daemon_rpc_model.h"
#include "monero_wallet_model.h"
/**
* Internal data model for monero_wallet_rpc.
*/
namespace monero {
/**
* key-value struct which can be serialized.
*/
struct key_value : public serializable_struct {
boost::optional<std::string> m_key;
boost::optional<std::string> m_value;
key_value() { }
key_value(const std::string& key): m_key(key) { }
key_value(const std::string& key, const std::string& value): m_key(key), m_value(value) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<key_value>& attributes);
};
/**
* Models a RPC Monero key image.
*/
struct monero_rpc_key_image : public monero_key_image {
monero_rpc_key_image(const std::shared_ptr<monero_key_image> &key_image);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Params ---------------------------------
struct monero_query_key_params : public serializable_struct {
boost::optional<std::string> m_key_type;
monero_query_key_params(const std::string& key_type): m_key_type(key_type) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_query_output_params : public serializable_struct {
boost::optional<std::string> m_key_image;
monero_query_output_params(const std::string& key_image): m_key_image(key_image) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_address_params : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddress_indices;
monero_get_address_params(const std::string& address): m_address(address) { }
monero_get_address_params(uint32_t account_index): m_account_index(account_index) { }
monero_get_address_params(uint32_t account_index, const std::vector<uint32_t>& subaddress_indices): m_account_index(account_index), m_subaddress_indices(subaddress_indices) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_integrated_address_params : public serializable_struct {
boost::optional<std::string> m_integrated_address;
boost::optional<std::string> m_standard_address;
boost::optional<std::string> m_payment_id;
monero_integrated_address_params(const std::string& integrated_address): m_integrated_address(integrated_address) {}
monero_integrated_address_params(const std::string& standard_address, const std::string& payment_id): m_standard_address(standard_address), m_payment_id(payment_id) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_multisig_params : public serializable_struct {
// TODO monero-docs document this parameter
boost::optional<bool> m_enable_multisig_experimental;
boost::optional<bool> m_refresh_after_import;
std::vector<std::string> m_multisig_info;
std::vector<std::string> m_multisig_hexes;
boost::optional<std::string> m_multisig_tx_hex;
boost::optional<int> m_threshold;
boost::optional<std::string> m_password;
monero_multisig_params(bool enable_multisig_experimental = true): m_enable_multisig_experimental(enable_multisig_experimental) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, const std::string& password): m_multisig_info(multisig_hexes), m_password(password) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, const char* password): monero_multisig_params(multisig_hexes, std::string(password)) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, int threshold, const std::string& password): m_multisig_info(multisig_hexes), m_threshold(threshold), m_password(password) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, const bool refresh_after_import): m_multisig_hexes(multisig_hexes), m_refresh_after_import(refresh_after_import) { }
monero_multisig_params(const std::string& multisig_tx_hex): m_multisig_tx_hex(multisig_tx_hex) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_payment_uri_params : public serializable_struct {
boost::optional<std::string> m_uri;
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_amount;
boost::optional<std::string> m_payment_id;
boost::optional<std::string> m_recipient_name;
boost::optional<std::string> m_tx_description;
monero_payment_uri_params() { }
monero_payment_uri_params(const std::string& uri): m_uri(uri) { }
monero_payment_uri_params(const monero_tx_config& config);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_balance_params : public serializable_struct {
boost::optional<uint32_t> m_account_idx;
std::vector<uint32_t> m_address_indices;
boost::optional<bool> m_all_accounts;
boost::optional<bool> m_strict;
monero_get_balance_params(bool all_accounts, bool strict = false): m_all_accounts(all_accounts), m_strict(strict) { }
monero_get_balance_params(uint32_t account_idx, const std::vector<uint32_t>& address_indices, bool all_accounts = false, bool strict = false): m_account_idx(account_idx), m_address_indices(address_indices), m_all_accounts(all_accounts), m_strict(strict) { }
monero_get_balance_params(uint32_t account_idx, boost::optional<uint32_t> address_idx, bool all_accounts = false, bool strict = false);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_close_wallet_params : public serializable_struct {
boost::optional<bool> m_save;
monero_close_wallet_params(bool save = false): m_save(save) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_change_wallet_password_params : public serializable_struct {
boost::optional<std::string> m_old_password;
boost::optional<std::string> m_new_password;
monero_change_wallet_password_params(const std::string& old_password, const std::string& new_password): m_old_password(old_password), m_new_password(new_password) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_set_daemon_params : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<std::string> m_username;
boost::optional<std::string> m_password;
boost::optional<std::string> m_proxy;
boost::optional<bool> m_trusted;
boost::optional<std::string> m_ssl_support;
boost::optional<ssl_options> m_ssl_options;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_account_tag_params : public serializable_struct {
std::vector<uint32_t> m_account_indices;
boost::optional<std::string> m_tag;
boost::optional<std::string> m_label;
boost::optional<std::string> m_description;
monero_account_tag_params() {}
monero_account_tag_params(const std::string& tag, const std::string& description = ""): m_tag(tag), m_description(description) { }
monero_account_tag_params(const std::vector<uint32_t>& account_indices): m_account_indices(account_indices) { }
monero_account_tag_params(const std::string& tag, const std::vector<uint32_t>& account_indices): m_tag(tag), m_account_indices(account_indices) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_tx_notes_params : public serializable_struct {
std::vector<std::string> m_tx_hashes;
std::vector<std::string> m_notes;
monero_tx_notes_params(const std::vector<std::string>& tx_hashes): m_tx_hashes(tx_hashes) { }
monero_tx_notes_params(const std::vector<std::string>& tx_hashes, const std::vector<std::string>& notes): m_tx_hashes(tx_hashes), m_notes(notes) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_address_book_entry_params : public serializable_struct {
boost::optional<uint64_t> m_index; // TODO: not boost::optional
boost::optional<bool> m_set_address;
boost::optional<std::string> m_address;
boost::optional<bool> m_set_description;
boost::optional<std::string> m_description;
std::vector<uint64_t> m_entries;
monero_address_book_entry_params(uint64_t index): m_index(index) { }
monero_address_book_entry_params(const std::vector<uint64_t>& entries): m_entries(entries) { }
monero_address_book_entry_params(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description): m_index(index), m_set_address(set_address), m_address(address), m_set_description(set_description), m_description(description) { }
monero_address_book_entry_params(const std::string& address, const std::string& description): m_address(address), m_description(description) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_verify_sign_message_params : public serializable_struct {
boost::optional<std::string> m_data;
boost::optional<std::string> m_address;
boost::optional<std::string> m_signature;
boost::optional<monero_message_signature_type> m_signature_type;
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_address_index;
monero_verify_sign_message_params(const std::string &data, const std::string &address, const std::string& signature): m_data(data), m_address(address), m_signature(signature) { }
monero_verify_sign_message_params(const std::string &data, monero_message_signature_type signature_type, uint32_t account_index, uint32_t address_index): m_data(data), m_signature_type(signature_type), m_account_index(account_index), m_address_index(address_index) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_check_tx_key_params : public serializable_struct {
boost::optional<std::string> m_tx_hash;
boost::optional<std::string> m_address;
boost::optional<std::string> m_tx_key;
monero_check_tx_key_params(const std::string &tx_hash): m_tx_hash(tx_hash) { }
monero_check_tx_key_params(const std::string &tx_hash, const std::string &tx_key, const std::string &address): m_tx_hash(tx_hash), m_tx_key(tx_key), m_address(address) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_sign_describe_transfer_params : public serializable_struct {
boost::optional<std::string> m_unsigned_txset;
boost::optional<std::string> m_multisig_txset;
boost::optional<bool> m_export_raw;
boost::optional<bool> m_get_tx_keys;
monero_sign_describe_transfer_params() { }
monero_sign_describe_transfer_params(const std::string &unsigned_txset) : m_unsigned_txset(unsigned_txset) { }
monero_sign_describe_transfer_params(const std::string &unsigned_txset, const std::string &multisig_txset) : m_unsigned_txset(unsigned_txset), m_multisig_txset(multisig_txset) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_wallet_relay_tx_params : public serializable_struct {
boost::optional<std::string> m_hex;
boost::optional<std::string> m_signed_tx_hex;
monero_wallet_relay_tx_params() {}
monero_wallet_relay_tx_params(const std::string &hex): m_hex(hex) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_sweep_params : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddr_indices;
boost::optional<std::string> m_key_image;
boost::optional<bool> m_relay;
boost::optional<monero_tx_priority> m_priority;
boost::optional<std::string> m_payment_id;
boost::optional<uint64_t> m_below_amount;
boost::optional<bool> m_get_tx_key;
boost::optional<bool> m_get_tx_keys;
boost::optional<bool> m_get_tx_hex;
boost::optional<bool> m_get_tx_metadata;
monero_sweep_params(bool relay = false): m_relay(relay) { }
monero_sweep_params(const monero_tx_config& config);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_create_edit_subaddress_params : public serializable_struct {
boost::optional<std::string> m_label;
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_subaddress_index;
monero_create_edit_subaddress_params(uint32_t account_idx, const std::string& label): m_account_index(account_idx), m_label(label) { }
monero_create_edit_subaddress_params(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label): m_account_index(account_idx), m_subaddress_index(subaddress_idx), m_label(label) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_wallet_data_params : public serializable_struct {
boost::optional<bool> m_all;
std::vector<std::shared_ptr<monero_key_image>> m_key_images;
boost::optional<uint64_t> m_offset;
boost::optional<std::string> m_outputs_hex;
monero_wallet_data_params(const std::vector<std::shared_ptr<monero_key_image>> &key_images, uint64_t offset): m_key_images(key_images), m_offset(offset) { };
monero_wallet_data_params(bool all): m_all(all) { }
monero_wallet_data_params(const std::string& outputs_hex): m_outputs_hex(outputs_hex) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_create_open_wallet_params : public serializable_struct {
boost::optional<std::string> m_filename;
boost::optional<std::string> m_password;
boost::optional<std::string> m_language;
boost::optional<std::string> m_seed;
boost::optional<std::string> m_seed_offset;
boost::optional<uint64_t> m_restore_height;
boost::optional<bool> m_autosave_current;
boost::optional<bool> m_enable_multisig_experimental;
boost::optional<std::string> m_address;
boost::optional<std::string> m_view_key;
boost::optional<std::string> m_spend_key;
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password): m_filename(filename), m_password(password), m_autosave_current(false) { }
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password, const boost::optional<std::string> &language): m_filename(filename), m_password(password), m_language(language), m_autosave_current(false) { }
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password, const boost::optional<std::string> &seed, const boost::optional<std::string> &seed_offset, const boost::optional<uint64_t> &restore_height, const boost::optional<std::string> &language, const boost::optional<bool> &autosave_current, const boost::optional<bool> &enable_multisig_experimental): m_filename(filename), m_password(password), m_seed(seed), m_seed_offset(seed_offset), m_restore_height(restore_height), m_language(language), m_autosave_current(autosave_current), m_enable_multisig_experimental(enable_multisig_experimental) { }
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password, const boost::optional<std::string> &address, const boost::optional<std::string> &view_key, const boost::optional<std::string> &spend_key, const boost::optional<uint64_t> &restore_height, const boost::optional<bool> &autosave_current): m_filename(filename), m_password(password), m_address(address), m_view_key(view_key), m_spend_key(spend_key), m_restore_height(restore_height), m_autosave_current(autosave_current) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_reserve_proof_params : public serializable_struct {
boost::optional<bool> m_all;
boost::optional<std::string> m_message;
boost::optional<std::string> m_tx_hash;
boost::optional<uint32_t> m_account_index;
boost::optional<uint64_t> m_amount;
boost::optional<std::string> m_address;
boost::optional<std::string> m_signature;
monero_reserve_proof_params(const std::string &message, bool all = true): m_all(all), m_message(message) { }
monero_reserve_proof_params(const std::string &address, const std::string &message, const std::string &signature): m_address(address), m_message(message), m_signature(signature) { }
monero_reserve_proof_params(const std::string &tx_hash, const std::string &address, const std::string &message, const std::string &signature): m_tx_hash(tx_hash), m_address(address), m_message(message), m_signature(signature) { }
monero_reserve_proof_params(const std::string &tx_hash, const std::string &message): m_tx_hash(tx_hash), m_message(message) { }
monero_reserve_proof_params(uint32_t account_index, uint64_t amount, const std::string &message): m_account_index(account_index), m_amount(amount), m_message(message) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_wallet_refresh_params : public serializable_struct {
boost::optional<bool> m_enable;
boost::optional<uint64_t> m_period;
boost::optional<uint64_t> m_start_height;
monero_wallet_refresh_params() { }
monero_wallet_refresh_params(bool enable): m_enable(enable) { }
monero_wallet_refresh_params(bool enable, uint64_t period): m_enable(enable), m_period(period) { }
monero_wallet_refresh_params(uint64_t start_height): m_start_height(start_height) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_transfer_params : public serializable_struct {
std::vector<uint32_t> m_subtract_fee_from_outputs;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddress_indices;
boost::optional<std::string> m_payment_id;
boost::optional<bool> m_do_not_relay;
boost::optional<int> m_priority;
boost::optional<bool> m_get_tx_hex;
boost::optional<bool> m_get_tx_metadata;
boost::optional<bool> m_get_tx_keys;
boost::optional<bool> m_get_tx_key;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
monero_transfer_params(const monero_tx_config &config);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_transfers_params : public serializable_struct {
boost::optional<bool> m_in;
boost::optional<bool> m_out;
boost::optional<bool> m_pool;
boost::optional<bool> m_pending;
boost::optional<bool> m_failed;
boost::optional<uint64_t> m_min_height;
boost::optional<uint64_t> m_max_height;
boost::optional<bool> m_all_accounts;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddr_indices;
bool filter_by_height() const { return m_min_height != boost::none || m_max_height != boost::none; }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_incoming_transfers_params : public serializable_struct {
boost::optional<std::string> m_transfer_type;
boost::optional<bool> m_verbose;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddr_indices;
monero_get_incoming_transfers_params(const std::string& transfer_type, bool verbose = true): m_transfer_type(transfer_type), m_verbose(verbose) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Response ---------------------------------
struct monero_multisig_response {
boost::optional<int> m_num_outputs;
boost::optional<std::string> m_multisig_info;
std::vector<std::string> m_tx_hashes;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_response>& response);
};
struct monero_get_balance_response {
boost::optional<uint64_t> m_balance;
boost::optional<uint64_t> m_unlocked_balance;
boost::optional<bool> m_multisig_import_needed;
boost::optional<uint64_t> m_time_to_unlock;
boost::optional<uint64_t> m_blocks_to_unlock;
std::vector<std::shared_ptr<monero_subaddress>> m_per_subaddress;
monero_get_balance_response(): m_balance(0), m_unlocked_balance(0) { }
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_get_balance_response>& response);
};
// ------------------------------ RPC Deserialization ---------------------------------
void deserialize_subaddress(const boost::property_tree::ptree& node, const std::shared_ptr<monero_subaddress>& subaddress);
void deserialize_subaddresses(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_subaddress>>& subaddresses);
void deserialize_account(const boost::property_tree::ptree& node, monero_account& account);
void deserialize_accounts(const boost::property_tree::ptree& node, std::vector<monero_account>& accounts);
void init_sent_tx(const monero_tx_config &config, std::shared_ptr<monero_tx_wallet> &tx, bool copy_destinations);
void deserialize_tx_with_transfer_and_merge(const boost::property_tree::ptree& node, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map);
void deserialize_tx_with_output_and_merge(const boost::property_tree::ptree& node, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map);
void deserialize_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
void deserialize_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set, const std::shared_ptr<monero_tx_wallet> &tx, bool is_outgoing, const monero_tx_config &config);
void deserialize_sent_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
void deserialize_sent_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set, std::vector<std::shared_ptr<monero_tx_wallet>> &txs, const boost::optional<monero_tx_config> &conf);
void deserialize_described_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
void deserialize_submitted_tx_hashes(const boost::property_tree::ptree& node, std::vector<std::string>& tx_hashes);
void deserialize_relayed_tx_hash(const boost::property_tree::ptree& node, std::vector<std::string>& tx_hashes);
int deserialize_num_created_txs(const boost::property_tree::ptree& node, bool can_split);
std::string deserialize_tx_key(const boost::property_tree::ptree& node);
void deserialize_tx_notes(const boost::property_tree::ptree& node, std::vector<std::string>& tx_notes);
void deserialize_integrated_address(const boost::property_tree::ptree& node, monero_integrated_address& subaddress);
void deserialize_key_image_export_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image_export_result>& result);
void deserialize_key_image_import_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image_import_result>& result);
void deserialize_message_signature_result(const boost::property_tree::ptree& node, monero_message_signature_result& result);
void deserialize_check_tx(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_tx>& check);
void deserialize_check_reserve(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_reserve>& check);
void deserialize_multisig_info(const boost::property_tree::ptree& node, monero_multisig_info& info);
void deserialize_multisig_init_result(const boost::property_tree::ptree& node, monero_multisig_init_result& res);
void deserialize_multisig_sign_result(const boost::property_tree::ptree& node, monero_multisig_sign_result& res);
void deserialize_address_book_entries(const boost::property_tree::ptree& node, std::vector<monero_address_book_entry>& entries);
void deserialize_seed_languages(const boost::property_tree::ptree& node, std::vector<std::string>& languages);
uint64_t deserialize_address_book_index(const boost::property_tree::ptree& node);
void deserialize_account_tags(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_account_tag>>& account_tags);
std::string deserialize_signature(const boost::property_tree::ptree& node);
std::string deserialize_exported_outputs(const boost::property_tree::ptree& node);
uint64_t deserialize_block_height(const boost::property_tree::ptree& node);
int deserialize_num_imported_outputs(const boost::property_tree::ptree& node);
bool deserialize_frozen_output_info(const boost::property_tree::ptree& node);
monero_tx_priority deserialize_tx_priority(const boost::property_tree::ptree& node);
std::string deserialize_payment_uri(const boost::property_tree::ptree& node);
void deserialize_payment_uri(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_config>& tx_config);
void deserialize_sync_result(const boost::property_tree::ptree& node, monero_sync_result& sync_result);
}
Binary file not shown.
Binary file not shown.
+290
View File
@@ -0,0 +1,290 @@
import Foundation
import os
/// On-device verification against vectors generated from the reference
/// implementation (xrpl.js + ripple-keypairs + ripple-binary-codec). Runs only
/// when the app is launched with `-koinsSelfTest`.
enum SelfTest {
private static let log = OSLog(subsystem: "com.koins.app", category: "selftest")
/// Writes to both stdout and the unified log so results are visible however
/// the app was launched.
private static func emit(_ line: String) {
print(line)
os_log("%{public}@", log: log, type: .default, line)
}
static func runIfRequested() {
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTest") {
print("KOINS_SELFTEST begin")
run()
print("KOINS_SELFTEST end")
}
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTestNetwork") {
runNetwork()
}
}
/// End-to-end check of the live network paths (XRP RPC + native Monero
/// wallet). Triggered with `-koinsSelfTestNetwork`; cleans up after itself.
static func runNetwork() {
// Detached so the test does not inherit the main actor: the UI thread must
// stay free, otherwise the launch-time call would deadlock.
Task.detached(priority: .userInitiated) {
emit("KOINS_NET begin")
// The test creates temporary wallets, so snapshot the real ones and put
// them back afterwards instead of wiping them.
let backup = (xrp: WalletStore.wallet(.xrp),
xmr: WalletStore.wallet(.xmr),
xrpSecret: WalletStore.secret(.xrp),
xmrSecret: WalletStore.secret(.xmr))
func restoreWallets() {
if let xrp = backup.xrp { WalletStore.saveWallet(xrp, chain: .xrp) } else { WalletStore.removeWallet(.xrp) }
if let secret = backup.xrpSecret { WalletStore.saveSecret(secret, chain: .xrp) }
if let xmr = backup.xmr { WalletStore.saveWallet(xmr, chain: .xmr) } else { WalletStore.removeWallet(.xmr) }
if let secret = backup.xmrSecret { WalletStore.saveSecret(secret, chain: .xmr) }
emit("KOINS_NET restored saved wallets")
}
do {
let (record, phrase) = try XRPWalletService.shared.createWallet(name: "koins-net-selftest")
emit("KOINS_NET xrp address \(record.address) phraseWords \(phrase.split(separator: " ").count)")
let balance = try await XRPWalletService.shared.balance(address: record.address)
emit("KOINS_NET xrp funded=\(balance.funded) balance=\(balance.total)")
let history = try await XRPWalletService.shared.transactions(address: record.address)
emit("KOINS_NET xrp transactions=\(history.count)")
// A known funded account, to exercise the funded-balance path.
for reference in ["rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C"] {
let reference = try await XRPWalletService.shared.balance(address: reference)
emit("KOINS_NET xrp reference funded=\(reference.funded) balance=\(reference.total)")
let referenceHistory = try await XRPWalletService.shared.transactions(address: "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C")
emit("KOINS_NET xrp reference transactions=\(referenceHistory.count)")
}
} catch {
emit("KOINS_NET xrp FAIL \(error)")
}
do {
let created = try await MoneroService.shared.createWallet(name: "koins-net-selftest")
let words = created.mnemonic.split(separator: " ").count
emit("KOINS_NET xmr address \(created.address) mnemonicWords \(words)")
let started = try await MoneroService.shared.startSync()
emit("KOINS_NET xmr startedSync \(started)")
for attempt in 1...6 {
try? await Task.sleep(nanoseconds: 5_000_000_000)
let status = try await MoneroService.shared.syncStatus()
emit("KOINS_NET xmr status \(attempt) height=\(status.height) daemon=\(status.daemonHeight) connected=\(status.connected) syncing=\(status.syncing)")
}
let balance = try await MoneroService.shared.balance()
emit("KOINS_NET xmr balance \(balance.total) unlocked \(balance.unlocked)")
await MoneroService.shared.stopSync()
await MoneroService.shared.removeWallet(name: "koins-net-selftest")
} catch {
emit("KOINS_NET xmr FAIL \(error)")
}
restoreWallets()
emit("KOINS_NET end")
}
}
static func run() {
var failures = 0
func expect(_ name: String, _ actual: String, _ wanted: String) {
let ok = actual.lowercased() == wanted.lowercased()
if !ok { failures += 1 }
let line = "KOINS_SELFTEST \(name): \(ok ? "PASS" : "FAIL") \(actual)"
print(line)
os_log("%{public}@", log: log, type: .default, line)
}
expect("ripemd160", Hex.encode(RIPEMD160.hash(Array("abc".utf8))),
"8eb208f7e05d987a9b044a8e98c6b087f15a0bfc")
// ed25519 vector
let edSeed = "sEdSPx8CLbFUna3DJdvJ74aQPLdemqu"
let edAddress = "rEhh6f9rj5UUBhFzGGaxS5zYU2CCqKFXBC"
let edPublicKey = "EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F1734"
if let keypair = try? XRPKey.keypair(fromSeed: edSeed) {
expect("ed25519 pubkey", Hex.encode(keypair.publicKey), edPublicKey)
expect("ed25519 address", keypair.address, edAddress)
} else {
failures += 1
print("KOINS_SELFTEST ed25519: FAIL keypair threw")
}
// secp256k1 vector: exercises the OpenSSL-backed derivation on device.
let secpSeed = "spFbtYoPd7Hg7AD9XZn7CBwUKC5Qe"
let secpPublicKey = "0322DC9BB14903F71186BC46B0107D55A069D23CD97BBD205491A992EC68FA0ABE"
let secpAddress = "rpsRYc8DbXzfVN32w3hZjUtyuF1K89hu47"
do {
let keypair = try XRPKey.keypair(fromSeed: secpSeed)
expect("secp256k1 algorithm", keypair.algorithm.rawValue, "secp256k1")
expect("secp256k1 pubkey", Hex.encode(keypair.publicKey), secpPublicKey)
expect("secp256k1 address", keypair.address, secpAddress)
} catch {
failures += 1
print("KOINS_SELFTEST secp256k1: FAIL \(error)")
}
// Serialization + hashing reference vector.
let signingHex = "53545800120000220000000024000030392E00000309201B05E69F3B61400000000016E36068400000000000000C7321EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F173481149A26172134BFDA9708A73305AF2776A26DB7B4F5831492891F44F454D156327AA24F4C8BB72540E31BF1"
let referenceSignature = "F560672EA3103D2E43179302CD66ACF563A4EDFBA90ADE24E378B743135FD989EE132FA4B703021C2DD1979E770A6CEBC99556EA330AF9E6E7186673F837CB0D"
let expectedBlob = "120000220000000024000030392E00000309201B05E69F3B61400000000016E36068400000000000000C7321EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F17347440F560672EA3103D2E43179302CD66ACF563A4EDFBA90ADE24E378B743135FD989EE132FA4B703021C2DD1979E770A6CEBC99556EA330AF9E6E7186673F837CB0D81149A26172134BFDA9708A73305AF2776A26DB7B4F5831492891F44F454D156327AA24F4C8BB72540E31BF1"
let expectedHash = "F853B5391CD6317EA370830EEA3C047AC8D21E9FA00E4116D94C48E360B0434F"
var payment = XRPPayment(account: edAddress,
destination: "rNMovRR3WPbFLVaSbETCCR71XsqyxhJ9P6",
amountDrops: 1_500_000,
feeDrops: 12,
sequence: 12345,
lastLedgerSequence: 99000123,
destinationTag: 777,
signingPublicKey: Hex.decode(edPublicKey) ?? [],
signature: Hex.decode(referenceSignature))
if let payload = try? payment.signingPayload() {
expect("signing payload", Hex.encode(payload), signingHex)
} else {
failures += 1
}
if let blob = try? payment.txBlob() {
expect("tx blob", Hex.encode(blob), expectedBlob)
if let hash = try? payment.transactionHash() {
expect("tx hash", Hex.encode(hash), expectedHash)
} else {
failures += 1
}
} else {
failures += 1
}
// BIP-39 / BIP-32 (m/44'/144'/0'/0/0): the recovery phrase used for new XRP
// wallets. Vectors come from @scure/bip39 + @scure/bip32 + ripple-keypairs.
let zeroMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
expect("bip39 encode", BIP39.mnemonic(from: [UInt8](repeating: 0, count: 16)), zeroMnemonic)
expect("bip39 entropy", Hex.encode(BIP39.entropy(from: zeroMnemonic) ?? []), String(repeating: "00", count: 16))
expect("bip39 rejects bad checksum",
BIP39.entropy(from: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon") == nil ? "rejected" : "accepted",
"rejected")
// BIP-32 intermediate values, to isolate derivation problems.
if let master = BIP32.masterKey(fromSeed: BIP39.seed(from: zeroMnemonic)) {
expect("bip32 master key", Hex.encode(master.privateKey),
"1837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf67")
expect("bip32 master chain code", Hex.encode(master.chainCode),
"7923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e")
let H = BIP32.hardenedBit
for (level, path, expected) in [
(1, [H | 44], "cd46049fb82a4fccbd967c5234e61152f4df87d01d1ef833420814c67ee4b7ea"),
(2, [H | 44, H | 144], "a12250f1f38fb41b62f0f1a4ec9d3555d24788843cb30e6d98a777fe540adc1d"),
(3, [H | 44, H | 144, H | 0], "54bac451906ecd52648a49126a2606c9177761f14d31dda079c0f768ac3501fd"),
(4, [H | 44, H | 144, H | 0, 0], "e582ce1724e93eebe3f38f1d42c01620b66d136946fec450dfe71b62e65de54f"),
(5, BIP32.xrpPath, "90802a50aa84efb6cdb225f17c27616ea94048c179142fecf03f4712a07ea7a4"),
] {
if let child = BIP32.derive(path, from: master) {
expect("bip32 level \(level)", Hex.encode(child.privateKey), expected)
} else {
failures += 1
print("KOINS_SELFTEST bip32 level \(level): FAIL derivation returned nil")
}
}
} else {
failures += 1
print("KOINS_SELFTEST bip32 master: FAIL")
}
let mnemonicCases: [(String, String, String)] = [
(zeroMnemonic,
"031d68bc1a142e6766b2bdfb006ccfe135ef2e0e2e94abb5cf5c9ab6104776fbae",
"rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"),
("legal winner thank year wave sausage worth useful legal winner thank yellow",
"0283e0926eb0ab7bb8f5c0b5637d9e2526bdc398763af79cd904f013178961692d",
"rpaLPza5oxhJ7V8nFDwPm9F7kmZ16uc8do"),
]
for (position, vector) in mnemonicCases.enumerated() {
do {
let keypair = try XRPKey.keypair(fromMnemonic: vector.0)
expect("bip44 pubkey #\(position)", Hex.encode(keypair.publicKey), vector.1)
expect("bip44 address #\(position)", keypair.address, vector.2)
} catch {
failures += 1
print("KOINS_SELFTEST bip44 #\(position): FAIL \(error)")
}
}
// secp256k1 signing must produce a low-S DER signature.
if let keypair = try? XRPKey.keypair(fromSeed: secpSeed),
let message = Hex.decode(signingHex),
let signature = try? XRPKey.sign(message: message, keypair: keypair) {
let isDer = signature.count > 8 && signature[0] == 0x30
expect("secp256k1 signature is DER", isDer ? "yes" : "no", "yes")
if let order = BigNumber.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
let half = order.dividedByTwo(),
let s = DER.signatureScalar(signature) {
expect("secp256k1 signature is low-S", s.lessThanOrEqual(half) ? "yes" : "no", "yes")
}
print("KOINS_SELFTEST secp256k1 signature len: \(signature.count)")
} else {
failures += 1
print("KOINS_SELFTEST secp256k1 sign: FAIL")
}
let summary = "KOINS_SELFTEST summary: \(failures == 0 ? "ALL PASS" : "\(failures) FAILURE(S)")"
print(summary)
os_log("%{public}@", log: log, type: .default, summary)
}
}
/// Minimal big-endian integer helper used only for the low-S check.
struct BigNumber {
let bytes: [UInt8]
static func fromHex(_ hex: String) -> BigNumber? {
Hex.decode(hex).map { BigNumber(bytes: $0) }
}
func dividedByTwo() -> BigNumber? {
var result: [UInt8] = []
var carry: UInt16 = 0
for byte in bytes {
let value = carry * 256 + UInt16(byte)
result.append(UInt8(value / 2))
carry = value % 2
}
return BigNumber(bytes: result)
}
func lessThanOrEqual(_ other: BigNumber) -> Bool {
let left = trimmed()
let right = other.trimmed()
if left.count != right.count { return left.count < right.count }
for (a, b) in zip(left, right) where a != b { return a < b }
return true
}
private func trimmed() -> [UInt8] {
var value = bytes
while value.first == 0 && value.count > 1 { value.removeFirst() }
return value
}
}
enum DER {
/// Extracts the `s` component from a DER-encoded ECDSA signature.
static func signatureScalar(_ der: [UInt8]) -> BigNumber? {
guard der.count > 8, der[0] == 0x30 else { return nil }
var index = 2
guard der[index] == 0x02 else { return nil }
let rLength = Int(der[index + 1])
index += 2 + rLength
guard index + 2 <= der.count, der[index] == 0x02 else { return nil }
let sLength = Int(der[index + 1])
let start = index + 2
guard start + sLength <= der.count else { return nil }
return BigNumber(bytes: Array(der[start..<(start + sLength)]))
}
}
+92
View File
@@ -0,0 +1,92 @@
import CryptoKit
import Foundation
/// BIP-32 key derivation for the XRP account path `m/44'/144'/0'/0/0`. The
/// first three steps are hardened, the last two are not, so both cases are
/// supported (hardened steps sign with the parent private key, normal steps
/// with the parent public key).
enum BIP32 {
static let hardenedBit: UInt32 = 0x8000_0000
/// Standard XRP derivation path: 44' / 144' / 0' / 0 / 0.
static let xrpPath: [UInt32] = [
hardenedBit | 44,
hardenedBit | 144,
hardenedBit | 0,
0,
0,
]
struct Key {
var privateKey: [UInt8]
var chainCode: [UInt8]
var publicKey: [UInt8]
}
static func masterKey(fromSeed seed: [UInt8]) -> Key? {
let derived = hmac(key: Array("Bitcoin seed".utf8), data: seed)
guard derived.count == 64 else { return nil }
let privateKey = Array(derived[0..<32])
guard isValidScalar(privateKey),
let publicKey = Secp256k1Backend.publicKey?(Data(privateKey)) else { return nil }
return Key(privateKey: privateKey,
chainCode: Array(derived[32..<64]),
publicKey: Array(publicKey))
}
static func derive(_ path: [UInt32], from key: Key) -> Key? {
var current = key
for index in path {
guard let next = deriveChild(current, index: index) else { return nil }
current = next
}
return current
}
private static func deriveChild(_ parent: Key, index: UInt32) -> Key? {
let isHardened = index & hardenedBit != 0
var data: [UInt8] = []
if isHardened {
data.append(0x00)
data += parent.privateKey
} else {
data += parent.publicKey
}
data += [
UInt8((index >> 24) & 0xff),
UInt8((index >> 16) & 0xff),
UInt8((index >> 8) & 0xff),
UInt8(index & 0xff),
]
let derived = hmac(key: parent.chainCode, data: data)
guard derived.count == 64 else { return nil }
let left = Array(derived[0..<32])
guard isValidScalar(left),
let sum = Secp256k1Backend.addScalar?(Data(left), Data(parent.privateKey)) else { return nil }
let privateKey = Array(sum)
// The child public key is the same whether it is computed from the child
// private key or by point addition, so derive it directly.
guard isValidScalar(privateKey),
let publicKey = Secp256k1Backend.publicKey?(Data(privateKey)) else { return nil }
return Key(privateKey: privateKey,
chainCode: Array(derived[32..<64]),
publicKey: Array(publicKey))
}
/// A key is usable when 0 < key < the secp256k1 group order.
private static func isValidScalar(_ bytes: [UInt8]) -> Bool {
guard bytes.count == 32 else { return false }
let orderHex = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
guard let order = Hex.decode(orderHex) else { return false }
if bytes.allSatisfy({ $0 == 0 }) { return false }
for (left, right) in zip(bytes, order) where left != right { return left < right }
return false
}
private static func hmac(key: [UInt8], data: [UInt8]) -> [UInt8] {
Array(HMAC<SHA512>.authenticationCode(for: Data(data), using: SymmetricKey(data: Data(key))))
}
}
+94
View File
@@ -0,0 +1,94 @@
import CryptoKit
import Foundation
/// BIP-39 mnemonics. The XRP wallet uses a 12 word phrase as its recovery
/// secret, matching what hardware and mobile wallets expect.
enum BIP39 {
static let wordCount = 12
static let entropyBytes = 16
private static let checksumBits = entropyBytes / 4
private static let index: [String: Int] = {
var map: [String: Int] = [:]
for (position, word) in BIP39Wordlist.words.enumerated() { map[word] = position }
return map
}()
static func generateMnemonic() -> String {
var entropy = [UInt8](repeating: 0, count: entropyBytes)
for position in entropy.indices { entropy[position] = UInt8.random(in: 0...255) }
return mnemonic(from: entropy)
}
/// 16 bytes of entropy plus its 4 bit SHA-256 checksum, as 12 words.
static func mnemonic(from entropy: [UInt8]) -> String {
precondition(entropy.count == entropyBytes, "BIP-39 entropy must be 16 bytes")
var bits = entropy.flatMap { byte in (0..<8).map { (byte >> (7 - $0)) & 1 } }
let checksum = XRPHash.sha256(entropy)
bits += (0..<checksumBits).map { (checksum[0] >> (7 - $0)) & 1 }
var words: [String] = []
for start in stride(from: 0, to: bits.count, by: 11) {
var value = 0
for offset in 0..<11 { value = (value << 1) | Int(bits[start + offset]) }
words.append(BIP39Wordlist.words[value])
}
return words.joined(separator: " ")
}
static func entropy(from mnemonic: String) -> [UInt8]? {
let words = normalize(mnemonic).split(separator: " ").map(String.init)
guard words.count == wordCount else { return nil }
var bits: [Int] = []
for word in words {
guard let position = index[word] else { return nil }
bits += (0..<11).map { (position >> (10 - $0)) & 1 }
}
var entropy: [UInt8] = []
for start in stride(from: 0, to: entropyBytes * 8, by: 8) {
var value: UInt8 = 0
for offset in 0..<8 { value = (value << 1) | UInt8(bits[start + offset]) }
entropy.append(value)
}
let checksum = XRPHash.sha256(entropy)
for offset in 0..<checksumBits {
let expected = (checksum[0] >> (7 - offset)) & 1
guard bits[entropyBytes * 8 + offset] == expected else { return nil }
}
return entropy
}
/// 64 byte BIP-39 seed (PBKDF2-HMAC-SHA512, empty passphrase).
static func seed(from mnemonic: String) -> [UInt8] {
let password = Array(normalize(mnemonic).utf8)
let salt = Array("mnemonic".utf8)
// Derived length equals the hash length, so a single PBKDF2 block is needed.
let block = salt + [0, 0, 0, 1]
var previous = hmacSHA512(key: password, data: block)
var result = previous
for _ in 1..<2_048 {
previous = hmacSHA512(key: password, data: previous)
for position in result.indices { result[position] ^= previous[position] }
}
return result
}
static func isMnemonic(_ secret: String) -> Bool {
normalize(secret).split(separator: " ").count == wordCount
}
private static func normalize(_ text: String) -> String {
text.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.precomposedStringWithCompatibilityMapping
}
private static func hmacSHA512(key: [UInt8], data: [UInt8]) -> [UInt8] {
let authentication = HMAC<SHA512>.authenticationCode(for: Data(data),
using: SymmetricKey(data: Data(key)))
return Array(authentication)
}
}
+263
View File
@@ -0,0 +1,263 @@
import Foundation
/// BIP-39 English word list (2048 words), in the canonical order.
enum BIP39Wordlist {
static let words: [String] = [
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
"acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual",
"adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
"advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
"agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
"alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone",
"alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
"amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry",
"animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april",
"arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
"army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact",
"artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
"asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
"audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
"avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis",
"baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
"bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base",
"basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
"beef", "before", "begin", "behave", "behind", "believe", "below", "belt",
"bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
"bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black",
"blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
"blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body",
"boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
"borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain",
"brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
"bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother",
"brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
"bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus",
"business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
"cactus", "cage", "cake", "call", "calm", "camera", "camp", "can",
"canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
"capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry",
"cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
"catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling",
"celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
"champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap",
"check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
"chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar",
"cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
"claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff",
"climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
"clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut",
"code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
"come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm",
"congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
"copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch",
"country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
"craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream",
"credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
"cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch",
"crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
"current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad",
"damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
"day", "deal", "debate", "debris", "decade", "december", "decide", "decline",
"decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
"deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend",
"deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
"despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram",
"dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
"dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover",
"disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
"divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain",
"donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
"dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill",
"drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
"dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager",
"eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
"ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight",
"either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
"elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ",
"empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
"energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough",
"enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
"equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt",
"escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
"evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude",
"excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
"exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend",
"extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
"faith", "fall", "false", "fame", "family", "famous", "fan", "fancy",
"fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
"favorite", "feature", "february", "federal", "fee", "feed", "feel", "female",
"fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
"figure", "file", "film", "filter", "final", "find", "fine", "finger",
"finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
"fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight",
"flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
"foam", "focus", "fog", "foil", "fold", "follow", "food", "foot",
"force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
"foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend",
"fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
"fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy",
"gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
"gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius",
"genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
"ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass",
"glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
"goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip",
"govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
"gravity", "great", "green", "grid", "grief", "grit", "grocery", "group",
"grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
"gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy",
"harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
"head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet",
"help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
"hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow",
"home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
"host", "hotel", "hour", "hover", "hub", "huge", "human", "humble",
"humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
"hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill",
"illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
"improve", "impulse", "inch", "include", "income", "increase", "index", "indicate",
"indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
"inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane",
"insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
"invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory",
"jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
"job", "join", "joke", "journey", "joy", "judge", "juice", "jump",
"jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
"key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit",
"kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
"lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language",
"laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
"lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave",
"lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
"length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty",
"library", "license", "life", "lift", "light", "like", "limb", "limit",
"link", "lion", "liquid", "list", "little", "live", "lizard", "load",
"loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
"lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber",
"lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
"maid", "mail", "main", "major", "make", "mammal", "man", "manage",
"mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
"marine", "market", "marriage", "mask", "mass", "master", "match", "material",
"math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
"meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory",
"mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
"metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind",
"minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
"mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment",
"monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
"mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie",
"much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
"must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin",
"narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
"neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral",
"never", "news", "next", "nice", "night", "noble", "noise", "nominee",
"noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice",
"novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
"object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean",
"october", "odor", "off", "offer", "office", "often", "oil", "okay",
"old", "olive", "olympic", "omit", "once", "one", "onion", "online",
"only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
"orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich",
"other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
"own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page",
"pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
"parade", "parent", "park", "parrot", "party", "pass", "patch", "path",
"patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
"pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper",
"perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
"piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot",
"pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
"plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge",
"poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
"pool", "popular", "portion", "position", "possible", "post", "potato", "pottery",
"poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
"present", "pretty", "prevent", "price", "pride", "primary", "print", "priority",
"prison", "private", "prize", "problem", "process", "produce", "profit", "program",
"project", "promote", "proof", "property", "prosper", "protect", "proud", "provide",
"public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
"puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle",
"pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
"quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail",
"rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
"rare", "rate", "rather", "raven", "raw", "razor", "ready", "real",
"reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
"reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject",
"relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
"render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report",
"require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
"retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib",
"ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
"ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road",
"roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
"rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude",
"rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
"safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same",
"sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
"scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science",
"scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
"search", "season", "seat", "second", "secret", "section", "security", "seed",
"seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
"series", "service", "session", "settle", "setup", "seven", "shadow", "shaft",
"shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
"ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder",
"shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
"siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar",
"simple", "since", "sing", "siren", "sister", "situate", "six", "size",
"skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab",
"slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
"slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth",
"snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
"sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve",
"someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
"source", "south", "space", "spare", "spatial", "spawn", "speak", "special",
"speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
"spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray",
"spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
"staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay",
"steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
"stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street",
"strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
"submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest",
"suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
"sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain",
"swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
"swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table",
"tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
"task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten",
"tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
"theme", "then", "theory", "there", "they", "thing", "this", "thought",
"three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
"tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title",
"toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
"tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top",
"topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
"toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic",
"train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
"trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy",
"trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
"tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle",
"twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
"ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo",
"unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
"unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon",
"upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
"useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley",
"valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
"velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very",
"vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
"village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual",
"vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
"voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want",
"warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
"way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding",
"weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
"wheel", "when", "where", "whip", "whisper", "wide", "width", "wife",
"wild", "will", "win", "window", "wine", "wing", "wink", "winner",
"winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman",
"wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
"wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",
"yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo",
]
}
+76
View File
@@ -0,0 +1,76 @@
import Foundation
/// Base58 with the XRP Ledger alphabet and Base58Check helpers.
enum XRPBase58 {
static let alphabet: [Character] = Array("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz")
private static let valueOf: [Character: Int] = {
var map: [Character: Int] = [:]
for (index, character) in alphabet.enumerated() { map[character] = index }
return map
}()
static func encode(_ bytes: [UInt8]) -> String {
var zeros = 0
for byte in bytes {
if byte == 0 { zeros += 1 } else { break }
}
var digits: [Int] = []
for byte in bytes[zeros...] {
var carry = Int(byte)
for index in digits.indices {
carry += digits[index] << 8
digits[index] = carry % 58
carry /= 58
}
while carry > 0 {
digits.append(carry % 58)
carry /= 58
}
}
var output = String(repeating: String(alphabet[0]), count: zeros)
for digit in digits.reversed() { output.append(alphabet[digit]) }
return output
}
static func decode(_ string: String) throws -> [UInt8] {
let characters = Array(string)
var zeros = 0
for character in characters {
if character == alphabet[0] { zeros += 1 } else { break }
}
var bytes: [Int] = []
for character in characters[zeros...] {
guard let value = valueOf[character] else { throw XRPError.invalidBase58 }
var carry = value
for index in bytes.indices {
carry += bytes[index] * 58
bytes[index] = carry & 0xff
carry >>= 8
}
while carry > 0 {
bytes.append(carry & 0xff)
carry >>= 8
}
}
return [UInt8](repeating: 0, count: zeros) + bytes.reversed().map { UInt8($0) }
}
static func checkEncode(version: [UInt8], payload: [UInt8]) -> String {
let body = version + payload
return encode(body + XRPHash.checksum(body))
}
static func checkDecode(_ string: String, versionLength: Int) throws -> [UInt8] {
let decoded = try decode(string)
guard decoded.count > versionLength + 4 else { throw XRPError.invalidAddress }
let body = Array(decoded[0..<(decoded.count - 4)])
let checksum = Array(decoded[(decoded.count - 4)...])
guard XRPHash.checksum(body) == checksum else { throw XRPError.invalidAddress }
return Array(body[versionLength...])
}
}
+10
View File
@@ -0,0 +1,10 @@
#include <libkern/OSCacheControl.h>
#include <stddef.h>
// randomx (pulled in by monero's proof-of-work code) calls the compiler-rt
// builtin __clear_cache from its AArch64 JIT. libSystem exports it on macOS and
// the simulator, but not on iOS devices, so provide the same implementation
// compiler-rt uses on Darwin.
void __clear_cache(void *start, void *end) {
sys_icache_invalidate(start, (size_t)((char *)end - (char *)start));
}
+68
View File
@@ -0,0 +1,68 @@
import CryptoKit
import Foundation
enum XRPError: LocalizedError {
case invalidBase58
case invalidAddress
case invalidSeed
case unsupportedSeed
case rpc(String)
case network(String)
var errorDescription: String? {
switch self {
case .invalidBase58: return "Invalid Base58 text"
case .invalidAddress: return "Invalid XRP address"
case .invalidSeed: return "Check the recovery phrase or secret"
case .unsupportedSeed: return "Unsupported XRP secret"
case .rpc(let message): return message
case .network(let message): return message
}
}
}
enum XRPHash {
static func sha256(_ bytes: [UInt8]) -> [UInt8] {
Array(SHA256.hash(data: Data(bytes)))
}
static func sha512(_ bytes: [UInt8]) -> [UInt8] {
Array(SHA512.hash(data: Data(bytes)))
}
/// First 32 bytes of SHA-512, used by the XRP Ledger everywhere.
static func sha512Half(_ bytes: [UInt8]) -> [UInt8] {
Array(sha512(bytes).prefix(32))
}
static func ripemd160(_ bytes: [UInt8]) -> [UInt8] {
RIPEMD160.hash(bytes)
}
/// Base58Check double-SHA-256 checksum.
static func checksum(_ bytes: [UInt8]) -> [UInt8] {
Array(sha256(sha256(bytes)).prefix(4))
}
}
enum Hex {
static func encode(_ bytes: [UInt8]) -> String {
bytes.map { String(format: "%02x", $0) }.joined()
}
static func decode(_ string: String) -> [UInt8]? {
var text = string.trimmingCharacters(in: .whitespacesAndNewlines)
if text.hasPrefix("0x") || text.hasPrefix("0X") { text = String(text.dropFirst(2)) }
guard text.count % 2 == 0 else { return nil }
var bytes: [UInt8] = []
bytes.reserveCapacity(text.count / 2)
var index = text.startIndex
while index < text.endIndex {
let next = text.index(index, offsetBy: 2)
guard let byte = UInt8(text[index..<next], radix: 16) else { return nil }
bytes.append(byte)
index = next
}
return bytes
}
}
+114
View File
@@ -0,0 +1,114 @@
import Foundation
/// RIPEMD-160, needed for XRP account id derivation (there is no CryptoKit /
/// CommonCrypto implementation of this digest on iOS).
enum RIPEMD160 {
private static let roundLeft: [Int] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13,
]
private static let roundRight: [Int] = [
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11,
]
private static let shiftsLeft: [UInt32] = [
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6,
]
private static let shiftsRight: [UInt32] = [
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11,
]
private static let constantsLeft: [UInt32] = [0x0000_0000, 0x5A82_7999, 0x6ED9_EBA1, 0x8F1B_BCDC, 0xA953_FD4E]
private static let constantsRight: [UInt32] = [0x50A2_8BE6, 0x5C4D_D124, 0x6D70_3EF3, 0x7A6D_76E9, 0x0000_0000]
static func hash(_ message: [UInt8]) -> [UInt8] {
var padded = message
let bitLength = UInt64(message.count) * 8
padded.append(0x80)
while padded.count % 64 != 56 { padded.append(0) }
for shift in stride(from: 0, through: 56, by: 8) {
padded.append(UInt8((bitLength >> UInt64(shift)) & 0xff))
}
var h0: UInt32 = 0x6745_2301
var h1: UInt32 = 0xEFCD_AB89
var h2: UInt32 = 0x98BA_DCFE
var h3: UInt32 = 0x1032_5476
var h4: UInt32 = 0xC3D2_E1F0
var offset = 0
while offset < padded.count {
var words = [UInt32](repeating: 0, count: 16)
for index in 0..<16 {
let base = offset + index * 4
words[index] = UInt32(padded[base])
| (UInt32(padded[base + 1]) << 8)
| (UInt32(padded[base + 2]) << 16)
| (UInt32(padded[base + 3]) << 24)
}
var al = h0, bl = h1, cl = h2, dl = h3, el = h4
var ar = h0, br = h1, cr = h2, dr = h3, er = h4
for step in 0..<80 {
let round = step / 16
var leftTemp = rotate(al &+ f(round, bl, cl, dl) &+ words[roundLeft[step]] &+ constantsLeft[round], by: shiftsLeft[step]) &+ el
al = el; el = dl; dl = rotate(cl, by: 10); cl = bl; bl = leftTemp
let rightRound = 4 - round
leftTemp = rotate(ar &+ f(rightRound, br, cr, dr) &+ words[roundRight[step]] &+ constantsRight[round], by: shiftsRight[step]) &+ er
ar = er; er = dr; dr = rotate(cr, by: 10); cr = br; br = leftTemp
}
let temp = h1 &+ cl &+ dr
h1 = h2 &+ dl &+ er
h2 = h3 &+ el &+ ar
h3 = h4 &+ al &+ br
h4 = h0 &+ bl &+ cr
h0 = temp
offset += 64
}
var digest: [UInt8] = []
for word in [h0, h1, h2, h3, h4] {
digest.append(UInt8(word & 0xff))
digest.append(UInt8((word >> 8) & 0xff))
digest.append(UInt8((word >> 16) & 0xff))
digest.append(UInt8((word >> 24) & 0xff))
}
return digest
}
private static func f(_ round: Int, _ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
switch round {
case 0: return x ^ y ^ z
case 1: return (x & y) | (~x & z)
case 2: return (x | ~y) ^ z
case 3: return (x & z) | (y & ~z)
default: return x ^ (y | ~z)
}
}
private static func rotate(_ value: UInt32, by amount: UInt32) -> UInt32 {
(value << amount) | (value >> (32 - amount))
}
}
+76
View File
@@ -0,0 +1,76 @@
import Foundation
import Security
/// Small Keychain wrapper for wallet secrets. Secrets never touch UserDefaults.
enum Keychain {
private static let service = "com.koins.app"
static func set(_ value: String, for key: String) {
guard let data = value.data(using: .utf8) else { return }
remove(key)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
SecItemAdd(query as CFDictionary, nil)
}
static func get(_ key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
let data = item as? Data,
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value
}
static func remove(_ key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}
enum WalletStore {
private static let defaults = UserDefaults.standard
private static func walletKey(_ chain: Chain) -> String { "koins.wallet.\(chain.rawValue)" }
private static func secretKey(_ chain: Chain) -> String { "koins.secret.\(chain.rawValue)" }
static func wallet(_ chain: Chain) -> WalletRecord? {
guard let data = defaults.data(forKey: walletKey(chain)) else { return nil }
return try? JSONDecoder().decode(WalletRecord.self, from: data)
}
static func saveWallet(_ wallet: WalletRecord, chain: Chain) {
guard let data = try? JSONEncoder().encode(wallet) else { return }
defaults.set(data, forKey: walletKey(chain))
}
static func removeWallet(_ chain: Chain) {
defaults.removeObject(forKey: walletKey(chain))
Keychain.remove(secretKey(chain))
}
static func secret(_ chain: Chain) -> String? {
Keychain.get(secretKey(chain))
}
static func saveSecret(_ secret: String, chain: Chain) {
Keychain.set(secret, for: secretKey(chain))
}
}
+260
View File
@@ -0,0 +1,260 @@
import CoreImage.CIFilterBuiltins
import SwiftUI
enum Theme {
static let background = Color(red: 0.035, green: 0.035, blue: 0.05)
static let surface = Color(red: 0.09, green: 0.09, blue: 0.12)
static let surfaceAlt = Color(red: 0.13, green: 0.13, blue: 0.17)
static let border = Color.white.opacity(0.08)
static let accent = Color(red: 1.0, green: 0.56, blue: 0.16)
static let accentSoft = Color(red: 1.0, green: 0.56, blue: 0.16).opacity(0.15)
static let text = Color.white
static let muted = Color.white.opacity(0.55)
static let danger = Color(red: 1.0, green: 0.36, blue: 0.36)
static let success = Color(red: 0.35, green: 0.85, blue: 0.55)
}
struct PrimaryButton: View {
var title: String
var disabled: Bool = false
var action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 16, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 15)
.background(disabled ? Theme.surfaceAlt : Theme.accent)
.foregroundStyle(disabled ? Theme.muted : Color.black)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.disabled(disabled)
}
}
struct SecondaryButton: View {
var title: String
var action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 15, weight: .medium))
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Theme.surface)
.foregroundStyle(Theme.text)
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
}
struct Card<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
content
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Theme.surface)
.overlay(RoundedRectangle(cornerRadius: 20, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
struct FieldLabel: View {
var text: String
var body: some View {
Text(text)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Theme.muted)
.padding(.leading, 4)
}
}
struct KoinsTextField: View {
var placeholder: String
@Binding var text: String
var multiline = false
var keyboard: UIKeyboardType = .default
var autocapitalization: TextInputAutocapitalization = .never
var body: some View {
Group {
if multiline {
TextEditor(text: $text)
.scrollContentBackground(.hidden)
.frame(minHeight: 96)
.padding(8)
} else {
TextField(placeholder, text: $text)
.keyboardType(keyboard)
.textInputAutocapitalization(autocapitalization)
.autocorrectionDisabled()
.padding(.horizontal, 14)
.padding(.vertical, 13)
}
}
.font(.system(size: 15))
.foregroundStyle(Theme.text)
.background(Theme.surface)
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
struct MessageBanner: View {
var message: String
var isError: Bool
var body: some View {
Text(message)
.font(.system(size: 13))
.foregroundStyle(isError ? Theme.danger : Theme.success)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background((isError ? Theme.danger : Theme.success).opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}
struct ChainPicker: View {
@Binding var selection: Chain
var body: some View {
HStack(spacing: 6) {
ForEach(Chain.allCases, id: \.self) { chain in
Button {
selection = chain
} label: {
Text(chain.title)
.font(.system(size: 13, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(selection == chain ? Theme.accent : Color.clear)
.foregroundStyle(selection == chain ? Color.black : Theme.muted)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
}
.padding(4)
.background(Theme.surface)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
struct QRCodeView: View {
var text: String
var body: some View {
if let image = QRCodeView.makeImage(from: text) {
Image(uiImage: image)
.interpolation(.none)
.resizable()
.scaledToFit()
.padding(12)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
} else {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Theme.surfaceAlt)
.aspectRatio(1, contentMode: .fit)
}
}
private static func makeImage(from text: String) -> UIImage? {
guard !text.isEmpty else { return nil }
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(text.utf8)
filter.correctionLevel = "M"
guard let output = filter.outputImage else { return nil }
let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
let context = CIContext()
guard let cgImage = context.createCGImage(scaled, from: scaled.extent) else { return nil }
return UIImage(cgImage: cgImage)
}
}
struct TransactionRow: View {
var record: TransactionRecord
var symbol: String
private var formattedDate: String {
guard let timestamp = record.timestamp else { return "Pending" }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: timestamp)
}
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(record.direction == .incoming ? Theme.success.opacity(0.15) : Theme.accentSoft)
.frame(width: 38, height: 38)
Image(systemName: record.direction == .incoming ? "arrow.down.left" : "arrow.up.right")
.font(.system(size: 15, weight: .bold))
.foregroundStyle(record.direction == .incoming ? Theme.success : Theme.accent)
}
VStack(alignment: .leading, spacing: 3) {
Text(record.direction == .incoming ? "Received" : "Sent")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Theme.text)
Text(record.confirmed ? formattedDate : "Pending confirmation")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
Spacer()
VStack(alignment: .trailing, spacing: 3) {
Text("\(record.direction == .incoming ? "+" : "-")\(record.amount) \(symbol)")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(record.direction == .incoming ? Theme.success : Theme.text)
if let fee = record.fee, !fee.isEmpty, record.direction == .outgoing {
Text("fee \(fee)")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
.padding(.vertical, 6)
}
}
struct ScreenHeader: View {
var title: String
var subtitle: String?
var onBack: (() -> Void)?
var body: some View {
HStack(alignment: .center, spacing: 12) {
if let onBack {
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Theme.text)
.frame(width: 34, height: 34)
.background(Theme.surface)
.clipShape(Circle())
}
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.system(size: 22, weight: .bold))
.foregroundStyle(Theme.text)
if let subtitle {
Text(subtitle)
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
Spacer()
}
}
}
+228
View File
@@ -0,0 +1,228 @@
import SwiftUI
struct DashboardView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var showSeed = false
@State private var revealedSeed: String?
@State private var seedError: String?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: model.wallet?.name ?? "Wallet",
subtitle: model.chain.title,
onBack: { route = .home })
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Balance")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
Text("\(model.balance.total) \(model.symbol)")
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
Text("\(model.balance.unlocked) \(model.symbol) spendable now")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
AddressRow(address: model.displayAddress)
}
}
if model.chain == .xmr {
Card {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Sync")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.text)
Spacer()
if let status = model.syncStatus { SyncBadge(status: status) }
}
if let status = model.syncStatus {
SyncProgress(status: status, eta: model.syncEta)
} else {
Text("Sync status is not available yet.")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
}
HStack(spacing: 10) {
PrimaryButton(title: "Send") { route = .send }
SecondaryButton(title: "Receive") { route = .receive }
}
if !model.transactions.isEmpty {
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Activity")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
ForEach(model.transactions) { record in
TransactionRow(record: record, symbol: model.symbol)
if record.id != model.transactions.last?.id {
Divider().overlay(Theme.border)
}
}
}
}
}
if let message = model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
SecondaryButton(title: "Reveal recovery phrase") {
Task {
seedError = nil
do {
revealedSeed = try await model.revealSeed()
showSeed = true
} catch {
seedError = error.localizedDescription
}
}
}
if let seedError {
MessageBanner(message: seedError, isError: true)
}
}
.sheet(isPresented: $showSeed) {
if let revealedSeed {
SeedRevealView(seed: revealedSeed, symbol: model.symbol) { showSeed = false }
}
}
}
}
struct SendView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var destination = ""
@State private var amount = ""
@State private var tag = ""
@State private var priority: TransactionPriority = .defaultPriority
@State private var localError: String?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Send \(model.symbol)",
subtitle: "Balance \(model.balance.total) \(model.symbol)",
onBack: { route = .home })
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: model.chain == .xrp ? "Destination address" : "Recipient address")
KoinsTextField(placeholder: model.chain == .xrp ? "r…" : "4…", text: $destination)
}
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Amount (\(model.symbol))")
KoinsTextField(placeholder: "0.0", text: $amount, keyboard: .decimalPad)
}
if model.chain == .xrp {
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Destination tag (optional)")
KoinsTextField(placeholder: "e.g. 12345", text: $tag, keyboard: .numberPad)
}
}
if model.chain == .xmr {
VStack(alignment: .leading, spacing: 8) {
FieldLabel(text: "Priority")
Picker("Priority", selection: $priority) {
ForEach([TransactionPriority.low, .medium, .high, .defaultPriority], id: \.rawValue) { item in
Text(item.label).tag(item)
}
}
.pickerStyle(.segmented)
}
}
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
PrimaryButton(title: model.isBusy ? "Sending…" : "Send", disabled: model.isBusy) {
Task {
localError = nil
model.clearMessages()
do {
try await model.send(to: destination, amount: amount, tag: tag, priority: priority)
amount = ""
tag = ""
route = .dashboard
} catch {
localError = error.localizedDescription
}
}
}
Spacer(minLength: 0)
}
}
}
struct ReceiveView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
private var address: String {
model.displayAddress
}
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Receive \(model.symbol)",
subtitle: "Share this address to get paid",
onBack: { route = .home })
Card {
VStack(spacing: 16) {
QRCodeView(text: address)
.frame(maxWidth: 240)
Text(address)
.font(.system(size: 12, design: .monospaced))
.foregroundStyle(Theme.muted)
.multilineTextAlignment(.center)
.lineLimit(3)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity)
}
SecondaryButton(title: "Copy address") {
UIPasteboard.general.string = address
}
if model.chain == .xrp {
Card {
Text("XRP accounts must hold at least 1 XRP as a reserve to be active. Only send XRP on the XRP Ledger to this address.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
} else {
Card {
Text("Send only Monero (XMR) to this address. Transfers need confirmations before the balance becomes spendable.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
Spacer(minLength: 0)
}
}
}
+379
View File
@@ -0,0 +1,379 @@
import SwiftUI
enum Route: Equatable {
case home
case create
case importWallet
case dashboard
case send
case receive
}
struct SeedBox: Identifiable {
let id = UUID()
let seed: String
let symbol: String
}
struct RootView: View {
@EnvironmentObject private var model: AppModel
@State private var route: Route = .home
@State private var seedBox: SeedBox?
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
ScrollView {
VStack(spacing: 18) {
switch route {
case .home:
HomeView(route: $route)
case .create:
CreateWalletView(route: $route, seedBox: $seedBox)
case .importWallet:
ImportWalletView(route: $route)
case .dashboard:
DashboardView(route: $route)
case .send:
SendView(route: $route)
case .receive:
ReceiveView(route: $route)
}
}
.padding(.horizontal, 20)
.padding(.top, 12)
.padding(.bottom, 40)
}
.scrollDismissesKeyboard(.interactively)
}
.preferredColorScheme(.dark)
.task { await model.bootstrap() }
.sheet(item: $seedBox) { box in
SeedRevealView(seed: box.seed, symbol: box.symbol) { seedBox = nil }
}
.onChange(of: model.chain) { _ in
if route == .dashboard { route = .home }
}
}
}
struct HomeView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var showDeleteConfirm = false
var body: some View {
VStack(spacing: 18) {
header
if model.hasWallet, model.chain == .xrp || model.moneroReady {
walletCard
} else if model.hasWallet {
moneroUnavailableCard
} else {
emptyState
}
if let message = model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
}
.confirmationDialog("Delete this wallet?",
isPresented: $showDeleteConfirm,
titleVisibility: .visible) {
Button("Delete wallet", role: .destructive) {
Task {
await model.deleteWallet()
route = .home
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("The keys stored on this device are removed. Make sure your recovery seed is backed up.")
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Koins")
.font(.system(size: 30, weight: .heavy))
.foregroundStyle(Theme.text)
Text("Monero and XRP wallet")
.font(.system(size: 13))
.foregroundStyle(Theme.muted)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var walletCard: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
set: { newValue in Task { await model.select(newValue) } }))
Card {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(model.wallet?.name ?? "Wallet")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Theme.text)
Spacer()
if model.chain == .xmr, let status = model.syncStatus {
SyncBadge(status: status)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Balance")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
Text("\(model.balance.total) \(model.symbol)")
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
Text("\(model.balance.unlocked) \(model.symbol) unlocked")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
AddressRow(address: model.displayAddress)
if model.chain == .xmr, let status = model.syncStatus {
SyncProgress(status: status, eta: model.syncEta)
}
}
}
HStack(spacing: 10) {
PrimaryButton(title: "Send") { route = .send }
SecondaryButton(title: "Receive") { route = .receive }
}
if model.chain == .xmr {
SecondaryButton(title: model.syncStatus?.syncing == true ? "Syncing…" : "Sync now") {
Task { await model.startMoneroSync() }
}
}
HStack(spacing: 10) {
SecondaryButton(title: "Refresh") { Task { await model.refresh(force: true) } }
SecondaryButton(title: "Delete") { showDeleteConfirm = true }
}
if !model.transactions.isEmpty {
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Activity")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
ForEach(model.transactions) { record in
TransactionRow(record: record, symbol: model.symbol)
if record.id != model.transactions.last?.id {
Divider().overlay(Theme.border)
}
}
}
}
} else if model.chain == .xrp, model.balance.funded == false {
Text("This account is not funded yet. Send XRP to the address above to activate it.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
if model.chain == .xrp, model.moneroReady, let status = model.syncStatus, status.syncing {
Card {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Circle()
.fill(status.connected ? Theme.success : Theme.danger)
.frame(width: 7, height: 7)
Text("Monero is syncing in the background")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Theme.text)
Spacer()
Text("\(Int(status.progress * 100))%")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Theme.muted)
}
SyncProgress(status: status, eta: model.syncEta)
}
}
}
}
}
private var moneroUnavailableCard: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
set: { newValue in Task { await model.select(newValue) } }))
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Monero wallet")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Theme.text)
Text("The wallet is on this device but could not be opened. It is safe to retry; nothing was deleted.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
SecondaryButton(title: "Retry") {
Task { await model.select(.xmr) }
}
}
}
}
}
private var emptyState: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
set: { newValue in Task { await model.select(newValue) } }))
Card {
VStack(alignment: .leading, spacing: 8) {
Text("\(model.chain.title) wallet")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Theme.text)
Text("Create a new wallet on this device, or import one with its recovery seed.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
VStack(spacing: 10) {
PrimaryButton(title: "Create wallet") { route = .create }
SecondaryButton(title: "Import wallet") { route = .importWallet }
}
}
}
}
struct SyncBadge: View {
var status: SyncStatus
private var label: String {
if status.syncing {
let percent = status.progress * 100
return percent < 10 ? String(format: "Syncing %.1f%%", percent) : "Syncing \(Int(percent))%"
}
return status.connected ? "Synced" : "Offline"
}
var body: some View {
HStack(spacing: 6) {
Circle()
.fill(status.connected ? Theme.success : Theme.danger)
.frame(width: 7, height: 7)
Text(label)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
}
}
}
/// Live scan progress: how far the wallet has reached, and roughly how long the
/// remaining blocks will take at the observed rate.
struct SyncProgress: View {
var status: SyncStatus
var eta: String?
var body: some View {
VStack(alignment: .leading, spacing: 6) {
ProgressView(value: status.progress)
.tint(Theme.accent)
HStack(spacing: 8) {
Text("Block \(status.height.formatted()) of \(status.daemonHeight.formatted())")
Spacer()
if status.syncing {
Text(eta.map { "about \($0) left" } ?? "estimating…")
} else if status.connected {
Text("up to date")
}
}
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
struct AddressRow: View {
var address: String
@State private var copied = false
var body: some View {
HStack(spacing: 8) {
Text(address.isEmpty ? "—" : address)
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(Theme.muted)
.lineLimit(1)
.truncationMode(.middle)
Spacer(minLength: 6)
Button {
UIPasteboard.general.string = address
copied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false }
} label: {
Image(systemName: copied ? "checkmark" : "doc.on.doc")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Theme.accent)
}
.disabled(address.isEmpty)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(Theme.surfaceAlt)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}
struct SeedRevealView: View {
var seed: String
var symbol: String
var onDone: () -> Void
@State private var acknowledged = false
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
VStack(alignment: .leading, spacing: 16) {
Text("Your recovery phrase")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Theme.text)
Text("Write these words down in order and keep them offline. Anyone with this phrase can spend your \(symbol).")
.font(.system(size: 13))
.foregroundStyle(Theme.muted)
Text(seed)
.font(.system(size: 15, design: .monospaced))
.foregroundStyle(Theme.text)
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Theme.surface)
.overlay(RoundedRectangle(cornerRadius: 16, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
Button {
UIPasteboard.general.string = seed
} label: {
Label("Copy phrase", systemImage: "doc.on.doc")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Theme.accent)
}
Toggle(isOn: $acknowledged) {
Text("I have written down my phrase")
.font(.system(size: 13))
.foregroundStyle(Theme.muted)
}
.tint(Theme.accent)
Spacer()
PrimaryButton(title: "Continue", disabled: !acknowledged, action: onDone)
}
.padding(22)
}
}
}
+142
View File
@@ -0,0 +1,142 @@
import SwiftUI
struct CreateWalletView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@Binding var seedBox: SeedBox?
@State private var name = ""
@State private var localError: String?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Create wallet",
subtitle: "A new \(model.chain.title) wallet on this device",
onBack: { route = .home })
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Wallet name")
KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name)
}
if model.chain == .xmr {
Card {
Text("The wallet file is created locally and encrypted on this device. Syncing starts right away and can take a while for a new Monero wallet.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
PrimaryButton(title: model.isBusy ? "Generating…" : "Generate wallet",
disabled: model.isBusy) {
Task {
localError = nil
model.clearMessages()
do {
let seed = try await model.createWallet(name: name)
seedBox = SeedBox(seed: seed, symbol: model.symbol)
route = .dashboard
} catch {
localError = error.localizedDescription
}
}
}
Spacer(minLength: 0)
}
}
}
struct ImportWalletView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var name = ""
@State private var secret = ""
@State private var restoreHeight = ""
@State private var localError: String?
@State private var tipError: String?
@State private var isLoadingTip = false
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Import wallet",
subtitle: "Restore a \(model.chain.title) wallet from its seed",
onBack: { route = .home })
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Wallet name")
KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name)
}
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: model.chain == .xrp ? "Recovery phrase" : "Mnemonic seed")
KoinsTextField(placeholder: model.chain == .xrp ? "12 words, or a family seed (sEd…)" : "25 word seed phrase",
text: $secret,
multiline: model.chain == .xmr)
}
if model.chain == .xmr {
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Restore height (optional)")
KoinsTextField(placeholder: "e.g. 3200000", text: $restoreHeight, keyboard: .numberPad)
}
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Without a height the wallet scans the whole chain — millions of blocks, which takes hours on a phone. Set the block your wallet was first used, or start from the current tip if you only care about new funds.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
Button {
Task {
tipError = nil
isLoadingTip = true
defer { isLoadingTip = false }
do {
let tip = try await MoneroService.shared.daemonHeight()
restoreHeight = String(tip)
} catch {
tipError = error.localizedDescription
}
}
} label: {
Text(isLoadingTip ? "Fetching chain tip…" : "Use the current chain tip")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Theme.accent)
}
.disabled(isLoadingTip)
if let tipError {
Text(tipError)
.font(.system(size: 11))
.foregroundStyle(Theme.danger)
}
}
}
}
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
PrimaryButton(title: model.isBusy ? "Importing…" : "Import wallet",
disabled: model.isBusy) {
Task {
localError = nil
model.clearMessages()
do {
let height = UInt64(restoreHeight.trimmingCharacters(in: .whitespaces))
try await model.importWallet(name: name, secret: secret, restoreHeight: height)
route = .dashboard
} catch {
localError = error.localizedDescription
}
}
}
Spacer(minLength: 0)
}
}
}
+224
View File
@@ -0,0 +1,224 @@
import Foundation
/// Monero amounts are atomic units (1 XMR = 1e12 piconero).
enum MoneroAmount {
static let atomicPerXMR: UInt64 = 1_000_000_000_000
static func xmr(fromAtomic value: String) -> String {
guard let atomic = UInt64(value) else { return "0" }
return xmr(fromAtomic: atomic)
}
static func xmr(fromAtomic value: UInt64) -> String {
let whole = value / atomicPerXMR
let fraction = value % atomicPerXMR
if fraction == 0 { return String(whole) }
var text = String(format: "%012llu", fraction)
while text.hasSuffix("0") { text.removeLast() }
return "\(whole).\(text)"
}
static func atomic(fromXMR value: String) throws -> UInt64 {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, !text.hasPrefix("-") else { throw XRPError.rpc("Enter a valid amount") }
let parts = text.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") }
guard let whole = UInt64(parts[0].isEmpty ? "0" : String(parts[0])) else {
throw XRPError.rpc("Enter a valid amount")
}
var fraction: UInt64 = 0
if parts.count == 2 {
let fractionText = String(parts[1])
guard fractionText.count <= 12, fractionText.allSatisfy({ $0.isNumber }) else {
throw XRPError.rpc("XMR supports at most 12 decimal places")
}
let padded = fractionText.padding(toLength: 12, withPad: "0", startingAt: 0)
guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") }
fraction = parsed
}
let (wholeAtomic, overflow) = whole.multipliedReportingOverflow(by: atomicPerXMR)
guard !overflow else { throw XRPError.rpc("Amount is too large") }
let (total, carry) = wholeAtomic.addingReportingOverflow(fraction)
guard !carry else { throw XRPError.rpc("Amount is too large") }
return total
}
}
/// Swift wrapper around the Objective-C++ monero-cpp bridge.
final class MoneroService {
static let shared = MoneroService()
/// Wallet files that exist on disk without a stored record, so a lost
/// preference file does not strand a wallet.
static func orphanedWalletNames() -> [String] {
let manager = FileManager.default
guard let documents = manager.urls(for: .documentDirectory, in: .userDomainMask).first,
let items = try? manager.contentsOfDirectory(at: documents,
includingPropertiesForKeys: nil) else {
return []
}
let keys = Set(items.filter { $0.pathExtension == "keys" }
.map { $0.deletingPathExtension().lastPathComponent })
return items
.filter { $0.pathExtension.isEmpty && !$0.lastPathComponent.hasPrefix(".") }
.map { $0.lastPathComponent }
.filter { keys.contains($0) }
.sorted()
}
/// The wallet file is encrypted with this password; the recovery seed lives
/// in the Keychain.
static let walletPassword = "koins"
/// Default daemon, kept in sync with the bridge. Used for the cheap
/// daemon-only queries that do not need an open wallet.
static let daemonHost = "xmr-node.cakewallet.com"
/// Current chain tip, so an import can start scanning from now instead of
/// from the genesis block.
func daemonHeight() async throws -> UInt64 {
guard let url = URL(string: "http://\(Self.daemonHost):18081/json_rpc") else {
throw XRPError.network("Invalid Monero daemon address")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
request.httpBody = try JSONSerialization.data(withJSONObject: [
"jsonrpc": "2.0",
"id": "koins",
"method": "get_info",
])
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw XRPError.network("Monero daemon is unreachable")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = json["result"] as? [String: Any],
let height = (result["height"] as? NSNumber)?.uint64Value else {
throw XRPError.network("Could not read the Monero chain tip")
}
return height
}
private func invoke(_ request: [String: Any]) async throws -> [String: Any] {
let json = try JSONSerialization.data(withJSONObject: request)
let requestString = String(data: json, encoding: .utf8) ?? "{}"
let responseString: String = await withCheckedContinuation { continuation in
KoinsMoneroWallet.invoke(requestString) { response in
continuation.resume(returning: response)
}
}
guard let data = responseString.data(using: .utf8),
let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw XRPError.rpc("Unexpected response from the Monero wallet")
}
if let error = response["error"] as? String, !error.isEmpty {
throw XRPError.rpc(error)
}
return response
}
// MARK: - Wallet lifecycle
func createWallet(name: String) async throws -> (address: String, mnemonic: String) {
let response = try await invoke(["op": "createWallet",
"name": name,
"password": Self.walletPassword,
"language": "English"])
return (response["address"] as? String ?? "", response["mnemonic"] as? String ?? "")
}
func openWallet(name: String) async throws -> String {
let response = try await invoke(["op": "openWallet",
"name": name,
"password": Self.walletPassword])
return response["address"] as? String ?? ""
}
func restoreWallet(name: String,
seed: String,
restoreHeight: UInt64) async throws -> String {
let response = try await invoke(["op": "restoreWallet",
"name": name,
"password": Self.walletPassword,
"seed": seed,
"restoreHeight": restoreHeight])
return response["address"] as? String ?? ""
}
func removeWallet(name: String) async {
_ = try? await invoke(["op": "removeWallet", "name": name])
}
func closeWallet() async {
_ = try? await invoke(["op": "closeWallet"])
}
func seed() async throws -> String {
let response = try await invoke(["op": "getSeed"])
return response["mnemonic"] as? String ?? ""
}
// MARK: - Reads
func balance() async throws -> Balance {
let response = try await invoke(["op": "getBalance"])
let total = response["balance"] as? String ?? "0"
let unlocked = response["unlocked"] as? String ?? total
return Balance(total: MoneroAmount.xmr(fromAtomic: total),
unlocked: MoneroAmount.xmr(fromAtomic: unlocked),
funded: (UInt64(total) ?? 0) > 0)
}
func transactions() async throws -> [TransactionRecord] {
let response = try await invoke(["op": "getTransactions"])
let entries = response["transactions"] as? [[String: Any]] ?? []
return entries.map { entry in
let timestamp = (entry["timestamp"] as? NSNumber)?.doubleValue ?? 0
return TransactionRecord(
id: entry["hash"] as? String ?? UUID().uuidString,
amount: MoneroAmount.xmr(fromAtomic: entry["amount"] as? String ?? "0"),
timestamp: timestamp > 0 ? Date(timeIntervalSince1970: timestamp) : nil,
direction: (entry["direction"] as? String) == "in" ? .incoming : .outgoing,
confirmed: (entry["confirmed"] as? Bool) ?? false,
fee: MoneroAmount.xmr(fromAtomic: entry["fee"] as? String ?? "0"),
tag: nil,
counterparty: nil)
}
}
// MARK: - Sync
func startSync() async throws -> Bool {
let response = try await invoke(["op": "startSync"])
return (response["syncing"] as? Bool) ?? false
}
func stopSync() async {
_ = try? await invoke(["op": "stopSync"])
}
func syncStatus() async throws -> SyncStatus {
let response = try await invoke(["op": "getSyncStatus"])
return SyncStatus(height: (response["height"] as? NSNumber)?.uint64Value ?? 0,
daemonHeight: (response["daemonHeight"] as? NSNumber)?.uint64Value ?? 0,
syncing: (response["syncing"] as? Bool) ?? false,
connected: (response["connected"] as? Bool) ?? false)
}
// MARK: - Sends
func send(address: String, amountXMR: String, priority: TransactionPriority) async throws -> (hash: String, fee: String) {
let atomic = try MoneroAmount.atomic(fromXMR: amountXMR)
let response = try await invoke(["op": "transferTo",
"address": address.trimmingCharacters(in: .whitespacesAndNewlines),
"amount": String(atomic),
"priority": priority.rawValue])
return (response["txHash"] as? String ?? "",
MoneroAmount.xmr(fromAtomic: response["fee"] as? String ?? "0"))
}
}
+53
View File
@@ -0,0 +1,53 @@
import Foundation
/// Exact decimal <-> drops conversion (1 XRP = 1_000_000 drops) without
/// floating point rounding errors.
enum XRPAmount {
static let dropsPerXRP: UInt64 = 1_000_000
static let maxDrops: UInt64 = 100_000_000_000 * dropsPerXRP // 1e11 XRP
static func drops(fromXRP value: String) throws -> UInt64 {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, !text.hasPrefix("-"), !text.hasPrefix("+") else {
throw XRPError.rpc("Enter a valid amount")
}
let parts = text.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") }
let wholeText = parts[0].isEmpty ? "0" : String(parts[0])
guard let whole = UInt64(wholeText) else { throw XRPError.rpc("Enter a valid amount") }
var fraction: UInt64 = 0
if parts.count == 2 {
let fractionText = String(parts[1])
guard fractionText.count <= 6, fractionText.allSatisfy({ $0.isNumber }) else {
throw XRPError.rpc("XRP supports at most 6 decimal places")
}
let padded = fractionText.padding(toLength: 6, withPad: "0", startingAt: 0)
guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") }
fraction = parsed
}
let (wholeDrops, overflow) = whole.multipliedReportingOverflow(by: dropsPerXRP)
guard !overflow else { throw XRPError.rpc("Amount is too large") }
let (total, carry) = wholeDrops.addingReportingOverflow(fraction)
guard !carry, total <= maxDrops else { throw XRPError.rpc("Amount is too large") }
return total
}
static func xrp(fromDrops drops: String) -> String {
guard let value = UInt64(drops.trimmingCharacters(in: .whitespacesAndNewlines)) else {
return "0"
}
return xrp(fromDrops: value)
}
static func xrp(fromDrops value: UInt64) -> String {
let whole = value / dropsPerXRP
let fraction = value % dropsPerXRP
if fraction == 0 { return String(whole) }
var fractionText = String(format: "%06llu", fraction)
while fractionText.hasSuffix("0") { fractionText.removeLast() }
return "\(whole).\(fractionText)"
}
}
+136
View File
@@ -0,0 +1,136 @@
import Foundation
/// Canonical XRP Ledger binary serialization for the Payment transactions this
/// wallet sends, plus the signing / transaction-id hashing around it.
struct XRPPayment {
var account: String
var destination: String
var amountDrops: UInt64
var feeDrops: UInt64
var sequence: UInt32
var lastLedgerSequence: UInt32?
var destinationTag: UInt32?
var signingPublicKey: [UInt8]
var signature: [UInt8]?
private static let typeUInt16: UInt8 = 1
private static let typeUInt32: UInt8 = 2
private static let typeAmount: UInt8 = 6
private static let typeBlob: UInt8 = 7
private static let typeAccountID: UInt8 = 8
func serialized(includingSignature: Bool) throws -> [UInt8] {
var output: [UInt8] = []
// Fields must appear in (type, field) order.
output += header(type: Self.typeUInt16, field: 2) // TransactionType
output += uint16(0) // ttPAYMENT
output += header(type: Self.typeUInt32, field: 2) // Flags
output += uint32(0)
output += header(type: Self.typeUInt32, field: 4) // Sequence
output += uint32(sequence)
if let tag = destinationTag {
output += header(type: Self.typeUInt32, field: 14) // DestinationTag
output += uint32(tag)
}
if let lastLedger = lastLedgerSequence {
output += header(type: Self.typeUInt32, field: 27) // LastLedgerSequence
output += uint32(lastLedger)
}
output += header(type: Self.typeAmount, field: 1) // Amount
output += try amount(amountDrops)
output += header(type: Self.typeAmount, field: 8) // Fee
output += try amount(feeDrops)
output += header(type: Self.typeBlob, field: 3) // SigningPubKey
output += variableLength(signingPublicKey)
if includingSignature, let signature {
output += header(type: Self.typeBlob, field: 4) // TxnSignature
output += variableLength(signature)
}
output += header(type: Self.typeAccountID, field: 1) // Account
output += variableLength(try XRPKey.accountId(fromAddress: account))
output += header(type: Self.typeAccountID, field: 3) // Destination
output += variableLength(try XRPKey.accountId(fromAddress: destination))
return output
}
/// Bytes the private key signs (HashPrefix.transactionSig + tx).
func signingPayload() throws -> [UInt8] {
[0x53, 0x54, 0x58, 0x00] + (try serialized(includingSignature: false))
}
/// The signed transaction blob submitted to the network (no hash prefix).
func txBlob() throws -> [UInt8] {
guard signature != nil else { throw XRPError.rpc("Transaction is not signed") }
return try serialized(includingSignature: true)
}
func transactionHash() throws -> [UInt8] {
guard signature != nil else { throw XRPError.rpc("Transaction is not signed") }
let blob = try serialized(includingSignature: true)
return XRPHash.sha512Half([0x54, 0x58, 0x4e, 0x00] + blob)
}
// MARK: - Encoding helpers
private func header(type: UInt8, field: UInt8) -> [UInt8] {
if field < 16 { return [(type << 4) | field] }
return [type << 4, field]
}
private func uint16(_ value: UInt16) -> [UInt8] {
[UInt8(value >> 8), UInt8(value & 0xff)]
}
private func uint32(_ value: UInt32) -> [UInt8] {
[
UInt8((value >> 24) & 0xff),
UInt8((value >> 16) & 0xff),
UInt8((value >> 8) & 0xff),
UInt8(value & 0xff),
]
}
/// Native (XRP) amount: 8 bytes big-endian drops with the XRP flag bit set.
private func amount(_ drops: UInt64) throws -> [UInt8] {
guard drops <= XRPAmount.maxDrops else { throw XRPError.rpc("Amount is too large") }
var bytes = uint64(drops)
bytes[0] |= 0x40
return bytes
}
private func uint64(_ value: UInt64) -> [UInt8] {
var bytes: [UInt8] = []
for shift in stride(from: 56, through: 0, by: -8) {
bytes.append(UInt8((value >> UInt64(shift)) & 0xff))
}
return bytes
}
private func variableLength(_ bytes: [UInt8]) -> [UInt8] {
var output: [UInt8] = []
let count = bytes.count
if count < 192 {
output.append(UInt8(count))
} else if count <= 12_480 {
output.append(UInt8((count >> 8) + 192))
output.append(UInt8(count & 0xff))
} else {
output.append(UInt8((count >> 16) + 240))
output.append(UInt8((count >> 8) & 0xff))
output.append(UInt8(count & 0xff))
}
return output + bytes
}
}
+72
View File
@@ -0,0 +1,72 @@
import Foundation
/// Minimal XRP Ledger JSON-RPC client. A couple of public endpoints are tried
/// in order so a single outage does not break the wallet.
final class XRPClient {
static let shared = XRPClient()
private let endpoints = [
"https://s1.ripple.com:51234/",
"https://s2.ripple.com:51234/",
"https://xrplcluster.com/",
]
private var preferredEndpoint = 0
private let session: URLSession
private init() {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 20
configuration.waitsForConnectivity = false
session = URLSession(configuration: configuration)
}
func rpc(_ method: String, _ params: [String: Any] = [:]) async throws -> [String: Any] {
var lastError: Error = XRPError.network("XRP network is unavailable")
for attempt in 0..<endpoints.count {
let index = (preferredEndpoint + attempt) % endpoints.count
guard let url = URL(string: endpoints[index]) else { continue }
do {
let result = try await send(method, params, to: url)
preferredEndpoint = index
return result
} catch {
lastError = error
}
}
throw lastError
}
private func send(_ method: String, _ params: [String: Any], to url: URL) async throws -> [String: Any] {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = ["method": method, "params": [params], "id": 1]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw XRPError.network("Ledger request failed (\(method))")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw XRPError.network("Malformed ledger response")
}
let result = json["result"] as? [String: Any] ?? [:]
if let error = result["error"] as? String {
let message = result["error_message"] as? String ?? error
throw XRPError.rpc(message)
}
if let error = json["error"] as? String {
throw XRPError.rpc(json["error_message"] as? String ?? error)
}
return result
}
static func isAccountNotFound(_ error: Error) -> Bool {
if case XRPError.rpc(let message) = error {
return message == "actNotFound" || message.contains("Account not found")
}
return false
}
}
+148
View File
@@ -0,0 +1,148 @@
import CryptoKit
import Foundation
enum XRPAlgorithm: String {
case ed25519
case secp256k1
}
/// Set by the app target so this file stays compilable outside iOS.
enum Secp256k1Backend {
static var derivePrivateKey: ((Data) -> Data?)?
static var publicKey: ((Data) -> Data?)?
static var sign: ((Data, Data) -> Data?)?
static var addScalar: ((Data, Data) -> Data?)?
}
struct XRPKeypair {
let algorithm: XRPAlgorithm
/// ed25519: 32-byte seed. secp256k1: 32-byte scalar.
let privateKey: [UInt8]
/// ed25519: 0xED + 32-byte key. secp256k1: 33-byte compressed point.
let publicKey: [UInt8]
var address: String { XRPKey.address(fromPublicKey: publicKey) }
var signingPublicKeyHex: String { Hex.encode(publicKey) }
}
enum XRPKey {
static let ed25519SeedVersion: [UInt8] = [0x01, 0xe1, 0x4b]
static let familySeedVersion: [UInt8] = [0x21]
static let accountIdVersion: [UInt8] = [0x00]
static let ed25519Prefix: UInt8 = 0xED
// MARK: - Seeds
static func generateSeed(algorithm: XRPAlgorithm = .ed25519) -> String {
var entropy = [UInt8](repeating: 0, count: 16)
for index in entropy.indices { entropy[index] = UInt8.random(in: 0...255) }
let version = algorithm == .ed25519 ? ed25519SeedVersion : familySeedVersion
return XRPBase58.checkEncode(version: version, payload: entropy)
}
static func decodeSeed(_ seed: String) throws -> (entropy: [UInt8], algorithm: XRPAlgorithm) {
let decoded = try XRPBase58.decode(seed.trimmingCharacters(in: .whitespacesAndNewlines))
guard decoded.count > 4 else { throw XRPError.invalidSeed }
let body = Array(decoded[0..<(decoded.count - 4)])
let checksum = Array(decoded[(decoded.count - 4)...])
guard XRPHash.checksum(body) == checksum else { throw XRPError.invalidSeed }
if body.count == ed25519SeedVersion.count + 16, Array(body.prefix(3)) == ed25519SeedVersion {
return (Array(body.dropFirst(3)), .ed25519)
}
if body.count == familySeedVersion.count + 16, body[0] == familySeedVersion[0] {
return (Array(body.dropFirst(1)), .secp256k1)
}
throw XRPError.unsupportedSeed
}
// MARK: - Keys
/// Recovery phrase for a new wallet: 12 BIP-39 words.
static func generateMnemonic() -> String {
BIP39.generateMnemonic()
}
/// Accepts either a 12 word BIP-39 recovery phrase or a legacy XRP family
/// seed (`sEd…` / `s…`).
static func keypair(fromSecret secret: String) throws -> XRPKeypair {
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
if BIP39.isMnemonic(trimmed) {
return try keypair(fromMnemonic: trimmed)
}
return try keypair(fromSeed: trimmed)
}
/// Derives the account at `m/44'/144'/0'/0/0`, the path hardware and mobile
/// wallets use for XRP.
static func keypair(fromMnemonic mnemonic: String) throws -> XRPKeypair {
guard BIP39.entropy(from: mnemonic) != nil else {
throw XRPError.invalidSeed
}
let seed = BIP39.seed(from: mnemonic)
guard let master = BIP32.masterKey(fromSeed: seed),
let derived = BIP32.derive(BIP32.xrpPath, from: master) else {
throw XRPError.unsupportedSeed
}
return XRPKeypair(algorithm: .secp256k1,
privateKey: derived.privateKey,
publicKey: derived.publicKey)
}
static func keypair(fromSeed seed: String) throws -> XRPKeypair {
let (entropy, algorithm) = try decodeSeed(seed)
switch algorithm {
case .ed25519:
let raw = XRPHash.sha512Half(entropy)
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(raw))
let publicKey = [ed25519Prefix] + Array(privateKey.publicKey.rawRepresentation)
return XRPKeypair(algorithm: .ed25519, privateKey: raw, publicKey: publicKey)
case .secp256k1:
guard let derive = Secp256k1Backend.derivePrivateKey,
let toPublic = Secp256k1Backend.publicKey,
let privateKey = derive(Data(entropy)),
let publicKey = toPublic(privateKey) else {
throw XRPError.unsupportedSeed
}
return XRPKeypair(algorithm: .secp256k1,
privateKey: Array(privateKey),
publicKey: Array(publicKey))
}
}
static func address(fromPublicKey publicKey: [UInt8]) -> String {
let accountId = XRPHash.ripemd160(XRPHash.sha256(publicKey))
return XRPBase58.checkEncode(version: accountIdVersion, payload: accountId)
}
static func isValidAddress(_ address: String) -> Bool {
(try? XRPBase58.checkDecode(address, versionLength: 1)) != nil
}
static func accountId(fromAddress address: String) throws -> [UInt8] {
let payload = try XRPBase58.checkDecode(address, versionLength: 1)
guard payload.count == 20 else { throw XRPError.invalidAddress }
return payload
}
// MARK: - Signing
/// ed25519 returns a 64-byte signature; secp256k1 a DER-encoded one, matching
/// what the XRP Ledger expects for each signing scheme.
static func sign(message: [UInt8], keypair: XRPKeypair) throws -> [UInt8] {
switch keypair.algorithm {
case .ed25519:
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(keypair.privateKey))
let signature = try privateKey.signature(for: Data(message))
return Array(signature)
case .secp256k1:
let digest = XRPHash.sha512Half(message)
guard let signFn = Secp256k1Backend.sign,
let signature = signFn(Data(digest), Data(keypair.privateKey)) else {
throw XRPError.unsupportedSeed
}
return Array(signature)
}
}
}
@@ -0,0 +1,157 @@
import Foundation
/// High level XRP operations: wallet creation/import, balances, history, sends.
final class XRPWalletService {
static let shared = XRPWalletService()
private let rippleEpoch: TimeInterval = 946_684_800
// MARK: - Wallet lifecycle
func createWallet(name: String) throws -> (WalletRecord, String) {
let mnemonic = XRPKey.generateMnemonic()
let keypair = try XRPKey.keypair(fromMnemonic: mnemonic)
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
name: name,
address: keypair.address,
createdAt: Date())
WalletStore.saveWallet(record, chain: .xrp)
WalletStore.saveSecret(mnemonic, chain: .xrp)
return (record, mnemonic)
}
func importWallet(name: String, secret: String) throws -> WalletRecord {
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
let keypair = try XRPKey.keypair(fromSecret: trimmed)
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
name: name,
address: keypair.address,
createdAt: Date())
WalletStore.saveWallet(record, chain: .xrp)
WalletStore.saveSecret(trimmed, chain: .xrp)
return record
}
func address(forSecret secret: String) throws -> String {
try XRPKey.keypair(fromSecret: secret.trimmingCharacters(in: .whitespacesAndNewlines)).address
}
// MARK: - Reads
func balance(address: String) async throws -> Balance {
do {
let result = try await XRPClient.shared.rpc("account_info",
["account": address,
"ledger_index": "current"])
let accountData = result["account_data"] as? [String: Any] ?? [:]
let drops = accountData["Balance"] as? String ?? "0"
return Balance(total: XRPAmount.xrp(fromDrops: drops), unlocked: "", funded: true)
} catch {
if XRPClient.isAccountNotFound(error) {
return Balance(total: "0", unlocked: "", funded: false)
}
throw error
}
}
func transactions(address: String) async throws -> [TransactionRecord] {
let result = try await XRPClient.shared.rpc("account_tx",
["account": address,
"ledger_index_min": -1,
"ledger_index_max": -1,
"limit": 40])
let entries = result["transactions"] as? [[String: Any]] ?? []
var records: [TransactionRecord] = []
for entry in entries {
guard let tx = entry["tx_json"] as? [String: Any] ?? entry["tx"] as? [String: Any] else { continue }
guard (tx["TransactionType"] as? String) == "Payment" else { continue }
let meta = entry["meta"] as? [String: Any] ?? entry["metaData"] as? [String: Any] ?? [:]
if let txResult = meta["TransactionResult"] as? String, txResult != "tesSUCCESS" { continue }
// IOU / MPT deliveries carry an object; only native XRP is shown.
let delivered = (meta["delivered_amount"] as? String)
?? (meta["DeliveredAmount"] as? String)
?? (tx["Amount"] as? String)
guard let deliveredDrops = delivered else { continue }
let destination = tx["Destination"] as? String ?? ""
let isIncoming = destination == address
let date = (tx["date"] as? NSNumber).map { Date(timeIntervalSince1970: $0.doubleValue + rippleEpoch) }
records.append(TransactionRecord(
id: entry["hash"] as? String ?? tx["hash"] as? String ?? UUID().uuidString,
amount: XRPAmount.xrp(fromDrops: deliveredDrops),
timestamp: date,
direction: isIncoming ? .incoming : .outgoing,
confirmed: (entry["validated"] as? Bool) ?? false,
fee: (tx["Fee"] as? String).map { XRPAmount.xrp(fromDrops: $0) },
tag: (tx["DestinationTag"] as? NSNumber)?.intValue,
counterparty: isIncoming ? (tx["Account"] as? String) : destination))
}
return records
}
// MARK: - Sends
func send(secret: String,
to: String,
amountXRP: String,
destinationTag: UInt32?) async throws -> (hash: String, fee: String) {
let destination = to.trimmingCharacters(in: .whitespacesAndNewlines)
guard XRPKey.isValidAddress(destination) else {
throw XRPError.rpc("Enter a valid XRP address")
}
let keypair = try XRPKey.keypair(fromSeed: secret)
let amountDrops = try XRPAmount.drops(fromXRP: amountXRP)
async let infoCall = XRPClient.shared.rpc("account_info",
["account": keypair.address,
"ledger_index": "current"])
async let feeCall = XRPClient.shared.rpc("fee", [:])
async let ledgerCall = XRPClient.shared.rpc("ledger_current", [:])
let info = try await infoCall
let accountData = info["account_data"] as? [String: Any] ?? [:]
guard let sequence = (accountData["Sequence"] as? NSNumber)?.uint32Value else {
throw XRPError.rpc("Could not read the account sequence")
}
let balanceDrops = UInt64((accountData["Balance"] as? String) ?? "0") ?? 0
let feeResult = try await feeCall
let feeDrops = (feeResult["drops"] as? [String: Any])
.flatMap { $0["open_ledger_fee"] as? String }
.flatMap { UInt64($0) } ?? 12
let ledger = try await ledgerCall
let ledgerIndex = (ledger["ledger_current_index"] as? NSNumber)?.uint32Value
guard amountDrops + feeDrops <= balanceDrops else {
throw XRPError.rpc("Insufficient balance to cover the amount and network fee")
}
var payment = XRPPayment(account: keypair.address,
destination: destination,
amountDrops: amountDrops,
feeDrops: feeDrops,
sequence: sequence,
lastLedgerSequence: ledgerIndex.map { $0 + 20 },
destinationTag: destinationTag,
signingPublicKey: keypair.publicKey,
signature: nil)
let signature = try XRPKey.sign(message: payment.signingPayload(), keypair: keypair)
payment.signature = signature
let blob = Hex.encode(try payment.txBlob())
let result = try await XRPClient.shared.rpc("submit", ["tx_blob": blob])
let engineResult = result["engine_result"] as? String ?? ""
guard engineResult.hasPrefix("tes") else {
let message = result["engine_result_message"] as? String ?? "Transaction failed"
throw XRPError.rpc("\(engineResult): \(message)")
}
return (Hex.encode(try payment.transactionHash()), XRPAmount.xrp(fromDrops: feeDrops))
}
}
+120
View File
@@ -0,0 +1,120 @@
# Monero native library for iOS
The wallet runs monero-project `wallet2` (plus monero-cpp as the C++ API) on
device. Everything is compiled into a single static archive that the app links.
## Artifacts
| File | Architecture | Platform |
|---|---|---|
| `libs/libkoins_monero_merged.a` | arm64 | iOS simulator |
| `libs/libkoins_monero_device.a` | arm64 | iOS device |
Both are needed: they are the same CPU architecture but different platforms, so
they cannot be merged into one fat archive. `project.yml` selects the right one
per SDK.
## Prerequisites
- Xcode with the iOS device and simulator SDKs
- CMake 3.2x (monero 0.18 is not tested with CMake 4)
- GNU make (the builds use the "Unix Makefiles" generator)
- Roughly 10 GB of disk and a long coffee break
## Sources
- monero-project v0.18.5.1
- monero-cpp (woodser), which keeps monero-project at `external/monero-project`
## Toolchain customisations
The files under `depends/` are the ones modified from upstream, kept here so the
archive can be regenerated:
| File | Change |
|---|---|
| `depends/hosts/ios.mk` | new host; `IOS_PLATFORM=SIMULATOR` (default) or `DEVICE` selects the SDK and the target triple (`aarch64-apple-ios[-simulator]`) |
| `depends/packages/packages.mk` | `ios_packages := boost openssl zeromq expat unbound sodium` (no libiconv) |
| `depends/packages/boost.mk` | boost for the `ios` toolset: darwin toolset, libtool archiver, c++11, no b2 python |
| `depends/packages/zeromq.mk` | ios config opts (`--host=aarch64-apple-darwin`), no libsodium, no curve |
| `depends/packages/unbound.mk` | drops the autoconf dependency |
| `depends/packages/sodium.mk` | drops the autoconf dependency |
| `depends/funcs.mk` | create the prefix `lib` directory before extracting dependency archives |
One source change is also required, in monero-project:
- `src/cryptonote_basic/miner.cpp` — wrap the IOKit includes and the related
code with `defined(TARGET_OS_OSX) && TARGET_OS_OSX`; IOKit is unavailable on
iOS. Two locations, around lines 51 and 986.
Two more steps are needed when targeting a device:
- Copy monero's patched boost serialisation headers into the dependency prefix,
because monero's build otherwise only finds them for the platform it was
installed for:
```sh
cp external/boost/archive/portable_binary_*.hpp \
contrib/depends/$HOST/include/boost/archive/
```
- randomx's AArch64 JIT references `__clear_cache`, which libSystem exports on
macOS and the simulator but not on iOS. The app supplies it in
`Koins/Support/ClearCache.c`.
## Build
Set `PLATFORM` to `SIMULATOR` or `DEVICE`; the only differences are the depends
host flag and the output archive name.
```sh
MONERO=/path/to/monero-project
PLATFORM=SIMULATOR # or DEVICE
HOST=aarch64-apple-ios
# 1. Cross-compiled dependencies (boost, openssl, zeromq, expat, unbound, sodium)
cd "$MONERO/contrib/depends"
make HOST=$HOST IOS_PLATFORM=$PLATFORM -j"$(sysctl -n hw.ncpu)"
# 2. monero-project, static wallet library
cd "$MONERO"
mkdir -p build/ios-$PLATFORM && cd build/ios-$PLATFORM
cmake -G "Unix Makefiles" \
-DCMAKE_TOOLCHAIN_FILE="$MONERO/contrib/depends/$HOST/share/toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DSTATIC=ON -DBUILD_SHARED_LIBS=OFF \
-DBUILD_TESTS=OFF -DBUILD_DOCUMENTATION=OFF -DUSE_DEVICE_TREZOR=OFF \
../..
make -j"$(sysctl -n hw.ncpu)" wallet # produces lib/libwallet.a
# 3. monero-cpp
cd /path/to/monero-cpp
mkdir -p build-ios-$PLATFORM && cd build-ios-$PLATFORM
cmake -G "Unix Makefiles" \
-DCMAKE_TOOLCHAIN_FILE="$MONERO/contrib/depends/$HOST/share/toolchain.cmake" \
../..
make -j"$(sysctl -n hw.ncpu)" # produces libmonero-cpp.a
# 4. Merge into one archive for the app
libtool -static -o libkoins_monero_merged.a \
libmonero-cpp.a \
"$MONERO/build/ios-$PLATFORM/lib/libwallet.a" \
"$MONERO/build/ios-$PLATFORM"/src/*/lib*.a \
"$MONERO/build/ios-$PLATFORM"/external/*/lib*.a \
"$MONERO/build/ios-$PLATFORM"/contrib/epee/src/libepee.a \
"$MONERO/contrib/depends/$HOST"/lib/lib*.a
```
Copy the result to `libs/`, using `libkoins_monero_merged.a` for the simulator
and `libkoins_monero_device.a` for the device.
## Notes
- Only the simulator archive is needed to run in the simulator; the device
archive is needed for a real iPhone, and Xcode requires the matching one at
link time.
- Building the device slice after a simulator build requires moving the
simulator depends prefix aside first, since depends installs into
`contrib/depends/<host>` and both platforms share the host name.
- The app also links the OpenSSL that is embedded in this archive, which is how
the XRP wallet gets secp256k1 without an extra dependency.
+270
View File
@@ -0,0 +1,270 @@
define int_vars
#Set defaults for vars which may be overridden per-package
$(1)_cc=$($($(1)_type)_CC)
$(1)_cxx=$($($(1)_type)_CXX)
$(1)_objc=$($($(1)_type)_OBJC)
$(1)_objcxx=$($($(1)_type)_OBJCXX)
$(1)_ar=$($($(1)_type)_AR)
$(1)_ranlib=$($($(1)_type)_RANLIB)
$(1)_libtool=$($($(1)_type)_LIBTOOL)
$(1)_nm=$($($(1)_type)_NM)
$(1)_cflags=$($($(1)_type)_CFLAGS) $($($(1)_type)_$(release_type)_CFLAGS)
$(1)_cxxflags=$($($(1)_type)_CXXFLAGS) $($($(1)_type)_$(release_type)_CXXFLAGS)
$(1)_arflags=$($($(1)_type)_ARFLAGS) $($($(1)_type)_$(release_type)_ARFLAGS)
$(1)_ldflags=$($($(1)_type)_LDFLAGS) $($($(1)_type)_$(release_type)_LDFLAGS) -L$($($(1)_type)_prefix)/lib
$(1)_cppflags=$($($(1)_type)_CPPFLAGS) $($($(1)_type)_$(release_type)_CPPFLAGS) -I$($($(1)_type)_prefix)/include
$(1)_recipe_hash:=
endef
define int_get_all_dependencies
$(sort $(foreach dep,$(2),$(2) $(call int_get_all_dependencies,$(1),$($(dep)_dependencies))))
endef
define fetch_file_inner
( mkdir -p $$($(1)_download_dir) && echo Fetching $(3) from $(2) && \
$(build_DOWNLOAD) "$$($(1)_download_dir)/$(4).temp" "$(2)/$(3)" && \
echo "$(5) $$($(1)_download_dir)/$(4).temp" > $$($(1)_download_dir)/.$(4).hash && \
$(build_SHA256SUM) -c $$($(1)_download_dir)/.$(4).hash && \
mv $$($(1)_download_dir)/$(4).temp $$($(1)_source_dir)/$(4) && \
rm -rf $$($(1)_download_dir) )
endef
define fetch_file
( test -f $$($(1)_source_dir)/$(4) || \
( $(call fetch_file_inner,$(1),$(2),$(3),$(4),$(5)) || \
$(call fetch_file_inner,$(1),$(FALLBACK_DOWNLOAD_PATH),$(4),$(4),$(5))))
endef
define int_get_build_recipe_hash
$(eval $(1)_all_file_checksums:=$(shell $(build_SHA256SUM) $(meta_depends) packages/$(1).mk $(addprefix $(PATCHES_PATH)/$(1)/,$($(1)_patches)) | cut -d" " -f1))
$(eval $(1)_recipe_hash:=$(shell echo -n "$($(1)_all_file_checksums)" | $(build_SHA256SUM) | cut -d" " -f1))
endef
define int_get_build_id
$(eval $(1)_dependencies += $($(1)_$(host_arch)_$(host_os)_dependencies) $($(1)_$(host_os)_dependencies))
$(eval $(1)_all_dependencies:=$(call int_get_all_dependencies,$(1),$($($(1)_type)_native_toolchain) $($(1)_dependencies)))
$(foreach dep,$($(1)_all_dependencies),$(eval $(1)_build_id_deps+=$(dep)-$($(dep)_version)-$($(dep)_recipe_hash)))
$(eval $(1)_build_id_long:=$(1)-$($(1)_version)-$($(1)_recipe_hash)-$(release_type) $($(1)_build_id_deps) $($($(1)_type)_id_string))
$(eval $(1)_build_id:=$(shell echo -n "$($(1)_build_id_long)" | $(build_SHA256SUM) | cut -c-$(HASH_LENGTH)))
final_build_id_long+=$($(package)_build_id_long)
#compute package-specific paths
$(1)_build_subdir?=.
$(1)_download_file?=$($(1)_file_name)
$(1)_source_dir:=$(SOURCES_PATH)
$(1)_source:=$$($(1)_source_dir)/$($(1)_file_name)
$(1)_staging_dir=$(base_staging_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)
$(1)_staging_prefix_dir:=$$($(1)_staging_dir)$($($(1)_type)_prefix)
$(1)_extract_dir:=$(base_build_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)
$(1)_download_dir:=$(base_download_dir)/$(1)-$($(1)_version)
$(1)_build_dir:=$$($(1)_extract_dir)/$$($(1)_build_subdir)
$(1)_cached_checksum:=$(BASE_CACHE)/$(host)/$(1)/$(1)-$($(1)_version)-$($(1)_build_id).tar.gz.hash
$(1)_patch_dir:=$(base_build_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)/.patches-$($(1)_build_id)
$(1)_prefixbin:=$($($(1)_type)_prefix)/bin/
$(1)_cached:=$(BASE_CACHE)/$(host)/$(1)/$(1)-$($(1)_version)-$($(1)_build_id).tar.gz
$(1)_all_sources=$($(1)_file_name) $($(1)_extra_sources)
#stamps
$(1)_fetched=$(SOURCES_PATH)/download-stamps/.stamp_fetched-$(1)-$($(1)_file_name).hash
$(1)_extracted=$$($(1)_extract_dir)/.stamp_extracted
$(1)_preprocessed=$$($(1)_extract_dir)/.stamp_preprocessed
$(1)_cleaned=$$($(1)_extract_dir)/.stamp_cleaned
$(1)_built=$$($(1)_build_dir)/.stamp_built
$(1)_configured=$$($(1)_build_dir)/.stamp_configured
$(1)_staged=$$($(1)_staging_dir)/.stamp_staged
$(1)_postprocessed=$$($(1)_staging_prefix_dir)/.stamp_postprocessed
$(1)_download_path_fixed=$(subst :,\:,$$($(1)_download_path))
#default commands
$(1)_fetch_cmds ?= $(call fetch_file,$(1),$(subst \:,:,$$($(1)_download_path_fixed)),$$($(1)_download_file),$($(1)_file_name),$($(1)_sha256_hash))
$(1)_extract_cmds ?= mkdir -p $$($(1)_extract_dir) && echo "$$($(1)_sha256_hash) $$($(1)_source)" > $$($(1)_extract_dir)/.$$($(1)_file_name).hash && $(build_SHA256SUM) -c $$($(1)_extract_dir)/.$$($(1)_file_name).hash && tar --strip-components=1 -xf $$($(1)_source)
$(1)_preprocess_cmds ?=
$(1)_build_cmds ?=
$(1)_config_cmds ?=
$(1)_stage_cmds ?=
$(1)_set_vars ?=
all_sources+=$$($(1)_fetched)
endef
#$(foreach dep_target,$($(1)_all_dependencies),$(eval $(1)_dependency_targets=$($(dep_target)_cached)))
define int_config_attach_build_config
$(eval $(call $(1)_set_vars,$(1)))
$(1)_cflags+=$($(1)_cflags_$(release_type))
$(1)_cflags+=$($(1)_cflags_$(host_arch)) $($(1)_cflags_$(host_arch)_$(release_type))
$(1)_cflags+=$($(1)_cflags_$(host_os)) $($(1)_cflags_$(host_os)_$(release_type))
$(1)_cflags+=$($(1)_cflags_$(host_arch)_$(host_os)) $($(1)_cflags_$(host_arch)_$(host_os)_$(release_type))
$(1)_cxxflags+=$($(1)_cxxflags_$(release_type))
$(1)_cxxflags+=$($(1)_cxxflags_$(host_arch)) $($(1)_cxxflags_$(host_arch)_$(release_type))
$(1)_cxxflags+=$($(1)_cxxflags_$(host_os)) $($(1)_cxxflags_$(host_os)_$(release_type))
$(1)_cxxflags+=$($(1)_cxxflags_$(host_arch)_$(host_os)) $($(1)_cxxflags_$(host_arch)_$(host_os)_$(release_type))
$(1)_arflags+=$($(1)_arflags_$(release_type))
$(1)_arflags+=$($(1)_arflags_$(host_arch)) $($(1)_arflags_$(host_arch)_$(release_type))
$(1)_arflags+=$($(1)_arflags_$(host_os)) $($(1)_arflags_$(host_os)_$(release_type))
$(1)_arflags+=$($(1)_arflags_$(host_arch)_$(host_os)) $($(1)_arflags_$(host_arch)_$(host_os)_$(release_type))
$(1)_cppflags+=$($(1)_cppflags_$(release_type))
$(1)_cppflags+=$($(1)_cppflags_$(host_arch)) $($(1)_cppflags_$(host_arch)_$(release_type))
$(1)_cppflags+=$($(1)_cppflags_$(host_os)) $($(1)_cppflags_$(host_os)_$(release_type))
$(1)_cppflags+=$($(1)_cppflags_$(host_arch)_$(host_os)) $($(1)_cppflags_$(host_arch)_$(host_os)_$(release_type))
$(1)_ldflags+=$($(1)_ldflags_$(release_type))
$(1)_ldflags+=$($(1)_ldflags_$(host_arch)) $($(1)_ldflags_$(host_arch)_$(release_type))
$(1)_ldflags+=$($(1)_ldflags_$(host_os)) $($(1)_ldflags_$(host_os)_$(release_type))
$(1)_ldflags+=$($(1)_ldflags_$(host_arch)_$(host_os)) $($(1)_ldflags_$(host_arch)_$(host_os)_$(release_type))
$(1)_build_opts+=$$($(1)_build_opts_$(release_type))
$(1)_build_opts+=$$($(1)_build_opts_$(host_arch)) $$($(1)_build_opts_$(host_arch)_$(release_type))
$(1)_build_opts+=$$($(1)_build_opts_$(host_os)) $$($(1)_build_opts_$(host_os)_$(release_type))
$(1)_build_opts+=$$($(1)_build_opts_$(host_arch)_$(host_os)) $$($(1)_build_opts_$(host_arch)_$(host_os)_$(release_type))
$(1)_config_opts+=$$($(1)_config_opts_$(release_type))
$(1)_config_opts+=$$($(1)_config_opts_$(host_arch)) $$($(1)_config_opts_$(host_arch)_$(release_type))
$(1)_config_opts+=$$($(1)_config_opts_$(host_os)) $$($(1)_config_opts_$(host_os)_$(release_type))
$(1)_config_opts+=$$($(1)_config_opts_$(host_arch)_$(host_os)) $$($(1)_config_opts_$(host_arch)_$(host_os)_$(release_type))
$(1)_config_env+=$$($(1)_config_env_$(release_type))
$(1)_config_env+=$($(1)_config_env_$(host_arch)) $($(1)_config_env_$(host_arch)_$(release_type))
$(1)_config_env+=$($(1)_config_env_$(host_os)) $($(1)_config_env_$(host_os)_$(release_type))
$(1)_config_env+=$($(1)_config_env_$(host_arch)_$(host_os)) $($(1)_config_env_$(host_arch)_$(host_os)_$(release_type))
$(1)_build_env+=$$($(1)_build_env_$(release_type))
$(1)_build_env+=$($(1)_build_env_$(host_arch)) $($(1)_build_env_$(host_arch)_$(release_type))
$(1)_build_env+=$($(1)_build_env_$(host_os)) $($(1)_build_env_$(host_os)_$(release_type))
$(1)_build_env+=$($(1)_build_env_$(host_arch)_$(host_os)) $($(1)_build_env_$(host_arch)_$(host_os)_$(release_type))
$(1)_config_env+=PKG_CONFIG_LIBDIR=$($($(1)_type)_prefix)/lib/pkgconfig
$(1)_config_env+=PKG_CONFIG_PATH=$($($(1)_type)_prefix)/share/pkgconfig
$(1)_config_env+=PATH="$(build_prefix)/bin:$(PATH)"
$(1)_build_env+=PATH="$(build_prefix)/bin:$(PATH)"
$(1)_stage_env+=PATH="$(build_prefix)/bin:$(PATH)"
$(1)_autoconf=./configure --build=$(BUILD) --host=$($($(1)_type)_host) --prefix=$($($(1)_type)_prefix) $$($(1)_config_opts) CC="$$($(1)_cc)" CXX="$$($(1)_cxx)"
ifeq ($(filter $(1),libusb unbound),)
$(1)_autoconf += --disable-dependency-tracking
endif
ifneq ($($(1)_nm),)
$(1)_autoconf += NM="$$($(1)_nm)"
endif
ifneq ($($(1)_ranlib),)
$(1)_autoconf += RANLIB="$$($(1)_ranlib)"
endif
ifneq ($($(1)_ar),)
$(1)_autoconf += AR="$$($(1)_ar)"
endif
ifneq ($($(1)_arflags),)
$(1)_autoconf += ARFLAGS="$$($(1)_arflags)"
endif
ifneq ($($(1)_cflags),)
$(1)_autoconf += CFLAGS="$$($(1)_cflags)"
endif
ifneq ($($(1)_cxxflags),)
$(1)_autoconf += CXXFLAGS="$$($(1)_cxxflags)"
endif
ifneq ($($(1)_cppflags),)
$(1)_autoconf += CPPFLAGS="$$($(1)_cppflags)"
endif
ifneq ($($(1)_ldflags),)
$(1)_autoconf += LDFLAGS="$$($(1)_ldflags)"
endif
endef
define int_add_cmds
$($(1)_fetched):
$(AT)mkdir -p $$(@D) $(SOURCES_PATH)
$(AT)rm -f $$@
$(AT)touch $$@
$(AT)cd $$(@D); $(call $(1)_fetch_cmds,$(1))
$(AT)cd $($(1)_source_dir); $(foreach source,$($(1)_all_sources),$(build_SHA256SUM) $(source) >> $$(@);)
$(AT)touch $$@
$($(1)_extracted): | $($(1)_fetched)
$(AT)echo Extracting $(1)...
$(AT)mkdir -p $$(@D)
$(AT)cd $$(@D); $(call $(1)_extract_cmds,$(1))
$(AT)touch $$@
$($(1)_preprocessed): | $($(1)_dependencies) $($(1)_extracted)
$(AT)echo Preprocessing $(1)...
$(AT)mkdir -p $$(@D) $($(1)_patch_dir)
$(AT)$(foreach patch,$($(1)_patches),cd $(PATCHES_PATH)/$(1); cp $(patch) $($(1)_patch_dir) ;)
$(AT)cd $$(@D); $(call $(1)_preprocess_cmds, $(1))
$(AT)touch $$@
$($(1)_configured): | $($(1)_preprocessed)
$(AT)echo Configuring $(1)...
$(AT)rm -rf $(host_prefix); mkdir -p $(host_prefix)/lib; cd $(host_prefix); $(foreach package,$($(1)_all_dependencies), $(if $(strip $($(package)_cached)), tar xf $($(package)_cached);,))
$(AT)mkdir -p $$(@D)
$(AT)+cd $$(@D); $($(1)_config_env) $(call $(1)_config_cmds, $(1))
$(AT)touch $$@
$($(1)_built): | $($(1)_configured)
$(AT)echo Building $(1)...
$(AT)mkdir -p $$(@D)
$(AT)+cd $$(@D); $($(1)_build_env) $(call $(1)_build_cmds, $(1))
$(AT)touch $$@
$($(1)_staged): | $($(1)_built)
$(AT)echo Staging $(1)...
$(AT)mkdir -p $($(1)_staging_dir)/$(host_prefix)
$(AT)cd $($(1)_build_dir); $($(1)_stage_env) $(call $(1)_stage_cmds, $(1))
$(AT)rm -rf $($(1)_extract_dir)
$(AT)touch $$@
$($(1)_postprocessed): | $($(1)_staged)
$(AT)echo Postprocessing $(1)...
$(AT)cd $($(1)_staging_prefix_dir); $(call $(1)_postprocess_cmds)
$(AT)touch $$@
$($(1)_cached): | $($(1)_dependencies) $($(1)_postprocessed)
$(AT)echo Caching $(1)...
$(AT)cd $$($(1)_staging_dir)/$(host_prefix); find . | sort | tar --no-recursion -czf $$($(1)_staging_dir)/$$(@F) -T -
$(AT)mkdir -p $$(@D)
$(AT)rm -rf $$(@D) && mkdir -p $$(@D)
$(AT)mv $$($(1)_staging_dir)/$$(@F) $$(@)
$(AT)rm -rf $($(1)_staging_dir)
$($(1)_cached_checksum): $($(1)_cached)
$(AT)cd $$(@D); $(build_SHA256SUM) $$(<F) > $$(@)
.PHONY: $(1)
$(1): | $($(1)_cached_checksum)
.SECONDARY: $($(1)_cached) $($(1)_postprocessed) $($(1)_staged) $($(1)_built) $($(1)_configured) $($(1)_preprocessed) $($(1)_extracted) $($(1)_fetched)
endef
stages = fetched extracted preprocessed configured built staged postprocessed cached cached_checksum
define ext_add_stages
$(foreach stage,$(stages),
$(1)_$(stage): $($(1)_$(stage))
.PHONY: $(1)_$(stage))
endef
# These functions create the build targets for each package. They must be
# broken down into small steps so that each part is done for all packages
# before moving on to the next step. Otherwise, a package's info
# (build-id for example) would only be available to another package if it
# happened to be computed already.
#set the type for host/build packages.
$(foreach native_package,$(native_packages),$(eval $(native_package)_type=build))
$(foreach package,$(packages),$(eval $(package)_type=$(host_arch)_$(host_os)))
#set overridable defaults
$(foreach package,$(all_packages),$(eval $(call int_vars,$(package))))
#include package files
$(foreach package,$(all_packages),$(eval include packages/$(package).mk))
#compute a hash of all files that comprise this package's build recipe
$(foreach package,$(all_packages),$(eval $(call int_get_build_recipe_hash,$(package))))
#generate a unique id for this package, incorporating its dependencies as well
$(foreach package,$(all_packages),$(eval $(call int_get_build_id,$(package))))
#compute final vars after reading package vars
$(foreach package,$(all_packages),$(eval $(call int_config_attach_build_config,$(package))))
#create build targets
$(foreach package,$(all_packages),$(eval $(call int_add_cmds,$(package))))
#special exception: if a toolchain package exists, all non-native packages depend on it
$(foreach package,$(packages),$(eval $($(package)_unpacked): |$($($(host_arch)_$(host_os)_native_toolchain)_cached) ))
@@ -0,0 +1,41 @@
# iOS host for monero depends system (arm64 simulator for dev).
# Targets modern iOS (14+) on Apple Silicon simulators / devices.
IOS_MIN_VERSION=13.0
LD64_VERSION=609
ifndef IOS_PLATFORM
IOS_PLATFORM=SIMULATOR
endif
ifeq ($(IOS_PLATFORM),SIMULATOR)
IOS_SDK_ROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
CC_target=aarch64-apple-ios-simulator
else
IOS_SDK_ROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
CC_target=aarch64-apple-ios
endif
ios_CC=clang -target $(CC_target) -miphoneos-version-min=$(IOS_MIN_VERSION) --sysroot $(IOS_SDK_ROOT) -mlinker-version=$(LD64_VERSION)
ios_CXX=clang++ -target $(CC_target) -miphoneos-version-min=$(IOS_MIN_VERSION) --sysroot $(IOS_SDK_ROOT) -mlinker-version=$(LD64_VERSION) -stdlib=libc++
ios_CFLAGS=-pipe -isysroot $(IOS_SDK_ROOT)
ios_CXXFLAGS=-pipe -isysroot $(IOS_SDK_ROOT) -std=c++14 -stdlib=libc++
ios_ARFLAGS=cr
ios_release_CFLAGS=-O1
ios_release_CXXFLAGS=$(ios_release_CFLAGS)
ios_debug_CFLAGS=-O1
ios_debug_CXXFLAGS=$(ios_debug_CFLAGS)
ios_native_toolchain=native_cctools darwin_sdk
# iOS uses the system (macOS) ar/ranlib/strip/nm for cross-compiled archives.
ios_AR=ar
ios_RANLIB=ranlib
ios_STRIP=strip
ios_NM=nm
ios_LIBTOOL=libtool
ios_OTOOL=otool
ios_INSTALL_NAME_TOOL=install_name_tool
@@ -0,0 +1,46 @@
package=boost
$(package)_version=1.76.0
$(package)_download_path=https://archives.boost.io/release/$($(package)_version)/source/
$(package)_file_name=$(package)_$(subst .,_,$($(package)_version)).tar.gz
$(package)_sha256_hash=7bd7ddceec1a1dfdcbdb3e609b60d01739c38390a5f956385a12f3122049f0ca
$(package)_dependencies=
$(package)_patches=fix_integral_wrapper.patch
define $(package)_set_vars
$(package)_config_opts_release=variant=release
$(package)_config_opts_debug=variant=debug
$(package)_config_opts+=--layout=system --user-config=user-config.jam
$(package)_config_opts+=threading=multi link=static -sNO_BZIP2=1 -sNO_ZLIB=1
$(package)_config_opts_linux=threadapi=pthread runtime-link=shared
$(package)_config_opts_android=threadapi=pthread runtime-link=static target-os=android
$(package)_config_opts_darwin=--toolset=darwin runtime-link=shared
$(package)_config_opts_mingw32=binary-format=pe target-os=windows threadapi=win32 runtime-link=static
$(package)_config_opts_x86_64_mingw32=address-model=64
$(package)_config_opts_i686_mingw32=address-model=32
$(package)_config_opts_i686_linux=address-model=32 architecture=x86
$(package)_toolset_$(host_os)=gcc
$(package)_archiver_$(host_os)=$($(package)_ar)
$(package)_toolset_darwin=darwin
$(package)_archiver_darwin=$($(package)_libtool)
$(package)_config_libraries=chrono,filesystem,program_options,system,thread,test,date_time,regex,serialization,locale
$(package)_toolset_ios=darwin
$(package)_archiver_ios=libtool
$(package)_arflags_ios=
$(package)_cxxflags_ios=-fPIC -std=c++11 -stdlib=libc++
$(package)_cxxflags=-std=c++11
$(package)_cxxflags_linux=-fPIC
$(package)_cxxflags_freebsd=-fPIC
endef
define $(package)_preprocess_cmds
patch -p1 < $($(package)_patch_dir)/fix_integral_wrapper.patch &&\
echo "using $(boost_toolset_$(host_os)) : : $($(package)_cxx) : <root>\"$(IOS_SDK_ROOT)\" <cxxflags>\"$($(package)_cxxflags) $($(package)_cppflags) -isysroot $(IOS_SDK_ROOT)\" <linkflags>\"$($(package)_ldflags) -isysroot $(IOS_SDK_ROOT)\" <archiver>\"$(boost_archiver_$(host_os))\" <arflags>\"$($(package)_arflags)\" <striper>\"$(host_STRIP)\" <ranlib>\"$(host_RANLIB)\" <rc>\"$(host_WINDRES)\" : ;" > user-config.jam
endef
define $(package)_config_cmds
./bootstrap.sh --with-libraries=$(boost_config_libraries)
endef
define $(package)_stage_cmds
./b2 -d0 -j4 --prefix=$($(package)_staging_prefix_dir) $($(package)_config_opts) install
endef
@@ -0,0 +1,43 @@
packages:=boost openssl zeromq expat unbound
# iOS: iconv is provided by the system; libiconv-1.15 fails with modern clang.
ios_packages := boost openssl zeromq expat unbound sodium
# ccache is useless in gitian builds
ifneq ($(GITIAN),1)
native_packages := native_ccache
endif
hardware_packages := hidapi protobuf libusb
hardware_native_packages := native_protobuf
android_native_packages = android_ndk
android_packages = ncurses readline sodium
darwin_native_packages = $(hardware_native_packages)
darwin_packages = ncurses readline sodium $(hardware_packages)
# not really native...
freebsd_native_packages = freebsd_base
freebsd_packages = ncurses readline sodium
linux_packages = eudev ncurses readline sodium $(hardware_packages)
linux_native_packages = $(hardware_native_packages)
ifeq ($(build_tests),ON)
packages += gtest
endif
ifneq ($(host_arch),riscv64)
linux_packages += unwind
endif
mingw32_packages = icu4c sodium $(hardware_packages)
mingw32_native_packages = $(hardware_native_packages)
ifneq ($(build_os),darwin)
darwin_native_packages += darwin_sdk native_clang native_cctools native_libtapi
endif
@@ -0,0 +1,32 @@
package=sodium
$(package)_version=1.0.18
$(package)_download_path=https://download.libsodium.org/libsodium/releases/
$(package)_file_name=libsodium-$($(package)_version).tar.gz
$(package)_sha256_hash=6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1
$(package)_patches=disable-glibc-getrandom-getentropy.patch fix-whitespace.patch
define $(package)_set_vars
$(package)_config_opts=--enable-static --disable-shared
$(package)_config_opts+=--prefix=$(host_prefix)
endef
define $(package)_preprocess_cmds
patch -p1 < $($(package)_patch_dir)/fix-whitespace.patch
endef
define $(package)_config_cmds
$($(package)_autoconf) AR_FLAGS=$($(package)_arflags)
endef
define $(package)_build_cmds
$(MAKE)
endef
define $(package)_stage_cmds
$(MAKE) DESTDIR=$($(package)_staging_dir) install
endef
define $(package)_postprocess_cmds
rm lib/*.la
endef
@@ -0,0 +1,36 @@
package=unbound
$(package)_version=1.25.1
$(package)_download_path=https://www.nlnetlabs.nl/downloads/$(package)/
$(package)_file_name=$(package)-$($(package)_version).tar.gz
$(package)_sha256_hash=0fe8b6277b0959cfd17562debac0aa5f71e0b02dc4ffa9c60271c583edab586f
$(package)_dependencies=openssl expat
$(package)_patches=disable-glibc-reallocarray.patch hardcode_config_date.patch
define $(package)_set_vars
$(package)_config_opts=--disable-shared --enable-static --without-pyunbound --prefix=$(host_prefix)
$(package)_config_opts+=--with-libexpat=$(host_prefix) --with-ssl=$(host_prefix) --with-libevent=no
$(package)_config_opts+=--without-pythonmodule --disable-flto --with-pthreads --with-libunbound-only
$(package)_config_opts_linux=--with-pic
$(package)_config_opts_w64=--enable-static-exe --sysconfdir=/etc --prefix=$(host_prefix) --target=$(host_prefix)
$(package)_config_opts_x86_64_darwin=ac_cv_func_SHA384_Init=yes
$(package)_build_opts_mingw32=LDFLAGS="$($(package)_ldflags) -lpthread"
$(package)_cflags_mingw32+="-D_WIN32_WINNT=0x600"
endef
define $(package)_preprocess_cmds
rm doc/*.odp doc/*.pdf contrib/*.tar.gz contrib/*.tar.bz2 &&\
rm -rf testdata dnscrypt/testdata
endef
define $(package)_config_cmds
$($(package)_autoconf) ac_cv_func_getentropy=no
endef
define $(package)_build_cmds
$(MAKE) $($(package)_build_opts)
endef
define $(package)_stage_cmds
$(MAKE) DESTDIR=$($(package)_staging_dir) install
endef
@@ -0,0 +1,37 @@
package=zeromq
$(package)_version=4.3.4
$(package)_download_path=https://github.com/zeromq/libzmq/releases/download/v$($(package)_version)/
$(package)_file_name=$(package)-$($(package)_version).tar.gz
$(package)_sha256_hash=c593001a89f5a85dd2ddf564805deb860e02471171b3f204944857336295c3e5
$(package)_patches=06aba27b04c5822cb88a69677382a0f053367143.patch
define $(package)_set_vars
$(package)_config_opts=--without-documentation --disable-shared --without-libsodium --disable-curve
$(package)_config_opts_linux=--with-pic
$(package)_config_opts_freebsd=--with-pic
$(package)_config_opts_ios=--host=aarch64-apple-darwin --with-pic
$(package)_cxxflags=-std=c++11
$(package)_cxxflags_ios=-std=c++11 -isysroot $(IOS_SDK_ROOT) -stdlib=libc++ -Wno-deprecated-declarations
endef
define $(package)_preprocess_cmds
patch -p1 < $($(package)_patch_dir)/06aba27b04c5822cb88a69677382a0f053367143.patch
endef
define $(package)_config_cmds
$($(package)_autoconf) AR_FLAGS=$($(package)_arflags)
endef
define $(package)_build_cmds
$(MAKE) src/libzmq.la
endef
define $(package)_stage_cmds
$(MAKE) DESTDIR=$($(package)_staging_dir) install-libLTLIBRARIES install-includeHEADERS install-pkgconfigDATA
endef
define $(package)_postprocess_cmds
rm -rf bin share &&\
rm lib/*.la
endef
+94
View File
@@ -0,0 +1,94 @@
# Koins iOS
Native SwiftUI wallet for Monero and XRP. iOS only, no JavaScript runtime.
## Layout
```
Koins/
KoinsApp.swift App entry point
AppModel.swift Observable state, orchestrates both chains
Models.swift Chain / wallet / transaction models
SelfTest.swift Launch-time verification (see below)
Bridge/
Koins-Bridging-Header.h
KoinsMoneroWallet.h/.mm ObjC++ bridge to the monero-cpp wallet
KoinsSecp256k1.h/.mm secp256k1 via the OpenSSL bundled in the monero lib
Monero/
libs/ prebuilt monero archives (simulator + device)
include/ monero-cpp headers
toolchain/ how those archives are built, plus the depends patches
Support/ Base58, RIPEMD-160, hashing, Keychain, wallet store
Wallet/ XRP keys, binary codec, RPC client, services
Views/ SwiftUI screens
```
## How the chains are implemented
- **Monero**: the wallet runs on device through `libkoins_monero_merged.a`
(monero-project `wallet2` + monero-cpp, built for `aarch64-apple-ios` and the
arm64 simulator). `KoinsMoneroWallet` serialises all C++ work on a private
queue and exchanges JSON with Swift. Wallet files live in the app's Documents
directory; the recovery seed is stored in the Keychain.
- **XRP**: implemented directly in Swift, no third-party package.
- Recovery phrase: a 12 word BIP-39 mnemonic, derived to the standard XRP
account path `m/44'/144'/0'/0/0` (BIP-32, secp256k1), so the same words
restore the same account in hardware and other mobile wallets.
- Imports also accept legacy XRP family seeds (`sEd…` / `s…`), including the
Ed25519 and secp256k1 variants.
- Key derivation and signing: Ed25519 via CryptoKit and secp256k1 via the
OpenSSL build embedded in the monero static library.
- Transaction serialization: canonical XRP binary encoding plus the
`STX`/`TXN` hash prefixes, matching `ripple-binary-codec`.
- Network: JSON-RPC against public XRP Ledger endpoints with failover.
## Build
```sh
cd apps/ios
xcodegen generate
xcodebuild -project Koins.xcodeproj -scheme Koins -configuration Debug \
-sdk iphonesimulator -destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -derivedDataPath /tmp/koins-dd build
```
Install and run on a booted simulator:
```sh
xcrun simctl install <udid> /tmp/koins-dd/Build/Products/Debug-iphonesimulator/Koins.app
xcrun simctl launch <udid> com.koins.app
```
Re-run `xcodegen generate` after adding or removing source files. No CocoaPods
and no Swift packages are used. The same commands work for a device: pass
`-sdk iphoneos -destination 'generic/platform=iOS'` and Xcode links the device
Monero archive automatically.
## Verification
The XRP implementation is checked against vectors produced by the reference
JavaScript stack (`xrpl`, `ripple-keypairs`, `ripple-binary-codec`).
Cryptography and serialization (no network):
```sh
xcrun simctl launch <udid> com.koins.app -koinsSelfTest
xcrun simctl spawn <udid> log show --last 1m --predicate 'subsystem == "com.koins.app"' --style compact
```
Live network check (creates and then removes a temporary wallet on both chains,
talks to a Monero daemon and the XRP Ledger):
```sh
xcrun simctl launch <udid> com.koins.app -koinsSelfTestNetwork
```
## Notes
- The Monero daemon default is `xmr-node.cakewallet.com:18081` with two
fallbacks; `MoneroService` reconnects on demand.
- Ed25519 and OpenSSL ECDSA both use randomised nonces, so signatures are valid
but not byte-identical to deterministic implementations such as
`ripple-keypairs`. Signatures are verified rather than compared.
- Refresh the simulator build when `/tmp` is cleared: the intermediate build
products live in `/tmp/koins-dd`.
+73
View File
@@ -0,0 +1,73 @@
name: Koins
options:
bundleIdPrefix: com.koins
deploymentTarget:
iOS: "16.0"
xcodeVersion: "16.0"
settings:
base:
ENABLE_USER_SCRIPT_SANDBOXING: NO
CLANG_CXX_LANGUAGE_STANDARD: "c++14"
GCC_TREAT_WARNINGS_AS_ERRORS: NO
CLANG_CXX_LIBRARY: "libc++"
SWIFT_VERSION: "5.0"
targets:
Koins:
type: application
platform: iOS
sources:
- Koins
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.koins.app
INFOPLIST_FILE: Koins/Info.plist
CODE_SIGN_STYLE: Automatic
CODE_SIGNING_ALLOWED: NO
SWIFT_OBJC_BRIDGING_HEADER: Koins/Bridge/Koins-Bridging-Header.h
OTHER_LDFLAGS[sdk=iphoneos*]:
- "$(inherited)"
- "-lkoins_monero_device"
OTHER_LDFLAGS[sdk=iphonesimulator*]:
- "$(inherited)"
- "-lkoins_monero_merged"
LIBRARY_SEARCH_PATHS:
- "$(SRCROOT)/Koins/Monero/libs"
HEADER_SEARCH_PATHS:
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/wallet"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/common"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/utils"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/daemon"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/src"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/contrib"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/external/rapidjson/include"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/contrib/epee/include"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/external/easylogging++"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/external/boost"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/contrib/depends/aarch64-apple-ios/include"
OTHER_CPLUSPLUSFLAGS:
- "$(inherited)"
- "-DKOINS_MONERO_NATIVE=1"
- "-DBOOST_UUID_DISABLE_ALIGNMENT"
GCC_PREPROCESSOR_DEFINITIONS:
- "$(inherited)"
- "KOINS_MONERO_NATIVE=1"
info:
path: Koins/Info.plist
properties:
UILaunchScreen: {}
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
UIUserInterfaceStyle: Dark
NSAppTransportSecurity:
NSAllowsArbitraryLoads: true
schemes:
Koins:
build:
targets:
Koins: all
run:
config: Debug
+4 -1
View File
@@ -18,6 +18,7 @@
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"electrobun": "^1.18.1", "electrobun": "^1.18.1",
"env-paths": "^4.0.0", "env-paths": "^4.0.0",
"jsqr": "^1.4.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"monero-ts": "^0.11.8", "monero-ts": "^0.11.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@@ -640,6 +641,8 @@
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"jsqr": ["jsqr@1.4.0", "", {}, "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"koins": ["koins@workspace:apps/desktop"], "koins": ["koins@workspace:apps/desktop"],
@@ -846,7 +849,7 @@
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"sv-router": ["sv-router@0.18.0", "", { "dependencies": { "esm-env": "^1.2.2" }, "peerDependencies": { "svelte": "^5" }, "bin": { "sv-router": "./src/cli/index.js" } }, "sha512-gLky7FBGj8P6MAna1wxYniHJxMrXgaeTNnkZ7WH387ddRyUohfDtvEgWQFfB4oLL2XxWlw8Lfmk03TtBdK/JTA=="], "sv-router": ["sv-router@0.18.1", "", { "dependencies": { "esm-env": "^1.2.2" }, "peerDependencies": { "svelte": "^5" }, "bin": { "sv-router": "./src/cli/index.js" } }, "sha512-xMJxgDwmcWtLDopU6xdEiBICRN40ucTlK63GUZf6cQk8689LUaUD5FlMrdtTdPFRQ2kfJBeJgoaI1k+XOuIQSQ=="],
"svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="], "svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="],
-1
View File
@@ -4,7 +4,6 @@
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev", "dev": "turbo run dev",
"dev:mobile": "turbo run dev:hmr",
"lint": "turbo run lint", "lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"", "format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types" "check-types": "turbo run check-types"