Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ff5385a9b | ||
|
|
2f70931cba | ||
|
|
e67ed9e063 | ||
|
|
c9679e35c4 | ||
|
|
7e739e1b77 | ||
|
|
4fa1584dfc | ||
|
|
31429e6923 | ||
|
|
782143e87b | ||
|
|
994ba5ed6b | ||
|
|
e3846fe881 | ||
|
|
edfefb6210 | ||
|
|
58d862161e | ||
|
|
0b1b253d30 | ||
|
|
78e1cefad4 | ||
|
|
b989b0f791 | ||
|
|
2728bf161e | ||
|
|
51dc007107 | ||
|
|
27dc122632 | ||
|
|
e9720391b7 | ||
|
|
c0903f4f02 | ||
|
|
399502cf72 | ||
|
|
e53c6aa7a9 | ||
|
|
021672b47c | ||
|
|
caca4321cc | ||
|
|
1a23c730c6 | ||
|
|
807388ea6b | ||
|
|
f4037a188a | ||
|
|
6d50566f37 | ||
|
|
6b52b32e20 | ||
|
|
a3af802166 | ||
|
|
3e1a39d4d7 | ||
|
|
d80b58b704 | ||
|
|
babd85e614 | ||
|
|
d15152a013 | ||
|
|
3913889a22 | ||
|
|
da0c56658b | ||
|
|
743700b82c | ||
|
|
8953596194 | ||
|
|
5df3ae7058 | ||
|
|
7b4ba84fee | ||
|
|
20f29faa80 | ||
|
|
c0f6417925 | ||
|
|
c61fea938a | ||
|
|
bdfee2a553 | ||
|
|
1c5e5b617f | ||
|
|
11e0bcdaf6 | ||
|
|
4270d380fb | ||
|
|
af6251669a | ||
|
|
6eec1f998d | ||
|
|
d9ca8a0047 | ||
|
|
a06c463653 | ||
|
|
3e1b8fc554 | ||
|
|
edb4f6b1f3 | ||
|
|
c2a405a9e1 | ||
|
|
6809cf09c6 | ||
|
|
9df8ce1867 | ||
|
|
1cccdc70e0 | ||
|
|
f517f7fe44 | ||
|
|
c9e492ad95 | ||
|
|
50a7771d97 |
@@ -1,6 +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" / "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.
|
||||||
- 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."
|
- 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`, 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.
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ dist-ssr
|
|||||||
artifacts
|
artifacts
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# compiled native helper (build artifact; source is biometric-helper.swift)
|
||||||
|
src/bun/native/biometric-helper
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
app: {
|
app: {
|
||||||
name: 'koins',
|
name: 'koins',
|
||||||
identifier: 'koins.tzgyn.com',
|
identifier: 'koins.tzgyn.com',
|
||||||
version: '0.0.1',
|
version: '0.0.12',
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// Vite builds to dist/, we copy from there
|
// Vite builds to dist/, we copy from there
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
https://icon.msgbyte.com/
|
|
||||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 783 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 789 KiB |
@@ -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",
|
||||||
|
|||||||
@@ -11,9 +11,16 @@ import { existsSync } from 'fs'
|
|||||||
import { getENV } from './lib/get-env'
|
import { getENV } from './lib/get-env'
|
||||||
import { createMoneroHandlers } from './rpc/monero'
|
import { createMoneroHandlers } from './rpc/monero'
|
||||||
import { createXrpHandlers } from './rpc/xrp'
|
import { createXrpHandlers } from './rpc/xrp'
|
||||||
|
import { stop as walletStop } from './lib/monero'
|
||||||
import type { MoneroWalletState } from './lib/monero'
|
import type { MoneroWalletState } from './lib/monero'
|
||||||
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
||||||
import { getXrpWalletList } from './lib/xrp'
|
import {
|
||||||
|
getSecret,
|
||||||
|
setSecret,
|
||||||
|
deleteSecret,
|
||||||
|
clearSecretsCache,
|
||||||
|
VAULT_SERVICE,
|
||||||
|
} from './lib/secrets-cache'
|
||||||
import type { RPC } from '../lib/rpc-schema'
|
import type { RPC } from '../lib/rpc-schema'
|
||||||
import { log } from './lib/logger'
|
import { log } from './lib/logger'
|
||||||
|
|
||||||
@@ -78,6 +85,24 @@ const moneroState = {
|
|||||||
downloading: false,
|
downloading: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function stopWalletServers(): Promise<void> {
|
||||||
|
if (moneroState.manager) {
|
||||||
|
const manager = moneroState.manager
|
||||||
|
moneroState.manager = null
|
||||||
|
await walletStop(manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the monero wallet and shut down wallet-rpc gracefully on app quit
|
||||||
|
// (otherwise an orphaned process keeps the scan progress only in memory).
|
||||||
|
for (const signal of ['SIGTERM', 'SIGINT'] as const) {
|
||||||
|
process.on(signal, () => {
|
||||||
|
log(`${signal} received, stopping wallet servers...`)
|
||||||
|
void stopWalletServers().finally(() => process.exit(0))
|
||||||
|
setTimeout(() => process.exit(0), 5000).unref?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const rpc = BrowserView.defineRPC<RPC>({
|
const rpc = BrowserView.defineRPC<RPC>({
|
||||||
maxRequestTime: 120000,
|
maxRequestTime: 120000,
|
||||||
handlers: {
|
handlers: {
|
||||||
@@ -89,23 +114,8 @@ const rpc = BrowserView.defineRPC<RPC>({
|
|||||||
rpc.setRequestHandler({
|
rpc.setRequestHandler({
|
||||||
resetApp: async () => {
|
resetApp: async () => {
|
||||||
try {
|
try {
|
||||||
const wallets = await getXrpWalletList()
|
await deleteSecret(VAULT_SERVICE, 'vault')
|
||||||
await Promise.all([
|
clearSecretsCache()
|
||||||
Bun.secrets.delete({
|
|
||||||
service: 'koins',
|
|
||||||
name: 'xrp_wallets',
|
|
||||||
}),
|
|
||||||
...wallets.flatMap((w) => [
|
|
||||||
Bun.secrets.delete({
|
|
||||||
service: 'koins',
|
|
||||||
name: w.vaultKey,
|
|
||||||
}),
|
|
||||||
Bun.secrets.delete({
|
|
||||||
service: 'koins',
|
|
||||||
name: `xrp_auth_${w.id}`,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
console.log('[rpc] resetApp complete')
|
console.log('[rpc] resetApp complete')
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -131,10 +141,10 @@ rpc.setRequestHandler({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSecret: async ({ name, service }) => {
|
getSecret: async ({ name, service }) => {
|
||||||
return await Bun.secrets.get({ name, service })
|
return await getSecret(service, name)
|
||||||
},
|
},
|
||||||
setSecret: async ({ name, service, value }) => {
|
setSecret: async ({ name, service, value }) => {
|
||||||
await Bun.secrets.set({ name, service, value })
|
await setSecret(service, name, value)
|
||||||
},
|
},
|
||||||
openExternal: async ({ url }) => {
|
openExternal: async ({ url }) => {
|
||||||
Utils.openExternal(url)
|
Utils.openExternal(url)
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
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'
|
||||||
|
|
||||||
export type MoneroWalletState = {
|
export type MoneroWalletState = {
|
||||||
wallet: MoneroWalletRpc | null
|
wallet: MoneroWalletRpc | null
|
||||||
process: Bun.Subprocess | null
|
process: Bun.Subprocess | null
|
||||||
|
saveTimer: ReturnType<typeof setInterval> | null
|
||||||
rpcPort: number
|
rpcPort: number
|
||||||
rpcUser: string
|
rpcUser: string
|
||||||
rpcPassword: string
|
rpcPassword: string
|
||||||
@@ -22,6 +19,7 @@ export function createMoneroWalletState(): MoneroWalletState {
|
|||||||
return {
|
return {
|
||||||
wallet: null,
|
wallet: null,
|
||||||
process: null,
|
process: null,
|
||||||
|
saveTimer: null,
|
||||||
rpcPort: 28084,
|
rpcPort: 28084,
|
||||||
rpcUser: 'koins',
|
rpcUser: 'koins',
|
||||||
rpcPassword: 'koins-local-rpc',
|
rpcPassword: 'koins-local-rpc',
|
||||||
@@ -43,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
|
||||||
@@ -63,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() {},
|
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
||||||
data() {},
|
return
|
||||||
error() {},
|
}
|
||||||
},
|
lastErr = JSON.stringify(res)
|
||||||
})
|
|
||||||
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
|
||||||
return
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = String(e)
|
lastErr = String(e)
|
||||||
}
|
}
|
||||||
@@ -87,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,
|
||||||
@@ -97,17 +141,44 @@ export async function start(
|
|||||||
)
|
)
|
||||||
state.daemonAddress = daemonAddress
|
state.daemonAddress = daemonAddress
|
||||||
|
|
||||||
// Kill any existing monero-wallet-rpc on the port (stale process from dev reloads)
|
// Kill any existing monero-wallet-rpc on the port (stale process from
|
||||||
|
// dev reloads or an unclean quit). Prefer SIGTERM so a live wallet gets
|
||||||
|
// a chance to save its scan progress before exiting.
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawnSync(['lsof', '-ti', `tcp:${state.rpcPort}`], {
|
const proc = Bun.spawnSync(['lsof', '-ti', `tcp:${state.rpcPort}`], {
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
})
|
})
|
||||||
const pids = proc.stdout.toString().trim().split('\n').filter(Boolean)
|
const pids = proc.stdout.toString().trim().split('\n').filter(Boolean)
|
||||||
for (const pid of pids) {
|
if (pids.length) {
|
||||||
try {
|
console.log(
|
||||||
process.kill(Number(pid), 'SIGKILL')
|
`[monero] stale process(es) on port ${state.rpcPort}: ${pids.join(', ')}`,
|
||||||
console.log(`[monero] killed stale process ${pid} on port ${state.rpcPort}`)
|
)
|
||||||
} catch {}
|
for (const pid of pids) {
|
||||||
|
try {
|
||||||
|
process.kill(Number(pid), 'SIGTERM')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
const deadline = Date.now() + 5000
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const check = Bun.spawnSync(
|
||||||
|
['lsof', '-ti', `tcp:${state.rpcPort}`],
|
||||||
|
{ stdout: 'pipe' },
|
||||||
|
)
|
||||||
|
if (!check.stdout.toString().trim()) break
|
||||||
|
await new Promise((r) => setTimeout(r, 250))
|
||||||
|
}
|
||||||
|
const leftover = Bun.spawnSync(
|
||||||
|
['lsof', '-ti', `tcp:${state.rpcPort}`],
|
||||||
|
{ stdout: 'pipe' },
|
||||||
|
)
|
||||||
|
for (const pid of leftover.stdout.toString().trim().split('\n').filter(Boolean)) {
|
||||||
|
try {
|
||||||
|
process.kill(Number(pid), 'SIGKILL')
|
||||||
|
console.log(
|
||||||
|
`[monero] killed unresponsive process ${pid} on port ${state.rpcPort}`,
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
@@ -140,10 +211,33 @@ 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) {
|
||||||
|
clearInterval(state.saveTimer)
|
||||||
|
state.saveTimer = null
|
||||||
|
}
|
||||||
|
state.wallet = null
|
||||||
|
state.process = null
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log(`[monero] wallet-rpc pid: ${state.process.pid}`)
|
console.log(`[monero] wallet-rpc pid: ${state.process.pid}`)
|
||||||
|
|
||||||
|
// Auto-save the wallet every 60s so scan progress survives an
|
||||||
|
// unexpected kill (without `store`, the wallet file keeps its old
|
||||||
|
// refresh height and every restart resyncs from block 0).
|
||||||
|
state.saveTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
if (state.wallet) {
|
||||||
|
await rawRpc(state, 'store')
|
||||||
|
console.log(`[monero] wallet auto-saved`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[monero] wallet auto-save failed:`, e)
|
||||||
|
}
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
const reader = (state.process!.stdout as ReadableStream<Uint8Array>).getReader()
|
const reader = (state.process!.stdout as ReadableStream<Uint8Array>).getReader()
|
||||||
;(async () => {
|
;(async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -173,27 +267,48 @@ 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) {
|
||||||
console.log(`[monero] stopping wallet manager...`)
|
console.log(`[monero] stopping wallet manager...`)
|
||||||
|
if (state.saveTimer) {
|
||||||
|
clearInterval(state.saveTimer)
|
||||||
|
state.saveTimer = null
|
||||||
|
}
|
||||||
|
// Persist scan progress before shutting down.
|
||||||
|
try {
|
||||||
|
if (state.wallet) {
|
||||||
|
await rawRpc(state, 'store')
|
||||||
|
console.log(`[monero] wallet saved before stop`)
|
||||||
|
log(`[monero] wallet saved before stop`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[monero] wallet save before stop failed:`, e)
|
||||||
|
}
|
||||||
if (state.process) {
|
if (state.process) {
|
||||||
console.log(
|
const proc = state.process
|
||||||
`[monero] killing wallet-rpc process (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)])
|
||||||
|
if (proc.exitCode === null) {
|
||||||
|
console.log(
|
||||||
|
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
|
||||||
|
)
|
||||||
|
proc.kill('SIGKILL')
|
||||||
|
}
|
||||||
state.process = null
|
state.process = null
|
||||||
}
|
}
|
||||||
state.wallet = null
|
state.wallet = null
|
||||||
console.log(`[monero] wallet manager stopped`)
|
console.log(`[monero] wallet manager stopped`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((r) => setTimeout(r, ms))
|
||||||
|
}
|
||||||
|
|
||||||
export async function createWallet(
|
export async function createWallet(
|
||||||
state: MoneroWalletState,
|
state: MoneroWalletState,
|
||||||
name: string,
|
name: string,
|
||||||
@@ -206,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 }
|
||||||
}
|
}
|
||||||
@@ -237,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) {
|
||||||
@@ -253,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 }
|
||||||
}
|
}
|
||||||
@@ -265,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
|
||||||
@@ -303,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) {
|
||||||
@@ -448,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
|
||||||
}
|
}
|
||||||
@@ -492,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) {
|
||||||
@@ -570,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,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)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { getENV } from './get-env'
|
||||||
|
|
||||||
|
const env = getENV()
|
||||||
|
export const VAULT_SERVICE = env.appName
|
||||||
|
const VAULT_NAME = 'vault'
|
||||||
|
|
||||||
|
const cache = new Map<string, string>()
|
||||||
|
const key = (service: string, name: string) => `${service}:${name}`
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
let loadPromise: Promise<void> | null = null
|
||||||
|
let writeChain: Promise<void> = Promise.resolve()
|
||||||
|
|
||||||
|
async function ensureLoaded(): Promise<void> {
|
||||||
|
if (loaded) return
|
||||||
|
if (!loadPromise) {
|
||||||
|
loadPromise = (async () => {
|
||||||
|
const raw = await Bun.secrets.get({
|
||||||
|
service: VAULT_SERVICE,
|
||||||
|
name: VAULT_NAME,
|
||||||
|
})
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw) as Record<string, string>
|
||||||
|
for (const [k, v] of Object.entries(data)) cache.set(k, v)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[secrets] vault parse error:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
await loadPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persist(): Promise<void> {
|
||||||
|
const raw = JSON.stringify(Object.fromEntries(cache))
|
||||||
|
writeChain = writeChain.then(() =>
|
||||||
|
Bun.secrets.set({
|
||||||
|
service: VAULT_SERVICE,
|
||||||
|
name: VAULT_NAME,
|
||||||
|
value: raw,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return writeChain
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSecret(
|
||||||
|
service: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const k = key(service, name)
|
||||||
|
await ensureLoaded()
|
||||||
|
return cache.get(k) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSecret(
|
||||||
|
service: string,
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await ensureLoaded()
|
||||||
|
cache.set(key(service, name), value)
|
||||||
|
await persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSecret(
|
||||||
|
service: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (service === VAULT_SERVICE && name === VAULT_NAME) {
|
||||||
|
cache.clear()
|
||||||
|
loaded = false
|
||||||
|
loadPromise = null
|
||||||
|
await Bun.secrets.delete({ service, name })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ensureLoaded()
|
||||||
|
cache.delete(key(service, name))
|
||||||
|
await persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSecretsCache(): void {
|
||||||
|
cache.clear()
|
||||||
|
loaded = false
|
||||||
|
loadPromise = null
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
XrpTxEntry,
|
XrpTxEntry,
|
||||||
XrpWalletInfo,
|
XrpWalletInfo,
|
||||||
} from '../../../lib/rpc-schema'
|
} from '../../../lib/rpc-schema'
|
||||||
|
import { getSecret, setSecret } from '../secrets-cache'
|
||||||
|
|
||||||
const SERVER = 'wss://s1.ripple.com'
|
const SERVER = 'wss://s1.ripple.com'
|
||||||
const RIPPLE_EPOCH = 946684800
|
const RIPPLE_EPOCH = 946684800
|
||||||
@@ -54,7 +55,7 @@ export function walletForSigning(secret: string): Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
|
export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
|
||||||
const raw = await Bun.secrets.get({ service: SERVICE, name: WALLET_LIST_KEY })
|
const raw = await getSecret(SERVICE, WALLET_LIST_KEY)
|
||||||
if (!raw) return []
|
if (!raw) return []
|
||||||
try {
|
try {
|
||||||
return JSON.parse(raw)
|
return JSON.parse(raw)
|
||||||
@@ -66,11 +67,7 @@ export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
|
|||||||
export async function saveXrpWalletList(
|
export async function saveXrpWalletList(
|
||||||
wallets: XrpWalletInfo[],
|
wallets: XrpWalletInfo[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await Bun.secrets.set({
|
await setSecret(SERVICE, WALLET_LIST_KEY, JSON.stringify(wallets))
|
||||||
service: SERVICE,
|
|
||||||
name: WALLET_LIST_KEY,
|
|
||||||
value: JSON.stringify(wallets),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBalance(address: string): Promise<XrpBalance> {
|
export async function getBalance(address: string): Promise<XrpBalance> {
|
||||||
|
|||||||
@@ -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,31 +139,42 @@ 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')
|
||||||
await walletOpen(state.manager, name, password)
|
try {
|
||||||
|
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')
|
||||||
const { balance, unlocked } = await walletGetBalance(
|
try {
|
||||||
state.manager,
|
const { balance, unlocked } = await walletGetBalance(
|
||||||
)
|
state.manager,
|
||||||
const address = await walletGetAddress(state.manager)
|
)
|
||||||
const height = await walletGetHeight(state.manager)
|
const address = await walletGetAddress(state.manager)
|
||||||
const daemonHeight = await walletGetDaemonHeight(state.manager)
|
const height = await walletGetHeight(state.manager)
|
||||||
console.log('[rpc] balance:', {
|
const daemonHeight = await walletGetDaemonHeight(state.manager)
|
||||||
balance: balance.toString(),
|
console.log('[rpc] balance:', {
|
||||||
unlocked: unlocked.toString(),
|
balance: balance.toString(),
|
||||||
address,
|
unlocked: unlocked.toString(),
|
||||||
height,
|
address,
|
||||||
daemonHeight,
|
height,
|
||||||
})
|
daemonHeight,
|
||||||
return {
|
})
|
||||||
balance: balance.toString(),
|
return {
|
||||||
unlocked: unlocked.toString(),
|
balance: balance.toString(),
|
||||||
address,
|
unlocked: unlocked.toString(),
|
||||||
height,
|
address,
|
||||||
daemonHeight,
|
height,
|
||||||
|
daemonHeight,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError('[rpc] moneroGetBalance failed', e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moneroGetTransactions: async ({
|
moneroGetTransactions: async ({
|
||||||
@@ -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')
|
||||||
const txs = await walletGetTransactions(
|
try {
|
||||||
state.manager,
|
const txs = await walletGetTransactions(
|
||||||
accountIndex,
|
state.manager,
|
||||||
)
|
accountIndex,
|
||||||
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
)
|
||||||
return txs
|
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||||
|
return txs
|
||||||
|
} catch (e) {
|
||||||
|
logError('[rpc] moneroGetTransactions failed', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moneroWalletStatus: async () => {
|
moneroWalletStatus: async () => {
|
||||||
if (!state.manager) {
|
if (!state.manager) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getFee,
|
getFee,
|
||||||
} from '../../lib/xrp'
|
} from '../../lib/xrp'
|
||||||
import type { XrpWalletInfo } from '../../../lib/rpc-schema'
|
import type { XrpWalletInfo } from '../../../lib/rpc-schema'
|
||||||
|
import { getSecret, setSecret, deleteSecret } from '../../lib/secrets-cache'
|
||||||
|
|
||||||
const SERVICE = 'koins'
|
const SERVICE = 'koins'
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export function createXrpHandlers() {
|
|||||||
const { address, seed } = generateWallet()
|
const { address, seed } = generateWallet()
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
const vaultKey = `xrp_seed_${id}`
|
const vaultKey = `xrp_seed_${id}`
|
||||||
await Bun.secrets.set({ service: SERVICE, name: vaultKey, value: seed })
|
await setSecret(SERVICE, vaultKey, seed)
|
||||||
const wallets = await getXrpWalletList()
|
const wallets = await getXrpWalletList()
|
||||||
const createdAt = new Date().toISOString()
|
const createdAt = new Date().toISOString()
|
||||||
const info: XrpWalletInfo = {
|
const info: XrpWalletInfo = {
|
||||||
@@ -58,11 +59,7 @@ export function createXrpHandlers() {
|
|||||||
const { address } = walletFromSecret(secret)
|
const { address } = walletFromSecret(secret)
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
const vaultKey = `xrp_seed_${id}`
|
const vaultKey = `xrp_seed_${id}`
|
||||||
await Bun.secrets.set({
|
await setSecret(SERVICE, vaultKey, secret.trim())
|
||||||
service: SERVICE,
|
|
||||||
name: vaultKey,
|
|
||||||
value: secret.trim(),
|
|
||||||
})
|
|
||||||
const wallets = await getXrpWalletList()
|
const wallets = await getXrpWalletList()
|
||||||
const createdAt = new Date().toISOString()
|
const createdAt = new Date().toISOString()
|
||||||
const info: XrpWalletInfo = {
|
const info: XrpWalletInfo = {
|
||||||
@@ -83,15 +80,15 @@ export function createXrpHandlers() {
|
|||||||
const wallet = wallets.find((w) => w.id === id)
|
const wallet = wallets.find((w) => w.id === id)
|
||||||
if (!wallet) throw new Error('Wallet not found')
|
if (!wallet) throw new Error('Wallet not found')
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Bun.secrets.delete({ service: SERVICE, name: wallet.vaultKey }),
|
deleteSecret(SERVICE, wallet.vaultKey),
|
||||||
Bun.secrets.delete({ service: SERVICE, name: `xrp_auth_${id}` }),
|
deleteSecret(SERVICE, `xrp_auth_${id}`),
|
||||||
])
|
])
|
||||||
await saveXrpWalletList(wallets.filter((w) => w.id !== id))
|
await saveXrpWalletList(wallets.filter((w) => w.id !== id))
|
||||||
console.log('[rpc] xrpDeleteWallet complete')
|
console.log('[rpc] xrpDeleteWallet complete')
|
||||||
},
|
},
|
||||||
xrpGetSeed: async ({ vaultKey }: { vaultKey: string }) => {
|
xrpGetSeed: async ({ vaultKey }: { vaultKey: string }) => {
|
||||||
console.log('[rpc] xrpGetSeed:', vaultKey)
|
console.log('[rpc] xrpGetSeed:', vaultKey)
|
||||||
const seed = await Bun.secrets.get({ service: SERVICE, name: vaultKey })
|
const seed = await getSecret(SERVICE, vaultKey)
|
||||||
if (!seed) throw new Error('Seed not found in keychain')
|
if (!seed) throw new Error('Seed not found in keychain')
|
||||||
return seed
|
return seed
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -17,7 +17,7 @@ export type {
|
|||||||
} from './rpc-schema'
|
} from './rpc-schema'
|
||||||
|
|
||||||
const rpc = Electroview.defineRPC<RPC>({
|
const rpc = Electroview.defineRPC<RPC>({
|
||||||
maxRequestTime: 10000,
|
maxRequestTime: 120000,
|
||||||
handlers: {
|
handlers: {
|
||||||
requests: {},
|
requests: {},
|
||||||
messages: {},
|
messages: {},
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -46,6 +48,45 @@ export const MoneroWallet = () => {
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
let selectedAccountIndex = $state(0)
|
let selectedAccountIndex = $state(0)
|
||||||
|
let opening = $state(false)
|
||||||
|
let openingDepth = 0
|
||||||
|
let passwordRequired = $state(false)
|
||||||
|
let startInFlight: Promise<{ running: boolean; connected: boolean }> | 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 = () => {
|
||||||
|
openingDepth += 1
|
||||||
|
opening = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const endOpening = () => {
|
||||||
|
openingDepth = Math.max(0, openingDepth - 1)
|
||||||
|
if (openingDepth === 0) opening = false
|
||||||
|
}
|
||||||
|
|
||||||
const checkBiometric = async () => {
|
const checkBiometric = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
@@ -65,26 +106,59 @@ export const MoneroWallet = () => {
|
|||||||
return authed === true
|
return authed === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
await checkStatus()
|
if (initPromise) return initPromise
|
||||||
if (installed && !running && !downloading) {
|
initPromise = (async () => {
|
||||||
await start()
|
|
||||||
await checkStatus()
|
await checkStatus()
|
||||||
}
|
if (installed && !running && !downloading) {
|
||||||
await checkBiometric()
|
await start()
|
||||||
ready = true
|
await checkStatus()
|
||||||
|
}
|
||||||
|
await checkBiometric()
|
||||||
|
ready = true
|
||||||
|
})()
|
||||||
|
return initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
accountType = 'monero'
|
if (walletOpen) return
|
||||||
if (installed && !running) {
|
if (loginInFlight) return loginInFlight
|
||||||
await start()
|
loginInFlight = (async () => {
|
||||||
await checkStatus()
|
accountType = 'monero'
|
||||||
}
|
error = ''
|
||||||
await listWallets()
|
beginOpening()
|
||||||
if (wallets.length > 0) {
|
try {
|
||||||
const pw = await moneroGetStoredPassword(wallets[0])
|
if (installed && !running) {
|
||||||
if (pw) await openExistingWallet(wallets[0], pw)
|
await start()
|
||||||
|
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()
|
||||||
|
if (wallets.length > 0) {
|
||||||
|
const pw = await moneroGetStoredPassword(wallets[0])
|
||||||
|
if (pw) await openExistingWallet(wallets[0], pw)
|
||||||
|
else passwordRequired = true
|
||||||
|
}
|
||||||
|
error = ''
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Failed to open wallet'
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
try {
|
||||||
|
await loginInFlight
|
||||||
|
} finally {
|
||||||
|
loginInFlight = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,26 +217,39 @@ export const MoneroWallet = () => {
|
|||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const pw = password ?? (await moneroGetStoredPassword(name))
|
const pw = password ?? (await moneroGetStoredPassword(name))
|
||||||
if (!pw) throw new Error('Password required')
|
if (!pw) throw new Error('Password required')
|
||||||
await electrobun.rpc.request.moneroOpenWallet({
|
beginOpening()
|
||||||
name,
|
try {
|
||||||
password: pw,
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
})
|
name,
|
||||||
walletName = name
|
password: pw,
|
||||||
walletOpen = true
|
})
|
||||||
await refresh()
|
walletName = name
|
||||||
|
walletOpen = true
|
||||||
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
const [binStatus] = await tryCatch(
|
const [binStatus] = await tryCatch(
|
||||||
electrobun.rpc.request.moneroBinaryStatus({}),
|
rpcTimeout(
|
||||||
|
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(
|
||||||
electrobun.rpc.request.moneroWalletStatus({}),
|
rpcTimeout(
|
||||||
|
electrobun.rpc.request.moneroWalletStatus({}),
|
||||||
|
30000,
|
||||||
|
'moneroWalletStatus',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (status) {
|
if (status) {
|
||||||
running = status.running
|
running = status.running
|
||||||
@@ -184,9 +271,20 @@ export const MoneroWallet = () => {
|
|||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
const status = await electrobun.rpc.request.moneroStart({})
|
beginOpening()
|
||||||
running = status.running
|
try {
|
||||||
connected = status.connected
|
if (!startInFlight) {
|
||||||
|
startInFlight = electrobun.rpc.request.moneroStart({})
|
||||||
|
const status = await startInFlight
|
||||||
|
running = status.running
|
||||||
|
connected = status.connected
|
||||||
|
} else {
|
||||||
|
await startInFlight
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
startInFlight = null
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async () => {
|
const stop = async () => {
|
||||||
@@ -195,6 +293,7 @@ export const MoneroWallet = () => {
|
|||||||
running = false
|
running = false
|
||||||
walletOpen = false
|
walletOpen = false
|
||||||
connected = false
|
connected = false
|
||||||
|
passwordRequired = false
|
||||||
address = ''
|
address = ''
|
||||||
balAtomic = '0'
|
balAtomic = '0'
|
||||||
unlockedAtomic = '0'
|
unlockedAtomic = '0'
|
||||||
@@ -209,16 +308,21 @@ export const MoneroWallet = () => {
|
|||||||
storePw?: boolean,
|
storePw?: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroCreateWallet({
|
beginOpening()
|
||||||
name,
|
try {
|
||||||
password,
|
const result = await electrobun.rpc.request.moneroCreateWallet({
|
||||||
})
|
name,
|
||||||
walletName = name
|
password,
|
||||||
walletOpen = true
|
})
|
||||||
address = result.address
|
walletName = name
|
||||||
if (storePw) await moneroStorePassword(name, password)
|
walletOpen = true
|
||||||
await refresh()
|
address = result.address
|
||||||
return result
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
|
await refresh()
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreWallet = async (
|
const restoreWallet = async (
|
||||||
@@ -229,25 +333,35 @@ export const MoneroWallet = () => {
|
|||||||
storePw?: boolean,
|
storePw?: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
beginOpening()
|
||||||
name,
|
try {
|
||||||
password,
|
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
||||||
mnemonic,
|
name,
|
||||||
restoreHeight,
|
password,
|
||||||
})
|
mnemonic,
|
||||||
walletName = name
|
restoreHeight,
|
||||||
walletOpen = true
|
})
|
||||||
address = result.address
|
walletName = name
|
||||||
if (storePw) await moneroStorePassword(name, password)
|
walletOpen = true
|
||||||
await refresh()
|
address = result.address
|
||||||
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openWallet = async (name: string, password: string) => {
|
const openWallet = async (name: string, password: string) => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
beginOpening()
|
||||||
walletName = name
|
try {
|
||||||
walletOpen = true
|
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
||||||
await refresh()
|
walletName = name
|
||||||
|
walletOpen = true
|
||||||
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoUnlock = async (name?: string) => {
|
const autoUnlock = async (name?: string) => {
|
||||||
@@ -255,42 +369,157 @@ export const MoneroWallet = () => {
|
|||||||
const target = name ?? wallets[0]
|
const target = name ?? wallets[0]
|
||||||
if (!target) return false
|
if (!target) return false
|
||||||
const pw = await moneroGetStoredPassword(target)
|
const pw = await moneroGetStoredPassword(target)
|
||||||
if (!pw) return false
|
if (!pw) {
|
||||||
await electrobun.rpc.request.moneroOpenWallet({
|
passwordRequired = true
|
||||||
name: target,
|
return false
|
||||||
password: pw,
|
}
|
||||||
})
|
beginOpening()
|
||||||
walletName = target
|
try {
|
||||||
walletOpen = true
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
await refresh()
|
name: target,
|
||||||
return true
|
password: pw,
|
||||||
|
})
|
||||||
|
walletName = target
|
||||||
|
walletOpen = true
|
||||||
|
passwordRequired = false
|
||||||
|
error = ''
|
||||||
|
await refresh()
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Failed to open wallet'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unlockWallet = async (password: string, remember = false) => {
|
||||||
|
const target = wallets[0]
|
||||||
|
if (!target) return
|
||||||
|
await openWallet(target, password)
|
||||||
|
passwordRequired = false
|
||||||
|
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
|
||||||
loading = true
|
if (!rpc) return
|
||||||
try {
|
if (refreshInFlight) return refreshInFlight
|
||||||
const [bal] = await tryCatch(
|
refreshInFlight = (async () => {
|
||||||
electrobun.rpc.request.moneroGetBalance({}),
|
loading = true
|
||||||
)
|
let failed = false
|
||||||
if (bal) {
|
const wasSyncing = syncing
|
||||||
balAtomic = bal.balance
|
let lastError: unknown = null
|
||||||
unlockedAtomic = bal.unlocked
|
try {
|
||||||
address = bal.address
|
const [bal, balErr] = await tryCatch(
|
||||||
height = bal.height
|
rpcTimeout(
|
||||||
daemonHeight = bal.daemonHeight
|
rpc.request.moneroGetBalance({}),
|
||||||
|
20000,
|
||||||
|
'moneroGetBalance',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (balErr) {
|
||||||
|
failed = true
|
||||||
|
lastError = balErr
|
||||||
|
}
|
||||||
|
if (bal) {
|
||||||
|
balAtomic = bal.balance
|
||||||
|
unlockedAtomic = bal.unlocked
|
||||||
|
address = bal.address
|
||||||
|
height = bal.height
|
||||||
|
daemonHeight = bal.daemonHeight
|
||||||
|
}
|
||||||
|
const [result, txErr] = await tryCatch(
|
||||||
|
rpcTimeout(
|
||||||
|
rpc.request.moneroGetTransactions({
|
||||||
|
accountIndex: selectedAccountIndex,
|
||||||
|
}),
|
||||||
|
20000,
|
||||||
|
'moneroGetTransactions',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (txErr) {
|
||||||
|
failed = true
|
||||||
|
lastError = txErr
|
||||||
|
}
|
||||||
|
txs = result ?? []
|
||||||
|
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) {
|
||||||
|
syncing = true
|
||||||
|
scheduleSyncRetry()
|
||||||
|
frontendLogError(
|
||||||
|
`monero refresh failed (height ${height}/${daemonHeight})`,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
refreshInFlight = null
|
||||||
}
|
}
|
||||||
const [result] = await tryCatch(
|
})()
|
||||||
electrobun.rpc.request.moneroGetTransactions({
|
return refreshInFlight
|
||||||
accountIndex: selectedAccountIndex,
|
}
|
||||||
}),
|
|
||||||
)
|
const fetchPrice = async () => {
|
||||||
txs = result ?? []
|
const rpc = electrobun.rpc
|
||||||
await fetchAccounts()
|
if (!rpc || priceInFlight) return
|
||||||
} catch (e) {
|
priceInFlight = true
|
||||||
error = e instanceof Error ? e.message : 'Monero refresh failed'
|
try {
|
||||||
|
const [p] = await tryCatch(rpc.request.fetchMoneroPrice({}))
|
||||||
|
if (p) price = p.usd
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
priceInFlight = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +633,18 @@ export const MoneroWallet = () => {
|
|||||||
get loading() {
|
get loading() {
|
||||||
return loading
|
return loading
|
||||||
},
|
},
|
||||||
|
get opening() {
|
||||||
|
return opening
|
||||||
|
},
|
||||||
|
get passwordRequired() {
|
||||||
|
return passwordRequired
|
||||||
|
},
|
||||||
|
get syncing() {
|
||||||
|
return syncing
|
||||||
|
},
|
||||||
|
get price() {
|
||||||
|
return price
|
||||||
|
},
|
||||||
get error() {
|
get error() {
|
||||||
return error
|
return error
|
||||||
},
|
},
|
||||||
@@ -451,7 +692,9 @@ export const MoneroWallet = () => {
|
|||||||
restoreWallet,
|
restoreWallet,
|
||||||
openWallet,
|
openWallet,
|
||||||
autoUnlock,
|
autoUnlock,
|
||||||
|
unlockWallet,
|
||||||
refresh,
|
refresh,
|
||||||
|
fetchPrice,
|
||||||
send,
|
send,
|
||||||
sendAll,
|
sendAll,
|
||||||
createAccount,
|
createAccount,
|
||||||
@@ -459,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,19 +76,24 @@ export const XrpWallet = () => {
|
|||||||
biometricAvailable = ok === true
|
biometricAvailable = ok === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (initPromise) return initPromise
|
||||||
const [walletList] = await tryCatch(
|
initPromise = (async () => {
|
||||||
electrobun.rpc.request.xrpListWallets({}),
|
if (!electrobun.rpc) return
|
||||||
)
|
const [walletList] = await tryCatch(
|
||||||
if (walletList) {
|
electrobun.rpc.request.xrpListWallets({}),
|
||||||
wallets = walletList
|
)
|
||||||
if (walletList.length > 0 && !currentWalletId) {
|
if (walletList) {
|
||||||
currentWalletId = walletList[0].id
|
wallets = walletList
|
||||||
|
if (walletList.length > 0 && !currentWalletId) {
|
||||||
|
currentWalletId = walletList[0].id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
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()
|
||||||
|
|||||||
@@ -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()
|
// Balances live in the stores so they survive navigation; the effects
|
||||||
})
|
// only trigger background refetches.
|
||||||
fetchXmr()
|
$effect(() => {
|
||||||
|
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
|
||||||
|
xrpWallet.fetchBalances()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (moneroWallet.walletOpen) {
|
if (moneroWallet.walletOpen) {
|
||||||
fetchXmr()
|
moneroWallet.fetchPrice()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -179,42 +175,77 @@
|
|||||||
</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}
|
{#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">
|
||||||
${fmtUsd(portfolioUsd)}
|
${fmtUsd(portfolioUsd)}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<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">
|
||||||
<button
|
<button
|
||||||
onclick={() => navigate('/xrp')}
|
onclick={() => navigate('/xrp')}
|
||||||
class="text-left transition-transform active:scale-[0.98]">
|
class="text-left transition-transform active:scale-[0.98]">
|
||||||
<Card class="hover:border-foreground/20 transition-colors">
|
<Card class="h-44 hover:border-foreground/20 transition-colors">
|
||||||
<CardHeader class="pb-3">
|
<CardHeader class="pb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<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>
|
<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}
|
||||||
@@ -225,35 +256,61 @@
|
|||||||
<button
|
<button
|
||||||
onclick={() => navigate('/monero')}
|
onclick={() => navigate('/monero')}
|
||||||
class="text-left transition-transform active:scale-[0.98]">
|
class="text-left transition-transform active:scale-[0.98]">
|
||||||
<Card class="hover:border-foreground/20 transition-colors">
|
<Card class="h-44 hover:border-foreground/20 transition-colors">
|
||||||
<CardHeader class="pb-3">
|
<CardHeader class="pb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<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>
|
<CardContent class="flex flex-1 flex-col justify-center">
|
||||||
{#if loadingXmr && !moneroWallet.walletOpen}
|
{#if moneroWallet.walletOpen}
|
||||||
<div class="h-14 flex items-center"><Loader /></div>
|
|
||||||
{:else 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}
|
||||||
|
<p class="text-sm text-muted-foreground py-3">Wallet locked — unlock to view balance</p>
|
||||||
|
{:else if moneroWallet.opening}
|
||||||
|
<div class="h-14 flex items-center gap-2">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{moneroWallet.wallets.length > 0 ? 'Opening wallet...' : 'Starting wallet server...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if moneroWallet.error}
|
||||||
|
<div class="py-3">
|
||||||
|
<p class="text-xs text-destructive break-words">{moneroWallet.error}</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="mt-2 gap-1.5"
|
||||||
|
onclick={() => moneroWallet.login()}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{:else if moneroWallet.wallets.length > 0}
|
{:else if moneroWallet.wallets.length > 0}
|
||||||
<p class="text-sm text-muted-foreground py-3">Open wallet to view balance</p>
|
<div class="h-14 flex items-center gap-2">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
onclick={() => navigate('/monero')}
|
onclick={() => navigate('/monero')}
|
||||||
|
|||||||
@@ -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(),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -19,7 +19,24 @@
|
|||||||
let moneroMnemonic = $state('')
|
let moneroMnemonic = $state('')
|
||||||
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
||||||
|
|
||||||
let initStarted = false
|
let walletPassword = $state('')
|
||||||
|
let rememberPassword = $state(true)
|
||||||
|
let unlocking = $state(false)
|
||||||
|
let unlockError = $state('')
|
||||||
|
|
||||||
|
async function unlockWithPassword() {
|
||||||
|
if (unlocking || !walletPassword.trim()) return
|
||||||
|
unlocking = true
|
||||||
|
unlockError = ''
|
||||||
|
try {
|
||||||
|
await w.unlockWallet(walletPassword.trim(), rememberPassword)
|
||||||
|
walletPassword = ''
|
||||||
|
} catch (e) {
|
||||||
|
unlockError = e instanceof Error ? e.message : 'Failed to unlock wallet'
|
||||||
|
} finally {
|
||||||
|
unlocking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (w.walletOpen && w.walletName) {
|
if (w.walletOpen && w.walletName) {
|
||||||
@@ -30,17 +47,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!initStarted) {
|
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
|
||||||
initStarted = true
|
|
||||||
w.init().then(async () => {
|
|
||||||
if (w.accountType === 'monero') return
|
|
||||||
await w.login()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading) {
|
|
||||||
w.autoUnlock(w.wallets[0])
|
w.autoUnlock(w.wallets[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -66,6 +73,15 @@
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
{:else if w.opening}
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-center justify-center gap-2 py-8">
|
||||||
|
<Loader />
|
||||||
|
<span class="text-sm text-muted-foreground">
|
||||||
|
{!w.running ? 'Starting wallet server...' : 'Opening wallet...'}
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
{:else if !w.installed}
|
{:else if !w.installed}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -99,7 +115,48 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{:else if !w.walletOpen}
|
{:else if !w.walletOpen}
|
||||||
{#if w.wallets.length > 0}
|
{#if w.opening}
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-center justify-center py-8">
|
||||||
|
<Loader />
|
||||||
|
<span class="ml-2 text-sm text-muted-foreground">
|
||||||
|
Opening wallet...
|
||||||
|
</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{:else if w.passwordRequired && w.wallets.length > 0}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Wallet locked</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the password for {w.wallets[0]} to open it
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Wallet password"
|
||||||
|
bind:value={walletPassword} />
|
||||||
|
<label class="flex items-center gap-2 text-xs">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={rememberPassword}
|
||||||
|
class="h-3.5 w-3.5" />
|
||||||
|
Remember password for auto-unlock
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
onclick={unlockWithPassword}
|
||||||
|
disabled={unlocking || !walletPassword.trim()}>
|
||||||
|
{unlocking ? 'Opening...' : 'Unlock Wallet'}
|
||||||
|
</Button>
|
||||||
|
{#if unlockError}
|
||||||
|
<p class="text-xs text-destructive">{unlockError}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{:else if w.wallets.length > 0}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent class="flex items-center justify-center py-8">
|
<CardContent class="flex items-center justify-center py-8">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|||||||
@@ -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">
|
||||||
<label class="text-xs font-medium">Recipient Address</label>
|
<div class="flex items-center justify-between">
|
||||||
|
<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">
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
import Send from '@lucide/svelte/icons/send'
|
import Send from '@lucide/svelte/icons/send'
|
||||||
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
||||||
import Plus from '@lucide/svelte/icons/plus'
|
import Plus from '@lucide/svelte/icons/plus'
|
||||||
|
import Users from '@lucide/svelte/icons/users'
|
||||||
|
import Tags from '@lucide/svelte/icons/tags'
|
||||||
import Loader from '$lib/components/loader.svelte'
|
import Loader from '$lib/components/loader.svelte'
|
||||||
import QrCode from '$lib/components/qr.svelte'
|
import QrCode from '$lib/components/qr.svelte'
|
||||||
import { navigate, route } from 'sv-router/generated'
|
import { navigate, route } from 'sv-router/generated'
|
||||||
@@ -97,9 +99,53 @@
|
|||||||
|
|
||||||
let accountDialogOpen = $state(false)
|
let accountDialogOpen = $state(false)
|
||||||
let accountLabel = $state('')
|
let accountLabel = $state('')
|
||||||
|
let accountBusy = $state(false)
|
||||||
|
let accountError = $state('')
|
||||||
let subaddressDialogOpen = $state(false)
|
let subaddressDialogOpen = $state(false)
|
||||||
let subaddressAccountIndex = $state('')
|
let subaddressAccountIndex = $state('')
|
||||||
let subaddressLabel = $state('')
|
let subaddressLabel = $state('')
|
||||||
|
let subaddressBusy = $state(false)
|
||||||
|
let subaddressError = $state('')
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!subaddressAccountIndex && w.accounts.length > 0) {
|
||||||
|
subaddressAccountIndex = String(w.accounts[0].index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleCreateAccount() {
|
||||||
|
if (accountBusy) return
|
||||||
|
accountBusy = true
|
||||||
|
accountError = ''
|
||||||
|
try {
|
||||||
|
await w.createAccount(accountLabel || undefined)
|
||||||
|
accountDialogOpen = false
|
||||||
|
accountLabel = ''
|
||||||
|
} catch (e) {
|
||||||
|
accountError = e instanceof Error ? e.message : 'Failed to create account'
|
||||||
|
} finally {
|
||||||
|
accountBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateSubaddress() {
|
||||||
|
if (subaddressBusy || !subaddressAccountIndex) return
|
||||||
|
subaddressBusy = true
|
||||||
|
subaddressError = ''
|
||||||
|
try {
|
||||||
|
await w.createSubaddress(
|
||||||
|
parseInt(subaddressAccountIndex),
|
||||||
|
subaddressLabel || undefined,
|
||||||
|
)
|
||||||
|
subaddressDialogOpen = false
|
||||||
|
subaddressAccountIndex = ''
|
||||||
|
subaddressLabel = ''
|
||||||
|
} catch (e) {
|
||||||
|
subaddressError = e instanceof Error ? e.message : 'Failed to create subaddress'
|
||||||
|
} finally {
|
||||||
|
subaddressBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const walletName = $derived(route.params.name)
|
const walletName = $derived(route.params.name)
|
||||||
|
|
||||||
@@ -108,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))
|
||||||
@@ -134,19 +179,30 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!w.walletOpen || w.walletName !== walletName) {
|
if (w.walletOpen && w.walletName === walletName) {
|
||||||
|
w.refresh()
|
||||||
|
electrobun.rpc?.request
|
||||||
|
.fetchMoneroPrice({})
|
||||||
|
.then((p) => (moneroPrice = p))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!w.opening && (!w.walletOpen || w.walletName !== walletName)) {
|
||||||
navigate('/monero')
|
navigate('/monero')
|
||||||
}
|
}
|
||||||
w.refresh()
|
|
||||||
electrobun.rpc?.request
|
|
||||||
.fetchMoneroPrice({})
|
|
||||||
.then((p) => (moneroPrice = p))
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if !w.walletOpen || w.walletName !== walletName}
|
||||||
|
<div class="flex flex-col items-center justify-center gap-3 py-24">
|
||||||
|
<Loader />
|
||||||
|
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||||
|
</div>
|
||||||
|
{: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>
|
||||||
@@ -166,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">
|
||||||
@@ -340,14 +404,44 @@
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Create Account</Dialog.Title>
|
<div class="flex items-center gap-3">
|
||||||
<Dialog.Description>Optional label for the new account</Dialog.Description>
|
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||||
|
<Users size={16} class="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Dialog.Title>Create Account</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
New account with its own balance and addresses
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<div class="px-6 pb-4 space-y-3">
|
<div class="px-6 pb-4 space-y-3">
|
||||||
<Input placeholder="Account label (optional)" bind:value={accountLabel} />
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-xs font-medium" for="account-label">Label</label>
|
||||||
|
<Input id="account-label" placeholder="Savings" bind:value={accountLabel} />
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Optional — shown next to the account in your wallet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if accountError}
|
||||||
|
<p class="text-xs text-destructive">{accountError}</p>
|
||||||
|
{/if}
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button variant="outline" size="sm" onclick={() => { accountDialogOpen = false; accountLabel = '' }}>Cancel</Button>
|
<Button
|
||||||
<Button size="sm" onclick={async () => { await w.createAccount(accountLabel || undefined); accountDialogOpen = false; accountLabel = '' }}>Create</Button>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={accountBusy}
|
||||||
|
onclick={() => {
|
||||||
|
accountDialogOpen = false
|
||||||
|
accountLabel = ''
|
||||||
|
accountError = ''
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" disabled={accountBusy} onclick={handleCreateAccount}>
|
||||||
|
{accountBusy ? 'Creating...' : 'Create'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
@@ -360,29 +454,66 @@
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Create Subaddress</Dialog.Title>
|
<div class="flex items-center gap-3">
|
||||||
<Dialog.Description>Account index and optional label</Dialog.Description>
|
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||||
|
<Tags size={16} class="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<Dialog.Title>Create Subaddress</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Receiving address tied to an account
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<div class="px-6 pb-4 space-y-3">
|
<div class="px-6 pb-4 space-y-3">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label class="text-xs font-medium">Account</label>
|
<label class="text-xs font-medium" for="subaddress-account">Account</label>
|
||||||
<Select type="single" bind:value={subaddressAccountIndex}>
|
<Select type="single" bind:value={subaddressAccountIndex}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{subaddressAccountIndex ? `Account ${subaddressAccountIndex}` : 'Select account'}
|
Account {subaddressAccountIndex}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{#each w.accounts as acct}
|
{#each w.accounts as acct}
|
||||||
<SelectItem value={String(acct.index)}>
|
<SelectItem value={String(acct.index)}>
|
||||||
Account {acct.index}{acct.label ? ` - ${acct.label}` : ''}
|
Account {acct.index}{acct.label ? ` - ${acct.label}` : ''} ({atomicToXmr(acct.unlockedBalance)} XMR)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{/each}
|
{/each}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Where the subaddress belongs
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input placeholder="Subaddress label (optional)" bind:value={subaddressLabel} />
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-xs font-medium" for="subaddress-label">Label</label>
|
||||||
|
<Input id="subaddress-label" placeholder="Donations" bind:value={subaddressLabel} />
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Optional — helps you track who sent you funds
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if subaddressError}
|
||||||
|
<p class="text-xs text-destructive">{subaddressError}</p>
|
||||||
|
{/if}
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button variant="outline" size="sm" onclick={() => { subaddressDialogOpen = false; subaddressAccountIndex = ''; subaddressLabel = '' }}>Cancel</Button>
|
<Button
|
||||||
<Button size="sm" disabled={!subaddressAccountIndex} onclick={async () => { await w.createSubaddress(parseInt(subaddressAccountIndex), subaddressLabel || undefined); subaddressDialogOpen = false; subaddressAccountIndex = ''; subaddressLabel = '' }}>Create</Button>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={subaddressBusy}
|
||||||
|
onclick={() => {
|
||||||
|
subaddressDialogOpen = false
|
||||||
|
subaddressAccountIndex = ''
|
||||||
|
subaddressLabel = ''
|
||||||
|
subaddressError = ''
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={subaddressBusy || !subaddressAccountIndex}
|
||||||
|
onclick={handleCreateSubaddress}>
|
||||||
|
{subaddressBusy ? 'Creating...' : 'Create'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
@@ -455,4 +586,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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">
|
||||||
<label class="text-xs font-medium">Recipient Address</label>
|
<div class="flex items-center justify-between">
|
||||||
|
<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} />
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Generated by XcodeGen from project.yml
|
||||||
|
Koins.xcodeproj/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
build/
|
||||||
|
DerivedData/
|
||||||
|
*.xcworkspace
|
||||||
|
xcuserdata/
|
||||||
|
*.xcuserstate
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Branding
|
||||||
|
|
||||||
|
The app icon is the **official Monero mark**, monochrome.
|
||||||
|
|
||||||
|
`logo-official.svg` is `meta/safari-pinned-tab.svg` from
|
||||||
|
[monero-project/monero-site](https://github.com/monero-project/monero-site) —
|
||||||
|
the Monero Project's own traced, single-colour version of the logo, which is
|
||||||
|
exactly what a black-and-white icon needs. `logo-official.png` is that file
|
||||||
|
rasterised to 1024×1024:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
qlmanage -t -s 1024 -o /tmp Branding/logo-official.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
iOS cannot use SVG for app icons, so `make-icons.swift` produces the PNG the
|
||||||
|
asset catalog needs — the mark as published (black badge, white M) inset to 90%
|
||||||
|
so the platform's squircle mask does not clip it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd apps/ios
|
||||||
|
swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset
|
||||||
|
```
|
||||||
|
|
||||||
|
That writes the opaque 1024×1024 app icon (no alpha channel — the App Store
|
||||||
|
rejects icons with alpha) and `Branding/logo-inverted.png`, the same mark
|
||||||
|
inverted for dark surfaces (white badge, black M, black ground).
|
||||||
|
|
||||||
|
The asset catalog uses the single-size format, so Xcode derives every size the
|
||||||
|
device needs from the 1024.
|
||||||
|
|
||||||
|
Note: the Monero name and mark belong to the Monero Project — check their brand
|
||||||
|
guidance before shipping this publicly.
|
||||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M3370 6894 c-410 -21 -760 -95 -1116 -235 -1129 -445 -1939 -1467
|
||||||
|
-2114 -2666 -69 -479 -37 -963 97 -1436 l36 -127 508 0 509 0 0 1427 0 1428
|
||||||
|
1105 -1105 1105 -1105 1105 1105 1105 1105 0 -1428 0 -1427 509 0 508 0 36
|
||||||
|
127 c134 473 166 957 97 1436 -212 1456 -1350 2616 -2800 2856 -207 34 -511
|
||||||
|
54 -690 45z"/>
|
||||||
|
<path d="M2028 2635 l-3 -900 -713 -5 -713 -5 79 -117 c526 -777 1341 -1305
|
||||||
|
2252 -1457 200 -34 338 -45 570 -45 305 0 533 27 805 93 824 202 1538 700
|
||||||
|
2016 1409 l80 117 -716 3 -715 2 0 903 0 902 -735 -735 -735 -735 -735 735
|
||||||
|
-735 735 -2 -900z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,134 @@
|
|||||||
|
// Regenerates the app icon PNGs from the official Monero mark.
|
||||||
|
//
|
||||||
|
// cd apps/ios
|
||||||
|
// swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset
|
||||||
|
//
|
||||||
|
// Input: Branding/logo-official.png — the monochrome mark published in
|
||||||
|
// monero-project/monero-site (meta/safari-pinned-tab.svg), rasterised.
|
||||||
|
// It is a black badge with the M knocked out, drawn on white.
|
||||||
|
// Output: AppIcon-1024.png - the inverse: white badge, black M, on black
|
||||||
|
// (reads well on the app's dark UI)
|
||||||
|
// AppIcon-light-1024.png - the mark as published: black badge, white M,
|
||||||
|
// on white
|
||||||
|
//
|
||||||
|
// The artwork is inset to 90% of the canvas so the iOS squircle mask does not
|
||||||
|
// clip the badge's edges. iOS cannot use SVG for app icons, which is why this
|
||||||
|
// rasterises instead of linking the SVG.
|
||||||
|
import AppKit
|
||||||
|
import CoreGraphics
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Bitmap {
|
||||||
|
let width: Int
|
||||||
|
let height: Int
|
||||||
|
var pixels: [UInt8] // RGBA, premultiplied
|
||||||
|
|
||||||
|
func luminance(_ offset: Int) -> Int {
|
||||||
|
(Int(pixels[offset]) * 299 + Int(pixels[offset + 1]) * 587 + Int(pixels[offset + 2]) * 114) / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBitmap(_ path: String) -> Bitmap? {
|
||||||
|
guard let image = NSImage(contentsOfFile: path),
|
||||||
|
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
|
||||||
|
let width = cgImage.width
|
||||||
|
let height = cgImage.height
|
||||||
|
var pixels = [UInt8](repeating: 0, count: width * height * 4)
|
||||||
|
guard let context = CGContext(data: &pixels,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: width * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }
|
||||||
|
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||||
|
return Bitmap(width: width, height: height, pixels: pixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeImage(_ bitmap: Bitmap) -> CGImage? {
|
||||||
|
guard let provider = CGDataProvider(data: Data(bitmap.pixels) as CFData) else { return nil }
|
||||||
|
return CGImage(width: bitmap.width,
|
||||||
|
height: bitmap.height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bitsPerPixel: 32,
|
||||||
|
bytesPerRow: bitmap.width * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||||
|
provider: provider,
|
||||||
|
decode: nil,
|
||||||
|
shouldInterpolate: false,
|
||||||
|
intent: .defaultIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inverted(_ bitmap: Bitmap) -> Bitmap {
|
||||||
|
var pixels = bitmap.pixels
|
||||||
|
for offset in stride(from: 0, to: pixels.count, by: 4) {
|
||||||
|
let value = UInt8(255 - bitmap.luminance(offset))
|
||||||
|
pixels[offset] = value
|
||||||
|
pixels[offset + 1] = value
|
||||||
|
pixels[offset + 2] = value
|
||||||
|
}
|
||||||
|
return Bitmap(width: bitmap.width, height: bitmap.height, pixels: pixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composes the mark onto a square background, inset so the platform mask has
|
||||||
|
/// room, and writes an opaque PNG (App Store icons must not carry alpha).
|
||||||
|
func writeIcon(_ source: Bitmap, size: Int, inset: CGFloat, background: NSColor, to path: String) {
|
||||||
|
guard let image = makeImage(source) else { return }
|
||||||
|
var pixels = [UInt8](repeating: 0, count: size * size * 4)
|
||||||
|
guard let context = CGContext(data: &pixels,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: size * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return }
|
||||||
|
|
||||||
|
context.setFillColor(background.cgColor)
|
||||||
|
context.fill(CGRect(x: 0, y: 0, width: size, height: size))
|
||||||
|
let side = CGFloat(size) * inset
|
||||||
|
let offset = (CGFloat(size) - side) / 2
|
||||||
|
context.interpolationQuality = .high
|
||||||
|
context.draw(image, in: CGRect(x: offset, y: offset, width: side, height: side))
|
||||||
|
|
||||||
|
var rgb = [UInt8](repeating: 0, count: size * size * 3)
|
||||||
|
for index in 0..<(size * size) {
|
||||||
|
rgb[index * 3] = pixels[index * 4]
|
||||||
|
rgb[index * 3 + 1] = pixels[index * 4 + 1]
|
||||||
|
rgb[index * 3 + 2] = pixels[index * 4 + 2]
|
||||||
|
}
|
||||||
|
guard let provider = CGDataProvider(data: Data(rgb) as CFData),
|
||||||
|
let flattened = CGImage(width: size,
|
||||||
|
height: size,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bitsPerPixel: 24,
|
||||||
|
bytesPerRow: size * 3,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
|
||||||
|
provider: provider,
|
||||||
|
decode: nil,
|
||||||
|
shouldInterpolate: false,
|
||||||
|
intent: .defaultIntent) else { return }
|
||||||
|
let representation = NSBitmapImageRep(cgImage: flattened)
|
||||||
|
guard let data = representation.representation(using: .png, properties: [:]) else { return }
|
||||||
|
try? data.write(to: URL(fileURLWithPath: path))
|
||||||
|
print("wrote \(path)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let arguments = CommandLine.arguments
|
||||||
|
let outputDirectory = arguments.count > 1 ? arguments[1] : "/tmp"
|
||||||
|
let sourcePath = arguments.count > 2 ? arguments[2] : "Branding/logo-official.png"
|
||||||
|
try? FileManager.default.createDirectory(atPath: outputDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
guard let source = loadBitmap(sourcePath) else {
|
||||||
|
print("cannot read source artwork at \(sourcePath)")
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let inset: CGFloat = 0.78
|
||||||
|
// Primary: the mark exactly as Monero publishes it (black badge, white M).
|
||||||
|
writeIcon(source, size: 1024, inset: inset, background: .white,
|
||||||
|
to: "\(outputDirectory)/AppIcon-1024.png")
|
||||||
|
// Alternate: inverted, for dark surfaces (white badge, black M, black ground).
|
||||||
|
writeIcon(inverted(source), size: 1024, inset: inset, background: .black,
|
||||||
|
to: "Branding/logo-inverted.png")
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
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?
|
||||||
|
@Published var moneroAccounts: [MoneroAccount] = []
|
||||||
|
@Published var prices = PriceService.Prices(currency: "USD")
|
||||||
|
|
||||||
|
/// Whether the app is behind the biometric lock screen right now.
|
||||||
|
@Published var locked = false
|
||||||
|
|
||||||
|
/// Face ID/Touch ID lock. On by default when the device has biometrics;
|
||||||
|
/// persisted across launches.
|
||||||
|
var faceIDEnabled: Bool {
|
||||||
|
get { UserDefaults.standard.object(forKey: "koins.faceID") as? Bool ?? true }
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: "koins.faceID")
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 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)?
|
||||||
|
private var lastCheckpoint = Date.distantPast
|
||||||
|
/// Invalidates in-flight refreshes: switching chains (or starting a newer
|
||||||
|
/// refresh) must stop an older request from writing its result to the UI.
|
||||||
|
private var refreshToken = UUID()
|
||||||
|
|
||||||
|
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: - Fiat display
|
||||||
|
|
||||||
|
/// The fiat currency the wallet shows; persisted, and changing it refetches
|
||||||
|
/// the price cache.
|
||||||
|
var fiatCurrency: String {
|
||||||
|
get { UserDefaults.standard.string(forKey: "koins.fiatCurrency") ?? "USD" }
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: "koins.fiatCurrency")
|
||||||
|
objectWillChange.send()
|
||||||
|
Task { await refreshPrices() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current chain's balance in the chosen fiat currency, when a price is
|
||||||
|
/// known.
|
||||||
|
var fiatBalance: String? {
|
||||||
|
fiatValue(of: balance.total, chain: chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a chain amount into the chosen fiat currency, when a price is
|
||||||
|
/// known.
|
||||||
|
func fiatValue(of amount: String, chain: Chain) -> String? {
|
||||||
|
let price = chain == .xrp ? prices.xrp : prices.xmr
|
||||||
|
guard let price, let value = Decimal(string: amount) else { return nil }
|
||||||
|
let fiat = value * Decimal(price)
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .currency
|
||||||
|
formatter.currencyCode = fiatCurrency
|
||||||
|
formatter.locale = Locale(identifier: "en_US")
|
||||||
|
return formatter.string(from: fiat as NSDecimalNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshPrices() async {
|
||||||
|
prices = await PriceService.shared.prices(currency: fiatCurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Biometric lock
|
||||||
|
|
||||||
|
// MARK: - Swaps (kyc.rip)
|
||||||
|
|
||||||
|
@Published var swapOrders: [SwapOrder] = []
|
||||||
|
|
||||||
|
func loadSwapOrders() {
|
||||||
|
swapOrders = WalletStore.swapOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateSwap(amount: String, from: Chain, to: Chain) async throws -> SwapEstimate {
|
||||||
|
guard let amountNumber = Double(amount.trimmingCharacters(in: .whitespaces)),
|
||||||
|
amountNumber > 0 else {
|
||||||
|
throw SwapError.api("Enter a valid amount")
|
||||||
|
}
|
||||||
|
return try await SwapService.shared.estimate(from: from.rawValue, to: to.rawValue, amount: amountNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSwap(route: SwapRoute, estimateID: String, amountFrom: String,
|
||||||
|
from: Chain, to: Chain) async throws -> SwapOrder {
|
||||||
|
guard let amountNumber = Double(amountFrom.trimmingCharacters(in: .whitespaces)), amountNumber > 0 else {
|
||||||
|
throw SwapError.api("Enter a valid amount")
|
||||||
|
}
|
||||||
|
guard let destination = (to == .xrp ? xrpWallet?.address : xmrWallet?.address), !destination.isEmpty else {
|
||||||
|
throw SwapError.api("No \(to.title) wallet to receive into")
|
||||||
|
}
|
||||||
|
let order = try await SwapService.shared.createOrder(route: route,
|
||||||
|
estimateID: estimateID,
|
||||||
|
from: from.rawValue,
|
||||||
|
to: to.rawValue,
|
||||||
|
addressTo: destination,
|
||||||
|
amountFrom: amountNumber)
|
||||||
|
WalletStore.saveSwapOrder(order)
|
||||||
|
loadSwapOrders()
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends the swap deposit using the same payment path as a normal send.
|
||||||
|
func sendSwapDeposit(_ order: SwapOrder, accountIndex: Int = 0) async throws {
|
||||||
|
try await send(to: order.depositAddress,
|
||||||
|
amount: String(order.depositAmount),
|
||||||
|
tag: order.depositMemo ?? "",
|
||||||
|
priority: .defaultPriority,
|
||||||
|
accountIndex: accountIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshSwapStatus(_ order: SwapOrder) async -> SwapOrder {
|
||||||
|
guard let updated = try? await SwapService.shared.orderStatus(order) else { return order }
|
||||||
|
var order = order
|
||||||
|
order.status = updated.status
|
||||||
|
order.txIn = updated.txIn ?? order.txIn
|
||||||
|
order.txOut = updated.txOut ?? order.txOut
|
||||||
|
if order.txIn?.isEmpty == true { order.txIn = nil }
|
||||||
|
if order.txOut?.isEmpty == true { order.txOut = nil }
|
||||||
|
order.supportUrl = updated.supportUrl ?? order.supportUrl
|
||||||
|
if let index = swapOrders.firstIndex(where: { $0.id == order.id }) {
|
||||||
|
swapOrders[index] = order
|
||||||
|
}
|
||||||
|
WalletStore.saveSwapOrder(order)
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists a local change to an order (for example, the deposit was sent).
|
||||||
|
func saveSwapOrder(_ order: SwapOrder) {
|
||||||
|
WalletStore.saveSwapOrder(order)
|
||||||
|
loadSwapOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSwapOrder(id: String) {
|
||||||
|
WalletStore.removeSwapOrder(id: id)
|
||||||
|
loadSwapOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticate() async {
|
||||||
|
if await Biometrics.authenticate(reason: "Unlock Koins") {
|
||||||
|
locked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires biometrics for sensitive actions when the lock is enabled; a
|
||||||
|
/// device without biometrics is not asked (the wallet is still usable).
|
||||||
|
func requireAuth(reason: String) async throws {
|
||||||
|
guard faceIDEnabled, Biometrics.isAvailable else { return }
|
||||||
|
let ok = await Biometrics.authenticate(reason: reason)
|
||||||
|
if !ok { throw XRPError.rpc("Authentication failed") }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cache
|
||||||
|
|
||||||
|
/// Restores the last-known state for the chain on screen instantly; the
|
||||||
|
/// refresh that follows replaces it with live data.
|
||||||
|
private func showCached(_ forChain: Chain) {
|
||||||
|
guard let cached = WalletStore.cachedState(forChain) else { return }
|
||||||
|
balance = cached.balance
|
||||||
|
transactions = cached.transactions
|
||||||
|
if let accounts = cached.accounts { moneroAccounts = accounts }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func persistState(for forChain: Chain) {
|
||||||
|
guard (forChain == .xrp ? xrpWallet : xmrWallet) != nil else { return }
|
||||||
|
WalletStore.saveCachedState(
|
||||||
|
ChainCache(balance: balance,
|
||||||
|
transactions: transactions,
|
||||||
|
accounts: forChain == .xmr ? moneroAccounts : nil,
|
||||||
|
savedAt: Date()),
|
||||||
|
chain: forChain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
// Restore the chain the user was last on, as long as that wallet exists.
|
||||||
|
let savedChain = UserDefaults.standard.string(forKey: "koins.chain").flatMap(Chain.init(rawValue:))
|
||||||
|
if let savedChain, (savedChain == .xrp ? xrpWallet : xmrWallet) != nil {
|
||||||
|
chain = savedChain
|
||||||
|
} else if xrpWallet != nil || moneroReady {
|
||||||
|
chain = xrpWallet != nil ? .xrp : .xmr
|
||||||
|
}
|
||||||
|
if xrpWallet != nil || xmrWallet != nil, faceIDEnabled, Biometrics.isAvailable {
|
||||||
|
locked = true
|
||||||
|
}
|
||||||
|
loadSwapOrders()
|
||||||
|
showCached(chain)
|
||||||
|
await refresh(force: true)
|
||||||
|
startAutoRefresh()
|
||||||
|
Task { await refreshPrices() }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
UserDefaults.standard.set(newChain.rawValue, forKey: "koins.chain")
|
||||||
|
refreshToken = UUID()
|
||||||
|
errorMessage = nil
|
||||||
|
statusMessage = nil
|
||||||
|
balance = Balance(total: "0", unlocked: "", funded: false)
|
||||||
|
transactions = []
|
||||||
|
moneroAccounts = []
|
||||||
|
syncStatus = nil
|
||||||
|
showCached(newChain)
|
||||||
|
|
||||||
|
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 }
|
||||||
|
let token = UUID()
|
||||||
|
refreshToken = token
|
||||||
|
let requestChain = chain
|
||||||
|
errorMessage = nil
|
||||||
|
await refreshPrices()
|
||||||
|
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)
|
||||||
|
let fetchedBalance = try await balanceCall
|
||||||
|
let fetchedTransactions = try await txCall
|
||||||
|
guard !isStale(token, requestChain) else { return }
|
||||||
|
balance = fetchedBalance
|
||||||
|
transactions = fetchedTransactions
|
||||||
|
persistState(for: requestChain)
|
||||||
|
} catch {
|
||||||
|
guard !isStale(token, requestChain) else { return }
|
||||||
|
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)
|
||||||
|
await checkpointWalletIfSyncing()
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
let status = try await MoneroService.shared.syncStatus()
|
||||||
|
if !isStale(token, requestChain) {
|
||||||
|
updateSyncStatus(status)
|
||||||
|
}
|
||||||
|
refreshTick += 1
|
||||||
|
await checkpointWalletIfSyncing()
|
||||||
|
if force || refreshTick == 1 || refreshTick % 4 == 0 {
|
||||||
|
async let balanceCall = MoneroService.shared.balance()
|
||||||
|
async let txCall = MoneroService.shared.transactions()
|
||||||
|
async let accountsCall = MoneroService.shared.accounts()
|
||||||
|
let fetchedBalance = try await balanceCall
|
||||||
|
let fetchedTransactions = try await txCall
|
||||||
|
let fetchedAccounts = try await accountsCall
|
||||||
|
guard !isStale(token, requestChain) else { return }
|
||||||
|
balance = fetchedBalance
|
||||||
|
transactions = fetchedTransactions
|
||||||
|
moneroAccounts = fetchedAccounts
|
||||||
|
persistState(for: requestChain)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
guard !isStale(token, requestChain) else { return }
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when a newer refresh has started or the user moved to another chain
|
||||||
|
/// while this request was in flight.
|
||||||
|
private func isStale(_ token: UUID, _ requestChain: Chain) -> Bool {
|
||||||
|
token != refreshToken || requestChain != chain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
try await requireAuth(reason: "Reveal your recovery phrase")
|
||||||
|
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,
|
||||||
|
accountIndex: Int = 0) async throws {
|
||||||
|
try await requireAuth(reason: "Authorize this payment")
|
||||||
|
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,
|
||||||
|
accountIndex: accountIndex)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the app leaves the foreground: iOS can suspend or kill it at
|
||||||
|
/// any point, so flush the wallet while there is still time.
|
||||||
|
func persistWallet() async {
|
||||||
|
guard moneroReady else { return }
|
||||||
|
await MoneroService.shared.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAccount(label: String = "") async throws {
|
||||||
|
_ = try await MoneroService.shared.createAccount(label: label)
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSubaddress(accountIndex: Int, label: String = "") async throws {
|
||||||
|
_ = try await MoneroService.shared.createSubaddress(accountIndex: accountIndex, label: label)
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The account a payment should leave from by default: the first one with
|
||||||
|
/// spendable funds, otherwise the primary account.
|
||||||
|
var defaultMoneroAccountIndex: Int {
|
||||||
|
moneroAccounts.first { (Decimal(string: $0.unlocked) ?? 0) > 0 }?.index ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes the account/subaddress tree independently of the active chain.
|
||||||
|
func loadAccounts() async {
|
||||||
|
guard moneroReady else { return }
|
||||||
|
if let accounts = try? await MoneroService.shared.accounts() {
|
||||||
|
moneroAccounts = accounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// monero-cpp only writes the wallet file from save(), and its sync loop
|
||||||
|
/// never does, so without this a restart re-scans from the beginning.
|
||||||
|
private func checkpointWalletIfSyncing() async {
|
||||||
|
guard moneroReady, syncStatus?.syncing == true else { return }
|
||||||
|
guard Date().timeIntervalSince(lastCheckpoint) >= 60 else { return }
|
||||||
|
lastCheckpoint = Date()
|
||||||
|
await MoneroService.shared.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#import "KoinsMoneroWallet.h"
|
||||||
|
#import "KoinsSecp256k1.h"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep wallet files out of iCloud/iTunes backups: the seed in the Keychain is
|
||||||
|
// device-only and the file is rebuildable from it, so backing it up would only
|
||||||
|
// consume the user's quota (it peaks near 120 MB while a wallet restored from
|
||||||
|
// genesis scans). Must run after every write, since wallet2 rewrites these
|
||||||
|
// files on each save.
|
||||||
|
static void excludeWalletFilesFromBackup(const std::string &walletPathString) {
|
||||||
|
NSString *base = [NSString stringWithUTF8String:walletPathString.c_str()];
|
||||||
|
if (base.length == 0) return;
|
||||||
|
for (NSString *suffix in @[ @"", @".keys", @".address.txt", @".mms" ]) {
|
||||||
|
NSURL *url = [NSURL fileURLWithPath:[base stringByAppendingString:suffix]];
|
||||||
|
[url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static monero_wallet_full *g_wallet = nullptr;
|
||||||
|
static std::string g_walletPath;
|
||||||
|
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 (...) {}
|
||||||
|
g_walletPath = path;
|
||||||
|
excludeWalletFilesFromBackup(g_walletPath);
|
||||||
|
} 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()];
|
||||||
|
g_walletPath = path;
|
||||||
|
excludeWalletFilesFromBackup(g_walletPath);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
g_walletPath = walletPath(req[@"name"]);
|
||||||
|
excludeWalletFilesFromBackup(g_walletPath);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
g_walletPath.clear();
|
||||||
|
} else if ([op isEqualToString:@"saveWallet"]) {
|
||||||
|
// monero-cpp never persists while syncing, so scan progress survives a
|
||||||
|
// restart only if this is called explicitly. save() pauses the sync
|
||||||
|
// thread while it writes.
|
||||||
|
if (g_wallet) {
|
||||||
|
g_wallet->save();
|
||||||
|
excludeWalletFilesFromBackup(g_walletPath);
|
||||||
|
resp[@"saved"] = @YES;
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
NSMutableDictionary *entry = [NSMutableDictionary dictionary];
|
||||||
|
entry[@"hash"] = [NSString stringWithUTF8String:((tx && tx->m_hash) ? *tx->m_hash : "").c_str()];
|
||||||
|
entry[@"amount"] = @((transfer->m_amount ? *transfer->m_amount : 0)).stringValue;
|
||||||
|
entry[@"timestamp"] = @((tx && tx->m_received_timestamp) ? *tx->m_received_timestamp : 0);
|
||||||
|
entry[@"direction"] = (isIn && *isIn) ? @"in" : @"out";
|
||||||
|
entry[@"confirmed"] = @((tx && tx->m_is_confirmed) ? *tx->m_is_confirmed : false);
|
||||||
|
entry[@"height"] = @(txHeight ? *txHeight : 0);
|
||||||
|
entry[@"fee"] = @((tx && tx->m_fee) ? *tx->m_fee : 0).stringValue;
|
||||||
|
|
||||||
|
// Extra detail for the transaction view.
|
||||||
|
if (tx) {
|
||||||
|
if (tx->m_payment_id) entry[@"paymentId"] = [NSString stringWithUTF8String:tx->m_payment_id->c_str()];
|
||||||
|
if (tx->m_num_confirmations) entry[@"confirmations"] = @(*tx->m_num_confirmations);
|
||||||
|
if (tx->m_weight) entry[@"weight"] = @(*tx->m_weight);
|
||||||
|
if (tx->m_note) entry[@"note"] = [NSString stringWithUTF8String:tx->m_note->c_str()];
|
||||||
|
if (tx->m_unlock_time) entry[@"unlockTime"] = @(*tx->m_unlock_time);
|
||||||
|
}
|
||||||
|
if (transfer->m_account_index) entry[@"accountIndex"] = @(*transfer->m_account_index);
|
||||||
|
|
||||||
|
if (isIn && *isIn) {
|
||||||
|
auto incoming = std::dynamic_pointer_cast<monero_incoming_transfer>(transfer);
|
||||||
|
if (incoming) {
|
||||||
|
if (incoming->m_subaddress_index) entry[@"subaddressIndex"] = @(*incoming->m_subaddress_index);
|
||||||
|
if (incoming->m_address) entry[@"counterparty"] = [NSString stringWithUTF8String:incoming->m_address->c_str()];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto outgoing = std::dynamic_pointer_cast<monero_outgoing_transfer>(transfer);
|
||||||
|
if (outgoing) {
|
||||||
|
if (!outgoing->m_addresses.empty()) entry[@"counterparty"] = [NSString stringWithUTF8String:outgoing->m_addresses.front().c_str()];
|
||||||
|
if (!outgoing->m_subaddress_indices.empty()) entry[@"subaddressIndex"] = @(outgoing->m_subaddress_indices.front());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[entries addObject:entry];
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
os_log_error(OS_LOG_DEFAULT, "[koins] getTransactions: %{public}s", e.what());
|
||||||
|
}
|
||||||
|
resp[@"transactions"] = entries;
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"getAccounts"]) {
|
||||||
|
NSMutableArray *accounts = [NSMutableArray new];
|
||||||
|
if (!g_wallet) {
|
||||||
|
resp[@"accounts"] = accounts;
|
||||||
|
} else {
|
||||||
|
for (const auto &account : g_wallet->get_accounts(true, "")) {
|
||||||
|
NSMutableDictionary *accountDict = [NSMutableDictionary new];
|
||||||
|
accountDict[@"index"] = @(account.m_index ? *account.m_index : 0);
|
||||||
|
accountDict[@"address"] = [NSString stringWithUTF8String:(account.m_primary_address ? *account.m_primary_address : "").c_str()];
|
||||||
|
accountDict[@"balance"] = @((account.m_balance ? *account.m_balance : 0)).stringValue;
|
||||||
|
accountDict[@"unlocked"] = @((account.m_unlocked_balance ? *account.m_unlocked_balance : 0)).stringValue;
|
||||||
|
NSMutableArray *subaddresses = [NSMutableArray new];
|
||||||
|
for (const auto &sub : account.m_subaddresses) {
|
||||||
|
[subaddresses addObject:@{
|
||||||
|
@"accountIndex" : @(sub.m_account_index ? *sub.m_account_index : 0),
|
||||||
|
@"index" : @(sub.m_index ? *sub.m_index : 0),
|
||||||
|
@"address" : [NSString stringWithUTF8String:(sub.m_address ? *sub.m_address : "").c_str()],
|
||||||
|
@"label" : [NSString stringWithUTF8String:(sub.m_label ? *sub.m_label : "").c_str()],
|
||||||
|
@"balance" : @((sub.m_balance ? *sub.m_balance : 0)).stringValue,
|
||||||
|
@"unlocked" : @((sub.m_unlocked_balance ? *sub.m_unlocked_balance : 0)).stringValue,
|
||||||
|
@"isUsed" : @((sub.m_is_used ? *sub.m_is_used : false)),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
accountDict[@"subaddresses"] = subaddresses;
|
||||||
|
[accounts addObject:accountDict];
|
||||||
|
}
|
||||||
|
resp[@"accounts"] = accounts;
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"createAccount"]) {
|
||||||
|
if (!g_wallet) {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
} else {
|
||||||
|
monero_account account = g_wallet->create_account(std::string([req[@"label"] UTF8String]));
|
||||||
|
g_wallet->save();
|
||||||
|
excludeWalletFilesFromBackup(g_walletPath);
|
||||||
|
resp[@"index"] = @(account.m_index ? *account.m_index : 0);
|
||||||
|
resp[@"address"] = [NSString stringWithUTF8String:(account.m_primary_address ? *account.m_primary_address : "").c_str()];
|
||||||
|
resp[@"balance"] = @((account.m_balance ? *account.m_balance : 0)).stringValue;
|
||||||
|
resp[@"unlocked"] = @((account.m_unlocked_balance ? *account.m_unlocked_balance : 0)).stringValue;
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"createSubaddress"]) {
|
||||||
|
if (!g_wallet) {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
} else {
|
||||||
|
uint32_t accountIndex = (uint32_t)[req[@"accountIndex"] integerValue];
|
||||||
|
monero_subaddress sub = g_wallet->create_subaddress(accountIndex, std::string([req[@"label"] UTF8String]));
|
||||||
|
g_wallet->save();
|
||||||
|
excludeWalletFilesFromBackup(g_walletPath);
|
||||||
|
resp[@"accountIndex"] = @(sub.m_account_index ? *sub.m_account_index : 0);
|
||||||
|
resp[@"index"] = @(sub.m_index ? *sub.m_index : 0);
|
||||||
|
resp[@"address"] = [NSString stringWithUTF8String:(sub.m_address ? *sub.m_address : "").c_str()];
|
||||||
|
resp[@"label"] = [NSString stringWithUTF8String:(sub.m_label ? *sub.m_label : "").c_str()];
|
||||||
|
}
|
||||||
|
} 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_account_index = (uint32_t)[req[@"accountIndex"] integerValue];
|
||||||
|
// create_txs relays the transaction when m_relay is set, so it must
|
||||||
|
// not be submitted a second time.
|
||||||
|
config.m_relay = true;
|
||||||
|
auto txs = g_wallet->create_txs(config);
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?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>NSCameraUsageDescription</key>
|
||||||
|
<string>Scan a QR code to fill in a payment address.</string>
|
||||||
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
<string>Unlock Koins and approve payments.</string>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>UIUserInterfaceStyle</key>
|
||||||
|
<string>Dark</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web block explorers used for "view on the web" links.
|
||||||
|
enum BlockExplorer {
|
||||||
|
static func transactionURL(chain: Chain, hash: String) -> URL? {
|
||||||
|
guard !hash.isEmpty else { return nil }
|
||||||
|
switch chain {
|
||||||
|
case .xmr: return URL(string: "https://xmrchain.net/tx/\(hash)")
|
||||||
|
case .xrp: return URL(string: "https://livenet.xrpl.org/transactions/\(hash)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same lookup for a raw ticker ("XMR"/"XRP"), as used by the swap API.
|
||||||
|
static func transactionURL(ticker: String, hash: String) -> URL? {
|
||||||
|
switch ticker.uppercased() {
|
||||||
|
case "XMR": return transactionURL(chain: .xmr, hash: hash)
|
||||||
|
case "XRP": return transactionURL(chain: .xrp, hash: hash)
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletRecord: Codable, Identifiable, Equatable {
|
||||||
|
let id: String
|
||||||
|
var name: String
|
||||||
|
var address: String
|
||||||
|
var createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Direction: String, Codable {
|
||||||
|
case incoming
|
||||||
|
case outgoing
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransactionRecord: Identifiable, Codable {
|
||||||
|
let id: String
|
||||||
|
let amount: String
|
||||||
|
let timestamp: Date?
|
||||||
|
let direction: Direction
|
||||||
|
let confirmed: Bool
|
||||||
|
var fee: String?
|
||||||
|
var tag: Int?
|
||||||
|
var counterparty: String?
|
||||||
|
var confirmations: UInt64?
|
||||||
|
var height: UInt64?
|
||||||
|
var paymentId: String?
|
||||||
|
var accountIndex: Int?
|
||||||
|
var subaddressIndex: Int?
|
||||||
|
var note: String?
|
||||||
|
var weight: UInt64?
|
||||||
|
var ledgerIndex: UInt64?
|
||||||
|
var sequence: UInt64?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Balance: Codable {
|
||||||
|
var total: String
|
||||||
|
var unlocked: String
|
||||||
|
var funded: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persisted per chain so a switch or relaunch shows the last-known state
|
||||||
|
/// instantly while the network catches up.
|
||||||
|
struct ChainCache: Codable {
|
||||||
|
var balance: Balance
|
||||||
|
var transactions: [TransactionRecord]
|
||||||
|
var accounts: [MoneroAccount]?
|
||||||
|
var savedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MoneroSubaddress: Identifiable, Codable {
|
||||||
|
let accountIndex: Int
|
||||||
|
let index: Int
|
||||||
|
let address: String
|
||||||
|
let label: String
|
||||||
|
let balance: String
|
||||||
|
let unlocked: String
|
||||||
|
let isUsed: Bool
|
||||||
|
|
||||||
|
var id: String { "\(accountIndex)/\(index)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MoneroAccount: Identifiable, Codable {
|
||||||
|
let index: Int
|
||||||
|
let address: String
|
||||||
|
let balance: String
|
||||||
|
let unlocked: String
|
||||||
|
var subaddresses: [MoneroSubaddress]
|
||||||
|
|
||||||
|
var id: Int { index }
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
index == 0 ? "Primary account" : "Account \(index + 1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
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 var report: [String] = []
|
||||||
|
|
||||||
|
private static func emit(_ line: String) {
|
||||||
|
print(line)
|
||||||
|
os_log("%{public}@", log: log, type: .default, line)
|
||||||
|
report.append(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the collected lines into the app container so the result can be
|
||||||
|
/// read back off the device (console output is not always reachable).
|
||||||
|
private static func flushReport() {
|
||||||
|
guard let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||||
|
let url = documents.appendingPathComponent("koins-selftest.txt")
|
||||||
|
try? report.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func runIfRequested() {
|
||||||
|
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTest") {
|
||||||
|
emit("KOINS_SELFTEST begin")
|
||||||
|
run()
|
||||||
|
emit("KOINS_SELFTEST end")
|
||||||
|
}
|
||||||
|
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTestNetwork") {
|
||||||
|
runNetwork()
|
||||||
|
}
|
||||||
|
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTestSwap") {
|
||||||
|
runSwapCheckIfRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)")
|
||||||
|
|
||||||
|
let accounts = try await MoneroService.shared.accounts()
|
||||||
|
emit("KOINS_NET xmr accounts \(accounts.count) subaddresses \(accounts.first?.subaddresses.count ?? 0)")
|
||||||
|
let account = try await MoneroService.shared.createAccount(label: "Savings")
|
||||||
|
emit("KOINS_NET xmr new account index \(account.index) address \(account.address.prefix(12))")
|
||||||
|
let subaddress = try await MoneroService.shared.createSubaddress(accountIndex: 0, label: "From Alice")
|
||||||
|
emit("KOINS_NET xmr new subaddress index \(subaddress.index) address \(subaddress.address.prefix(12))")
|
||||||
|
let reloaded = try await MoneroService.shared.accounts()
|
||||||
|
emit("KOINS_NET xmr accounts after \(reloaded.count) subaddresses \(reloaded.first?.subaddresses.count ?? 0)")
|
||||||
|
|
||||||
|
await MoneroService.shared.stopSync()
|
||||||
|
await MoneroService.shared.removeWallet(name: "koins-net-selftest")
|
||||||
|
} catch {
|
||||||
|
emit("KOINS_NET xmr FAIL \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreWallets()
|
||||||
|
emit("KOINS_NET end")
|
||||||
|
flushReport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporary: exercise the kyc.rip estimate path with real values.
|
||||||
|
static func runSwapCheckIfRequested() {
|
||||||
|
guard ProcessInfo.processInfo.arguments.contains("-koinsSelfTestSwap") else { return }
|
||||||
|
Task.detached {
|
||||||
|
do {
|
||||||
|
let estimate = try await SwapService.shared.estimate(from: "xmr", to: "xrp", amount: 1)
|
||||||
|
emit("KOINS_SWAP amountTo \(estimate.amountTo)")
|
||||||
|
for route in estimate.routes.prefix(3) {
|
||||||
|
emit("KOINS_SWAP route \(route.provider) (\(route.engine)) -> \(route.amountTo) eta \(route.eta ?? -1)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
emit("KOINS_SWAP FAIL \(error)")
|
||||||
|
}
|
||||||
|
flushReport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func run() {
|
||||||
|
var failures = 0
|
||||||
|
|
||||||
|
func expect(_ name: String, _ actual: String, _ wanted: String) {
|
||||||
|
let ok = actual.lowercased() == wanted.lowercased()
|
||||||
|
if !ok { failures += 1 }
|
||||||
|
emit("KOINS_SELFTEST \(name): \(ok ? "PASS" : "FAIL") \(actual)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectTrue(_ name: String, _ value: Bool) {
|
||||||
|
if !value { failures += 1 }
|
||||||
|
emit("KOINS_SELFTEST \(name): \(value ? "PASS" : "FAIL")")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
emit("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
|
||||||
|
emit("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
|
||||||
|
emit("KOINS_SELFTEST bip32 level \(level): FAIL derivation returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
emit("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
|
||||||
|
emit("KOINS_SELFTEST bip44 #\(position): FAIL \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR round trip: generate an image, read it back, parse the payment URI.
|
||||||
|
let xmrAddress = "46cRPeU77sBVGSVXPyCjvJUdC9AjqvETWb6giUGskdnWN8ZifindmrH7mcztKU52iKgvWXY4dyPujg9LYUY1Q7dvDiwHNCE"
|
||||||
|
let xrpAddress = "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C"
|
||||||
|
|
||||||
|
func roundTrip(_ name: String, _ text: String, checks: [(String, String)]) {
|
||||||
|
guard let image = QRCodeImage.make(text) else {
|
||||||
|
failures += 1
|
||||||
|
emit("KOINS_SELFTEST \(name): FAIL could not render QR image")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let decoded = QRCodeReader.decodeText(in: image) else {
|
||||||
|
failures += 1
|
||||||
|
emit("KOINS_SELFTEST \(name): FAIL vision could not read the QR image (\(Int(image.size.width))px)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let payload = QRCodeReader.parse(decoded) else {
|
||||||
|
failures += 1
|
||||||
|
emit("KOINS_SELFTEST \(name): FAIL parse of '\(decoded)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (label, expected) in checks {
|
||||||
|
let actual: String
|
||||||
|
switch label {
|
||||||
|
case "address": actual = payload.address
|
||||||
|
case "amount": actual = payload.amount ?? ""
|
||||||
|
case "tag": actual = payload.tag.map(String.init) ?? ""
|
||||||
|
case "paymentId": actual = payload.paymentId ?? ""
|
||||||
|
case "memo": actual = payload.memo ?? ""
|
||||||
|
default: actual = ""
|
||||||
|
}
|
||||||
|
expect("\(name) \(label)", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roundTrip("qr monero uri",
|
||||||
|
"monero:\(xmrAddress)?tx_amount=1.5&tx_payment_id=abc123&tx_description=Rent",
|
||||||
|
checks: [("address", xmrAddress), ("amount", "1.5"), ("paymentId", "abc123"), ("memo", "Rent")])
|
||||||
|
roundTrip("qr monero plain", xmrAddress, checks: [("address", xmrAddress)])
|
||||||
|
roundTrip("qr xrp uri",
|
||||||
|
"ripple:\(xrpAddress)?amount=12.5&dt=99",
|
||||||
|
checks: [("address", xrpAddress), ("amount", "12.5"), ("tag", "99")])
|
||||||
|
roundTrip("qr xrp plain", xrpAddress, checks: [("address", xrpAddress)])
|
||||||
|
|
||||||
|
expectTrue("qr monero detected", AddressFormat.isMonero(xmrAddress))
|
||||||
|
expectTrue("qr xrp detected", AddressFormat.isXRP(xrpAddress))
|
||||||
|
expectTrue("qr rejects junk", QRCodeReader.parse("hello world")?.address == "hello world")
|
||||||
|
expectTrue("qr not monero", !AddressFormat.isMonero(xrpAddress))
|
||||||
|
expectTrue("qr not xrp", !AddressFormat.isXRP(xmrAddress))
|
||||||
|
|
||||||
|
emit("biometrics available=\(Biometrics.isAvailable) label=\(Biometrics.label)")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
emit("KOINS_SELFTEST secp256k1 signature len: \(signature.count)")
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
emit("KOINS_SELFTEST secp256k1 sign: FAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("KOINS_SELFTEST summary: \(failures == 0 ? "ALL PASS" : "\(failures) FAILURE(S)")")
|
||||||
|
flushReport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)]))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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...])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Foundation
|
||||||
|
import LocalAuthentication
|
||||||
|
|
||||||
|
/// Face ID / Touch ID gating for the app lock and for sensitive actions.
|
||||||
|
enum Biometrics {
|
||||||
|
static var isAvailable: Bool {
|
||||||
|
LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var label: String {
|
||||||
|
let context = LAContext()
|
||||||
|
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||||
|
return context.biometryType == .faceID ? "Face ID" : "Touch ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Biometrics with the system passcode fallback (the alert offers "Use
|
||||||
|
/// Passcode" after a failed face scan).
|
||||||
|
static func authenticate(reason: String) async -> Bool {
|
||||||
|
guard isAvailable else { return false }
|
||||||
|
let context = LAContext()
|
||||||
|
context.localizedCancelTitle = "Cancel"
|
||||||
|
return (try? await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
|
||||||
|
localizedReason: reason)) ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Fiat prices for the two tickers the wallet supports, fetched from a public
|
||||||
|
/// price API. Cached briefly in memory and in UserDefaults so screens do not
|
||||||
|
/// refetch on every refresh, and so the currency conversion shows up instantly
|
||||||
|
/// on a cold start.
|
||||||
|
final class PriceService {
|
||||||
|
static let shared = PriceService()
|
||||||
|
|
||||||
|
struct Prices: Codable {
|
||||||
|
var xrp: Double?
|
||||||
|
var xmr: Double?
|
||||||
|
var currency: String
|
||||||
|
var fetchedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let cacheLifetime: TimeInterval = 60
|
||||||
|
private static let defaultsKey = "koins.prices"
|
||||||
|
|
||||||
|
private var memory: Prices?
|
||||||
|
private var inFlight: Task<Prices, Never>?
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
if let data = UserDefaults.standard.data(forKey: Self.defaultsKey),
|
||||||
|
let saved = try? JSONDecoder().decode(Prices.self, from: data) {
|
||||||
|
memory = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prices(currency: String) async -> Prices {
|
||||||
|
if let memory,
|
||||||
|
memory.currency == currency,
|
||||||
|
let fetchedAt = memory.fetchedAt,
|
||||||
|
Date().timeIntervalSince(fetchedAt) < Self.cacheLifetime {
|
||||||
|
return memory
|
||||||
|
}
|
||||||
|
if let inFlight { return await inFlight.value }
|
||||||
|
|
||||||
|
let task = Task<Prices, Never> { [weak self] in
|
||||||
|
guard let self else { return Prices(currency: currency, fetchedAt: nil) }
|
||||||
|
let fetched = await self.fetch(currency: currency)
|
||||||
|
self.memory = fetched
|
||||||
|
if let data = try? JSONEncoder().encode(fetched) {
|
||||||
|
UserDefaults.standard.set(data, forKey: Self.defaultsKey)
|
||||||
|
}
|
||||||
|
return fetched
|
||||||
|
}
|
||||||
|
inFlight = task
|
||||||
|
let result = await task.value
|
||||||
|
inFlight = nil
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetch(currency: String) async -> Prices {
|
||||||
|
if let prices = try? await fromCoinGecko(currency: currency) { return prices }
|
||||||
|
return (try? await fromKraken(currency: currency)) ?? Prices(currency: currency, fetchedAt: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fromCoinGecko(currency: String) async throws -> Prices {
|
||||||
|
let code = currency.lowercased()
|
||||||
|
guard let url = URL(string: "https://api.coingecko.com/api/v3/simple/price?ids=monero,ripple&vs_currencies=\(code)") else {
|
||||||
|
throw XRPError.network("Invalid price endpoint")
|
||||||
|
}
|
||||||
|
let (data, response) = try await URLSession.shared.data(from: url)
|
||||||
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
||||||
|
throw XRPError.network("CoinGecko request failed")
|
||||||
|
}
|
||||||
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
|
||||||
|
return Prices(
|
||||||
|
xrp: (json["ripple"] as? [String: Any]).flatMap { ($0[code] as? NSNumber)?.doubleValue },
|
||||||
|
xmr: (json["monero"] as? [String: Any]).flatMap { ($0[code] as? NSNumber)?.doubleValue },
|
||||||
|
currency: currency,
|
||||||
|
fetchedAt: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kraken only lists USD and EUR for both assets; anything else just yields
|
||||||
|
/// no price rather than a wrong one.
|
||||||
|
private func fromKraken(currency: String) async throws -> Prices {
|
||||||
|
let supported: [String: (xrp: String, xmr: String)] = [
|
||||||
|
"USD": ("XXRPZUSD", "XXMRZUSD"),
|
||||||
|
"EUR": ("XXRPZEUR", "XXMRZEUR"),
|
||||||
|
]
|
||||||
|
guard let pairs = supported[currency] else {
|
||||||
|
throw XRPError.network("Kraken has no \(currency) pairs")
|
||||||
|
}
|
||||||
|
guard let url = URL(string: "https://api.kraken.com/0/public/Ticker?pair=\(pairs.xrp),\(pairs.xmr)") else {
|
||||||
|
throw XRPError.network("Invalid price endpoint")
|
||||||
|
}
|
||||||
|
let (data, response) = try await URLSession.shared.data(from: url)
|
||||||
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
||||||
|
throw XRPError.network("Kraken request failed")
|
||||||
|
}
|
||||||
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
|
||||||
|
let result = json["result"] as? [String: Any] ?? [:]
|
||||||
|
|
||||||
|
func lastTrade(_ key: String) -> Double? {
|
||||||
|
(result[key] as? [String: Any])
|
||||||
|
.flatMap { entry in (entry["c"] as? [Any])?.first }
|
||||||
|
.flatMap { ($0 as? String).flatMap(Double.init) }
|
||||||
|
}
|
||||||
|
return Prices(xrp: lastTrade(pairs.xrp),
|
||||||
|
xmr: lastTrade(pairs.xmr),
|
||||||
|
currency: currency,
|
||||||
|
fetchedAt: Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import CoreImage
|
||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Vision
|
||||||
|
|
||||||
|
/// Renders a QR image (used by the UI and by the self-test, which round-trips
|
||||||
|
/// an image through the reader).
|
||||||
|
enum QRCodeImage {
|
||||||
|
static func make(_ text: String, scale: CGFloat = 10) -> 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: scale, y: scale))
|
||||||
|
let context = CIContext()
|
||||||
|
guard let cgImage = context.createCGImage(scaled, from: scaled.extent) else { return nil }
|
||||||
|
return UIImage(cgImage: cgImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A payment request decoded from a QR code: the address plus whatever else the
|
||||||
|
/// code carried.
|
||||||
|
struct QRPayload {
|
||||||
|
var address: String
|
||||||
|
var amount: String?
|
||||||
|
var tag: UInt32?
|
||||||
|
var paymentId: String?
|
||||||
|
var memo: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QRCodeReader {
|
||||||
|
/// Reads the first QR code found in an image (used for photo-library import).
|
||||||
|
static func decodeText(in data: Data) -> String? {
|
||||||
|
guard let image = UIImage(data: data), let cgImage = image.cgImage else { return nil }
|
||||||
|
return decodeText(in: cgImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decodeText(in image: UIImage) -> String? {
|
||||||
|
guard let cgImage = image.cgImage else { return nil }
|
||||||
|
return decodeText(in: cgImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func decodeText(in cgImage: CGImage) -> String? {
|
||||||
|
if let text = decodeWithVision(cgImage) { return text }
|
||||||
|
// Vision's barcode detector does not work on the iOS simulator, so fall
|
||||||
|
// back to CoreImage, which does (and adds redundancy on device).
|
||||||
|
return decodeWithCoreImage(cgImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func decodeWithVision(_ cgImage: CGImage) -> String? {
|
||||||
|
let request = VNDetectBarcodesRequest()
|
||||||
|
request.symbologies = [.qr]
|
||||||
|
let handler = VNImageRequestHandler(cgImage: cgImage, orientation: .up)
|
||||||
|
try? handler.perform([request])
|
||||||
|
for observation in request.results ?? [] {
|
||||||
|
if let payload = observation.payloadStringValue, !payload.isEmpty {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func decodeWithCoreImage(_ cgImage: CGImage) -> String? {
|
||||||
|
let detector = CIDetector(ofType: CIDetectorTypeQRCode,
|
||||||
|
context: nil,
|
||||||
|
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
|
||||||
|
let features = detector?.features(in: CIImage(cgImage: cgImage)) ?? []
|
||||||
|
for feature in features {
|
||||||
|
if let code = feature as? CIQRCodeFeature, let message = code.messageString, !message.isEmpty {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Understands plain addresses as well as the payment URIs wallets emit:
|
||||||
|
/// `monero:` (tx_amount, tx_payment_id, tx_description) and `ripple:`
|
||||||
|
/// (amount, dt).
|
||||||
|
static func parse(_ raw: String) -> QRPayload? {
|
||||||
|
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !text.isEmpty else { return nil }
|
||||||
|
|
||||||
|
var body = text
|
||||||
|
var query: [String: String] = [:]
|
||||||
|
|
||||||
|
if let separator = text.firstIndex(of: "?") {
|
||||||
|
body = String(text[text.startIndex..<separator])
|
||||||
|
let queryText = String(text[text.index(after: separator)...])
|
||||||
|
for pair in queryText.split(separator: "&") {
|
||||||
|
let parts = pair.split(separator: "=", maxSplits: 1)
|
||||||
|
guard let key = parts.first else { continue }
|
||||||
|
let value = parts.count > 1 ? String(parts[1]) : ""
|
||||||
|
query[key.lowercased()] = value.removingPercentEncoding ?? value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowered = body.lowercased()
|
||||||
|
if let range = lowered.range(of: "monero:") {
|
||||||
|
body = String(body[range.upperBound...])
|
||||||
|
} else if let range = lowered.range(of: "ripple:") {
|
||||||
|
body = String(body[range.upperBound...])
|
||||||
|
}
|
||||||
|
while body.hasPrefix("/") { body.removeFirst() }
|
||||||
|
|
||||||
|
let address = body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !address.isEmpty else { return nil }
|
||||||
|
|
||||||
|
var payload = QRPayload(address: address)
|
||||||
|
payload.amount = query["tx_amount"] ?? query["amount"]
|
||||||
|
payload.memo = query["tx_description"] ?? query["name"] ?? query["message"]
|
||||||
|
payload.paymentId = query["tx_payment_id"] ?? query["payment_id"] ?? query["pid"]
|
||||||
|
if let tag = query["dt"] ?? query["destination_tag"] ?? query["destinationtag"] {
|
||||||
|
payload.tag = UInt32(tag)
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AddressFormat {
|
||||||
|
private static let moneroAlphabet = Set("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
|
||||||
|
|
||||||
|
/// Monero addresses are 95 characters (primary/subaddress) or 106
|
||||||
|
/// (integrated), all base58.
|
||||||
|
static func isMonero(_ address: String) -> Bool {
|
||||||
|
let text = address.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard text.count == 95 || text.count == 106 else { return false }
|
||||||
|
return text.allSatisfy { moneroAlphabet.contains($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isXRP(_ address: String) -> Bool {
|
||||||
|
XRPKey.isValidAddress(address.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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))
|
||||||
|
defaults.removeObject(forKey: cacheKey(chain))
|
||||||
|
Keychain.remove(secretKey(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cached state (balance, transactions, accounts)
|
||||||
|
|
||||||
|
private static func cacheKey(_ chain: Chain) -> String { "koins.cache.\(chain.rawValue)" }
|
||||||
|
|
||||||
|
static func cachedState(_ chain: Chain) -> ChainCache? {
|
||||||
|
guard let data = defaults.data(forKey: cacheKey(chain)) else { return nil }
|
||||||
|
return try? JSONDecoder().decode(ChainCache.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveCachedState(_ state: ChainCache, chain: Chain) {
|
||||||
|
guard let data = try? JSONEncoder().encode(state) else { return }
|
||||||
|
defaults.set(data, forKey: cacheKey(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Swap orders (kyc.rip)
|
||||||
|
|
||||||
|
private static let swapOrdersKey = "koins.swapOrders"
|
||||||
|
|
||||||
|
static func swapOrders() -> [SwapOrder] {
|
||||||
|
guard let data = defaults.data(forKey: swapOrdersKey),
|
||||||
|
let orders = try? JSONDecoder().decode([SwapOrder].self, from: data) else { return [] }
|
||||||
|
return orders
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveSwapOrder(_ order: SwapOrder) {
|
||||||
|
var orders = swapOrders()
|
||||||
|
orders.removeAll { $0.id == order.id }
|
||||||
|
orders.insert(order, at: 0)
|
||||||
|
guard let data = try? JSONEncoder().encode(orders) else { return }
|
||||||
|
defaults.set(data, forKey: swapOrdersKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func removeSwapOrder(id: String) {
|
||||||
|
var orders = swapOrders()
|
||||||
|
orders.removeAll { $0.id == id }
|
||||||
|
guard let data = try? JSONEncoder().encode(orders) else { return }
|
||||||
|
defaults.set(data, forKey: swapOrdersKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func secret(_ chain: Chain) -> String? {
|
||||||
|
Keychain.get(secretKey(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveSecret(_ secret: String, chain: Chain) {
|
||||||
|
Keychain.set(secret, for: secretKey(chain))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A stack of account cards, Cake Wallet style: the front card is full size and
|
||||||
|
/// interactive, the rest peek from behind (scaled down and offset upwards).
|
||||||
|
/// Drag the front card sideways or down to send it to the back of the deck.
|
||||||
|
struct AccountStack: View {
|
||||||
|
var accounts: [MoneroAccount]
|
||||||
|
/// Converts an account's XMR amount to the chosen fiat currency, if a price
|
||||||
|
/// is available.
|
||||||
|
var fiat: (String) -> String?
|
||||||
|
var onQR: (MoneroAccount) -> Void
|
||||||
|
|
||||||
|
@State private var frontIndex = 0
|
||||||
|
@State private var dragOffset = CGSize.zero
|
||||||
|
|
||||||
|
private let maxDepth = 3
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .top) {
|
||||||
|
ForEach(Array(accounts.enumerated()), id: \.element.id) { index, account in
|
||||||
|
let depth = depth(of: index)
|
||||||
|
accountCard(account, depth: depth)
|
||||||
|
.offset(x: depth == 0 ? dragOffset.width : 0,
|
||||||
|
y: depth == 0 ? dragOffset.height : 0)
|
||||||
|
.gesture(depth == 0 ? frontDrag : nil)
|
||||||
|
.allowsHitTesting(depth == 0)
|
||||||
|
.zIndex(Double(accounts.count - depth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 190 + CGFloat(min(accounts.count - 1, maxDepth)) * 12)
|
||||||
|
.accessibilityHint("Drag the front card to cycle accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
|
||||||
|
/// How far behind the front an account sits (0 = front).
|
||||||
|
private func depth(of index: Int) -> Int {
|
||||||
|
(index - frontIndex + accounts.count) % accounts.count
|
||||||
|
}
|
||||||
|
|
||||||
|
private func accountCard(_ account: MoneroAccount, depth: Int) -> some View {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
Text(account.title)
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
Spacer()
|
||||||
|
Text("\(depth + 1) of \(accounts.count)")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text("\(account.balance) XMR")
|
||||||
|
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
if let fiat = fiat(account.balance) {
|
||||||
|
Text(fiat)
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
if account.unlocked != account.balance {
|
||||||
|
Text("\(account.unlocked) XMR unlocked")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(account.address)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
Spacer(minLength: 6)
|
||||||
|
Button {
|
||||||
|
onQR(account)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "qrcode")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.background(Theme.surfaceAlt)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cards behind the front one peek from the top, slightly smaller.
|
||||||
|
.scaleEffect(1 - 0.045 * CGFloat(min(depth, maxDepth)))
|
||||||
|
.offset(y: -CGFloat(min(depth, maxDepth)) * 12)
|
||||||
|
.opacity(depth > maxDepth ? 0 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
private var frontDrag: some Gesture {
|
||||||
|
DragGesture()
|
||||||
|
.onChanged { value in
|
||||||
|
dragOffset = value.translation
|
||||||
|
}
|
||||||
|
.onEnded { value in
|
||||||
|
let threshold: CGFloat = 90
|
||||||
|
let shouldAdvance = abs(value.translation.width) > threshold
|
||||||
|
|| value.translation.height > threshold
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
|
||||||
|
dragOffset = .zero
|
||||||
|
if shouldAdvance {
|
||||||
|
frontIndex = (frontIndex + 1) % accounts.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AccountsView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@EnvironmentObject private var drawers: DrawerCoordinator
|
||||||
|
@Binding var route: Route
|
||||||
|
|
||||||
|
@State private var error: String?
|
||||||
|
@State private var busy = false
|
||||||
|
@State private var showAccountPrompt = false
|
||||||
|
@State private var showSubaddressPrompt = false
|
||||||
|
@State private var pendingLabel = ""
|
||||||
|
@State private var subaddressAccount = 0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: "Accounts & addresses",
|
||||||
|
subtitle: "Each address gets its own QR code",
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
PrimaryButton(title: busy ? "Working…" : "New account", disabled: busy) {
|
||||||
|
showAccountPrompt = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error {
|
||||||
|
MessageBanner(message: error, isError: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(model.moneroAccounts) { account in
|
||||||
|
accountCard(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.moneroAccounts.isEmpty {
|
||||||
|
Text("No accounts yet. Accounts and addresses appear after the wallet opens.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: model.moneroReady) { await model.loadAccounts() }
|
||||||
|
.alert("Account name", isPresented: $showAccountPrompt) {
|
||||||
|
TextField("e.g. Savings", text: $pendingLabel)
|
||||||
|
Button("Create") { createAccount() }
|
||||||
|
Button("Cancel", role: .cancel) { pendingLabel = "" }
|
||||||
|
}
|
||||||
|
.alert("Address label", isPresented: $showSubaddressPrompt) {
|
||||||
|
TextField("e.g. From Alice", text: $pendingLabel)
|
||||||
|
Button("Create") { createSubaddress() }
|
||||||
|
Button("Cancel", role: .cancel) { pendingLabel = "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func accountCard(_ account: MoneroAccount) -> some View {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text(account.title)
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("\(account.balance) XMR")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
addressRow(title: "Primary address",
|
||||||
|
address: account.address,
|
||||||
|
isUsed: true)
|
||||||
|
|
||||||
|
if !account.subaddresses.isEmpty {
|
||||||
|
Divider().overlay(Theme.border)
|
||||||
|
ForEach(account.subaddresses.filter { $0.index != 0 }) { subaddress in
|
||||||
|
addressRow(title: subaddress.label.isEmpty ? "Address \(subaddress.index)" : subaddress.label,
|
||||||
|
address: subaddress.address,
|
||||||
|
isUsed: subaddress.isUsed,
|
||||||
|
balance: subaddress.balance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecondaryButton(title: "New address in this account") {
|
||||||
|
subaddressAccount = account.index
|
||||||
|
pendingLabel = ""
|
||||||
|
showSubaddressPrompt = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addressRow(title: String, address: String, isUsed: Bool, balance: String? = nil) -> some View {
|
||||||
|
Button {
|
||||||
|
drawers.qr = AddressQR(title: title, address: address)
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
Text(address.isEmpty ? "—" : address)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
if let balance {
|
||||||
|
Text("\(balance) XMR\(isUsed ? "" : " · unused")")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 6)
|
||||||
|
Image(systemName: "qrcode")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.background(Theme.surfaceAlt)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(address.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createAccount() {
|
||||||
|
busy = true
|
||||||
|
error = nil
|
||||||
|
Task {
|
||||||
|
defer { busy = false }
|
||||||
|
do {
|
||||||
|
try await model.addAccount(label: pendingLabel)
|
||||||
|
pendingLabel = ""
|
||||||
|
} catch {
|
||||||
|
self.error = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createSubaddress() {
|
||||||
|
busy = true
|
||||||
|
error = nil
|
||||||
|
Task {
|
||||||
|
defer { busy = false }
|
||||||
|
do {
|
||||||
|
try await model.addSubaddress(accountIndex: subaddressAccount, label: pendingLabel)
|
||||||
|
pendingLabel = ""
|
||||||
|
} catch {
|
||||||
|
self.error = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AddressQR: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let title: String
|
||||||
|
let address: String
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One address as a scannable QR, shown inside the shared bottom drawer.
|
||||||
|
struct AddressQRSheet: View {
|
||||||
|
var title: String
|
||||||
|
var address: String
|
||||||
|
var onDone: () -> Void
|
||||||
|
|
||||||
|
@State private var copied = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
QRCodeView(text: address)
|
||||||
|
.frame(maxWidth: 220)
|
||||||
|
|
||||||
|
Text(address)
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineLimit(4)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
|
||||||
|
SecondaryButton(title: copied ? "Copied" : "Copy address") {
|
||||||
|
UIPasteboard.general.string = address
|
||||||
|
copied = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryButton(title: "Done", action: onDone)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 22)
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// Presents a bottom-anchored drawer: it fills to the screen's bottom edge
|
||||||
|
/// (no gap), shows a dimmed backdrop, and can be dragged down to dismiss.
|
||||||
|
func bottomDrawer<Content: View>(isPresented: Binding<Bool>,
|
||||||
|
@ViewBuilder content: @escaping () -> Content) -> some View {
|
||||||
|
BottomDrawerHost(host: self, isPresented: isPresented, content: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One place for the drawers shown above every screen. Attaching them at the
|
||||||
|
/// root keeps them anchored to the window instead of to scrolled content.
|
||||||
|
@MainActor
|
||||||
|
final class DrawerCoordinator: ObservableObject {
|
||||||
|
@Published var transaction: TransactionRecord?
|
||||||
|
@Published var qr: AddressQR?
|
||||||
|
@Published var settings = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct BottomDrawerHost<Host: View, Content: View>: View {
|
||||||
|
let host: Host
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
@ViewBuilder var content: () -> Content
|
||||||
|
|
||||||
|
@State private var dragOffset: CGFloat = 0
|
||||||
|
|
||||||
|
private var bottomInset: CGFloat {
|
||||||
|
for scene in UIApplication.shared.connectedScenes {
|
||||||
|
guard let windowScene = scene as? UIWindowScene else { continue }
|
||||||
|
if let window = windowScene.keyWindow {
|
||||||
|
return window.safeAreaInsets.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxHeight: CGFloat {
|
||||||
|
for scene in UIApplication.shared.connectedScenes {
|
||||||
|
guard let windowScene = scene as? UIWindowScene else { continue }
|
||||||
|
if let window = windowScene.keyWindow {
|
||||||
|
return window.bounds.height * 0.92
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
host
|
||||||
|
|
||||||
|
if isPresented {
|
||||||
|
Color.black.opacity(0.5)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture { dismiss() }
|
||||||
|
.transition(.opacity)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Capsule()
|
||||||
|
.fill(Theme.muted.opacity(0.55))
|
||||||
|
.frame(width: 36, height: 5)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
content()
|
||||||
|
.padding(.top, 14)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.frame(maxHeight: maxHeight)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.clipShape(UnevenRoundedRectangle(topLeadingRadius: 20, topTrailingRadius: 20, style: .continuous))
|
||||||
|
// Push the panel's background down to the physical screen edge without
|
||||||
|
// taking the host content out of its safe area.
|
||||||
|
.offset(y: dragOffset + bottomInset)
|
||||||
|
.gesture(dismissDrag)
|
||||||
|
.transition(.move(edge: .bottom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.spring(response: 0.32, dampingFraction: 0.86), value: isPresented)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismiss() {
|
||||||
|
isPresented = false
|
||||||
|
dragOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dismissDrag: some Gesture {
|
||||||
|
DragGesture()
|
||||||
|
.onChanged { value in
|
||||||
|
dragOffset = max(0, value.translation.height)
|
||||||
|
}
|
||||||
|
.onEnded { value in
|
||||||
|
if value.translation.height > 120 || value.predictedEndTranslation.height > 200 {
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
dragOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A full-width button that opens a web page, styled like SecondaryButton.
|
||||||
|
struct LinkButton: View {
|
||||||
|
var title: String
|
||||||
|
var systemImage: String = "safari"
|
||||||
|
var url: URL
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Link(destination: url) {
|
||||||
|
Label(title, systemImage: systemImage)
|
||||||
|
.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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picks which Monero account a payment leaves from. Each account has its own
|
||||||
|
/// balance, so "Max" and the wallet's spend checks need the matching index.
|
||||||
|
struct MoneroAccountPicker: View {
|
||||||
|
var accounts: [MoneroAccount]
|
||||||
|
@Binding var selection: Int
|
||||||
|
var onSelect: (() -> Void)?
|
||||||
|
|
||||||
|
private var selected: MoneroAccount? {
|
||||||
|
accounts.first(where: { $0.index == selection }) ?? accounts.first
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: "Send from")
|
||||||
|
Menu {
|
||||||
|
ForEach(accounts) { account in
|
||||||
|
Button {
|
||||||
|
selection = account.index
|
||||||
|
onSelect?()
|
||||||
|
} label: {
|
||||||
|
Text("\(account.title) · \(account.balance) XMR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(selected?.title ?? "Primary account")
|
||||||
|
.font(.system(size: 15, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("\(selected?.unlocked ?? "0") XMR available")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.up.chevron.down")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
|
||||||
|
.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? {
|
||||||
|
QRCodeImage.make(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransactionRow: View {
|
||||||
|
var record: TransactionRecord
|
||||||
|
var symbol: String
|
||||||
|
|
||||||
|
private var subtitle: String {
|
||||||
|
if let timestamp = record.timestamp {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
return record.confirmed ? formatter.string(from: timestamp) : "Pending confirmation"
|
||||||
|
}
|
||||||
|
// Monero transfers carry no timestamp; the block height is the honest label.
|
||||||
|
if let height = record.height, height > 0 {
|
||||||
|
return record.confirmed ? "Block \(height)" : "In the transaction pool"
|
||||||
|
}
|
||||||
|
return record.confirmed ? "Confirmed" : "Pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
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(subtitle)
|
||||||
|
.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)
|
||||||
|
// Buttons only respond where the label has content; the middle of the row
|
||||||
|
// is a Spacer, so without this the gap does not accept taps.
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Monero's chain sync as its own section, rather than buried in the wallet card.
|
||||||
|
struct SyncCard: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
Text("Sync")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
SecondaryButton(title: model.syncStatus?.syncing == true ? "Syncing…" : "Sync now") {
|
||||||
|
Task { await model.startMoneroSync() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A small circular icon button for utility actions (refresh, delete, …).
|
||||||
|
struct IconButton: View {
|
||||||
|
var systemName: String
|
||||||
|
var destructive = false
|
||||||
|
var action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(systemName: systemName)
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(destructive ? Theme.danger : Theme.muted)
|
||||||
|
.frame(width: 42, height: 42)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.overlay(Circle().stroke(Theme.border))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
import PhotosUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DashboardView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@EnvironmentObject private var drawers: DrawerCoordinator
|
||||||
|
@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 let fiat = model.fiatBalance {
|
||||||
|
Text("\(fiat)")
|
||||||
|
.font(.system(size: 13, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
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(Array(model.transactions.enumerated()), id: \.offset) { index, record in
|
||||||
|
Button {
|
||||||
|
drawers.transaction = record
|
||||||
|
} label: {
|
||||||
|
TransactionRow(record: record, symbol: model.symbol)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
if index < model.transactions.count - 1 {
|
||||||
|
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 accountIndex = 0
|
||||||
|
@State private var accountPicked = false
|
||||||
|
@State private var localError: String?
|
||||||
|
@State private var status: String?
|
||||||
|
@State private var showScanner = false
|
||||||
|
@State private var photoItem: PhotosPickerItem?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: "Send \(model.symbol)",
|
||||||
|
subtitle: "Balance \(model.balance.total) \(model.symbol)",
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
SecondaryButton(title: "Scan QR") { showScanner = true }
|
||||||
|
PhotosPicker(selection: $photoItem, matching: .images, photoLibrary: .shared()) {
|
||||||
|
Text("Photo")
|
||||||
|
.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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xmr, model.moneroAccounts.count > 1 {
|
||||||
|
MoneroAccountPicker(accounts: model.moneroAccounts,
|
||||||
|
selection: $accountIndex,
|
||||||
|
onSelect: { accountPicked = true })
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = status {
|
||||||
|
MessageBanner(message: message, isError: false)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
accountIndex: accountIndex)
|
||||||
|
amount = ""
|
||||||
|
tag = ""
|
||||||
|
route = .dashboard
|
||||||
|
} catch {
|
||||||
|
localError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showScanner) {
|
||||||
|
QRScannerView(onScan: { value in
|
||||||
|
showScanner = false
|
||||||
|
applyScanned(value)
|
||||||
|
}, onCancel: {
|
||||||
|
showScanner = false
|
||||||
|
})
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
.onChange(of: photoItem) { item in
|
||||||
|
guard let item else { return }
|
||||||
|
Task {
|
||||||
|
if let data = try? await item.loadTransferable(type: Data.self) {
|
||||||
|
if let value = QRCodeReader.decodeText(in: data) {
|
||||||
|
applyScanned(value)
|
||||||
|
} else {
|
||||||
|
localError = "No QR code found in that image"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localError = "That image could not be read"
|
||||||
|
}
|
||||||
|
photoItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if !accountPicked { accountIndex = model.defaultMoneroAccountIndex }
|
||||||
|
}
|
||||||
|
.onChange(of: model.moneroAccounts.count) { _ in
|
||||||
|
if !accountPicked { accountIndex = model.defaultMoneroAccountIndex }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills the form from a scanned code, accepting both plain addresses and
|
||||||
|
/// wallet payment URIs (`monero:…?tx_amount=`, `ripple:…?amount=&dt=`).
|
||||||
|
private func applyScanned(_ value: String) {
|
||||||
|
localError = nil
|
||||||
|
status = nil
|
||||||
|
guard let payload = QRCodeReader.parse(value) else {
|
||||||
|
localError = "That QR code is not a payment request"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isForThisChain = model.chain == .xrp
|
||||||
|
? AddressFormat.isXRP(payload.address)
|
||||||
|
: AddressFormat.isMonero(payload.address)
|
||||||
|
guard isForThisChain else {
|
||||||
|
localError = model.chain == .xrp
|
||||||
|
? "That looks like a Monero address. Switch to Monero to use it."
|
||||||
|
: "That looks like an XRP address. Switch to XRP to use it."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = payload.address
|
||||||
|
if let scannedAmount = payload.amount, !scannedAmount.isEmpty {
|
||||||
|
amount = scannedAmount
|
||||||
|
}
|
||||||
|
if model.chain == .xrp, let scannedTag = payload.tag {
|
||||||
|
tag = String(scannedTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
var filled: [String] = ["address"]
|
||||||
|
if payload.amount?.isEmpty == false { filled.append("amount") }
|
||||||
|
if payload.tag != nil { filled.append("tag") }
|
||||||
|
if payload.paymentId?.isEmpty == false { filled.append("payment ID") }
|
||||||
|
status = "Filled \(filled.joined(separator: ", ")) from the QR code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Full-screen biometric lock, overlaid on the app whenever `model.locked`.
|
||||||
|
struct LockScreen: View {
|
||||||
|
var unlock: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Theme.background.ignoresSafeArea()
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "lock.shield")
|
||||||
|
.font(.system(size: 44, weight: .light))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
Text("Koins is locked")
|
||||||
|
.font(.system(size: 22, weight: .bold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("Authenticate to continue")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
Spacer()
|
||||||
|
PrimaryButton(title: "Unlock with \(Biometrics.label)", action: unlock)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(24)
|
||||||
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.task { unlock() }
|
||||||
|
}
|
||||||
|
}
|
||||||