Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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() {},
|
|
||||||
data() {},
|
|
||||||
error() {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
lastErr = JSON.stringify(res)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = String(e)
|
lastErr = String(e)
|
||||||
}
|
}
|
||||||
@@ -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,18 +141,45 @@ 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)
|
||||||
|
if (pids.length) {
|
||||||
|
console.log(
|
||||||
|
`[monero] stale process(es) on port ${state.rpcPort}: ${pids.join(', ')}`,
|
||||||
|
)
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
try {
|
try {
|
||||||
process.kill(Number(pid), 'SIGKILL')
|
process.kill(Number(pid), 'SIGTERM')
|
||||||
console.log(`[monero] killed stale process ${pid} on port ${state.rpcPort}`)
|
|
||||||
} catch {}
|
} 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 {}
|
||||||
|
|
||||||
console.log(`[monero] starting wallet-rpc process...`)
|
console.log(`[monero] starting wallet-rpc process...`)
|
||||||
@@ -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) {
|
||||||
|
const proc = state.process
|
||||||
|
console.log(`[monero] sending SIGTERM to wallet-rpc (pid ${proc.pid})`)
|
||||||
|
proc.kill()
|
||||||
|
await Promise.race([proc.exited, sleep(3000)])
|
||||||
|
if (proc.exitCode === null) {
|
||||||
console.log(
|
console.log(
|
||||||
`[monero] killing wallet-rpc process (pid ${state.process.pid})`,
|
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
|
||||||
)
|
)
|
||||||
state.process.kill()
|
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,12 +139,19 @@ export function createMoneroHandlers(state: {
|
|||||||
console.log('[rpc] moneroOpenWallet:', name)
|
console.log('[rpc] moneroOpenWallet:', name)
|
||||||
if (!state.manager)
|
if (!state.manager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
try {
|
||||||
await walletOpen(state.manager, name, password)
|
await walletOpen(state.manager, name, password)
|
||||||
|
log(`[rpc] moneroOpenWallet ok: ${name}`)
|
||||||
|
} catch (e) {
|
||||||
|
logError(`[rpc] moneroOpenWallet failed (${name})`, e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moneroGetBalance: async () => {
|
moneroGetBalance: async () => {
|
||||||
console.log('[rpc] moneroGetBalance')
|
console.log('[rpc] moneroGetBalance')
|
||||||
if (!state.manager)
|
if (!state.manager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
try {
|
||||||
const { balance, unlocked } = await walletGetBalance(
|
const { balance, unlocked } = await walletGetBalance(
|
||||||
state.manager,
|
state.manager,
|
||||||
)
|
)
|
||||||
@@ -155,6 +172,10 @@ export function createMoneroHandlers(state: {
|
|||||||
height,
|
height,
|
||||||
daemonHeight,
|
daemonHeight,
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError('[rpc] moneroGetBalance failed', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moneroGetTransactions: async ({
|
moneroGetTransactions: async ({
|
||||||
accountIndex,
|
accountIndex,
|
||||||
@@ -164,12 +185,17 @@ export function createMoneroHandlers(state: {
|
|||||||
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
console.log('[rpc] moneroGetTransactions', { accountIndex })
|
||||||
if (!state.manager)
|
if (!state.manager)
|
||||||
throw new Error('Monero wallet RPC not started')
|
throw new Error('Monero wallet RPC not started')
|
||||||
|
try {
|
||||||
const txs = await walletGetTransactions(
|
const txs = await walletGetTransactions(
|
||||||
state.manager,
|
state.manager,
|
||||||
accountIndex,
|
accountIndex,
|
||||||
)
|
)
|
||||||
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
console.log(`[rpc] moneroGetTransactions: ${txs.length} txs`)
|
||||||
return txs
|
return txs
|
||||||
|
} catch (e) {
|
||||||
|
logError('[rpc] moneroGetTransactions failed', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moneroWalletStatus: async () => {
|
moneroWalletStatus: async () => {
|
||||||
if (!state.manager) {
|
if (!state.manager) {
|
||||||
|
|||||||
@@ -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,7 +106,10 @@ export const MoneroWallet = () => {
|
|||||||
return authed === true
|
return authed === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
if (initPromise) return initPromise
|
||||||
|
initPromise = (async () => {
|
||||||
await checkStatus()
|
await checkStatus()
|
||||||
if (installed && !running && !downloading) {
|
if (installed && !running && !downloading) {
|
||||||
await start()
|
await start()
|
||||||
@@ -73,18 +117,48 @@ export const MoneroWallet = () => {
|
|||||||
}
|
}
|
||||||
await checkBiometric()
|
await checkBiometric()
|
||||||
ready = true
|
ready = true
|
||||||
|
})()
|
||||||
|
return initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
if (walletOpen) return
|
||||||
|
if (loginInFlight) return loginInFlight
|
||||||
|
loginInFlight = (async () => {
|
||||||
accountType = 'monero'
|
accountType = 'monero'
|
||||||
|
error = ''
|
||||||
|
beginOpening()
|
||||||
|
try {
|
||||||
if (installed && !running) {
|
if (installed && !running) {
|
||||||
await start()
|
await start()
|
||||||
await checkStatus()
|
await checkStatus()
|
||||||
|
} else if (startInFlight) {
|
||||||
|
// a start is already in flight (e.g. triggered by init);
|
||||||
|
// wait for it so we don't call open before the server is up
|
||||||
|
await startInFlight.catch(() => {})
|
||||||
|
await checkStatus()
|
||||||
|
}
|
||||||
|
if (!running) {
|
||||||
|
error = 'Wallet server failed to start. Please try again.'
|
||||||
|
return
|
||||||
}
|
}
|
||||||
await listWallets()
|
await listWallets()
|
||||||
if (wallets.length > 0) {
|
if (wallets.length > 0) {
|
||||||
const pw = await moneroGetStoredPassword(wallets[0])
|
const pw = await moneroGetStoredPassword(wallets[0])
|
||||||
if (pw) await openExistingWallet(wallets[0], pw)
|
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,6 +217,8 @@ 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')
|
||||||
|
beginOpening()
|
||||||
|
try {
|
||||||
await electrobun.rpc.request.moneroOpenWallet({
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
name,
|
name,
|
||||||
password: pw,
|
password: pw,
|
||||||
@@ -150,19 +226,30 @@ export const MoneroWallet = () => {
|
|||||||
walletName = name
|
walletName = name
|
||||||
walletOpen = true
|
walletOpen = true
|
||||||
await refresh()
|
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(
|
||||||
|
rpcTimeout(
|
||||||
electrobun.rpc.request.moneroBinaryStatus({}),
|
electrobun.rpc.request.moneroBinaryStatus({}),
|
||||||
|
15000,
|
||||||
|
'moneroBinaryStatus',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (binStatus) {
|
if (binStatus) {
|
||||||
installed = binStatus.installed
|
installed = binStatus.installed
|
||||||
downloading = binStatus.downloading
|
downloading = binStatus.downloading
|
||||||
}
|
}
|
||||||
const [status] = await tryCatch(
|
const [status] = await tryCatch(
|
||||||
|
rpcTimeout(
|
||||||
electrobun.rpc.request.moneroWalletStatus({}),
|
electrobun.rpc.request.moneroWalletStatus({}),
|
||||||
|
30000,
|
||||||
|
'moneroWalletStatus',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (status) {
|
if (status) {
|
||||||
running = status.running
|
running = status.running
|
||||||
@@ -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()
|
||||||
|
try {
|
||||||
|
if (!startInFlight) {
|
||||||
|
startInFlight = electrobun.rpc.request.moneroStart({})
|
||||||
|
const status = await startInFlight
|
||||||
running = status.running
|
running = status.running
|
||||||
connected = status.connected
|
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,6 +308,8 @@ 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')
|
||||||
|
beginOpening()
|
||||||
|
try {
|
||||||
const result = await electrobun.rpc.request.moneroCreateWallet({
|
const result = await electrobun.rpc.request.moneroCreateWallet({
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
@@ -219,6 +320,9 @@ export const MoneroWallet = () => {
|
|||||||
if (storePw) await moneroStorePassword(name, password)
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
await refresh()
|
await refresh()
|
||||||
return result
|
return result
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreWallet = async (
|
const restoreWallet = async (
|
||||||
@@ -229,6 +333,8 @@ 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')
|
||||||
|
beginOpening()
|
||||||
|
try {
|
||||||
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
@@ -240,14 +346,22 @@ export const MoneroWallet = () => {
|
|||||||
address = result.address
|
address = result.address
|
||||||
if (storePw) await moneroStorePassword(name, password)
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
await refresh()
|
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
|
||||||
|
beginOpening()
|
||||||
|
try {
|
||||||
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
||||||
walletName = name
|
walletName = name
|
||||||
walletOpen = true
|
walletOpen = true
|
||||||
await refresh()
|
await refresh()
|
||||||
|
} finally {
|
||||||
|
endOpening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoUnlock = async (name?: string) => {
|
const autoUnlock = async (name?: string) => {
|
||||||
@@ -255,24 +369,92 @@ 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) {
|
||||||
|
passwordRequired = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
beginOpening()
|
||||||
|
try {
|
||||||
await electrobun.rpc.request.moneroOpenWallet({
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
name: target,
|
name: target,
|
||||||
password: pw,
|
password: pw,
|
||||||
})
|
})
|
||||||
walletName = target
|
walletName = target
|
||||||
walletOpen = true
|
walletOpen = true
|
||||||
|
passwordRequired = false
|
||||||
|
error = ''
|
||||||
await refresh()
|
await refresh()
|
||||||
return true
|
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
|
||||||
|
if (!rpc) return
|
||||||
|
if (refreshInFlight) return refreshInFlight
|
||||||
|
refreshInFlight = (async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
let failed = false
|
||||||
|
const wasSyncing = syncing
|
||||||
|
let lastError: unknown = null
|
||||||
try {
|
try {
|
||||||
const [bal] = await tryCatch(
|
const [bal, balErr] = await tryCatch(
|
||||||
electrobun.rpc.request.moneroGetBalance({}),
|
rpcTimeout(
|
||||||
|
rpc.request.moneroGetBalance({}),
|
||||||
|
20000,
|
||||||
|
'moneroGetBalance',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
if (balErr) {
|
||||||
|
failed = true
|
||||||
|
lastError = balErr
|
||||||
|
}
|
||||||
if (bal) {
|
if (bal) {
|
||||||
balAtomic = bal.balance
|
balAtomic = bal.balance
|
||||||
unlockedAtomic = bal.unlocked
|
unlockedAtomic = bal.unlocked
|
||||||
@@ -280,17 +462,64 @@ export const MoneroWallet = () => {
|
|||||||
height = bal.height
|
height = bal.height
|
||||||
daemonHeight = bal.daemonHeight
|
daemonHeight = bal.daemonHeight
|
||||||
}
|
}
|
||||||
const [result] = await tryCatch(
|
const [result, txErr] = await tryCatch(
|
||||||
electrobun.rpc.request.moneroGetTransactions({
|
rpcTimeout(
|
||||||
|
rpc.request.moneroGetTransactions({
|
||||||
accountIndex: selectedAccountIndex,
|
accountIndex: selectedAccountIndex,
|
||||||
}),
|
}),
|
||||||
|
20000,
|
||||||
|
'moneroGetTransactions',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
if (txErr) {
|
||||||
|
failed = true
|
||||||
|
lastError = txErr
|
||||||
|
}
|
||||||
txs = result ?? []
|
txs = result ?? []
|
||||||
await fetchAccounts()
|
const ok = await fetchAccounts()
|
||||||
|
if (!ok) {
|
||||||
|
failed = true
|
||||||
|
lastError = new Error('moneroGetAccounts failed')
|
||||||
|
}
|
||||||
|
if (failed) {
|
||||||
|
// The wallet-rpc is likely busy syncing; keep the stale
|
||||||
|
// balance and retry shortly instead of showing an error.
|
||||||
|
syncing = true
|
||||||
|
scheduleSyncRetry()
|
||||||
|
frontendLogError(
|
||||||
|
`monero refresh failed (height ${height}/${daemonHeight})`,
|
||||||
|
lastError,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (wasSyncing) {
|
||||||
|
frontendLog(`monero sync complete at height ${height}`)
|
||||||
|
}
|
||||||
|
syncing = false
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Monero refresh failed'
|
syncing = true
|
||||||
|
scheduleSyncRetry()
|
||||||
|
frontendLogError(
|
||||||
|
`monero refresh failed (height ${height}/${daemonHeight})`,
|
||||||
|
e,
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
|
refreshInFlight = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return refreshInFlight
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPrice = async () => {
|
||||||
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc || priceInFlight) return
|
||||||
|
priceInFlight = true
|
||||||
|
try {
|
||||||
|
const [p] = await tryCatch(rpc.request.fetchMoneroPrice({}))
|
||||||
|
if (p) price = p.usd
|
||||||
|
} finally {
|
||||||
|
priceInFlight = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,7 +76,10 @@ export const XrpWallet = () => {
|
|||||||
biometricAvailable = ok === true
|
biometricAvailable = ok === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
if (initPromise) return initPromise
|
||||||
|
initPromise = (async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
const [walletList] = await tryCatch(
|
const [walletList] = await tryCatch(
|
||||||
electrobun.rpc.request.xrpListWallets({}),
|
electrobun.rpc.request.xrpListWallets({}),
|
||||||
@@ -86,6 +92,8 @@ export const XrpWallet = () => {
|
|||||||
}
|
}
|
||||||
await checkBiometric()
|
await checkBiometric()
|
||||||
ready = true
|
ready = true
|
||||||
|
})()
|
||||||
|
return initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
@@ -319,6 +327,34 @@ export const XrpWallet = () => {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc || fetchBalancesInFlight) return
|
||||||
|
if (wallets.length === 0) return
|
||||||
|
fetchBalancesInFlight = true
|
||||||
|
balancesLoading = true
|
||||||
|
try {
|
||||||
|
const [p] = await tryCatch(rpc.request.fetchXrpPrice({}))
|
||||||
|
if (p) price = p.usd
|
||||||
|
await Promise.allSettled(
|
||||||
|
wallets.map(async (w) => {
|
||||||
|
const [res] = await tryCatch(
|
||||||
|
rpc.request.xrpGetBalance({ address: w.address }),
|
||||||
|
)
|
||||||
|
if (res) {
|
||||||
|
balances[w.address] = {
|
||||||
|
balance: parseFloat(res.balance),
|
||||||
|
funded: res.funded,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
balancesLoading = false
|
||||||
|
fetchBalancesInFlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const send = async (
|
const send = async (
|
||||||
to: string,
|
to: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
@@ -399,6 +435,12 @@ export const XrpWallet = () => {
|
|||||||
get wallets() {
|
get wallets() {
|
||||||
return wallets
|
return wallets
|
||||||
},
|
},
|
||||||
|
get balances() {
|
||||||
|
return balances
|
||||||
|
},
|
||||||
|
get balancesLoading() {
|
||||||
|
return balancesLoading
|
||||||
|
},
|
||||||
get currentWalletId() {
|
get currentWalletId() {
|
||||||
return currentWalletId
|
return currentWalletId
|
||||||
},
|
},
|
||||||
@@ -424,6 +466,7 @@ export const XrpWallet = () => {
|
|||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
refresh,
|
refresh,
|
||||||
|
fetchBalances,
|
||||||
createWallet,
|
createWallet,
|
||||||
importWallet,
|
importWallet,
|
||||||
lock,
|
lock,
|
||||||
@@ -442,4 +485,15 @@ export const XrpWallet = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton: calling XrpWallet() always returns the same instance, so no
|
||||||
|
// component can ever create a second, disconnected wallet state.
|
||||||
|
type XrpWalletStore = ReturnType<typeof createXrpWallet>
|
||||||
|
|
||||||
|
let xrpWalletInstance: XrpWalletStore | null = null
|
||||||
|
|
||||||
|
export const XrpWallet = (): XrpWalletStore => {
|
||||||
|
if (!xrpWalletInstance) xrpWalletInstance = createXrpWallet()
|
||||||
|
return xrpWalletInstance
|
||||||
|
}
|
||||||
|
|
||||||
export const xrpWallet = XrpWallet()
|
export const xrpWallet = XrpWallet()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||||
import { electrobun } from '$lib/electrobun.js'
|
|
||||||
import { navigate } from 'sv-router/generated'
|
import { navigate } from 'sv-router/generated'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import {
|
import {
|
||||||
@@ -16,29 +15,23 @@
|
|||||||
import WalletIcon from '@lucide/svelte/icons/wallet'
|
import WalletIcon from '@lucide/svelte/icons/wallet'
|
||||||
import Loader from '$lib/components/loader.svelte'
|
import Loader from '$lib/components/loader.svelte'
|
||||||
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||||
|
import { arc } from 'd3-shape'
|
||||||
let xrpPrice = $state<string | null>(null)
|
import { tweened } from 'svelte/motion'
|
||||||
let xmrPrice = $state<string | null>(null)
|
import { cubicOut } from 'svelte/easing'
|
||||||
let xrpBalances = $state<Record<string, { balance: number; funded: boolean }>>({})
|
|
||||||
let loadingXrp = $state(true)
|
|
||||||
let loadingXmr = $state(true)
|
|
||||||
|
|
||||||
let feError = $state<string | null>(null)
|
let feError = $state<string | null>(null)
|
||||||
|
|
||||||
const usd = (amount: string, price: string | null) =>
|
|
||||||
price ? parseFloat(amount) * parseFloat(price) : 0
|
|
||||||
|
|
||||||
const xrpTotalUsd = $derived.by(() => {
|
const xrpTotalUsd = $derived.by(() => {
|
||||||
const price = xrpPrice
|
const p = xrpWallet.price
|
||||||
return xrpWallet.wallets.reduce((sum, w) => {
|
return xrpWallet.wallets.reduce((sum, w) => {
|
||||||
const b = xrpBalances[w.address]?.balance ?? 0
|
const b = xrpWallet.balances[w.address]?.balance ?? 0
|
||||||
return sum + (price ? b * parseFloat(price) : 0)
|
return sum + (p ? b * parseFloat(p) : 0)
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
const xrpTotalBalance = $derived.by(() =>
|
const xrpTotalBalance = $derived.by(() =>
|
||||||
xrpWallet.wallets.reduce((sum, w) => {
|
xrpWallet.wallets.reduce((sum, w) => {
|
||||||
return sum + (xrpBalances[w.address]?.balance ?? 0)
|
return sum + (xrpWallet.balances[w.address]?.balance ?? 0)
|
||||||
}, 0),
|
}, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,17 +39,54 @@
|
|||||||
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
|
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
|
||||||
)
|
)
|
||||||
const xmrTotalUsd = $derived(
|
const xmrTotalUsd = $derived(
|
||||||
moneroWallet.walletOpen && xmrPrice
|
moneroWallet.walletOpen && moneroWallet.price
|
||||||
? xmrBalance * parseFloat(xmrPrice)
|
? xmrBalance * parseFloat(moneroWallet.price)
|
||||||
: 0,
|
: 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
const portfolioUsd = $derived(xrpTotalUsd + xmrTotalUsd)
|
const portfolioUsd = $derived(xrpTotalUsd + xmrTotalUsd)
|
||||||
|
|
||||||
|
const xrpShare = $derived(portfolioUsd > 0 ? xrpTotalUsd / portfolioUsd : 0)
|
||||||
|
const xmrShare = $derived(
|
||||||
|
portfolioUsd > 0 ? xmrTotalUsd / portfolioUsd : 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const tweenedUsd = tweened(0, { duration: 600, easing: cubicOut })
|
||||||
|
const tweenedXrpShare = tweened(0, { duration: 600, easing: cubicOut })
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
tweenedUsd.set(portfolioUsd)
|
||||||
|
tweenedXrpShare.set(xrpShare)
|
||||||
|
})
|
||||||
|
|
||||||
|
const donut = $derived.by(() => {
|
||||||
|
if (portfolioUsd <= 0) return { xrp: '', xmr: '' }
|
||||||
|
const a = arc<{ startAngle: number; endAngle: number }>()
|
||||||
|
.innerRadius(62)
|
||||||
|
.outerRadius(80)
|
||||||
|
.padAngle(0.02)
|
||||||
|
const start = -Math.PI / 2
|
||||||
|
const endXrp = start + $tweenedXrpShare * 2 * Math.PI
|
||||||
|
return {
|
||||||
|
xrp: a({ startAngle: start, endAngle: endXrp }) ?? '',
|
||||||
|
xmr: a({ startAngle: endXrp, endAngle: start + 2 * Math.PI }) ?? '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const hasXrp = $derived(xrpWallet.wallets.length > 0)
|
const hasXrp = $derived(xrpWallet.wallets.length > 0)
|
||||||
const hasXmr = $derived(moneroWallet.wallets.length > 0)
|
const hasXmr = $derived(moneroWallet.wallets.length > 0)
|
||||||
const noWallets = $derived(!hasXrp && !hasXmr)
|
const noWallets = $derived(!hasXrp && !hasXmr)
|
||||||
|
|
||||||
|
const moneroDot = $derived(
|
||||||
|
!moneroWallet.running
|
||||||
|
? 'bg-muted'
|
||||||
|
: !moneroWallet.connected
|
||||||
|
? 'bg-red-500'
|
||||||
|
: moneroWallet.syncing || moneroWallet.height < moneroWallet.daemonHeight
|
||||||
|
? 'bg-yellow-500 animate-pulse'
|
||||||
|
: 'bg-emerald-500',
|
||||||
|
)
|
||||||
|
|
||||||
const fmtUsd = (v: number) =>
|
const fmtUsd = (v: number) =>
|
||||||
v.toLocaleString(undefined, {
|
v.toLocaleString(undefined, {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
@@ -69,57 +99,23 @@
|
|||||||
maximumFractionDigits: dp,
|
maximumFractionDigits: dp,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchXrp() {
|
const fmtPct = (v: number) => `${Math.round(v * 100)}%`
|
||||||
loadingXrp = true
|
|
||||||
const rpc = electrobun.rpc
|
|
||||||
if (!rpc) {
|
|
||||||
loadingXrp = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const p = await rpc.request.fetchXrpPrice({})
|
|
||||||
if (p) xrpPrice = p.usd
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
await Promise.allSettled(
|
|
||||||
xrpWallet.wallets.map(async (w) => {
|
|
||||||
const res = await rpc.request.xrpGetBalance({
|
|
||||||
address: w.address,
|
|
||||||
})
|
|
||||||
xrpBalances[w.address] = {
|
|
||||||
balance: parseFloat(res.balance),
|
|
||||||
funded: res.funded,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
loadingXrp = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchXmr() {
|
|
||||||
loadingXmr = true
|
|
||||||
const rpc = electrobun.rpc
|
|
||||||
if (!rpc) {
|
|
||||||
loadingXmr = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const p = await rpc.request.fetchMoneroPrice({})
|
|
||||||
xmrPrice = p?.usd ?? null
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
loadingXmr = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
frontendLog('dashboard mounted')
|
frontendLog('dashboard mounted')
|
||||||
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
|
||||||
xrpWallet.init().then(async () => {
|
|
||||||
await fetchXrp()
|
|
||||||
})
|
})
|
||||||
fetchXmr()
|
|
||||||
|
// Balances live in the stores so they survive navigation; the effects
|
||||||
|
// only trigger background refetches.
|
||||||
|
$effect(() => {
|
||||||
|
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
|
||||||
|
xrpWallet.fetchBalances()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (moneroWallet.walletOpen) {
|
if (moneroWallet.walletOpen) {
|
||||||
fetchXmr()
|
moneroWallet.fetchPrice()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -179,9 +175,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center pt-4 pb-2">
|
<div class="flex flex-col items-center pt-4 pb-2">
|
||||||
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
||||||
{#if loadingXrp || loadingXmr}
|
{#if moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen) || (xrpWallet.wallets.length > 0 && !xrpWallet.price) || (moneroWallet.walletOpen && !moneroWallet.price)}
|
||||||
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
||||||
@@ -190,31 +186,66 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader class="pb-2">
|
||||||
|
<CardTitle class="text-base">Portfolio Allocation</CardTitle>
|
||||||
|
<CardDescription class="text-xs">
|
||||||
|
Share of your balance by asset
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-col items-center gap-5 py-6">
|
||||||
|
<div class="relative">
|
||||||
|
<svg viewBox="-100 -100 200 200" class="size-48">
|
||||||
|
<path d={donut.xrp} fill="#0EA5E9" />
|
||||||
|
<path d={donut.xmr} fill="#F97316" />
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<p
|
||||||
|
class="text-lg font-semibold tabular-nums">$
|
||||||
|
{fmtUsd($tweenedUsd)}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Portfolio
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-5 text-xs">
|
||||||
|
<span class="flex items-center gap-1.5">
|
||||||
|
<span class="size-2.5 rounded-full bg-sky-500"></span>
|
||||||
|
XRP · {fmtPct($tweenedXrpShare)}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1.5">
|
||||||
|
<span class="size-2.5 rounded-full bg-orange-500"></span>
|
||||||
|
XMR · {fmtPct(1 - $tweenedXrpShare)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<button
|
<button
|
||||||
onclick={() => navigate('/xrp')}
|
onclick={() => navigate('/xrp')}
|
||||||
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">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<label class="text-xs font-medium">Recipient Address</label>
|
<label class="text-xs font-medium">Recipient Address</label>
|
||||||
|
<QrUpload
|
||||||
|
onDecode={(text) => {
|
||||||
|
const parsed = parseQrPayload(text, 'monero')
|
||||||
|
recipient = parsed.address
|
||||||
|
if (parsed.amount && !sendAll) amountXmr = parsed.amount
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Monero address starting with 4..."
|
placeholder="Monero address starting with 4..."
|
||||||
bind:value={recipient} />
|
bind:value={recipient} />
|
||||||
|
|||||||
@@ -15,10 +15,6 @@
|
|||||||
const w = moneroWallet
|
const w = moneroWallet
|
||||||
|
|
||||||
let moneroSettingsPw = $state('')
|
let moneroSettingsPw = $state('')
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
w.init()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-xl">
|
<div class="mx-auto max-w-xl">
|
||||||
|
|||||||
@@ -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) {
|
||||||
navigate('/monero')
|
|
||||||
}
|
|
||||||
w.refresh()
|
w.refresh()
|
||||||
electrobun.rpc?.request
|
electrobun.rpc?.request
|
||||||
.fetchMoneroPrice({})
|
.fetchMoneroPrice({})
|
||||||
.then((p) => (moneroPrice = p))
|
.then((p) => (moneroPrice = p))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!w.opening && (!w.walletOpen || w.walletName !== walletName)) {
|
||||||
|
navigate('/monero')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</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>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<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.Title>Create Account</Dialog.Title>
|
||||||
<Dialog.Description>Optional label for the new account</Dialog.Description>
|
<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>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<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.Title>Create Subaddress</Dialog.Title>
|
||||||
<Dialog.Description>Account index and optional label</Dialog.Description>
|
<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>
|
||||||
@@ -456,3 +587,4 @@
|
|||||||
</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">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<label class="text-xs font-medium">Recipient Address</label>
|
<label class="text-xs font-medium">Recipient Address</label>
|
||||||
|
<QrUpload
|
||||||
|
onDecode={(text) => {
|
||||||
|
const parsed = parseQrPayload(text, 'xrp')
|
||||||
|
recipient = parsed.address
|
||||||
|
if (parsed.destinationTag) destinationTag = parsed.destinationTag
|
||||||
|
if (parsed.amount) amountXrp = parsed.amount
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder="XRP address starting with r..."
|
placeholder="XRP address starting with r..."
|
||||||
bind:value={recipient} />
|
bind:value={recipient} />
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Generated by XcodeGen from project.yml
|
||||||
|
Koins.xcodeproj/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
build/
|
||||||
|
DerivedData/
|
||||||
|
*.xcworkspace
|
||||||
|
xcuserdata/
|
||||||
|
*.xcuserstate
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AppModel: ObservableObject {
|
||||||
|
@Published var chain: Chain = .xrp
|
||||||
|
@Published var xrpWallet: WalletRecord?
|
||||||
|
@Published var xmrWallet: WalletRecord?
|
||||||
|
@Published var balance = Balance(total: "0", unlocked: "", funded: false)
|
||||||
|
@Published var transactions: [TransactionRecord] = []
|
||||||
|
@Published var syncStatus: SyncStatus?
|
||||||
|
@Published var isBusy = false
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var statusMessage: String?
|
||||||
|
@Published var moneroReady = false
|
||||||
|
@Published var moneroAddress: String?
|
||||||
|
/// Rough time remaining for the Monero sync, derived from observed block rate.
|
||||||
|
@Published var syncEta: String?
|
||||||
|
|
||||||
|
private var moneroOpened = false
|
||||||
|
private var refreshTick = 0
|
||||||
|
private var refreshTask: Task<Void, Never>?
|
||||||
|
private var lastSample: (height: UInt64, date: Date)?
|
||||||
|
|
||||||
|
var wallet: WalletRecord? {
|
||||||
|
chain == .xrp ? xrpWallet : xmrWallet
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasWallet: Bool { wallet != nil }
|
||||||
|
|
||||||
|
/// Address for the chain currently on screen. Kept chain-aware because the
|
||||||
|
/// Monero address stays cached once its wallet is open.
|
||||||
|
var displayAddress: String {
|
||||||
|
chain == .xrp ? (xrpWallet?.address ?? "") : (moneroAddress ?? xmrWallet?.address ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbol: String { chain.symbol }
|
||||||
|
|
||||||
|
// MARK: - Bootstrap
|
||||||
|
|
||||||
|
func bootstrap() async {
|
||||||
|
xrpWallet = WalletStore.wallet(.xrp)
|
||||||
|
xmrWallet = WalletStore.wallet(.xmr)
|
||||||
|
if xmrWallet == nil, let orphan = MoneroService.orphanedWalletNames().first {
|
||||||
|
// Wallet file on disk with no stored record (for example after the
|
||||||
|
// preferences were lost): re-attach it so funds are not stranded.
|
||||||
|
await adoptMoneroWallet(named: orphan)
|
||||||
|
}
|
||||||
|
if let xmrWallet {
|
||||||
|
await openMonero(afterRestore: false, wallet: xmrWallet)
|
||||||
|
}
|
||||||
|
if xrpWallet != nil || moneroReady {
|
||||||
|
chain = xrpWallet != nil ? .xrp : .xmr
|
||||||
|
}
|
||||||
|
await refresh(force: true)
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func adoptMoneroWallet(named name: String) async {
|
||||||
|
do {
|
||||||
|
let address = try await MoneroService.shared.openWallet(name: name)
|
||||||
|
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
|
||||||
|
name: name,
|
||||||
|
address: address,
|
||||||
|
createdAt: Date())
|
||||||
|
xmrWallet = record
|
||||||
|
WalletStore.saveWallet(record, chain: .xmr)
|
||||||
|
moneroOpened = true
|
||||||
|
moneroReady = true
|
||||||
|
moneroAddress = address
|
||||||
|
statusMessage = "Recovered the existing Monero wallet “\(name)” from this device."
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Found a Monero wallet file but could not open it: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polls the wallet while the app is in the foreground. Monero sync progress
|
||||||
|
/// changes constantly, so it is refreshed far more often than XRP.
|
||||||
|
func startAutoRefresh() {
|
||||||
|
guard refreshTask == nil else { return }
|
||||||
|
refreshTask = Task { [weak self] in
|
||||||
|
while !Task.isCancelled {
|
||||||
|
guard let self else { return }
|
||||||
|
let seconds: UInt64 = self.chain == .xmr ? 5 : 30
|
||||||
|
try? await Task.sleep(nanoseconds: seconds * 1_000_000_000)
|
||||||
|
if Task.isCancelled { return }
|
||||||
|
await self.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func select(_ newChain: Chain) async {
|
||||||
|
chain = newChain
|
||||||
|
errorMessage = nil
|
||||||
|
statusMessage = nil
|
||||||
|
balance = Balance(total: "0", unlocked: "", funded: false)
|
||||||
|
transactions = []
|
||||||
|
syncStatus = nil
|
||||||
|
|
||||||
|
if newChain == .xmr, let xmrWallet, !moneroOpened {
|
||||||
|
await openMonero(afterRestore: false, wallet: xmrWallet)
|
||||||
|
}
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Monero lifecycle
|
||||||
|
|
||||||
|
private func openMonero(afterRestore: Bool, wallet: WalletRecord) async {
|
||||||
|
isBusy = true
|
||||||
|
defer { isBusy = false }
|
||||||
|
do {
|
||||||
|
let address = try await MoneroService.shared.openWallet(name: wallet.name)
|
||||||
|
moneroOpened = true
|
||||||
|
moneroReady = true
|
||||||
|
moneroAddress = address.isEmpty ? wallet.address : address
|
||||||
|
if let address = moneroAddress, address != wallet.address {
|
||||||
|
var updated = wallet
|
||||||
|
updated.address = address
|
||||||
|
xmrWallet = updated
|
||||||
|
WalletStore.saveWallet(updated, chain: .xmr)
|
||||||
|
}
|
||||||
|
// Always try to catch up, not only right after a restore.
|
||||||
|
_ = try? await MoneroService.shared.startSync()
|
||||||
|
} catch {
|
||||||
|
// Keep the stored wallet so a transient daemon failure is not fatal.
|
||||||
|
moneroOpened = false
|
||||||
|
moneroReady = false
|
||||||
|
moneroAddress = nil
|
||||||
|
errorMessage = "Monero wallet could not be opened: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Create / import
|
||||||
|
|
||||||
|
func createWallet(name: String) async throws -> String {
|
||||||
|
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { throw XRPError.rpc("Enter a wallet name") }
|
||||||
|
isBusy = true
|
||||||
|
defer { isBusy = false }
|
||||||
|
|
||||||
|
if chain == .xrp {
|
||||||
|
let (record, seed) = try XRPWalletService.shared.createWallet(name: trimmed)
|
||||||
|
xrpWallet = record
|
||||||
|
await refresh()
|
||||||
|
return seed
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try await MoneroService.shared.createWallet(name: trimmed)
|
||||||
|
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
|
||||||
|
name: trimmed,
|
||||||
|
address: result.address,
|
||||||
|
createdAt: Date())
|
||||||
|
xmrWallet = record
|
||||||
|
WalletStore.saveWallet(record, chain: .xmr)
|
||||||
|
if !result.mnemonic.isEmpty {
|
||||||
|
WalletStore.saveSecret(result.mnemonic, chain: .xmr)
|
||||||
|
}
|
||||||
|
moneroOpened = true
|
||||||
|
moneroReady = true
|
||||||
|
moneroAddress = result.address
|
||||||
|
_ = try? await MoneroService.shared.startSync()
|
||||||
|
await refresh(force: true)
|
||||||
|
return result.mnemonic
|
||||||
|
}
|
||||||
|
|
||||||
|
func importWallet(name: String, secret: String, restoreHeight: UInt64?) async throws {
|
||||||
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let trimmedSecret = secret.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmedName.isEmpty else { throw XRPError.rpc("Enter a wallet name") }
|
||||||
|
guard !trimmedSecret.isEmpty else { throw XRPError.rpc("Enter your seed or secret") }
|
||||||
|
isBusy = true
|
||||||
|
defer { isBusy = false }
|
||||||
|
|
||||||
|
if chain == .xrp {
|
||||||
|
xrpWallet = try XRPWalletService.shared.importWallet(name: trimmedName, secret: trimmedSecret)
|
||||||
|
await refresh(force: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = try await MoneroService.shared.restoreWallet(name: trimmedName,
|
||||||
|
seed: trimmedSecret,
|
||||||
|
restoreHeight: restoreHeight ?? 0)
|
||||||
|
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
|
||||||
|
name: trimmedName,
|
||||||
|
address: address,
|
||||||
|
createdAt: Date())
|
||||||
|
xmrWallet = record
|
||||||
|
WalletStore.saveWallet(record, chain: .xmr)
|
||||||
|
WalletStore.saveSecret(trimmedSecret, chain: .xmr)
|
||||||
|
moneroOpened = true
|
||||||
|
moneroReady = true
|
||||||
|
moneroAddress = address
|
||||||
|
_ = try? await MoneroService.shared.startSync()
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Reads
|
||||||
|
|
||||||
|
func refresh(force: Bool = false) async {
|
||||||
|
guard hasWallet else { return }
|
||||||
|
errorMessage = nil
|
||||||
|
if chain == .xrp, let wallet = xrpWallet {
|
||||||
|
do {
|
||||||
|
async let balanceCall = XRPWalletService.shared.balance(address: wallet.address)
|
||||||
|
async let txCall = XRPWalletService.shared.transactions(address: wallet.address)
|
||||||
|
balance = try await balanceCall
|
||||||
|
transactions = try await txCall
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
// The Monero wallet keeps scanning while XRP is on screen, so keep its
|
||||||
|
// progress current here too.
|
||||||
|
if moneroReady, let status = try? await MoneroService.shared.syncStatus() {
|
||||||
|
updateSyncStatus(status)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard moneroReady else { return }
|
||||||
|
do {
|
||||||
|
// Sync progress is cheap and polled every tick; the balance and transfer
|
||||||
|
// list touch the wallet's data files, so they are refreshed less often.
|
||||||
|
updateSyncStatus(try await MoneroService.shared.syncStatus())
|
||||||
|
refreshTick += 1
|
||||||
|
if force || refreshTick == 1 || refreshTick % 4 == 0 {
|
||||||
|
async let balanceCall = MoneroService.shared.balance()
|
||||||
|
async let txCall = MoneroService.shared.transactions()
|
||||||
|
balance = try await balanceCall
|
||||||
|
transactions = try await txCall
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks how fast blocks are being scanned so the UI can show an estimate.
|
||||||
|
private func updateSyncStatus(_ status: SyncStatus) {
|
||||||
|
syncStatus = status
|
||||||
|
let now = Date()
|
||||||
|
defer { lastSample = (status.height, now) }
|
||||||
|
|
||||||
|
guard status.syncing,
|
||||||
|
let sample = lastSample,
|
||||||
|
status.height > sample.height,
|
||||||
|
status.daemonHeight > status.height else {
|
||||||
|
if !status.syncing { syncEta = nil }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = now.timeIntervalSince(sample.date)
|
||||||
|
guard elapsed > 1 else { return }
|
||||||
|
let blocksPerSecond = Double(status.height - sample.height) / elapsed
|
||||||
|
guard blocksPerSecond > 0 else { return }
|
||||||
|
|
||||||
|
let remaining = Double(status.daemonHeight - status.height) / blocksPerSecond
|
||||||
|
syncEta = Self.formatDuration(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func formatDuration(_ seconds: Double) -> String {
|
||||||
|
guard seconds.isFinite, seconds > 0 else { return "unknown" }
|
||||||
|
let total = Int(seconds.rounded())
|
||||||
|
let hours = total / 3600
|
||||||
|
let minutes = (total % 3600) / 60
|
||||||
|
if hours >= 24 { return "\(hours / 24)d \(hours % 24)h" }
|
||||||
|
if hours > 0 { return "\(hours)h \(minutes)m" }
|
||||||
|
if minutes > 0 { return "\(minutes)m" }
|
||||||
|
return "under a minute"
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMoneroSync() async {
|
||||||
|
guard moneroReady else {
|
||||||
|
if let wallet = xmrWallet { await openMonero(afterRestore: true, wallet: wallet) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = try? await MoneroService.shared.startSync()
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func revealSeed() async throws -> String {
|
||||||
|
if chain == .xrp {
|
||||||
|
guard let seed = WalletStore.secret(.xrp) else {
|
||||||
|
throw XRPError.rpc("No stored secret for this wallet")
|
||||||
|
}
|
||||||
|
return seed
|
||||||
|
}
|
||||||
|
if let stored = WalletStore.secret(.xmr), !stored.isEmpty { return stored }
|
||||||
|
return try await MoneroService.shared.seed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sends
|
||||||
|
|
||||||
|
func send(to: String, amount: String, tag: String, priority: TransactionPriority) async throws {
|
||||||
|
let destinationTag = UInt32(tag.trimmingCharacters(in: .whitespaces))
|
||||||
|
if chain == .xrp {
|
||||||
|
guard let seed = WalletStore.secret(.xrp) else {
|
||||||
|
throw XRPError.rpc("Wallet secret is unavailable")
|
||||||
|
}
|
||||||
|
statusMessage = nil
|
||||||
|
_ = try await XRPWalletService.shared.send(secret: seed,
|
||||||
|
to: to,
|
||||||
|
amountXRP: amount,
|
||||||
|
destinationTag: destinationTag)
|
||||||
|
statusMessage = "Payment sent"
|
||||||
|
await refresh(force: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = try await MoneroService.shared.send(address: to, amountXMR: amount, priority: priority)
|
||||||
|
statusMessage = "Payment sent"
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete
|
||||||
|
|
||||||
|
func deleteWallet() async {
|
||||||
|
if chain == .xrp {
|
||||||
|
WalletStore.removeWallet(.xrp)
|
||||||
|
xrpWallet = nil
|
||||||
|
} else {
|
||||||
|
if let wallet = xmrWallet { await MoneroService.shared.removeWallet(name: wallet.name) }
|
||||||
|
WalletStore.removeWallet(.xmr)
|
||||||
|
xmrWallet = nil
|
||||||
|
moneroOpened = false
|
||||||
|
moneroReady = false
|
||||||
|
moneroAddress = nil
|
||||||
|
syncStatus = nil
|
||||||
|
}
|
||||||
|
balance = Balance(total: "0", unlocked: "", funded: false)
|
||||||
|
transactions = []
|
||||||
|
errorMessage = nil
|
||||||
|
statusMessage = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearMessages() {
|
||||||
|
errorMessage = nil
|
||||||
|
statusMessage = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#import "KoinsMoneroWallet.h"
|
||||||
|
#import "KoinsSecp256k1.h"
|
||||||
@@ -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,276 @@
|
|||||||
|
#import "KoinsMoneroWallet.h"
|
||||||
|
|
||||||
|
#import <os/log.h>
|
||||||
|
|
||||||
|
#include "monero_wallet_full.h"
|
||||||
|
|
||||||
|
static std::string walletPath(NSString *name) {
|
||||||
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||||
|
NSString *dir = paths.firstObject ?: NSTemporaryDirectory();
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
if (![fm fileExistsAtPath:dir]) {
|
||||||
|
[fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
|
||||||
|
}
|
||||||
|
return std::string([dir stringByAppendingPathComponent:name].UTF8String);
|
||||||
|
}
|
||||||
|
|
||||||
|
static monero_wallet_full *g_wallet = nullptr;
|
||||||
|
static NSString *g_daemonURL = @"xmr-node.cakewallet.com:18081";
|
||||||
|
static NSArray<NSString *> *g_fallbackDaemons = nil;
|
||||||
|
static std::atomic<bool> s_cachedConnected(false);
|
||||||
|
static std::atomic<unsigned long long> s_cachedDaemonHeight(0);
|
||||||
|
static std::atomic<long long> s_lastDaemonProbe(0);
|
||||||
|
|
||||||
|
static void connectDaemon(monero_wallet_full *w) {
|
||||||
|
if (!w) return;
|
||||||
|
@autoreleasepool {
|
||||||
|
if (!g_fallbackDaemons) {
|
||||||
|
g_fallbackDaemons = @[
|
||||||
|
@"xmr-node.cakewallet.com:18081",
|
||||||
|
@"node.monerodevs.org:18089",
|
||||||
|
@"monero.stackwallet.com:18081",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
NSMutableArray<NSString *> *candidates = [NSMutableArray arrayWithObject:g_daemonURL];
|
||||||
|
for (NSString *url in g_fallbackDaemons) {
|
||||||
|
if (![url isEqualToString:g_daemonURL]) [candidates addObject:url];
|
||||||
|
}
|
||||||
|
for (NSString *url in candidates) {
|
||||||
|
try {
|
||||||
|
w->set_daemon_connection(std::string(url.UTF8String));
|
||||||
|
if (w->is_connected_to_daemon()) {
|
||||||
|
g_daemonURL = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Try the next daemon.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation KoinsMoneroWallet
|
||||||
|
|
||||||
|
+ (void)invoke:(NSString *)jsonRequest
|
||||||
|
completion:(void (^)(NSString *response))completion {
|
||||||
|
static dispatch_queue_t invokeQueue;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
invokeQueue = dispatch_queue_create("com.koins.monero.invoke", DISPATCH_QUEUE_SERIAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch_async(invokeQueue, ^{
|
||||||
|
NSMutableDictionary *resp = [NSMutableDictionary new];
|
||||||
|
try {
|
||||||
|
NSData *data = [jsonRequest dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
NSDictionary *req = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||||
|
NSString *op = req[@"op"] ?: @"";
|
||||||
|
resp[@"op"] = op;
|
||||||
|
|
||||||
|
if ([op isEqualToString:@"setDaemon"]) {
|
||||||
|
NSString *url = req[@"url"];
|
||||||
|
if (url.length > 0) g_daemonURL = url;
|
||||||
|
resp[@"url"] = g_daemonURL;
|
||||||
|
} else if ([op isEqualToString:@"createWallet"]) {
|
||||||
|
NSString *name = req[@"name"];
|
||||||
|
std::string path = walletPath(name);
|
||||||
|
std::string password = std::string([req[@"password"] UTF8String]);
|
||||||
|
bool exists = false;
|
||||||
|
try { exists = monero_wallet_full::wallet_exists(path); } catch (...) {}
|
||||||
|
if (exists) {
|
||||||
|
g_wallet = monero_wallet_full::open_wallet(path, password, monero_network_type::MAINNET, false);
|
||||||
|
connectDaemon(g_wallet);
|
||||||
|
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
|
||||||
|
try { resp[@"mnemonic"] = [NSString stringWithUTF8String:g_wallet->get_seed().c_str()]; } catch (...) {}
|
||||||
|
} else {
|
||||||
|
monero_wallet_config config;
|
||||||
|
config.m_path = path;
|
||||||
|
config.m_password = password;
|
||||||
|
config.m_network_type = monero_network_type::MAINNET;
|
||||||
|
config.m_language = std::string([req[@"language"] ?: @"English" UTF8String]);
|
||||||
|
config.m_save_current = false;
|
||||||
|
monero_wallet_full *w = monero_wallet_full::create_wallet(config, nullptr);
|
||||||
|
g_wallet = w;
|
||||||
|
connectDaemon(w);
|
||||||
|
resp[@"address"] = [NSString stringWithUTF8String:w->get_address(0, 0).c_str()];
|
||||||
|
resp[@"mnemonic"] = [NSString stringWithUTF8String:w->get_seed().c_str()];
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"openWallet"] || [op isEqualToString:@"restoreWallet"]) {
|
||||||
|
if (g_wallet) {
|
||||||
|
try { g_wallet->close(true); } catch (...) {}
|
||||||
|
delete g_wallet;
|
||||||
|
g_wallet = nullptr;
|
||||||
|
}
|
||||||
|
if ([op isEqualToString:@"openWallet"]) {
|
||||||
|
g_wallet = monero_wallet_full::open_wallet(
|
||||||
|
walletPath(req[@"name"]),
|
||||||
|
std::string([req[@"password"] UTF8String]),
|
||||||
|
monero_network_type::MAINNET, false);
|
||||||
|
} else {
|
||||||
|
monero_wallet_config config;
|
||||||
|
config.m_path = walletPath(req[@"name"]);
|
||||||
|
config.m_password = std::string([req[@"password"] UTF8String]);
|
||||||
|
config.m_network_type = monero_network_type::MAINNET;
|
||||||
|
config.m_seed = std::string([req[@"seed"] UTF8String]);
|
||||||
|
config.m_restore_height = (uint64_t)[req[@"restoreHeight"] longLongValue];
|
||||||
|
config.m_language = "English";
|
||||||
|
config.m_save_current = false;
|
||||||
|
g_wallet = monero_wallet_full::create_wallet(config, nullptr);
|
||||||
|
}
|
||||||
|
connectDaemon(g_wallet);
|
||||||
|
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
|
||||||
|
} else if ([op isEqualToString:@"getSeed"]) {
|
||||||
|
if (g_wallet) {
|
||||||
|
resp[@"mnemonic"] = [NSString stringWithUTF8String:g_wallet->get_seed().c_str()];
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"removeWallet"]) {
|
||||||
|
NSString *name = req[@"name"];
|
||||||
|
if (g_wallet) {
|
||||||
|
try { g_wallet->close(false); } catch (...) {}
|
||||||
|
delete g_wallet;
|
||||||
|
g_wallet = nullptr;
|
||||||
|
}
|
||||||
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||||
|
NSString *dir = paths.firstObject;
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
for (NSString *ext in @[ @"", @".keys", @".address.txt", @".mms" ]) {
|
||||||
|
NSString *file = [dir stringByAppendingPathComponent:[name stringByAppendingString:ext]];
|
||||||
|
[fm removeItemAtPath:file error:nil];
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"closeWallet"]) {
|
||||||
|
if (g_wallet) {
|
||||||
|
try { g_wallet->close(true); } catch (...) {}
|
||||||
|
delete g_wallet;
|
||||||
|
g_wallet = nullptr;
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"getBalance"]) {
|
||||||
|
if (g_wallet) {
|
||||||
|
uint64_t balance = 0, unlocked = 0;
|
||||||
|
try {
|
||||||
|
balance = g_wallet->get_balance();
|
||||||
|
unlocked = g_wallet->get_unlocked_balance();
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
os_log_error(OS_LOG_DEFAULT, "[koins] getBalance: %{public}s", e.what());
|
||||||
|
}
|
||||||
|
resp[@"balance"] = @(balance).stringValue;
|
||||||
|
resp[@"unlocked"] = @(unlocked).stringValue;
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"getAddress"]) {
|
||||||
|
if (g_wallet) {
|
||||||
|
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"getTransactions"]) {
|
||||||
|
NSMutableArray *entries = [NSMutableArray new];
|
||||||
|
if (!g_wallet) {
|
||||||
|
resp[@"transactions"] = entries;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
monero_transfer_query query;
|
||||||
|
for (const auto &transfer : g_wallet->get_transfers(query)) {
|
||||||
|
const auto &tx = transfer->m_tx;
|
||||||
|
const auto isIn = transfer->is_incoming();
|
||||||
|
boost::optional<uint64_t> txHeight = tx ? tx->get_height() : boost::none;
|
||||||
|
[entries addObject:@{
|
||||||
|
@"hash" : [NSString stringWithUTF8String:((tx && tx->m_hash) ? *tx->m_hash : "").c_str()],
|
||||||
|
@"amount" : @((transfer->m_amount ? *transfer->m_amount : 0)).stringValue,
|
||||||
|
@"timestamp" : @((tx && tx->m_received_timestamp) ? *tx->m_received_timestamp : 0),
|
||||||
|
@"direction" : (isIn && *isIn) ? @"in" : @"out",
|
||||||
|
@"confirmed" : @((tx && tx->m_is_confirmed) ? *tx->m_is_confirmed : false),
|
||||||
|
@"height" : @(txHeight ? *txHeight : 0),
|
||||||
|
@"fee" : @((tx && tx->m_fee) ? *tx->m_fee : 0).stringValue,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
os_log_error(OS_LOG_DEFAULT, "[koins] getTransactions: %{public}s", e.what());
|
||||||
|
}
|
||||||
|
resp[@"transactions"] = entries;
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"startSync"]) {
|
||||||
|
if (g_wallet) {
|
||||||
|
connectDaemon(g_wallet);
|
||||||
|
try {
|
||||||
|
if (g_wallet->is_connected_to_daemon()) {
|
||||||
|
g_wallet->start_syncing(5000);
|
||||||
|
resp[@"syncing"] = @YES;
|
||||||
|
} else {
|
||||||
|
resp[@"syncing"] = @NO;
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
resp[@"syncing"] = @NO;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
}
|
||||||
|
} else if ([op isEqualToString:@"stopSync"]) {
|
||||||
|
try { if (g_wallet) g_wallet->stop_syncing(); } catch (...) {}
|
||||||
|
} else if ([op isEqualToString:@"getSyncStatus"]) {
|
||||||
|
uint64_t height = 0, daemonHeight = 0;
|
||||||
|
bool connected = false;
|
||||||
|
if (g_wallet) {
|
||||||
|
try {
|
||||||
|
height = g_wallet->get_height();
|
||||||
|
// is_connected_to_daemon performs a live round trip; public
|
||||||
|
// daemons rate limit aggressive polling, so cache for 10s.
|
||||||
|
long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000);
|
||||||
|
if (now - s_lastDaemonProbe.load() > 10000) {
|
||||||
|
s_lastDaemonProbe.store(now);
|
||||||
|
connected = g_wallet->is_connected_to_daemon();
|
||||||
|
s_cachedConnected = connected;
|
||||||
|
} else {
|
||||||
|
connected = s_cachedConnected;
|
||||||
|
}
|
||||||
|
daemonHeight = s_cachedDaemonHeight;
|
||||||
|
if (connected) {
|
||||||
|
daemonHeight = g_wallet->get_daemon_height();
|
||||||
|
s_cachedDaemonHeight = daemonHeight;
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
os_log_error(OS_LOG_DEFAULT, "[koins] getSyncStatus: %{public}s", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp[@"height"] = @(height);
|
||||||
|
resp[@"daemonHeight"] = @(daemonHeight);
|
||||||
|
resp[@"syncing"] = @(connected && height < daemonHeight);
|
||||||
|
resp[@"connected"] = @(connected);
|
||||||
|
} else if ([op isEqualToString:@"transferTo"]) {
|
||||||
|
if (!g_wallet) {
|
||||||
|
resp[@"error"] = @"No wallet open";
|
||||||
|
} else {
|
||||||
|
monero_tx_config config;
|
||||||
|
auto destination = std::make_shared<monero_destination>();
|
||||||
|
destination->m_address = std::string([req[@"address"] UTF8String]);
|
||||||
|
destination->m_amount = (uint64_t)[req[@"amount"] longLongValue];
|
||||||
|
config.m_destinations = { destination };
|
||||||
|
config.m_priority = (monero_tx_priority)[req[@"priority"] integerValue];
|
||||||
|
config.m_relay = true;
|
||||||
|
auto txs = g_wallet->create_txs(config);
|
||||||
|
const auto &txSet = txs[0]->m_tx_set;
|
||||||
|
std::string signedHex = (txSet && txSet->m_signed_tx_hex) ? *txSet->m_signed_tx_hex : "";
|
||||||
|
g_wallet->submit_txs(signedHex);
|
||||||
|
resp[@"txHash"] = [NSString stringWithUTF8String:(txs[0]->m_hash ? *txs[0]->m_hash : "").c_str()];
|
||||||
|
resp[@"fee"] = @((txs[0]->m_fee ? *txs[0]->m_fee : 0)).stringValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp[@"error"] = [NSString stringWithFormat:@"Unknown op: %@", op];
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
os_log_error(OS_LOG_DEFAULT, "[koins] monero %{public}@ failed: %{public}s",
|
||||||
|
resp[@"op"], e.what());
|
||||||
|
resp[@"error"] = [NSString stringWithUTF8String:e.what()];
|
||||||
|
} catch (...) {
|
||||||
|
resp[@"error"] = @"Unknown native exception";
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *out = [NSJSONSerialization dataWithJSONObject:resp options:0 error:nil];
|
||||||
|
NSString *outStr = [[NSString alloc] initWithData:out encoding:NSUTF8StringEncoding];
|
||||||
|
completion(outStr ?: @"{\"error\":\"encode failed\"}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -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,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>UIUserInterfaceStyle</key>
|
||||||
|
<string>Dark</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -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,69 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum Chain: String, Codable, CaseIterable {
|
||||||
|
case xrp
|
||||||
|
case xmr
|
||||||
|
|
||||||
|
var symbol: String { self == .xrp ? "XRP" : "XMR" }
|
||||||
|
var title: String { self == .xrp ? "XRP Ledger" : "Monero" }
|
||||||
|
var subtitle: String {
|
||||||
|
self == .xrp ? "Fast, low-fee payments" : "Private, untraceable payments"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletRecord: Codable, Identifiable, Equatable {
|
||||||
|
let id: String
|
||||||
|
var name: String
|
||||||
|
var address: String
|
||||||
|
var createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
case incoming
|
||||||
|
case outgoing
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransactionRecord: Identifiable {
|
||||||
|
let id: String
|
||||||
|
let amount: String
|
||||||
|
let timestamp: Date?
|
||||||
|
let direction: Direction
|
||||||
|
let confirmed: Bool
|
||||||
|
var fee: String?
|
||||||
|
var tag: Int?
|
||||||
|
var counterparty: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Balance {
|
||||||
|
var total: String
|
||||||
|
var unlocked: String
|
||||||
|
var funded: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SyncStatus {
|
||||||
|
var height: UInt64
|
||||||
|
var daemonHeight: UInt64
|
||||||
|
var syncing: Bool
|
||||||
|
var connected: Bool
|
||||||
|
|
||||||
|
var progress: Double {
|
||||||
|
guard daemonHeight > 0 else { return 0 }
|
||||||
|
return min(1, Double(height) / Double(daemonHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransactionPriority: Int {
|
||||||
|
case defaultPriority = 0
|
||||||
|
case low = 1
|
||||||
|
case medium = 2
|
||||||
|
case high = 3
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .defaultPriority: return "Default"
|
||||||
|
case .low: return "Low"
|
||||||
|
case .medium: return "Medium"
|
||||||
|
case .high: return "High"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) woodser
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are
|
||||||
|
* permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||||
|
* conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||||
|
* of conditions and the following disclaimer in the documentation and/or other
|
||||||
|
* materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||||
|
* used to endorse or promote products derived from this software without specific
|
||||||
|
* prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||||
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||||
|
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||||
|
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public interface for libmonero-cpp library.
|
||||||
|
*/
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception when interacting with a Monero wallet or daemon.
|
||||||
|
*/
|
||||||
|
class monero_error : public std::exception {
|
||||||
|
public:
|
||||||
|
std::string message;
|
||||||
|
|
||||||
|
monero_error() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the exception.
|
||||||
|
*
|
||||||
|
* @param msg is a human-readable description of the error
|
||||||
|
*/
|
||||||
|
monero_error(const std::string& msg) : message(msg) {}
|
||||||
|
|
||||||
|
const char* what() const noexcept override {
|
||||||
|
return message.c_str();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception when interacting with the Monero daemon or wallet RPC API.
|
||||||
|
*/
|
||||||
|
class monero_rpc_error : public monero_error {
|
||||||
|
public:
|
||||||
|
int code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the exception.
|
||||||
|
*
|
||||||
|
* @param error_code is the error code (optional)
|
||||||
|
* @param msg is a human-readable description of the error
|
||||||
|
*/
|
||||||
|
monero_rpc_error(int error_code, const std::string& msg) : code(error_code) { message = msg; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the exception.
|
||||||
|
*
|
||||||
|
* @param msg is a human-readable description of the error
|
||||||
|
*/
|
||||||
|
monero_rpc_error(const std::string& msg) : code(-1) { message = msg; }
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
#include "monero_rpc_connection.h"
|
||||||
|
#include "utils/gen_utils.h"
|
||||||
|
#include "utils/monero_utils.h"
|
||||||
|
#include "daemon/monero_daemon_rpc_model.h"
|
||||||
|
#include "monero_error.h"
|
||||||
|
|
||||||
|
static const uint32_t DEFAULT_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
|
||||||
|
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
// --------------------------- MONERO REQUEST PARAMS ---------------------------
|
||||||
|
|
||||||
|
rapidjson::Value monero_request_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
|
||||||
|
rapidjson::Value root(rapidjson::kObjectType);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- MONERO RPC REQUEST ---------------------------
|
||||||
|
|
||||||
|
monero_rpc_request::monero_rpc_request(const std::string& method, const std::shared_ptr<serializable_struct>& params, bool json_rpc): m_method(method), m_params(params) {
|
||||||
|
if (params == nullptr) m_params = std::make_shared<monero_request_params>();
|
||||||
|
if (json_rpc) {
|
||||||
|
m_id = "0";
|
||||||
|
m_version = "2.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rapidjson::Value monero_rpc_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
|
||||||
|
if (!is_json_rpc()) {
|
||||||
|
if (m_params == nullptr) throw monero_error("No params provided");
|
||||||
|
return m_params->to_rapidjson_val(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create root
|
||||||
|
rapidjson::Value root(rapidjson::kObjectType);
|
||||||
|
|
||||||
|
// set string values
|
||||||
|
rapidjson::Value value_str(rapidjson::kStringType);
|
||||||
|
|
||||||
|
if (m_version != boost::none) monero_utils::add_json_member("jsonrpc", m_version.get(), allocator, root, value_str);
|
||||||
|
if (m_id != boost::none) monero_utils::add_json_member("id", m_id.get(), allocator, root, value_str);
|
||||||
|
if (m_method != boost::none) monero_utils::add_json_member("method", m_method.get(), allocator, root, value_str);
|
||||||
|
if (m_params != nullptr) root.AddMember("params", m_params->to_rapidjson_val(allocator), allocator);
|
||||||
|
|
||||||
|
// return root
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- MONERO RPC RESPONSE ---------------------------
|
||||||
|
|
||||||
|
void raise_rpc_error(const boost::property_tree::ptree& error_node, const boost::string_ref uri, const std::string& method) {
|
||||||
|
std::string err_message = "Unknown error";
|
||||||
|
int err_code = -1;
|
||||||
|
|
||||||
|
for (auto it = error_node.begin(); it != error_node.end(); ++it) {
|
||||||
|
std::string key_err = it->first;
|
||||||
|
if (key_err == std::string("message")) {
|
||||||
|
if (it->second.data().empty()) {
|
||||||
|
err_message = "Received error response from RPC request with method '" + method + "' to " + uri.to_string();
|
||||||
|
}
|
||||||
|
else err_message = it->second.data();
|
||||||
|
} else if (key_err == std::string("code")) {
|
||||||
|
try {
|
||||||
|
err_code = it->second.get_value<int>();
|
||||||
|
} catch (...) {
|
||||||
|
MERROR("non-numeric error code from a malformed/unusual RPC response");
|
||||||
|
err_code = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw monero_rpc_error(err_code, err_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- MONERO RPC CONNECTION ---------------------------
|
||||||
|
|
||||||
|
bool monero_rpc_connection::compare(int p1, int p2) {
|
||||||
|
if (p1 == p2) return false;
|
||||||
|
// 0 alway first
|
||||||
|
if (p1 == 0) return true;
|
||||||
|
if (p2 == 0) return false;
|
||||||
|
return p1 > p2;
|
||||||
|
}
|
||||||
|
|
||||||
|
monero_rpc_connection::monero_rpc_connection(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, int priority, const boost::optional<uint32_t>& timeout_ms) {
|
||||||
|
if (!uri.empty()) m_uri = uri;
|
||||||
|
else m_uri = boost::none;
|
||||||
|
if (!proxy_uri.empty()) m_proxy_uri = proxy_uri;
|
||||||
|
else m_proxy_uri = boost::none;
|
||||||
|
if (!zmq_uri.empty()) m_zmq_uri = zmq_uri;
|
||||||
|
else m_zmq_uri = boost::none;
|
||||||
|
m_priority = priority;
|
||||||
|
m_timeout_ms = timeout_ms;
|
||||||
|
set_credentials(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
monero_rpc_connection::monero_rpc_connection(const monero_rpc_connection& rpc) {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> src_lock(rpc.m_mutex);
|
||||||
|
m_uri = rpc.m_uri;
|
||||||
|
m_proxy_uri = rpc.m_proxy_uri;
|
||||||
|
m_zmq_uri = rpc.m_zmq_uri;
|
||||||
|
m_priority = rpc.m_priority;
|
||||||
|
m_timeout_ms = rpc.m_timeout_ms;
|
||||||
|
set_credentials(rpc.m_username.value_or(""), rpc.m_password.value_or(""));
|
||||||
|
m_is_online = rpc.m_is_online;
|
||||||
|
m_is_authenticated = rpc.m_is_authenticated;
|
||||||
|
m_response_time = rpc.m_response_time;
|
||||||
|
m_attributes = rpc.m_attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
rapidjson::Value monero_rpc_connection::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
// create root
|
||||||
|
rapidjson::Value root(rapidjson::kObjectType);
|
||||||
|
|
||||||
|
// set string values
|
||||||
|
rapidjson::Value value_str(rapidjson::kStringType);
|
||||||
|
if (m_uri != boost::none) monero_utils::add_json_member("uri", m_uri.get(), allocator, root, value_str);
|
||||||
|
if (m_username != boost::none) monero_utils::add_json_member("username", m_username.get(), allocator, root, value_str);
|
||||||
|
if (m_password != boost::none) monero_utils::add_json_member("password", m_password.get(), allocator, root, value_str);
|
||||||
|
if (m_proxy_uri != boost::none) monero_utils::add_json_member("proxyUri", m_proxy_uri.get(), allocator, root, value_str);
|
||||||
|
if (m_zmq_uri != boost::none) monero_utils::add_json_member("zmqUri", m_zmq_uri.get(), allocator, root, value_str);
|
||||||
|
|
||||||
|
// set num values
|
||||||
|
rapidjson::Value value_num(rapidjson::kNumberType);
|
||||||
|
monero_utils::add_json_member("priority", m_priority, allocator, root, value_num);
|
||||||
|
if (m_timeout_ms != boost::none) monero_utils::add_json_member("timeoutMs", m_timeout_ms.get(), allocator, root, value_num);
|
||||||
|
if (m_response_time != boost::none) monero_utils::add_json_member("responseTime", m_response_time.get(), allocator, root, value_num);
|
||||||
|
|
||||||
|
// set bool values
|
||||||
|
if (m_is_online != boost::none) monero_utils::add_json_member("isOnline", m_is_online.get(), allocator, root);
|
||||||
|
if (m_is_authenticated != boost::none) monero_utils::add_json_member("isAuthenticated", m_is_authenticated.get(), allocator, root);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool monero_rpc_connection::is_onion() const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
epee::net_utils::http::url_content parsed{};
|
||||||
|
if (!epee::net_utils::parse_url(m_uri.value_or(""), parsed)) return false;
|
||||||
|
return parsed.host.size() >= 6 && parsed.host.compare(parsed.host.size() - 6, 6, ".onion") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool monero_rpc_connection::is_i2p() const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
epee::net_utils::http::url_content parsed{};
|
||||||
|
if (!epee::net_utils::parse_url(m_uri.value_or(""), parsed)) return false;
|
||||||
|
return parsed.host.size() >= 8 && parsed.host.compare(parsed.host.size() - 8, 8, ".b32.i2p") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void monero_rpc_connection::set_credentials(const std::string& username, const std::string& password) {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
// disconnect http client if connected
|
||||||
|
if (m_http_client != nullptr && m_http_client->is_connected()) m_http_client->disconnect();
|
||||||
|
|
||||||
|
bool username_empty = username.empty();
|
||||||
|
bool password_empty = password.empty();
|
||||||
|
|
||||||
|
// check username and password consistency
|
||||||
|
if (!username_empty || !password_empty) {
|
||||||
|
if (password_empty) {
|
||||||
|
throw monero_error("password cannot be empty because username is not empty");
|
||||||
|
}
|
||||||
|
if (username_empty) {
|
||||||
|
throw monero_error("username cannot be empty because password is not empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check username and password changes
|
||||||
|
bool username_equals = (m_username == boost::none && username_empty) || (m_username != boost::none && *m_username == username);
|
||||||
|
bool password_equals = (m_password == boost::none && password_empty) || (m_password != boost::none && *m_password == password);
|
||||||
|
|
||||||
|
// connection reset values
|
||||||
|
if (!username_equals || !password_equals) {
|
||||||
|
m_is_online = boost::none;
|
||||||
|
m_is_authenticated = boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup username and password
|
||||||
|
if (!username_empty && !password_empty) {
|
||||||
|
m_username = username;
|
||||||
|
m_password = password;
|
||||||
|
} else {
|
||||||
|
m_username = boost::none;
|
||||||
|
m_password = boost::none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void monero_rpc_connection::set_attribute(const std::string& key, const std::string& val) {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
m_attributes[key] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string monero_rpc_connection::get_attribute(const std::string& key) const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
std::unordered_map<std::string, std::string>::const_iterator i = m_attributes.find(key);
|
||||||
|
if (i == m_attributes.end()) {
|
||||||
|
return std::string(""); // attribute not found
|
||||||
|
}
|
||||||
|
return i->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<bool> monero_rpc_connection::is_online() const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
return m_is_online;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<bool> monero_rpc_connection::is_authenticated() const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
return m_is_authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::optional<bool> monero_rpc_connection::is_connected() const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
if (m_is_online == boost::none) return boost::none;
|
||||||
|
return m_is_online.get() && (m_is_authenticated == boost::none || m_is_authenticated.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool monero_rpc_connection::check_connection(const boost::optional<uint32_t>& timeout_ms) {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
boost::optional<bool> is_online_before = m_is_online;
|
||||||
|
boost::optional<bool> is_authenticated_before = m_is_authenticated;
|
||||||
|
boost::optional<std::chrono::high_resolution_clock::time_point> start_time = boost::none;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// assume daemon connection
|
||||||
|
monero_get_blocks_by_height_request request(100);
|
||||||
|
start_time = std::chrono::high_resolution_clock::now();
|
||||||
|
send_binary_request(request, timeout_ms);
|
||||||
|
|
||||||
|
// set response time
|
||||||
|
m_response_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time.get()).count();
|
||||||
|
m_is_online = true;
|
||||||
|
m_is_authenticated = true;
|
||||||
|
}
|
||||||
|
catch (const monero_rpc_error& ex) {
|
||||||
|
m_is_online = false;
|
||||||
|
m_is_authenticated = boost::none;
|
||||||
|
m_response_time = boost::none;
|
||||||
|
|
||||||
|
if (ex.code == 401 || ex.code == 404) {
|
||||||
|
|
||||||
|
// fallback to latency check
|
||||||
|
m_is_online = true;
|
||||||
|
m_is_authenticated = ex.code == 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
m_is_online = false;
|
||||||
|
m_is_authenticated = boost::none;
|
||||||
|
m_response_time = boost::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gen_utils::bool_equals(true, m_is_online) && start_time != boost::none) {
|
||||||
|
m_response_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time.get()).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_online_before != m_is_online || is_authenticated_before != m_is_authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::property_tree::ptree monero_rpc_connection::send_json_request(const std::string& path, const std::shared_ptr<serializable_struct>& params, const boost::optional<uint32_t>& timeout_ms) const {
|
||||||
|
|
||||||
|
// send JSON-RPC request
|
||||||
|
monero_rpc_request request(path, params);
|
||||||
|
auto response = send_json_request(request, timeout_ms);
|
||||||
|
|
||||||
|
// assert JSON-RPC response is defined
|
||||||
|
if (response.m_result == boost::none) throw monero_error("Invalid Monero JSONRPC response");
|
||||||
|
return response.m_result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
monero_rpc_response monero_rpc_connection::send_json_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
|
||||||
|
|
||||||
|
// invoke JSON-RPC method
|
||||||
|
monero_rpc_response response;
|
||||||
|
send_rpc_request("/json_rpc", request, response, timeout_ms);
|
||||||
|
|
||||||
|
// return JSON-RPC response
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::property_tree::ptree monero_rpc_connection::send_path_request(const std::string& path, const std::shared_ptr<serializable_struct>& params, const boost::optional<uint32_t>& timeout_ms) const {
|
||||||
|
|
||||||
|
// send RPC request
|
||||||
|
monero_rpc_request request(path, params, false);
|
||||||
|
auto response = send_path_request(request, timeout_ms);
|
||||||
|
|
||||||
|
// assert RPC response is defined
|
||||||
|
if (response.m_response == boost::none) throw monero_error("Invalid Monero RPC response");
|
||||||
|
return response.m_response.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
monero_rpc_response monero_rpc_connection::send_path_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
|
||||||
|
|
||||||
|
// validate parameters
|
||||||
|
if (request.m_method == boost::none || request.m_method->empty()) throw monero_error("No RPC method set in path request");
|
||||||
|
|
||||||
|
// invoke RPC method
|
||||||
|
monero_rpc_response response;
|
||||||
|
send_rpc_request(std::string("/") + request.m_method.get(), request, response, timeout_ms);
|
||||||
|
|
||||||
|
// return RPC response
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
monero_rpc_response monero_rpc_connection::send_binary_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
|
||||||
|
|
||||||
|
// validate parameters
|
||||||
|
if (request.m_method == boost::none || request.m_method->empty()) throw monero_error("No RPC method set in binary request");
|
||||||
|
|
||||||
|
// invoke binary RPC method
|
||||||
|
monero_rpc_response response;
|
||||||
|
send_rpc_request(std::string("/") + request.m_method.get(), request, response, timeout_ms, true);
|
||||||
|
|
||||||
|
// return binary response
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
void monero_rpc_connection::ensure_configured() const {
|
||||||
|
|
||||||
|
// create http client on first use, so constructing a connection does not require network support
|
||||||
|
if (m_http_client == nullptr) {
|
||||||
|
auto factory = std::make_unique<net::http::client_factory>();
|
||||||
|
m_http_client = factory->create();
|
||||||
|
m_http_client->set_auto_connect(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only reconfigure when uri/creds/proxy changed since last apply
|
||||||
|
std::string uri = m_uri.value_or("");
|
||||||
|
std::string username = m_username.value_or("");
|
||||||
|
std::string password = m_password.value_or("");
|
||||||
|
std::string proxy_uri = m_proxy_uri.value_or("");
|
||||||
|
auto key = std::make_tuple(uri, username, password, proxy_uri);
|
||||||
|
|
||||||
|
if (m_applied == key) return; // unchanged, so reuse live connection
|
||||||
|
|
||||||
|
// setup connection credentials
|
||||||
|
boost::optional<epee::net_utils::http::login> login;
|
||||||
|
if(!username.empty() && !password.empty()) {
|
||||||
|
login = epee::net_utils::http::login();
|
||||||
|
login->username = username;
|
||||||
|
login->password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect ssl
|
||||||
|
epee::net_utils::ssl_support_t ssl = uri.rfind("https", 0) == 0 ? epee::net_utils::ssl_support_t::e_ssl_support_enabled : epee::net_utils::ssl_support_t::e_ssl_support_disabled;
|
||||||
|
|
||||||
|
if (!m_http_client->set_proxy(proxy_uri)) throw monero_error("Could not set proxy uri: " + proxy_uri);
|
||||||
|
if (!m_http_client->set_server(uri, login, std::move(ssl))) throw monero_error("Could not set uri: " + uri);
|
||||||
|
if (!m_http_client->connect(std::chrono::milliseconds(DEFAULT_TIMEOUT_MS))) MWARNING("Could not connect to server: " + uri);
|
||||||
|
|
||||||
|
m_applied = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string monero_rpc_connection::invoke_post(const boost::string_ref uri, const std::string& body, const boost::optional<uint32_t>& timeout_ms) const {
|
||||||
|
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
// request/response timeout: user-configurable, defaults to 3 minutes when not set.
|
||||||
|
uint32_t timeout = DEFAULT_TIMEOUT_MS;
|
||||||
|
if (timeout_ms != boost::none) timeout = timeout_ms.get();
|
||||||
|
else if (m_timeout_ms != boost::none) timeout = m_timeout_ms.get();
|
||||||
|
if (timeout == 0) timeout = std::numeric_limits<uint32_t>::max(); // timeout of 0 is treated as indefinite
|
||||||
|
|
||||||
|
// ensure_configured() bounds only the first connect; epee reconnects with this request timeout
|
||||||
|
ensure_configured();
|
||||||
|
const epee::net_utils::http::http_response_info* pri = NULL;
|
||||||
|
|
||||||
|
// invoke http json
|
||||||
|
const uint64_t received_before = m_http_client->get_bytes_received();
|
||||||
|
if (!m_http_client->invoke_post(uri, body, std::chrono::milliseconds(timeout), std::addressof(pri))) {
|
||||||
|
|
||||||
|
// epee never surfaces the 401 status, but a rejected login means the server answered;
|
||||||
|
// a dead or idle-closed socket yields no response bytes at all
|
||||||
|
bool received = m_http_client->get_bytes_received() > received_before;
|
||||||
|
if (received && m_http_client->is_connected()) {
|
||||||
|
throw monero_rpc_error(401, "Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop the socket: epee leaves EOF'd sockets marked connected, so without this the next
|
||||||
|
// call reuses the poisoned socket and hits the same misdiagnosis again
|
||||||
|
m_http_client->disconnect();
|
||||||
|
if (m_uri == boost::none || m_uri->empty()) throw monero_error("Cannot send RPC request: uri not set.");
|
||||||
|
throw monero_error("Network error: " + std::string(received ? "connection dropped mid-response" : "no response") + " from " + m_uri.get() + uri.to_string() + " (timeout: " + std::to_string(timeout) + "ms)");
|
||||||
|
}
|
||||||
|
if (!pri) throw monero_error("Could not get response info");
|
||||||
|
if (pri->m_response_code < 200 || pri->m_response_code > 299) {
|
||||||
|
std::string err_msg = std::to_string(pri->m_response_code) + " " + pri->m_response_comment + (pri->m_body.empty() ? "" : ": " + pri->m_body);
|
||||||
|
throw monero_rpc_error(pri->m_response_code, err_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return response info body
|
||||||
|
return pri->m_body;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deserialize_rpc_response(const std::string& json, monero_rpc_response& response, const boost::string_ref uri, const std::string& method) {
|
||||||
|
|
||||||
|
// parse json to property node
|
||||||
|
boost::property_tree::ptree node;
|
||||||
|
gen_utils::deserialize(json, node);
|
||||||
|
|
||||||
|
// parse JSON-RPC response
|
||||||
|
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
|
||||||
|
std::string key = it->first;
|
||||||
|
if (key == std::string("error")) raise_rpc_error(it->second, uri, method);
|
||||||
|
else if (key == std::string("jsonrpc")) response.m_jsonrpc = it->second.data();
|
||||||
|
else if (key == std::string("result")) response.m_result = it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not JSON-RPC, set RPC response
|
||||||
|
if (response.m_jsonrpc == boost::none) response.m_response = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void monero_rpc_connection::send_rpc_request(const boost::string_ref uri, const monero_rpc_request& request, monero_rpc_response& response, const boost::optional<uint32_t>& timeout_ms, bool binary) const {
|
||||||
|
std::string body;
|
||||||
|
if (request.m_method == boost::none) throw monero_error("Must provide a valid request method");
|
||||||
|
if (binary) monero_utils::json_to_binary(request.serialize(), body);
|
||||||
|
else body = request.serialize();
|
||||||
|
|
||||||
|
std::string result = invoke_post(uri, body, timeout_ms);
|
||||||
|
if (binary) response.m_binary = result;
|
||||||
|
else deserialize_rpc_response(result, response, uri, request.m_method.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<monero_rpc_connection> monero_rpc_connection::from_property_tree(const boost::property_tree::ptree& node) {
|
||||||
|
std::shared_ptr<monero_rpc_connection> connection = std::make_shared<monero_rpc_connection>();
|
||||||
|
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
|
||||||
|
std::string key = it->first;
|
||||||
|
if (key == std::string("uri")) connection->m_uri = it->second.data();
|
||||||
|
else if (key == std::string("username")) connection->m_username = it->second.data();
|
||||||
|
else if (key == std::string("password")) connection->m_password = it->second.data();
|
||||||
|
else if (key == std::string("proxyUri") || key == std::string("proxy_uri")) connection->m_proxy_uri = it->second.data();
|
||||||
|
else if (key == std::string("zmqUri")) connection->m_zmq_uri = it->second.data();
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) woodser
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are
|
||||||
|
* permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||||
|
* conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||||
|
* of conditions and the following disclaimer in the documentation and/or other
|
||||||
|
* materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||||
|
* used to endorse or promote products derived from this software without specific
|
||||||
|
* prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||||
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||||
|
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||||
|
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/thread/mutex.hpp>
|
||||||
|
#include <boost/thread/thread.hpp>
|
||||||
|
#include <boost/thread/condition_variable.hpp>
|
||||||
|
#include <boost/property_tree/ptree.hpp>
|
||||||
|
#include <boost/property_tree/json_parser.hpp>
|
||||||
|
#include "rapidjson/document.h"
|
||||||
|
#include "rapidjson/writer.h"
|
||||||
|
#include "rapidjson/stringbuffer.h"
|
||||||
|
#include "net/http.h"
|
||||||
|
#include "daemon/monero_daemon_model.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public interface for libmonero-cpp library.
|
||||||
|
*/
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models parameters for a request to a RPC server.
|
||||||
|
*/
|
||||||
|
struct monero_request_params : public serializable_struct {
|
||||||
|
monero_request_params() { }
|
||||||
|
|
||||||
|
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models a request to a RPC server.
|
||||||
|
*/
|
||||||
|
struct monero_rpc_request : public serializable_struct {
|
||||||
|
boost::optional<std::string> m_id;
|
||||||
|
boost::optional<std::string> m_version;
|
||||||
|
boost::optional<std::string> m_method;
|
||||||
|
std::shared_ptr<serializable_struct> m_params;
|
||||||
|
|
||||||
|
monero_rpc_request() { }
|
||||||
|
monero_rpc_request(const std::string& method, const std::shared_ptr<serializable_struct>& params, bool json_rpc = true);
|
||||||
|
|
||||||
|
bool is_json_rpc() const { return m_id != boost::none && m_version != boost::none; }
|
||||||
|
|
||||||
|
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models a response from a RPC server.
|
||||||
|
*/
|
||||||
|
struct monero_rpc_response {
|
||||||
|
boost::optional<std::string> m_jsonrpc;
|
||||||
|
boost::optional<boost::property_tree::ptree> m_result;
|
||||||
|
boost::optional<boost::property_tree::ptree> m_response;
|
||||||
|
boost::optional<std::string> m_binary;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains a connection and sends requests to a Monero RPC API.
|
||||||
|
*/
|
||||||
|
class monero_rpc_connection : public serializable_struct {
|
||||||
|
public:
|
||||||
|
boost::optional<std::string> m_uri;
|
||||||
|
boost::optional<std::string> m_username;
|
||||||
|
boost::optional<std::string> m_password;
|
||||||
|
boost::optional<std::string> m_proxy_uri;
|
||||||
|
boost::optional<std::string> m_zmq_uri; // TODO: implement zmq listener
|
||||||
|
boost::optional<uint32_t> m_timeout_ms; // RPC request timeout in milliseconds
|
||||||
|
boost::optional<uint64_t> m_response_time; // automatically set by calling check_connection()
|
||||||
|
int m_priority; // priority relative to other connections where 1 is highest, then 2, etc, and 0 is lowest (default)
|
||||||
|
|
||||||
|
static std::shared_ptr<monero_rpc_connection> from_property_tree(const boost::property_tree::ptree& node);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks RPC connection priority order.
|
||||||
|
*
|
||||||
|
* @param c1 first priority to compare
|
||||||
|
* @param c2 second priority to compare
|
||||||
|
*/
|
||||||
|
static bool compare(int p1, int p2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new RPC connection.
|
||||||
|
*
|
||||||
|
* @param uri RPC connection uri
|
||||||
|
* @param username RPC connection authentication username
|
||||||
|
* @param password RPC connection authentication password
|
||||||
|
* @param proxy_uri RPC connection proxy uri
|
||||||
|
* @param zmq_uri RPC connection zmq uri
|
||||||
|
* @param priority RPC connection priority
|
||||||
|
* @param timeout_ms RPC connection timeout in milliseconds
|
||||||
|
*/
|
||||||
|
monero_rpc_connection(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", int priority = 0, const boost::optional<uint32_t>& timeout_ms = boost::none);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a RPC connection.
|
||||||
|
*
|
||||||
|
* @param rpc RPC connection to copy
|
||||||
|
*/
|
||||||
|
monero_rpc_connection(const monero_rpc_connection& rpc);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the connection uri is a TOR server.
|
||||||
|
*
|
||||||
|
* @return true or false to indicate if connection uri is a TOR server
|
||||||
|
*/
|
||||||
|
bool is_onion() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the connection uri is a I2P server.
|
||||||
|
*
|
||||||
|
* @return true or false to indicate if connection uri is a I2P server
|
||||||
|
*/
|
||||||
|
bool is_i2p() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set connection credentials.
|
||||||
|
*
|
||||||
|
* @param username username to use in RPC authentication
|
||||||
|
* @param password password to use in RPC authentication
|
||||||
|
*/
|
||||||
|
void set_credentials(const std::string& username, const std::string& password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set connection attribute.
|
||||||
|
*
|
||||||
|
* @param key is the attribute key
|
||||||
|
* @param val is the attribute value
|
||||||
|
*/
|
||||||
|
void set_attribute(const std::string& key, const std::string& val);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection attribute.
|
||||||
|
*
|
||||||
|
* @param key is the attribute to get the value of
|
||||||
|
* @return key's value if set
|
||||||
|
*/
|
||||||
|
std::string get_attribute(const std::string& key) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the connection is online, which is set automatically by calling check_connection().
|
||||||
|
*
|
||||||
|
* @return true or false to indicate if online, or null if check_connection() has not been called
|
||||||
|
*/
|
||||||
|
boost::optional<bool> is_online() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the connection is authenticated, which is set automatically by calling check_connection().
|
||||||
|
*
|
||||||
|
* @return true if authenticated or no authentication, false if not authenticated, or null if not set
|
||||||
|
*/
|
||||||
|
boost::optional<bool> is_authenticated() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the connection is connected, which is set automatically by calling check_connection().
|
||||||
|
*
|
||||||
|
* @return true or false to indicate if connected, or null if check_connection() has not been called
|
||||||
|
*/
|
||||||
|
boost::optional<bool> is_connected() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the connection and update online, authentication, and response time status.
|
||||||
|
*
|
||||||
|
* @param timeout_ms the maximum response time before considered offline
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
bool check_connection(const boost::optional<uint32_t>& timeout_ms = boost::none);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to the RPC API.
|
||||||
|
*
|
||||||
|
* @param path specifies the method to request
|
||||||
|
* @param params are the request's input parameters
|
||||||
|
* @return the RPC API response as a map
|
||||||
|
*/
|
||||||
|
boost::property_tree::ptree send_json_request(const std::string& path, const std::shared_ptr<serializable_struct>& params = nullptr, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to the RPC API.
|
||||||
|
*
|
||||||
|
* @param request specifies the method to request with parameters
|
||||||
|
* @param timeout request timeout in milliseconds
|
||||||
|
* @return the RPC API response as a map
|
||||||
|
*/
|
||||||
|
monero_rpc_response send_json_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a RPC request to the given path and with the given paramters.
|
||||||
|
*
|
||||||
|
* E.g. "/get_transactions" with params
|
||||||
|
*
|
||||||
|
* @param path is the url path of the request to invoke
|
||||||
|
* @param params are request parameters sent in the body
|
||||||
|
* @return the RPC API response as a map
|
||||||
|
*/
|
||||||
|
boost::property_tree::ptree send_path_request(const std::string& path, const std::shared_ptr<serializable_struct>& params = nullptr, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a RPC request to the given path and with the given paramters.
|
||||||
|
*
|
||||||
|
* @param request specifies the method to request with parameters
|
||||||
|
* @param timeout request timeout in milliseconds
|
||||||
|
* @return the request's deserialized response
|
||||||
|
*/
|
||||||
|
monero_rpc_response send_path_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a binary RPC request.
|
||||||
|
*
|
||||||
|
* @param request specifies the method to request with paramesters
|
||||||
|
* @param timeout request timeout in milliseconds
|
||||||
|
* @return the request's deserialized response
|
||||||
|
*/
|
||||||
|
monero_rpc_response send_binary_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
|
||||||
|
|
||||||
|
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// instance variables
|
||||||
|
mutable boost::recursive_mutex m_mutex;
|
||||||
|
boost::optional<bool> m_is_online;
|
||||||
|
boost::optional<bool> m_is_authenticated;
|
||||||
|
mutable std::unique_ptr<epee::net_utils::http::abstract_http_client> m_http_client;
|
||||||
|
std::unordered_map<std::string, std::string> m_attributes;
|
||||||
|
mutable std::tuple<std::string, std::string, std::string, std::string> m_applied;
|
||||||
|
|
||||||
|
void ensure_configured() const;
|
||||||
|
std::string invoke_post(const boost::string_ref uri, const std::string& body, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
|
||||||
|
void send_rpc_request(const boost::string_ref uri, const monero_rpc_request& request, monero_rpc_response& response, const boost::optional<uint32_t>& timeout_ms = boost::none, bool binary = false) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) woodser
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are
|
||||||
|
* permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||||
|
* conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||||
|
* of conditions and the following disclaimer in the documentation and/or other
|
||||||
|
* materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||||
|
* used to endorse or promote products derived from this software without specific
|
||||||
|
* prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||||
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||||
|
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||||
|
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "monero_daemon.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public library intereface.
|
||||||
|
*/
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,805 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) woodser
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are
|
||||||
|
* permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||||
|
* conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||||
|
* of conditions and the following disclaimer in the documentation and/or other
|
||||||
|
* materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||||
|
* used to endorse or promote products derived from this software without specific
|
||||||
|
* prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||||
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||||
|
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||||
|
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/monero_rpc_connection.h"
|
||||||
|
#include "monero_daemon_model.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public library interface.
|
||||||
|
*/
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives notifications as a daemon is updated.
|
||||||
|
*/
|
||||||
|
class monero_daemon_listener {
|
||||||
|
public:
|
||||||
|
std::shared_ptr<monero_block_header> m_last_header;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new block is added to the chain.
|
||||||
|
*
|
||||||
|
* @param header is the header of the block added to the chain
|
||||||
|
*/
|
||||||
|
virtual void on_block_header(const std::shared_ptr<monero_block_header>& header) {
|
||||||
|
m_last_header = header;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monero daemon interface.
|
||||||
|
*/
|
||||||
|
class monero_daemon {
|
||||||
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual destructor.
|
||||||
|
*/
|
||||||
|
virtual ~monero_daemon() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a listener receive daemon notifications.
|
||||||
|
*
|
||||||
|
* @param listener is the listener to receive daemon notifications
|
||||||
|
*/
|
||||||
|
virtual void add_listener(monero_daemon_listener &listener) {
|
||||||
|
throw std::runtime_error("monero_daemon::add_listener(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a listener to receive daemon notifications.
|
||||||
|
*
|
||||||
|
* @param listener is the listener to unregister
|
||||||
|
*/
|
||||||
|
virtual void remove_listener(monero_daemon_listener &listener) {
|
||||||
|
throw std::runtime_error("monero_daemon::remove_listener(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the listeners registered with the daemon.
|
||||||
|
*/
|
||||||
|
virtual std::set<monero_daemon_listener*> get_listeners() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_listeners(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all listeners registered with the daemon.
|
||||||
|
*/
|
||||||
|
virtual void remove_listeners() {
|
||||||
|
throw std::runtime_error("monero_daemon::remove_listeners(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the daemon's version.
|
||||||
|
*
|
||||||
|
* @return the daemon's version
|
||||||
|
*/
|
||||||
|
virtual monero_version get_version() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_version(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the daemon is trusted or untrusted.
|
||||||
|
*
|
||||||
|
* @return true if the daemon is trusted, false otherwise
|
||||||
|
*/
|
||||||
|
virtual bool is_trusted() {
|
||||||
|
throw std::runtime_error("monero_daemon::is_trusted(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of blocks in the longest chain known to the node.
|
||||||
|
*
|
||||||
|
* @return the number of blocks
|
||||||
|
*/
|
||||||
|
virtual uint64_t get_height() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_height(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a block's hash by its height.
|
||||||
|
*
|
||||||
|
* @param height is the height of the block hash to get
|
||||||
|
* @return tthe block's hash at the given height
|
||||||
|
*/
|
||||||
|
virtual std::string get_block_hash(uint64_t height) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_hash(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a block template for mining a new block.
|
||||||
|
*
|
||||||
|
* @param wallet_address is the address of the wallet to receive miner transactions if block is successfully mined
|
||||||
|
* @param reserve_size is the reserve size (optional)
|
||||||
|
* @return a block template for mining a new block
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block_template> get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size = boost::none) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_template(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last block's header.
|
||||||
|
*
|
||||||
|
* @return the last block's header
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block_header> get_last_block_header() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_last_block_header(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a block header by its hash.
|
||||||
|
*
|
||||||
|
* @param block_hash is the hash of the block to get the header of
|
||||||
|
* @return the block's header
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block_header> get_block_header_by_hash(const std::string& block_hash) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_header_by_hash(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a block header by its height.
|
||||||
|
*
|
||||||
|
* @param height is the height of the block to get the header of
|
||||||
|
* @return the block's header
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block_header> get_block_header_by_height(uint64_t height) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_header_by_height(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get block headers for the given range.
|
||||||
|
*
|
||||||
|
* @param start_height is the start height lower bound inclusive (optional)
|
||||||
|
* @param end_height is the end height upper bound inclusive (optional)
|
||||||
|
* @return block headers in the given range
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_block_header>> get_block_headers_by_range(uint64_t start_height, uint64_t end_height) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_headers_by_range(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a block by hash.
|
||||||
|
*
|
||||||
|
* @param block_hash is the hash of the block to get
|
||||||
|
* @return the block with the given hash
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block> get_block_by_hash(const std::string& block_hash) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_by_hash(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blocks by hash.
|
||||||
|
*
|
||||||
|
* @param block_hashes are array of hashes; first 10 blocks hash goes sequential,
|
||||||
|
* next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64 and so on,
|
||||||
|
* and the last one is always genesis block
|
||||||
|
* @param start_height is the start height to get blocks by hash
|
||||||
|
* @param prune specifies if returned blocks should be pruned (defaults to false) // TODO: test default
|
||||||
|
* @return the retrieved blocks
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_blocks_by_hash(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a block by height.
|
||||||
|
*
|
||||||
|
* @param height is the height of the block to get
|
||||||
|
* @return the block at the given height
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block> get_block_by_height(uint64_t height) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_by_height(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blocks at the given heights.
|
||||||
|
*
|
||||||
|
* @param heights are the heights of the blocks to get
|
||||||
|
* @return blocks at the given heights
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_height(const std::vector<uint64_t>& heights) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_blocks_by_height(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blocks in the given height range.
|
||||||
|
*
|
||||||
|
* @param start_height is the start height lower bound inclusive (optional)
|
||||||
|
* @param end_height is the end height upper bound inclusive (optional)
|
||||||
|
* @return blocks in the given height range
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_blocks_by_range(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blocks in the given height range as chunked requests so that each request is
|
||||||
|
* not too big.
|
||||||
|
*
|
||||||
|
* @param start_height is the start height lower bound inclusive (optional)
|
||||||
|
* @param end_height is the end height upper bound inclusive (optional)
|
||||||
|
* @return blocks in the given height range
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_blocks_by_range_chunked(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get block hashes as a binary request to the daemon.
|
||||||
|
*
|
||||||
|
* @param block_hashes specify block hashes to fetch; first 10 blocks hash goes
|
||||||
|
* sequential, next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64
|
||||||
|
* and so on, and the last one is always genesis block
|
||||||
|
* @param start_height is the starting height of block hashes to return
|
||||||
|
* @return the requested block hashes
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::string> get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_block_hashes(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a transaction by hash.
|
||||||
|
*
|
||||||
|
* @param tx_hash is the hash of the transaction to get
|
||||||
|
* @param prune specifies if the returned tx should be pruned (defaults to false)
|
||||||
|
* @return the transaction with the given hash or null if not found
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_tx> get_tx(const std::string& tx_hash, bool prune = false) {
|
||||||
|
std::vector<std::string> hashes;
|
||||||
|
hashes.push_back(tx_hash);
|
||||||
|
auto txs = get_txs(hashes, prune);
|
||||||
|
std::shared_ptr<monero_tx> tx;
|
||||||
|
|
||||||
|
if (txs.size() > 0) {
|
||||||
|
tx = txs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transactions by hashes.
|
||||||
|
*
|
||||||
|
* @param tx_hashes are hashes of transactions to get
|
||||||
|
* @param prune specifies if the returned txs should be pruned (defaults to false)
|
||||||
|
* @return found transactions with the given hashes
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_tx>> get_txs(const std::vector<std::string>& tx_hashes, bool prune = false) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_txs(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a transaction hex by hash.
|
||||||
|
*
|
||||||
|
* @param tx_hash is the hash of the transaction to get hex from
|
||||||
|
* @param prune specifies if the returned tx hex should be pruned (defaults to false)
|
||||||
|
* @return the tx hex with the given hash
|
||||||
|
*/
|
||||||
|
virtual boost::optional<std::string> get_tx_hex(const std::string& tx_hash, bool prune = false) {
|
||||||
|
std::vector<std::string> hashes;
|
||||||
|
hashes.push_back(tx_hash);
|
||||||
|
auto hexes = get_tx_hexes(hashes, prune);
|
||||||
|
boost::optional<std::string> hex;
|
||||||
|
if (hexes.size() > 0) {
|
||||||
|
hex = hexes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transaction hexes by hashes.
|
||||||
|
*
|
||||||
|
* @param tx_hashes are hashes of transactions to get hexes from
|
||||||
|
* @param prune specifies if the returned tx hexes should be pruned (defaults to false)
|
||||||
|
* @return are the tx hexes
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::string> get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune = false) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_tx_hexes(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total emissions and fees from the genesis block to the current height.
|
||||||
|
*
|
||||||
|
* @param height is the height to start computing the miner sum
|
||||||
|
* @param num_blocks are the number of blocks to include in the sum
|
||||||
|
* @return the sum emission and fees since the geneis block
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_miner_tx_sum> get_miner_tx_sum(uint64_t height, uint64_t num_blocks) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_miner_tx_sum(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mining fee estimates per kB.
|
||||||
|
*
|
||||||
|
* @param grace_blocks TODO
|
||||||
|
* @return mining fee estimates per kB
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_fee_estimate> get_fee_estimate(uint64_t grace_blocks = 0) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_fee_estimate(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a transaction to the daemon's pool.
|
||||||
|
*
|
||||||
|
* @param tx_hex is the raw transaction hex to submit
|
||||||
|
* @param do_not_relay specifies if the tx should be relayed (optional)
|
||||||
|
* @return the submission results
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_submit_tx_result> submit_tx_hex(const std::string& tx_hex, bool do_not_relay = false) {
|
||||||
|
throw std::runtime_error("monero_daemon::submit_tx_hex(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relays a transaction by hash.
|
||||||
|
*
|
||||||
|
* @param tx_hash identifies the transaction to relay
|
||||||
|
*/
|
||||||
|
virtual void relay_tx_by_hash(const std::string& tx_hash) {
|
||||||
|
std::vector<std::string> tx_hashes;
|
||||||
|
tx_hashes.push_back(tx_hash);
|
||||||
|
relay_txs_by_hash(tx_hashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relays transactions by hash.
|
||||||
|
*
|
||||||
|
* @param tx_hashes identify the transactions to relay
|
||||||
|
*/
|
||||||
|
virtual void relay_txs_by_hash(const std::vector<std::string>& tx_hashes) {
|
||||||
|
throw std::runtime_error("monero_daemon::relay_txs_by_hash(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get valid transactions seen by the node but not yet mined into a block, as well
|
||||||
|
* as spent key image information for the tx pool.
|
||||||
|
*
|
||||||
|
* @return transactions in the transaction pool
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_tx>> get_tx_pool() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_tx_pool(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hashes of transactions in the transaction pool.
|
||||||
|
*
|
||||||
|
* @return hashes of transactions in the transaction pool
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::string> get_tx_pool_hashes() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_tx_pool_hashes(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all transaction pool backlog.
|
||||||
|
*
|
||||||
|
* @return transaction pool backlog entries
|
||||||
|
*/
|
||||||
|
virtual std::vector<monero_tx_backlog_entry> get_tx_pool_backlog() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_tx_pool_backlog(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transaction pool statistics.
|
||||||
|
*
|
||||||
|
* @return statistics about the transaction pool
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_tx_pool_stats> get_tx_pool_stats() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_tx_pool_stats(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes all transactions from the tx pool.
|
||||||
|
*/
|
||||||
|
virtual void flush_tx_pool() {
|
||||||
|
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush transactions from the tx pool.
|
||||||
|
*
|
||||||
|
* @param hashes are hashes of transactions to flush
|
||||||
|
*/
|
||||||
|
virtual void flush_tx_pool(const std::vector<std::string> &hashes) {
|
||||||
|
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush a single transaction from the tx pool.
|
||||||
|
*
|
||||||
|
* @param hash is the hash of transaction to flush
|
||||||
|
*/
|
||||||
|
virtual void flush_tx_pool(const std::string &hash) {
|
||||||
|
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the spent status of the given key image.
|
||||||
|
*
|
||||||
|
* @param key_image is key image hex to get the status of
|
||||||
|
* @return the status of the key image
|
||||||
|
*/
|
||||||
|
virtual monero_key_image_spent_status get_key_image_spent_status(const std::string& key_image) {
|
||||||
|
std::vector<std::string> key_images;
|
||||||
|
key_images.push_back(key_image);
|
||||||
|
auto statuses = get_key_image_spent_statuses(key_images);
|
||||||
|
if (statuses.empty()) throw std::runtime_error("Could not get key image spent status");
|
||||||
|
return statuses[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the spent status of each given key image.
|
||||||
|
*
|
||||||
|
* @param key_image are hex key images to get the statuses of
|
||||||
|
* @return the spent status for each key image
|
||||||
|
*/
|
||||||
|
virtual std::vector<monero_key_image_spent_status> get_key_image_spent_statuses(const std::vector<std::string>& key_images) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_key_image_spent_statuses(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get outputs identified by a list of output amounts and indices as a binary
|
||||||
|
* request.
|
||||||
|
*
|
||||||
|
* @param outputs identify each output by amount and index
|
||||||
|
* @return the identified outputs
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_output>> get_outputs(const std::vector<monero_output>& outputs) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_outputs(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a histogram of output amounts. For all amounts (possibly filtered by
|
||||||
|
* parameters), gives the number of outputs on the chain for that amount.
|
||||||
|
* RingCT outputs counts as 0 amount.
|
||||||
|
*
|
||||||
|
* @param amounts are amounts of outputs to make the histogram with
|
||||||
|
* @param min_count TODO
|
||||||
|
* @param max_count TODO
|
||||||
|
* @param is_unlocked makes a histogram with outputs with the specified lock state
|
||||||
|
* @param recent_cutoff TODO
|
||||||
|
* @return output histogram entries meeting the parameters
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_output_histogram_entry>> get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_output_histogram(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an output distribution.
|
||||||
|
*
|
||||||
|
* @param amounts are amounts of outputs to make the distribution with
|
||||||
|
* @param is_cumulative specifies if the results should be cumulative (defaults to TODO)
|
||||||
|
* @param start_height is the start height lower bound inclusive (optional)
|
||||||
|
* @param end_height is the end height upper bound inclusive (optional)
|
||||||
|
* @return output distribution entries meeting the parameters
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_output_distribution_entry>> get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative = boost::none, const boost::optional<uint64_t>& start_height = boost::none, const boost::optional<uint64_t>& end_height = boost::none) {
|
||||||
|
throw std::runtime_error("monero_daemon::get_output_distribution(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get general information about the state of the node and the network.
|
||||||
|
*
|
||||||
|
* @return general information about the node and network
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_daemon_info> get_info() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_info(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get synchronization information.
|
||||||
|
*
|
||||||
|
* @return contains sync information
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_daemon_sync_info> get_sync_info() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_sync_info(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up information regarding hard fork voting and readiness.
|
||||||
|
*
|
||||||
|
* @return hard fork information
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_hard_fork_info> get_hard_fork_info() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_hard_fork_info(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alternative chains seen by the node.
|
||||||
|
*
|
||||||
|
* @return alternative chains seen by the node
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_alt_chain>> get_alt_chains() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_alt_chains(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get known block hashes which are not on the main chain.
|
||||||
|
*
|
||||||
|
* @return known block hashes which are not on the main chain
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::string> get_alt_block_hashes() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_alt_block_hashes(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the download bandwidth limit.
|
||||||
|
*
|
||||||
|
* @return is the download bandwidth limit
|
||||||
|
*/
|
||||||
|
virtual int get_download_limit() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_download_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the download bandwidth limit.
|
||||||
|
*
|
||||||
|
* @param limit is the download limit to set (-1 to reset to default)
|
||||||
|
* @return int is the new download limit after setting
|
||||||
|
*/
|
||||||
|
virtual int set_download_limit(int limit) {
|
||||||
|
throw std::runtime_error("monero_daemon::set_download_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the download bandwidth limit.
|
||||||
|
*
|
||||||
|
* @return the download bandwidth limit after resetting
|
||||||
|
*/
|
||||||
|
virtual int reset_download_limit() {
|
||||||
|
throw std::runtime_error("monero_daemon::reset_download_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the upload bandwidth limit.
|
||||||
|
*
|
||||||
|
* @return is the upload bandwidth limit
|
||||||
|
*/
|
||||||
|
virtual int get_upload_limit() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_upload_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the upload bandwidth limit.
|
||||||
|
*
|
||||||
|
* @param limit is the upload limit to set (-1 to reset to default)
|
||||||
|
* @return int is the new upload limit after setting
|
||||||
|
*/
|
||||||
|
virtual int set_upload_limit(int limit) {
|
||||||
|
throw std::runtime_error("monero_daemon::set_upload_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the upload bandwidth limit.
|
||||||
|
*
|
||||||
|
* @return the upload bandwidth limit after resetting
|
||||||
|
*/
|
||||||
|
virtual int reset_upload_limit() {
|
||||||
|
throw std::runtime_error("monero_daemon::reset_upload_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get peers with active incoming or outgoing connections to the node.
|
||||||
|
*
|
||||||
|
* @return the daemon's peers
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_peer>> get_peers() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_peers(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all known peers including their last known online status.
|
||||||
|
*
|
||||||
|
* @return the daemon's known peers
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_peer>> get_known_peers() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_known_peers(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit number of outgoing peers.
|
||||||
|
*
|
||||||
|
* @param limit is the maximum number of outgoing peers
|
||||||
|
*/
|
||||||
|
virtual void set_outgoing_peer_limit(int limit) {
|
||||||
|
throw std::runtime_error("monero_daemon::set_outgoing_peer_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit number of incoming peers.
|
||||||
|
*
|
||||||
|
* @param limit is the maximum number of incoming peers
|
||||||
|
*/
|
||||||
|
virtual void set_incoming_peer_limit(int limit) {
|
||||||
|
throw std::runtime_error("monero_daemon::set_incoming_peer_limit(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get peer bans.
|
||||||
|
*
|
||||||
|
* @return entries about banned peers
|
||||||
|
*/
|
||||||
|
virtual std::vector<std::shared_ptr<monero_ban>> get_peer_bans() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_peer_bans(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ban peers nodes.
|
||||||
|
*
|
||||||
|
* @param bans are bans to apply against peer nodes
|
||||||
|
*/
|
||||||
|
virtual void set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) {
|
||||||
|
throw std::runtime_error("monero_daemon::set_peer_bans(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ban a peer node.
|
||||||
|
*
|
||||||
|
* @param ban contains information about a node to ban
|
||||||
|
*/
|
||||||
|
virtual void set_peer_ban(const std::shared_ptr<monero_ban>& ban) {
|
||||||
|
if (ban == nullptr) throw std::runtime_error("Ban is none");
|
||||||
|
std::vector<std::shared_ptr<monero_ban>> bans;
|
||||||
|
bans.push_back(ban);
|
||||||
|
set_peer_bans(bans);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start mining.
|
||||||
|
*
|
||||||
|
* @param address is the address given miner rewards if the daemon mines a block
|
||||||
|
* @param num_threads is the number of mining threads to run
|
||||||
|
* @param is_background specifies if the miner should run in the background or not
|
||||||
|
* @param ignore_battery specifies if the battery state (e.g. on laptop) should be ignored or not
|
||||||
|
*/
|
||||||
|
virtual void start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> is_background, boost::optional<bool> ignore_battery) {
|
||||||
|
throw std::runtime_error("monero_daemon::start_mining(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop mining.
|
||||||
|
*/
|
||||||
|
virtual void stop_mining() {
|
||||||
|
throw std::runtime_error("monero_daemon::stop_mining(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the daemon's mining status.
|
||||||
|
*
|
||||||
|
* @return the daemon's mining status
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_mining_status> get_mining_status() {
|
||||||
|
throw std::runtime_error("monero_daemon::get_mining_status(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate blocks to a wallet address (regtest only).
|
||||||
|
*
|
||||||
|
* @param wallet_address is the address of the wallet to receive miner transactions if block is successfully mined
|
||||||
|
* @param num_blocks is the number of blocks to generate
|
||||||
|
* @param prev_block_hash is the hash of the previous block to build on top of (optional, builds on the current tip if not given)
|
||||||
|
* @param starting_nonce is the starting nonce to use (optional)
|
||||||
|
* @return the result of generating blocks; height is the height of the last block generated
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_generate_blocks_result> generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash = boost::none, const boost::optional<uint32_t>& starting_nonce = boost::none) {
|
||||||
|
throw std::runtime_error("monero_daemon::generate_blocks(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a mined block to the network.
|
||||||
|
*
|
||||||
|
* @param block_blob is the mined block to submit
|
||||||
|
*/
|
||||||
|
virtual void submit_block(const std::string& block_blob) {
|
||||||
|
std::vector<std::string> block_blobs;
|
||||||
|
block_blobs.push_back(block_blob);
|
||||||
|
return submit_blocks(block_blobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit mined blocks to the network.
|
||||||
|
*
|
||||||
|
* @param block_blobs are the mined blocks to submit
|
||||||
|
*/
|
||||||
|
virtual void submit_blocks(const std::vector<std::string>& block_blobs) {
|
||||||
|
throw std::runtime_error("monero_daemon::submit_blocks(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune the blockchain.
|
||||||
|
*
|
||||||
|
* @param check specifies to check the pruning (default false)
|
||||||
|
* @return the prune result
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_prune_result> prune_blockchain(bool check) {
|
||||||
|
throw std::runtime_error("monero_daemon::prune_blockchain(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for update.
|
||||||
|
*
|
||||||
|
* @return the result of the update check
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_daemon_update_check_result> check_for_update() {
|
||||||
|
throw std::runtime_error("monero_daemon::check_for_update(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download an update.
|
||||||
|
*
|
||||||
|
* @param path is the path to download the update (optional)
|
||||||
|
* @return the result of the update download
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_daemon_update_download_result> download_update(const std::string& path = "") {
|
||||||
|
throw std::runtime_error("monero_daemon::download_update(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely disconnect and shut down the daemon.
|
||||||
|
*/
|
||||||
|
virtual void stop() {
|
||||||
|
throw std::runtime_error("monero_daemon::stop(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the header of the next block added to the chain.
|
||||||
|
*
|
||||||
|
* @return the header of the next block added to the chain
|
||||||
|
*/
|
||||||
|
virtual std::shared_ptr<monero_block_header> wait_for_next_block_header() {
|
||||||
|
throw std::runtime_error("monero_daemon::wait_for_next_block_header(): not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,290 @@
|
|||||||
|
import Foundation
|
||||||
|
import os
|
||||||
|
|
||||||
|
/// On-device verification against vectors generated from the reference
|
||||||
|
/// implementation (xrpl.js + ripple-keypairs + ripple-binary-codec). Runs only
|
||||||
|
/// when the app is launched with `-koinsSelfTest`.
|
||||||
|
enum SelfTest {
|
||||||
|
private static let log = OSLog(subsystem: "com.koins.app", category: "selftest")
|
||||||
|
|
||||||
|
/// Writes to both stdout and the unified log so results are visible however
|
||||||
|
/// the app was launched.
|
||||||
|
private static func emit(_ line: String) {
|
||||||
|
print(line)
|
||||||
|
os_log("%{public}@", log: log, type: .default, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func runIfRequested() {
|
||||||
|
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTest") {
|
||||||
|
print("KOINS_SELFTEST begin")
|
||||||
|
run()
|
||||||
|
print("KOINS_SELFTEST end")
|
||||||
|
}
|
||||||
|
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTestNetwork") {
|
||||||
|
runNetwork()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End-to-end check of the live network paths (XRP RPC + native Monero
|
||||||
|
/// wallet). Triggered with `-koinsSelfTestNetwork`; cleans up after itself.
|
||||||
|
static func runNetwork() {
|
||||||
|
// Detached so the test does not inherit the main actor: the UI thread must
|
||||||
|
// stay free, otherwise the launch-time call would deadlock.
|
||||||
|
Task.detached(priority: .userInitiated) {
|
||||||
|
emit("KOINS_NET begin")
|
||||||
|
|
||||||
|
// The test creates temporary wallets, so snapshot the real ones and put
|
||||||
|
// them back afterwards instead of wiping them.
|
||||||
|
let backup = (xrp: WalletStore.wallet(.xrp),
|
||||||
|
xmr: WalletStore.wallet(.xmr),
|
||||||
|
xrpSecret: WalletStore.secret(.xrp),
|
||||||
|
xmrSecret: WalletStore.secret(.xmr))
|
||||||
|
|
||||||
|
func restoreWallets() {
|
||||||
|
if let xrp = backup.xrp { WalletStore.saveWallet(xrp, chain: .xrp) } else { WalletStore.removeWallet(.xrp) }
|
||||||
|
if let secret = backup.xrpSecret { WalletStore.saveSecret(secret, chain: .xrp) }
|
||||||
|
if let xmr = backup.xmr { WalletStore.saveWallet(xmr, chain: .xmr) } else { WalletStore.removeWallet(.xmr) }
|
||||||
|
if let secret = backup.xmrSecret { WalletStore.saveSecret(secret, chain: .xmr) }
|
||||||
|
emit("KOINS_NET restored saved wallets")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let (record, phrase) = try XRPWalletService.shared.createWallet(name: "koins-net-selftest")
|
||||||
|
emit("KOINS_NET xrp address \(record.address) phraseWords \(phrase.split(separator: " ").count)")
|
||||||
|
let balance = try await XRPWalletService.shared.balance(address: record.address)
|
||||||
|
emit("KOINS_NET xrp funded=\(balance.funded) balance=\(balance.total)")
|
||||||
|
let history = try await XRPWalletService.shared.transactions(address: record.address)
|
||||||
|
emit("KOINS_NET xrp transactions=\(history.count)")
|
||||||
|
|
||||||
|
// A known funded account, to exercise the funded-balance path.
|
||||||
|
for reference in ["rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C"] {
|
||||||
|
let reference = try await XRPWalletService.shared.balance(address: reference)
|
||||||
|
emit("KOINS_NET xrp reference funded=\(reference.funded) balance=\(reference.total)")
|
||||||
|
let referenceHistory = try await XRPWalletService.shared.transactions(address: "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C")
|
||||||
|
emit("KOINS_NET xrp reference transactions=\(referenceHistory.count)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
emit("KOINS_NET xrp FAIL \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let created = try await MoneroService.shared.createWallet(name: "koins-net-selftest")
|
||||||
|
let words = created.mnemonic.split(separator: " ").count
|
||||||
|
emit("KOINS_NET xmr address \(created.address) mnemonicWords \(words)")
|
||||||
|
let started = try await MoneroService.shared.startSync()
|
||||||
|
emit("KOINS_NET xmr startedSync \(started)")
|
||||||
|
for attempt in 1...6 {
|
||||||
|
try? await Task.sleep(nanoseconds: 5_000_000_000)
|
||||||
|
let status = try await MoneroService.shared.syncStatus()
|
||||||
|
emit("KOINS_NET xmr status \(attempt) height=\(status.height) daemon=\(status.daemonHeight) connected=\(status.connected) syncing=\(status.syncing)")
|
||||||
|
}
|
||||||
|
let balance = try await MoneroService.shared.balance()
|
||||||
|
emit("KOINS_NET xmr balance \(balance.total) unlocked \(balance.unlocked)")
|
||||||
|
await MoneroService.shared.stopSync()
|
||||||
|
await MoneroService.shared.removeWallet(name: "koins-net-selftest")
|
||||||
|
} catch {
|
||||||
|
emit("KOINS_NET xmr FAIL \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreWallets()
|
||||||
|
emit("KOINS_NET end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func run() {
|
||||||
|
var failures = 0
|
||||||
|
|
||||||
|
func expect(_ name: String, _ actual: String, _ wanted: String) {
|
||||||
|
let ok = actual.lowercased() == wanted.lowercased()
|
||||||
|
if !ok { failures += 1 }
|
||||||
|
let line = "KOINS_SELFTEST \(name): \(ok ? "PASS" : "FAIL") \(actual)"
|
||||||
|
print(line)
|
||||||
|
os_log("%{public}@", log: log, type: .default, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect("ripemd160", Hex.encode(RIPEMD160.hash(Array("abc".utf8))),
|
||||||
|
"8eb208f7e05d987a9b044a8e98c6b087f15a0bfc")
|
||||||
|
|
||||||
|
// ed25519 vector
|
||||||
|
let edSeed = "sEdSPx8CLbFUna3DJdvJ74aQPLdemqu"
|
||||||
|
let edAddress = "rEhh6f9rj5UUBhFzGGaxS5zYU2CCqKFXBC"
|
||||||
|
let edPublicKey = "EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F1734"
|
||||||
|
if let keypair = try? XRPKey.keypair(fromSeed: edSeed) {
|
||||||
|
expect("ed25519 pubkey", Hex.encode(keypair.publicKey), edPublicKey)
|
||||||
|
expect("ed25519 address", keypair.address, edAddress)
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
print("KOINS_SELFTEST ed25519: FAIL keypair threw")
|
||||||
|
}
|
||||||
|
|
||||||
|
// secp256k1 vector: exercises the OpenSSL-backed derivation on device.
|
||||||
|
let secpSeed = "spFbtYoPd7Hg7AD9XZn7CBwUKC5Qe"
|
||||||
|
let secpPublicKey = "0322DC9BB14903F71186BC46B0107D55A069D23CD97BBD205491A992EC68FA0ABE"
|
||||||
|
let secpAddress = "rpsRYc8DbXzfVN32w3hZjUtyuF1K89hu47"
|
||||||
|
do {
|
||||||
|
let keypair = try XRPKey.keypair(fromSeed: secpSeed)
|
||||||
|
expect("secp256k1 algorithm", keypair.algorithm.rawValue, "secp256k1")
|
||||||
|
expect("secp256k1 pubkey", Hex.encode(keypair.publicKey), secpPublicKey)
|
||||||
|
expect("secp256k1 address", keypair.address, secpAddress)
|
||||||
|
} catch {
|
||||||
|
failures += 1
|
||||||
|
print("KOINS_SELFTEST secp256k1: FAIL \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialization + hashing reference vector.
|
||||||
|
let signingHex = "53545800120000220000000024000030392E00000309201B05E69F3B61400000000016E36068400000000000000C7321EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F173481149A26172134BFDA9708A73305AF2776A26DB7B4F5831492891F44F454D156327AA24F4C8BB72540E31BF1"
|
||||||
|
let referenceSignature = "F560672EA3103D2E43179302CD66ACF563A4EDFBA90ADE24E378B743135FD989EE132FA4B703021C2DD1979E770A6CEBC99556EA330AF9E6E7186673F837CB0D"
|
||||||
|
let expectedBlob = "120000220000000024000030392E00000309201B05E69F3B61400000000016E36068400000000000000C7321EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F17347440F560672EA3103D2E43179302CD66ACF563A4EDFBA90ADE24E378B743135FD989EE132FA4B703021C2DD1979E770A6CEBC99556EA330AF9E6E7186673F837CB0D81149A26172134BFDA9708A73305AF2776A26DB7B4F5831492891F44F454D156327AA24F4C8BB72540E31BF1"
|
||||||
|
let expectedHash = "F853B5391CD6317EA370830EEA3C047AC8D21E9FA00E4116D94C48E360B0434F"
|
||||||
|
|
||||||
|
var payment = XRPPayment(account: edAddress,
|
||||||
|
destination: "rNMovRR3WPbFLVaSbETCCR71XsqyxhJ9P6",
|
||||||
|
amountDrops: 1_500_000,
|
||||||
|
feeDrops: 12,
|
||||||
|
sequence: 12345,
|
||||||
|
lastLedgerSequence: 99000123,
|
||||||
|
destinationTag: 777,
|
||||||
|
signingPublicKey: Hex.decode(edPublicKey) ?? [],
|
||||||
|
signature: Hex.decode(referenceSignature))
|
||||||
|
if let payload = try? payment.signingPayload() {
|
||||||
|
expect("signing payload", Hex.encode(payload), signingHex)
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
}
|
||||||
|
if let blob = try? payment.txBlob() {
|
||||||
|
expect("tx blob", Hex.encode(blob), expectedBlob)
|
||||||
|
if let hash = try? payment.transactionHash() {
|
||||||
|
expect("tx hash", Hex.encode(hash), expectedHash)
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIP-39 / BIP-32 (m/44'/144'/0'/0/0): the recovery phrase used for new XRP
|
||||||
|
// wallets. Vectors come from @scure/bip39 + @scure/bip32 + ripple-keypairs.
|
||||||
|
let zeroMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
expect("bip39 encode", BIP39.mnemonic(from: [UInt8](repeating: 0, count: 16)), zeroMnemonic)
|
||||||
|
expect("bip39 entropy", Hex.encode(BIP39.entropy(from: zeroMnemonic) ?? []), String(repeating: "00", count: 16))
|
||||||
|
expect("bip39 rejects bad checksum",
|
||||||
|
BIP39.entropy(from: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon") == nil ? "rejected" : "accepted",
|
||||||
|
"rejected")
|
||||||
|
|
||||||
|
// BIP-32 intermediate values, to isolate derivation problems.
|
||||||
|
if let master = BIP32.masterKey(fromSeed: BIP39.seed(from: zeroMnemonic)) {
|
||||||
|
expect("bip32 master key", Hex.encode(master.privateKey),
|
||||||
|
"1837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf67")
|
||||||
|
expect("bip32 master chain code", Hex.encode(master.chainCode),
|
||||||
|
"7923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e")
|
||||||
|
let H = BIP32.hardenedBit
|
||||||
|
for (level, path, expected) in [
|
||||||
|
(1, [H | 44], "cd46049fb82a4fccbd967c5234e61152f4df87d01d1ef833420814c67ee4b7ea"),
|
||||||
|
(2, [H | 44, H | 144], "a12250f1f38fb41b62f0f1a4ec9d3555d24788843cb30e6d98a777fe540adc1d"),
|
||||||
|
(3, [H | 44, H | 144, H | 0], "54bac451906ecd52648a49126a2606c9177761f14d31dda079c0f768ac3501fd"),
|
||||||
|
(4, [H | 44, H | 144, H | 0, 0], "e582ce1724e93eebe3f38f1d42c01620b66d136946fec450dfe71b62e65de54f"),
|
||||||
|
(5, BIP32.xrpPath, "90802a50aa84efb6cdb225f17c27616ea94048c179142fecf03f4712a07ea7a4"),
|
||||||
|
] {
|
||||||
|
if let child = BIP32.derive(path, from: master) {
|
||||||
|
expect("bip32 level \(level)", Hex.encode(child.privateKey), expected)
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
print("KOINS_SELFTEST bip32 level \(level): FAIL derivation returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
print("KOINS_SELFTEST bip32 master: FAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mnemonicCases: [(String, String, String)] = [
|
||||||
|
(zeroMnemonic,
|
||||||
|
"031d68bc1a142e6766b2bdfb006ccfe135ef2e0e2e94abb5cf5c9ab6104776fbae",
|
||||||
|
"rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"),
|
||||||
|
("legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||||
|
"0283e0926eb0ab7bb8f5c0b5637d9e2526bdc398763af79cd904f013178961692d",
|
||||||
|
"rpaLPza5oxhJ7V8nFDwPm9F7kmZ16uc8do"),
|
||||||
|
]
|
||||||
|
for (position, vector) in mnemonicCases.enumerated() {
|
||||||
|
do {
|
||||||
|
let keypair = try XRPKey.keypair(fromMnemonic: vector.0)
|
||||||
|
expect("bip44 pubkey #\(position)", Hex.encode(keypair.publicKey), vector.1)
|
||||||
|
expect("bip44 address #\(position)", keypair.address, vector.2)
|
||||||
|
} catch {
|
||||||
|
failures += 1
|
||||||
|
print("KOINS_SELFTEST bip44 #\(position): FAIL \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// secp256k1 signing must produce a low-S DER signature.
|
||||||
|
if let keypair = try? XRPKey.keypair(fromSeed: secpSeed),
|
||||||
|
let message = Hex.decode(signingHex),
|
||||||
|
let signature = try? XRPKey.sign(message: message, keypair: keypair) {
|
||||||
|
let isDer = signature.count > 8 && signature[0] == 0x30
|
||||||
|
expect("secp256k1 signature is DER", isDer ? "yes" : "no", "yes")
|
||||||
|
if let order = BigNumber.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
|
||||||
|
let half = order.dividedByTwo(),
|
||||||
|
let s = DER.signatureScalar(signature) {
|
||||||
|
expect("secp256k1 signature is low-S", s.lessThanOrEqual(half) ? "yes" : "no", "yes")
|
||||||
|
}
|
||||||
|
print("KOINS_SELFTEST secp256k1 signature len: \(signature.count)")
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
print("KOINS_SELFTEST secp256k1 sign: FAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary = "KOINS_SELFTEST summary: \(failures == 0 ? "ALL PASS" : "\(failures) FAILURE(S)")"
|
||||||
|
print(summary)
|
||||||
|
os_log("%{public}@", log: log, type: .default, summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal big-endian integer helper used only for the low-S check.
|
||||||
|
struct BigNumber {
|
||||||
|
let bytes: [UInt8]
|
||||||
|
|
||||||
|
static func fromHex(_ hex: String) -> BigNumber? {
|
||||||
|
Hex.decode(hex).map { BigNumber(bytes: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func dividedByTwo() -> BigNumber? {
|
||||||
|
var result: [UInt8] = []
|
||||||
|
var carry: UInt16 = 0
|
||||||
|
for byte in bytes {
|
||||||
|
let value = carry * 256 + UInt16(byte)
|
||||||
|
result.append(UInt8(value / 2))
|
||||||
|
carry = value % 2
|
||||||
|
}
|
||||||
|
return BigNumber(bytes: result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lessThanOrEqual(_ other: BigNumber) -> Bool {
|
||||||
|
let left = trimmed()
|
||||||
|
let right = other.trimmed()
|
||||||
|
if left.count != right.count { return left.count < right.count }
|
||||||
|
for (a, b) in zip(left, right) where a != b { return a < b }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func trimmed() -> [UInt8] {
|
||||||
|
var value = bytes
|
||||||
|
while value.first == 0 && value.count > 1 { value.removeFirst() }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DER {
|
||||||
|
/// Extracts the `s` component from a DER-encoded ECDSA signature.
|
||||||
|
static func signatureScalar(_ der: [UInt8]) -> BigNumber? {
|
||||||
|
guard der.count > 8, der[0] == 0x30 else { return nil }
|
||||||
|
var index = 2
|
||||||
|
guard der[index] == 0x02 else { return nil }
|
||||||
|
let rLength = Int(der[index + 1])
|
||||||
|
index += 2 + rLength
|
||||||
|
guard index + 2 <= der.count, der[index] == 0x02 else { return nil }
|
||||||
|
let sLength = Int(der[index + 1])
|
||||||
|
let start = index + 2
|
||||||
|
guard start + sLength <= der.count else { return nil }
|
||||||
|
return BigNumber(bytes: Array(der[start..<(start + sLength)]))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,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,76 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
/// Small Keychain wrapper for wallet secrets. Secrets never touch UserDefaults.
|
||||||
|
enum Keychain {
|
||||||
|
private static let service = "com.koins.app"
|
||||||
|
|
||||||
|
static func set(_ value: String, for key: String) {
|
||||||
|
guard let data = value.data(using: .utf8) else { return }
|
||||||
|
remove(key)
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecValueData as String: data,
|
||||||
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||||
|
]
|
||||||
|
SecItemAdd(query as CFDictionary, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func get(_ key: String) -> String? {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecReturnData as String: true,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||||
|
]
|
||||||
|
var item: CFTypeRef?
|
||||||
|
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
|
||||||
|
let data = item as? Data,
|
||||||
|
let value = String(data: data, encoding: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
static func remove(_ key: String) {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
]
|
||||||
|
SecItemDelete(query as CFDictionary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WalletStore {
|
||||||
|
private static let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
private static func walletKey(_ chain: Chain) -> String { "koins.wallet.\(chain.rawValue)" }
|
||||||
|
private static func secretKey(_ chain: Chain) -> String { "koins.secret.\(chain.rawValue)" }
|
||||||
|
|
||||||
|
static func wallet(_ chain: Chain) -> WalletRecord? {
|
||||||
|
guard let data = defaults.data(forKey: walletKey(chain)) else { return nil }
|
||||||
|
return try? JSONDecoder().decode(WalletRecord.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveWallet(_ wallet: WalletRecord, chain: Chain) {
|
||||||
|
guard let data = try? JSONEncoder().encode(wallet) else { return }
|
||||||
|
defaults.set(data, forKey: walletKey(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func removeWallet(_ chain: Chain) {
|
||||||
|
defaults.removeObject(forKey: walletKey(chain))
|
||||||
|
Keychain.remove(secretKey(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func secret(_ chain: Chain) -> String? {
|
||||||
|
Keychain.get(secretKey(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveSecret(_ secret: String, chain: Chain) {
|
||||||
|
Keychain.set(secret, for: secretKey(chain))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum Theme {
|
||||||
|
static let background = Color(red: 0.035, green: 0.035, blue: 0.05)
|
||||||
|
static let surface = Color(red: 0.09, green: 0.09, blue: 0.12)
|
||||||
|
static let surfaceAlt = Color(red: 0.13, green: 0.13, blue: 0.17)
|
||||||
|
static let border = Color.white.opacity(0.08)
|
||||||
|
static let accent = Color(red: 1.0, green: 0.56, blue: 0.16)
|
||||||
|
static let accentSoft = Color(red: 1.0, green: 0.56, blue: 0.16).opacity(0.15)
|
||||||
|
static let text = Color.white
|
||||||
|
static let muted = Color.white.opacity(0.55)
|
||||||
|
static let danger = Color(red: 1.0, green: 0.36, blue: 0.36)
|
||||||
|
static let success = Color(red: 0.35, green: 0.85, blue: 0.55)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrimaryButton: View {
|
||||||
|
var title: String
|
||||||
|
var disabled: Bool = false
|
||||||
|
var action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 15)
|
||||||
|
.background(disabled ? Theme.surfaceAlt : Theme.accent)
|
||||||
|
.foregroundStyle(disabled ? Theme.muted : Color.black)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
}
|
||||||
|
.disabled(disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SecondaryButton: View {
|
||||||
|
var title: String
|
||||||
|
var action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 15, weight: .medium))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 14)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Card<Content: View>: View {
|
||||||
|
@ViewBuilder var content: Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content
|
||||||
|
.padding(18)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 20, style: .continuous).stroke(Theme.border))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FieldLabel: View {
|
||||||
|
var text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KoinsTextField: View {
|
||||||
|
var placeholder: String
|
||||||
|
@Binding var text: String
|
||||||
|
var multiline = false
|
||||||
|
var keyboard: UIKeyboardType = .default
|
||||||
|
var autocapitalization: TextInputAutocapitalization = .never
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if multiline {
|
||||||
|
TextEditor(text: $text)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.frame(minHeight: 96)
|
||||||
|
.padding(8)
|
||||||
|
} else {
|
||||||
|
TextField(placeholder, text: $text)
|
||||||
|
.keyboardType(keyboard)
|
||||||
|
.textInputAutocapitalization(autocapitalization)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 13)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 15))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MessageBanner: View {
|
||||||
|
var message: String
|
||||||
|
var isError: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(message)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundStyle(isError ? Theme.danger : Theme.success)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(12)
|
||||||
|
.background((isError ? Theme.danger : Theme.success).opacity(0.12))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChainPicker: View {
|
||||||
|
@Binding var selection: Chain
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
ForEach(Chain.allCases, id: \.self) { chain in
|
||||||
|
Button {
|
||||||
|
selection = chain
|
||||||
|
} label: {
|
||||||
|
Text(chain.title)
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(selection == chain ? Theme.accent : Color.clear)
|
||||||
|
.foregroundStyle(selection == chain ? Color.black : Theme.muted)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(4)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QRCodeView: View {
|
||||||
|
var text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let image = QRCodeView.makeImage(from: text) {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.interpolation(.none)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.padding(12)
|
||||||
|
.background(Color.white)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
|
.fill(Theme.surfaceAlt)
|
||||||
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeImage(from text: String) -> UIImage? {
|
||||||
|
guard !text.isEmpty else { return nil }
|
||||||
|
let filter = CIFilter.qrCodeGenerator()
|
||||||
|
filter.message = Data(text.utf8)
|
||||||
|
filter.correctionLevel = "M"
|
||||||
|
guard let output = filter.outputImage else { return nil }
|
||||||
|
let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
|
||||||
|
let context = CIContext()
|
||||||
|
guard let cgImage = context.createCGImage(scaled, from: scaled.extent) else { return nil }
|
||||||
|
return UIImage(cgImage: cgImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransactionRow: View {
|
||||||
|
var record: TransactionRecord
|
||||||
|
var symbol: String
|
||||||
|
|
||||||
|
private var formattedDate: String {
|
||||||
|
guard let timestamp = record.timestamp else { return "Pending" }
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
return formatter.string(from: timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(record.direction == .incoming ? Theme.success.opacity(0.15) : Theme.accentSoft)
|
||||||
|
.frame(width: 38, height: 38)
|
||||||
|
Image(systemName: record.direction == .incoming ? "arrow.down.left" : "arrow.up.right")
|
||||||
|
.font(.system(size: 15, weight: .bold))
|
||||||
|
.foregroundStyle(record.direction == .incoming ? Theme.success : Theme.accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text(record.direction == .incoming ? "Received" : "Sent")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text(record.confirmed ? formattedDate : "Pending confirmation")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(alignment: .trailing, spacing: 3) {
|
||||||
|
Text("\(record.direction == .incoming ? "+" : "-")\(record.amount) \(symbol)")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(record.direction == .incoming ? Theme.success : Theme.text)
|
||||||
|
if let fee = record.fee, !fee.isEmpty, record.direction == .outgoing {
|
||||||
|
Text("fee \(fee)")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScreenHeader: View {
|
||||||
|
var title: String
|
||||||
|
var subtitle: String?
|
||||||
|
var onBack: (() -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 12) {
|
||||||
|
if let onBack {
|
||||||
|
Button(action: onBack) {
|
||||||
|
Image(systemName: "chevron.left")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 22, weight: .bold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DashboardView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Binding var route: Route
|
||||||
|
|
||||||
|
@State private var showSeed = false
|
||||||
|
@State private var revealedSeed: String?
|
||||||
|
@State private var seedError: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: model.wallet?.name ?? "Wallet",
|
||||||
|
subtitle: model.chain.title,
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Balance")
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
Text("\(model.balance.total) \(model.symbol)")
|
||||||
|
.font(.system(size: 32, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
|
||||||
|
Text("\(model.balance.unlocked) \(model.symbol) spendable now")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
AddressRow(address: model.displayAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xmr {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
Text("Sync")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Spacer()
|
||||||
|
if let status = model.syncStatus { SyncBadge(status: status) }
|
||||||
|
}
|
||||||
|
if let status = model.syncStatus {
|
||||||
|
SyncProgress(status: status, eta: model.syncEta)
|
||||||
|
} else {
|
||||||
|
Text("Sync status is not available yet.")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
PrimaryButton(title: "Send") { route = .send }
|
||||||
|
SecondaryButton(title: "Receive") { route = .receive }
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.transactions.isEmpty {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Activity")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
ForEach(model.transactions) { record in
|
||||||
|
TransactionRow(record: record, symbol: model.symbol)
|
||||||
|
if record.id != model.transactions.last?.id {
|
||||||
|
Divider().overlay(Theme.border)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message = model.errorMessage {
|
||||||
|
MessageBanner(message: message, isError: true)
|
||||||
|
}
|
||||||
|
if let message = model.statusMessage {
|
||||||
|
MessageBanner(message: message, isError: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
SecondaryButton(title: "Reveal recovery phrase") {
|
||||||
|
Task {
|
||||||
|
seedError = nil
|
||||||
|
do {
|
||||||
|
revealedSeed = try await model.revealSeed()
|
||||||
|
showSeed = true
|
||||||
|
} catch {
|
||||||
|
seedError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let seedError {
|
||||||
|
MessageBanner(message: seedError, isError: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showSeed) {
|
||||||
|
if let revealedSeed {
|
||||||
|
SeedRevealView(seed: revealedSeed, symbol: model.symbol) { showSeed = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SendView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Binding var route: Route
|
||||||
|
|
||||||
|
@State private var destination = ""
|
||||||
|
@State private var amount = ""
|
||||||
|
@State private var tag = ""
|
||||||
|
@State private var priority: TransactionPriority = .defaultPriority
|
||||||
|
@State private var localError: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: "Send \(model.symbol)",
|
||||||
|
subtitle: "Balance \(model.balance.total) \(model.symbol)",
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: model.chain == .xrp ? "Destination address" : "Recipient address")
|
||||||
|
KoinsTextField(placeholder: model.chain == .xrp ? "r…" : "4…", text: $destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: "Amount (\(model.symbol))")
|
||||||
|
KoinsTextField(placeholder: "0.0", text: $amount, keyboard: .decimalPad)
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xrp {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: "Destination tag (optional)")
|
||||||
|
KoinsTextField(placeholder: "e.g. 12345", text: $tag, keyboard: .numberPad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xmr {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
FieldLabel(text: "Priority")
|
||||||
|
Picker("Priority", selection: $priority) {
|
||||||
|
ForEach([TransactionPriority.low, .medium, .high, .defaultPriority], id: \.rawValue) { item in
|
||||||
|
Text(item.label).tag(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message = localError ?? model.errorMessage {
|
||||||
|
MessageBanner(message: message, isError: true)
|
||||||
|
}
|
||||||
|
if let message = model.statusMessage {
|
||||||
|
MessageBanner(message: message, isError: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryButton(title: model.isBusy ? "Sending…" : "Send", disabled: model.isBusy) {
|
||||||
|
Task {
|
||||||
|
localError = nil
|
||||||
|
model.clearMessages()
|
||||||
|
do {
|
||||||
|
try await model.send(to: destination, amount: amount, tag: tag, priority: priority)
|
||||||
|
amount = ""
|
||||||
|
tag = ""
|
||||||
|
route = .dashboard
|
||||||
|
} catch {
|
||||||
|
localError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReceiveView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Binding var route: Route
|
||||||
|
|
||||||
|
private var address: String {
|
||||||
|
model.displayAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: "Receive \(model.symbol)",
|
||||||
|
subtitle: "Share this address to get paid",
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
Card {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
QRCodeView(text: address)
|
||||||
|
.frame(maxWidth: 240)
|
||||||
|
Text(address)
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineLimit(3)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
SecondaryButton(title: "Copy address") {
|
||||||
|
UIPasteboard.general.string = address
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xrp {
|
||||||
|
Card {
|
||||||
|
Text("XRP accounts must hold at least 1 XRP as a reserve to be active. Only send XRP on the XRP Ledger to this address.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Card {
|
||||||
|
Text("Send only Monero (XMR) to this address. Transfers need confirmations before the balance becomes spendable.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum Route: Equatable {
|
||||||
|
case home
|
||||||
|
case create
|
||||||
|
case importWallet
|
||||||
|
case dashboard
|
||||||
|
case send
|
||||||
|
case receive
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SeedBox: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let seed: String
|
||||||
|
let symbol: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RootView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@State private var route: Route = .home
|
||||||
|
@State private var seedBox: SeedBox?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Theme.background.ignoresSafeArea()
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 18) {
|
||||||
|
switch route {
|
||||||
|
case .home:
|
||||||
|
HomeView(route: $route)
|
||||||
|
case .create:
|
||||||
|
CreateWalletView(route: $route, seedBox: $seedBox)
|
||||||
|
case .importWallet:
|
||||||
|
ImportWalletView(route: $route)
|
||||||
|
case .dashboard:
|
||||||
|
DashboardView(route: $route)
|
||||||
|
case .send:
|
||||||
|
SendView(route: $route)
|
||||||
|
case .receive:
|
||||||
|
ReceiveView(route: $route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.bottom, 40)
|
||||||
|
}
|
||||||
|
.scrollDismissesKeyboard(.interactively)
|
||||||
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.task { await model.bootstrap() }
|
||||||
|
.sheet(item: $seedBox) { box in
|
||||||
|
SeedRevealView(seed: box.seed, symbol: box.symbol) { seedBox = nil }
|
||||||
|
}
|
||||||
|
.onChange(of: model.chain) { _ in
|
||||||
|
if route == .dashboard { route = .home }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Binding var route: Route
|
||||||
|
@State private var showDeleteConfirm = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 18) {
|
||||||
|
header
|
||||||
|
|
||||||
|
if model.hasWallet, model.chain == .xrp || model.moneroReady {
|
||||||
|
walletCard
|
||||||
|
} else if model.hasWallet {
|
||||||
|
moneroUnavailableCard
|
||||||
|
} else {
|
||||||
|
emptyState
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message = model.errorMessage {
|
||||||
|
MessageBanner(message: message, isError: true)
|
||||||
|
}
|
||||||
|
if let message = model.statusMessage {
|
||||||
|
MessageBanner(message: message, isError: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirmationDialog("Delete this wallet?",
|
||||||
|
isPresented: $showDeleteConfirm,
|
||||||
|
titleVisibility: .visible) {
|
||||||
|
Button("Delete wallet", role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await model.deleteWallet()
|
||||||
|
route = .home
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text("The keys stored on this device are removed. Make sure your recovery seed is backed up.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Koins")
|
||||||
|
.font(.system(size: 30, weight: .heavy))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("Monero and XRP wallet")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var walletCard: some View {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
ChainPicker(selection: Binding(get: { model.chain },
|
||||||
|
set: { newValue in Task { await model.select(newValue) } }))
|
||||||
|
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
Text(model.wallet?.name ?? "Wallet")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Spacer()
|
||||||
|
if model.chain == .xmr, let status = model.syncStatus {
|
||||||
|
SyncBadge(status: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Balance")
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
Text("\(model.balance.total) \(model.symbol)")
|
||||||
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
|
||||||
|
Text("\(model.balance.unlocked) \(model.symbol) unlocked")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddressRow(address: model.displayAddress)
|
||||||
|
|
||||||
|
if model.chain == .xmr, let status = model.syncStatus {
|
||||||
|
SyncProgress(status: status, eta: model.syncEta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
PrimaryButton(title: "Send") { route = .send }
|
||||||
|
SecondaryButton(title: "Receive") { route = .receive }
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xmr {
|
||||||
|
SecondaryButton(title: model.syncStatus?.syncing == true ? "Syncing…" : "Sync now") {
|
||||||
|
Task { await model.startMoneroSync() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
SecondaryButton(title: "Refresh") { Task { await model.refresh(force: true) } }
|
||||||
|
SecondaryButton(title: "Delete") { showDeleteConfirm = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.transactions.isEmpty {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Activity")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
ForEach(model.transactions) { record in
|
||||||
|
TransactionRow(record: record, symbol: model.symbol)
|
||||||
|
if record.id != model.transactions.last?.id {
|
||||||
|
Divider().overlay(Theme.border)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if model.chain == .xrp, model.balance.funded == false {
|
||||||
|
Text("This account is not funded yet. Send XRP to the address above to activate it.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xrp, model.moneroReady, let status = model.syncStatus, status.syncing {
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(status.connected ? Theme.success : Theme.danger)
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
Text("Monero is syncing in the background")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(status.progress * 100))%")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
SyncProgress(status: status, eta: model.syncEta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var moneroUnavailableCard: some View {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
ChainPicker(selection: Binding(get: { model.chain },
|
||||||
|
set: { newValue in Task { await model.select(newValue) } }))
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Monero wallet")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("The wallet is on this device but could not be opened. It is safe to retry; nothing was deleted.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
SecondaryButton(title: "Retry") {
|
||||||
|
Task { await model.select(.xmr) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyState: some View {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
ChainPicker(selection: Binding(get: { model.chain },
|
||||||
|
set: { newValue in Task { await model.select(newValue) } }))
|
||||||
|
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("\(model.chain.title) wallet")
|
||||||
|
.font(.system(size: 17, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("Create a new wallet on this device, or import one with its recovery seed.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
PrimaryButton(title: "Create wallet") { route = .create }
|
||||||
|
SecondaryButton(title: "Import wallet") { route = .importWallet }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SyncBadge: View {
|
||||||
|
var status: SyncStatus
|
||||||
|
|
||||||
|
private var label: String {
|
||||||
|
if status.syncing {
|
||||||
|
let percent = status.progress * 100
|
||||||
|
return percent < 10 ? String(format: "Syncing %.1f%%", percent) : "Syncing \(Int(percent))%"
|
||||||
|
}
|
||||||
|
return status.connected ? "Synced" : "Offline"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Circle()
|
||||||
|
.fill(status.connected ? Theme.success : Theme.danger)
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
Text(label)
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Live scan progress: how far the wallet has reached, and roughly how long the
|
||||||
|
/// remaining blocks will take at the observed rate.
|
||||||
|
struct SyncProgress: View {
|
||||||
|
var status: SyncStatus
|
||||||
|
var eta: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
ProgressView(value: status.progress)
|
||||||
|
.tint(Theme.accent)
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text("Block \(status.height.formatted()) of \(status.daemonHeight.formatted())")
|
||||||
|
Spacer()
|
||||||
|
if status.syncing {
|
||||||
|
Text(eta.map { "about \($0) left" } ?? "estimating…")
|
||||||
|
} else if status.connected {
|
||||||
|
Text("up to date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AddressRow: View {
|
||||||
|
var address: String
|
||||||
|
@State private var copied = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(address.isEmpty ? "—" : address)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
Spacer(minLength: 6)
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = address
|
||||||
|
copied = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false }
|
||||||
|
} label: {
|
||||||
|
Image(systemName: copied ? "checkmark" : "doc.on.doc")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
}
|
||||||
|
.disabled(address.isEmpty)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(Theme.surfaceAlt)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SeedRevealView: View {
|
||||||
|
var seed: String
|
||||||
|
var symbol: String
|
||||||
|
var onDone: () -> Void
|
||||||
|
|
||||||
|
@State private var acknowledged = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Theme.background.ignoresSafeArea()
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("Your recovery phrase")
|
||||||
|
.font(.system(size: 24, weight: .bold))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
Text("Write these words down in order and keep them offline. Anyone with this phrase can spend your \(symbol).")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
|
||||||
|
Text(seed)
|
||||||
|
.font(.system(size: 15, design: .monospaced))
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.padding(16)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 16, style: .continuous).stroke(Theme.border))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||||
|
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = seed
|
||||||
|
} label: {
|
||||||
|
Label("Copy phrase", systemImage: "doc.on.doc")
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $acknowledged) {
|
||||||
|
Text("I have written down my phrase")
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
.tint(Theme.accent)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
PrimaryButton(title: "Continue", disabled: !acknowledged, action: onDone)
|
||||||
|
}
|
||||||
|
.padding(22)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreateWalletView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Binding var route: Route
|
||||||
|
@Binding var seedBox: SeedBox?
|
||||||
|
|
||||||
|
@State private var name = ""
|
||||||
|
@State private var localError: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: "Create wallet",
|
||||||
|
subtitle: "A new \(model.chain.title) wallet on this device",
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: "Wallet name")
|
||||||
|
KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xmr {
|
||||||
|
Card {
|
||||||
|
Text("The wallet file is created locally and encrypted on this device. Syncing starts right away and can take a while for a new Monero wallet.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message = localError ?? model.errorMessage {
|
||||||
|
MessageBanner(message: message, isError: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryButton(title: model.isBusy ? "Generating…" : "Generate wallet",
|
||||||
|
disabled: model.isBusy) {
|
||||||
|
Task {
|
||||||
|
localError = nil
|
||||||
|
model.clearMessages()
|
||||||
|
do {
|
||||||
|
let seed = try await model.createWallet(name: name)
|
||||||
|
seedBox = SeedBox(seed: seed, symbol: model.symbol)
|
||||||
|
route = .dashboard
|
||||||
|
} catch {
|
||||||
|
localError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImportWalletView: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
|
@Binding var route: Route
|
||||||
|
|
||||||
|
@State private var name = ""
|
||||||
|
@State private var secret = ""
|
||||||
|
@State private var restoreHeight = ""
|
||||||
|
@State private var localError: String?
|
||||||
|
@State private var tipError: String?
|
||||||
|
@State private var isLoadingTip = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
|
ScreenHeader(title: "Import wallet",
|
||||||
|
subtitle: "Restore a \(model.chain.title) wallet from its seed",
|
||||||
|
onBack: { route = .home })
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: "Wallet name")
|
||||||
|
KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: model.chain == .xrp ? "Recovery phrase" : "Mnemonic seed")
|
||||||
|
KoinsTextField(placeholder: model.chain == .xrp ? "12 words, or a family seed (sEd…)" : "25 word seed phrase",
|
||||||
|
text: $secret,
|
||||||
|
multiline: model.chain == .xmr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.chain == .xmr {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
FieldLabel(text: "Restore height (optional)")
|
||||||
|
KoinsTextField(placeholder: "e.g. 3200000", text: $restoreHeight, keyboard: .numberPad)
|
||||||
|
}
|
||||||
|
Card {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Without a height the wallet scans the whole chain — millions of blocks, which takes hours on a phone. Set the block your wallet was first used, or start from the current tip if you only care about new funds.")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundStyle(Theme.muted)
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
tipError = nil
|
||||||
|
isLoadingTip = true
|
||||||
|
defer { isLoadingTip = false }
|
||||||
|
do {
|
||||||
|
let tip = try await MoneroService.shared.daemonHeight()
|
||||||
|
restoreHeight = String(tip)
|
||||||
|
} catch {
|
||||||
|
tipError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text(isLoadingTip ? "Fetching chain tip…" : "Use the current chain tip")
|
||||||
|
.font(.system(size: 13, weight: .medium))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
}
|
||||||
|
.disabled(isLoadingTip)
|
||||||
|
if let tipError {
|
||||||
|
Text(tipError)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(Theme.danger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message = localError ?? model.errorMessage {
|
||||||
|
MessageBanner(message: message, isError: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryButton(title: model.isBusy ? "Importing…" : "Import wallet",
|
||||||
|
disabled: model.isBusy) {
|
||||||
|
Task {
|
||||||
|
localError = nil
|
||||||
|
model.clearMessages()
|
||||||
|
do {
|
||||||
|
let height = UInt64(restoreHeight.trimmingCharacters(in: .whitespaces))
|
||||||
|
try await model.importWallet(name: name, secret: secret, restoreHeight: height)
|
||||||
|
route = .dashboard
|
||||||
|
} catch {
|
||||||
|
localError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Monero amounts are atomic units (1 XMR = 1e12 piconero).
|
||||||
|
enum MoneroAmount {
|
||||||
|
static let atomicPerXMR: UInt64 = 1_000_000_000_000
|
||||||
|
|
||||||
|
static func xmr(fromAtomic value: String) -> String {
|
||||||
|
guard let atomic = UInt64(value) else { return "0" }
|
||||||
|
return xmr(fromAtomic: atomic)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func xmr(fromAtomic value: UInt64) -> String {
|
||||||
|
let whole = value / atomicPerXMR
|
||||||
|
let fraction = value % atomicPerXMR
|
||||||
|
if fraction == 0 { return String(whole) }
|
||||||
|
var text = String(format: "%012llu", fraction)
|
||||||
|
while text.hasSuffix("0") { text.removeLast() }
|
||||||
|
return "\(whole).\(text)"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func atomic(fromXMR value: String) throws -> UInt64 {
|
||||||
|
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !text.isEmpty, !text.hasPrefix("-") else { throw XRPError.rpc("Enter a valid amount") }
|
||||||
|
let parts = text.split(separator: ".", omittingEmptySubsequences: false)
|
||||||
|
guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") }
|
||||||
|
guard let whole = UInt64(parts[0].isEmpty ? "0" : String(parts[0])) else {
|
||||||
|
throw XRPError.rpc("Enter a valid amount")
|
||||||
|
}
|
||||||
|
var fraction: UInt64 = 0
|
||||||
|
if parts.count == 2 {
|
||||||
|
let fractionText = String(parts[1])
|
||||||
|
guard fractionText.count <= 12, fractionText.allSatisfy({ $0.isNumber }) else {
|
||||||
|
throw XRPError.rpc("XMR supports at most 12 decimal places")
|
||||||
|
}
|
||||||
|
let padded = fractionText.padding(toLength: 12, withPad: "0", startingAt: 0)
|
||||||
|
guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") }
|
||||||
|
fraction = parsed
|
||||||
|
}
|
||||||
|
let (wholeAtomic, overflow) = whole.multipliedReportingOverflow(by: atomicPerXMR)
|
||||||
|
guard !overflow else { throw XRPError.rpc("Amount is too large") }
|
||||||
|
let (total, carry) = wholeAtomic.addingReportingOverflow(fraction)
|
||||||
|
guard !carry else { throw XRPError.rpc("Amount is too large") }
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swift wrapper around the Objective-C++ monero-cpp bridge.
|
||||||
|
final class MoneroService {
|
||||||
|
static let shared = MoneroService()
|
||||||
|
|
||||||
|
/// Wallet files that exist on disk without a stored record, so a lost
|
||||||
|
/// preference file does not strand a wallet.
|
||||||
|
static func orphanedWalletNames() -> [String] {
|
||||||
|
let manager = FileManager.default
|
||||||
|
guard let documents = manager.urls(for: .documentDirectory, in: .userDomainMask).first,
|
||||||
|
let items = try? manager.contentsOfDirectory(at: documents,
|
||||||
|
includingPropertiesForKeys: nil) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let keys = Set(items.filter { $0.pathExtension == "keys" }
|
||||||
|
.map { $0.deletingPathExtension().lastPathComponent })
|
||||||
|
return items
|
||||||
|
.filter { $0.pathExtension.isEmpty && !$0.lastPathComponent.hasPrefix(".") }
|
||||||
|
.map { $0.lastPathComponent }
|
||||||
|
.filter { keys.contains($0) }
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The wallet file is encrypted with this password; the recovery seed lives
|
||||||
|
/// in the Keychain.
|
||||||
|
static let walletPassword = "koins"
|
||||||
|
|
||||||
|
/// Default daemon, kept in sync with the bridge. Used for the cheap
|
||||||
|
/// daemon-only queries that do not need an open wallet.
|
||||||
|
static let daemonHost = "xmr-node.cakewallet.com"
|
||||||
|
|
||||||
|
/// Current chain tip, so an import can start scanning from now instead of
|
||||||
|
/// from the genesis block.
|
||||||
|
func daemonHeight() async throws -> UInt64 {
|
||||||
|
guard let url = URL(string: "http://\(Self.daemonHost):18081/json_rpc") else {
|
||||||
|
throw XRPError.network("Invalid Monero daemon address")
|
||||||
|
}
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.timeoutInterval = 15
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: [
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "koins",
|
||||||
|
"method": "get_info",
|
||||||
|
])
|
||||||
|
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
||||||
|
throw XRPError.network("Monero daemon is unreachable")
|
||||||
|
}
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
let result = json["result"] as? [String: Any],
|
||||||
|
let height = (result["height"] as? NSNumber)?.uint64Value else {
|
||||||
|
throw XRPError.network("Could not read the Monero chain tip")
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
private func invoke(_ request: [String: Any]) async throws -> [String: Any] {
|
||||||
|
let json = try JSONSerialization.data(withJSONObject: request)
|
||||||
|
let requestString = String(data: json, encoding: .utf8) ?? "{}"
|
||||||
|
|
||||||
|
let responseString: String = await withCheckedContinuation { continuation in
|
||||||
|
KoinsMoneroWallet.invoke(requestString) { response in
|
||||||
|
continuation.resume(returning: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = responseString.data(using: .utf8),
|
||||||
|
let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
throw XRPError.rpc("Unexpected response from the Monero wallet")
|
||||||
|
}
|
||||||
|
if let error = response["error"] as? String, !error.isEmpty {
|
||||||
|
throw XRPError.rpc(error)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Wallet lifecycle
|
||||||
|
|
||||||
|
func createWallet(name: String) async throws -> (address: String, mnemonic: String) {
|
||||||
|
let response = try await invoke(["op": "createWallet",
|
||||||
|
"name": name,
|
||||||
|
"password": Self.walletPassword,
|
||||||
|
"language": "English"])
|
||||||
|
return (response["address"] as? String ?? "", response["mnemonic"] as? String ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func openWallet(name: String) async throws -> String {
|
||||||
|
let response = try await invoke(["op": "openWallet",
|
||||||
|
"name": name,
|
||||||
|
"password": Self.walletPassword])
|
||||||
|
return response["address"] as? String ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreWallet(name: String,
|
||||||
|
seed: String,
|
||||||
|
restoreHeight: UInt64) async throws -> String {
|
||||||
|
let response = try await invoke(["op": "restoreWallet",
|
||||||
|
"name": name,
|
||||||
|
"password": Self.walletPassword,
|
||||||
|
"seed": seed,
|
||||||
|
"restoreHeight": restoreHeight])
|
||||||
|
return response["address"] as? String ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeWallet(name: String) async {
|
||||||
|
_ = try? await invoke(["op": "removeWallet", "name": name])
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeWallet() async {
|
||||||
|
_ = try? await invoke(["op": "closeWallet"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func seed() async throws -> String {
|
||||||
|
let response = try await invoke(["op": "getSeed"])
|
||||||
|
return response["mnemonic"] as? String ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Reads
|
||||||
|
|
||||||
|
func balance() async throws -> Balance {
|
||||||
|
let response = try await invoke(["op": "getBalance"])
|
||||||
|
let total = response["balance"] as? String ?? "0"
|
||||||
|
let unlocked = response["unlocked"] as? String ?? total
|
||||||
|
return Balance(total: MoneroAmount.xmr(fromAtomic: total),
|
||||||
|
unlocked: MoneroAmount.xmr(fromAtomic: unlocked),
|
||||||
|
funded: (UInt64(total) ?? 0) > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transactions() async throws -> [TransactionRecord] {
|
||||||
|
let response = try await invoke(["op": "getTransactions"])
|
||||||
|
let entries = response["transactions"] as? [[String: Any]] ?? []
|
||||||
|
return entries.map { entry in
|
||||||
|
let timestamp = (entry["timestamp"] as? NSNumber)?.doubleValue ?? 0
|
||||||
|
return TransactionRecord(
|
||||||
|
id: entry["hash"] as? String ?? UUID().uuidString,
|
||||||
|
amount: MoneroAmount.xmr(fromAtomic: entry["amount"] as? String ?? "0"),
|
||||||
|
timestamp: timestamp > 0 ? Date(timeIntervalSince1970: timestamp) : nil,
|
||||||
|
direction: (entry["direction"] as? String) == "in" ? .incoming : .outgoing,
|
||||||
|
confirmed: (entry["confirmed"] as? Bool) ?? false,
|
||||||
|
fee: MoneroAmount.xmr(fromAtomic: entry["fee"] as? String ?? "0"),
|
||||||
|
tag: nil,
|
||||||
|
counterparty: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sync
|
||||||
|
|
||||||
|
func startSync() async throws -> Bool {
|
||||||
|
let response = try await invoke(["op": "startSync"])
|
||||||
|
return (response["syncing"] as? Bool) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopSync() async {
|
||||||
|
_ = try? await invoke(["op": "stopSync"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncStatus() async throws -> SyncStatus {
|
||||||
|
let response = try await invoke(["op": "getSyncStatus"])
|
||||||
|
return SyncStatus(height: (response["height"] as? NSNumber)?.uint64Value ?? 0,
|
||||||
|
daemonHeight: (response["daemonHeight"] as? NSNumber)?.uint64Value ?? 0,
|
||||||
|
syncing: (response["syncing"] as? Bool) ?? false,
|
||||||
|
connected: (response["connected"] as? Bool) ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sends
|
||||||
|
|
||||||
|
func send(address: String, amountXMR: String, priority: TransactionPriority) async throws -> (hash: String, fee: String) {
|
||||||
|
let atomic = try MoneroAmount.atomic(fromXMR: amountXMR)
|
||||||
|
let response = try await invoke(["op": "transferTo",
|
||||||
|
"address": address.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
"amount": String(atomic),
|
||||||
|
"priority": priority.rawValue])
|
||||||
|
return (response["txHash"] as? String ?? "",
|
||||||
|
MoneroAmount.xmr(fromAtomic: response["fee"] as? String ?? "0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Exact decimal <-> drops conversion (1 XRP = 1_000_000 drops) without
|
||||||
|
/// floating point rounding errors.
|
||||||
|
enum XRPAmount {
|
||||||
|
static let dropsPerXRP: UInt64 = 1_000_000
|
||||||
|
static let maxDrops: UInt64 = 100_000_000_000 * dropsPerXRP // 1e11 XRP
|
||||||
|
|
||||||
|
static func drops(fromXRP value: String) throws -> UInt64 {
|
||||||
|
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !text.isEmpty, !text.hasPrefix("-"), !text.hasPrefix("+") else {
|
||||||
|
throw XRPError.rpc("Enter a valid amount")
|
||||||
|
}
|
||||||
|
let parts = text.split(separator: ".", omittingEmptySubsequences: false)
|
||||||
|
guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") }
|
||||||
|
|
||||||
|
let wholeText = parts[0].isEmpty ? "0" : String(parts[0])
|
||||||
|
guard let whole = UInt64(wholeText) else { throw XRPError.rpc("Enter a valid amount") }
|
||||||
|
|
||||||
|
var fraction: UInt64 = 0
|
||||||
|
if parts.count == 2 {
|
||||||
|
let fractionText = String(parts[1])
|
||||||
|
guard fractionText.count <= 6, fractionText.allSatisfy({ $0.isNumber }) else {
|
||||||
|
throw XRPError.rpc("XRP supports at most 6 decimal places")
|
||||||
|
}
|
||||||
|
let padded = fractionText.padding(toLength: 6, withPad: "0", startingAt: 0)
|
||||||
|
guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") }
|
||||||
|
fraction = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
let (wholeDrops, overflow) = whole.multipliedReportingOverflow(by: dropsPerXRP)
|
||||||
|
guard !overflow else { throw XRPError.rpc("Amount is too large") }
|
||||||
|
let (total, carry) = wholeDrops.addingReportingOverflow(fraction)
|
||||||
|
guard !carry, total <= maxDrops else { throw XRPError.rpc("Amount is too large") }
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
static func xrp(fromDrops drops: String) -> String {
|
||||||
|
guard let value = UInt64(drops.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return xrp(fromDrops: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func xrp(fromDrops value: UInt64) -> String {
|
||||||
|
let whole = value / dropsPerXRP
|
||||||
|
let fraction = value % dropsPerXRP
|
||||||
|
if fraction == 0 { return String(whole) }
|
||||||
|
var fractionText = String(format: "%06llu", fraction)
|
||||||
|
while fractionText.hasSuffix("0") { fractionText.removeLast() }
|
||||||
|
return "\(whole).\(fractionText)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Canonical XRP Ledger binary serialization for the Payment transactions this
|
||||||
|
/// wallet sends, plus the signing / transaction-id hashing around it.
|
||||||
|
struct XRPPayment {
|
||||||
|
var account: String
|
||||||
|
var destination: String
|
||||||
|
var amountDrops: UInt64
|
||||||
|
var feeDrops: UInt64
|
||||||
|
var sequence: UInt32
|
||||||
|
var lastLedgerSequence: UInt32?
|
||||||
|
var destinationTag: UInt32?
|
||||||
|
var signingPublicKey: [UInt8]
|
||||||
|
var signature: [UInt8]?
|
||||||
|
|
||||||
|
private static let typeUInt16: UInt8 = 1
|
||||||
|
private static let typeUInt32: UInt8 = 2
|
||||||
|
private static let typeAmount: UInt8 = 6
|
||||||
|
private static let typeBlob: UInt8 = 7
|
||||||
|
private static let typeAccountID: UInt8 = 8
|
||||||
|
|
||||||
|
func serialized(includingSignature: Bool) throws -> [UInt8] {
|
||||||
|
var output: [UInt8] = []
|
||||||
|
|
||||||
|
// Fields must appear in (type, field) order.
|
||||||
|
output += header(type: Self.typeUInt16, field: 2) // TransactionType
|
||||||
|
output += uint16(0) // ttPAYMENT
|
||||||
|
|
||||||
|
output += header(type: Self.typeUInt32, field: 2) // Flags
|
||||||
|
output += uint32(0)
|
||||||
|
|
||||||
|
output += header(type: Self.typeUInt32, field: 4) // Sequence
|
||||||
|
output += uint32(sequence)
|
||||||
|
|
||||||
|
if let tag = destinationTag {
|
||||||
|
output += header(type: Self.typeUInt32, field: 14) // DestinationTag
|
||||||
|
output += uint32(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lastLedger = lastLedgerSequence {
|
||||||
|
output += header(type: Self.typeUInt32, field: 27) // LastLedgerSequence
|
||||||
|
output += uint32(lastLedger)
|
||||||
|
}
|
||||||
|
|
||||||
|
output += header(type: Self.typeAmount, field: 1) // Amount
|
||||||
|
output += try amount(amountDrops)
|
||||||
|
|
||||||
|
output += header(type: Self.typeAmount, field: 8) // Fee
|
||||||
|
output += try amount(feeDrops)
|
||||||
|
|
||||||
|
output += header(type: Self.typeBlob, field: 3) // SigningPubKey
|
||||||
|
output += variableLength(signingPublicKey)
|
||||||
|
|
||||||
|
if includingSignature, let signature {
|
||||||
|
output += header(type: Self.typeBlob, field: 4) // TxnSignature
|
||||||
|
output += variableLength(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
output += header(type: Self.typeAccountID, field: 1) // Account
|
||||||
|
output += variableLength(try XRPKey.accountId(fromAddress: account))
|
||||||
|
|
||||||
|
output += header(type: Self.typeAccountID, field: 3) // Destination
|
||||||
|
output += variableLength(try XRPKey.accountId(fromAddress: destination))
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes the private key signs (HashPrefix.transactionSig + tx).
|
||||||
|
func signingPayload() throws -> [UInt8] {
|
||||||
|
[0x53, 0x54, 0x58, 0x00] + (try serialized(includingSignature: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The signed transaction blob submitted to the network (no hash prefix).
|
||||||
|
func txBlob() throws -> [UInt8] {
|
||||||
|
guard signature != nil else { throw XRPError.rpc("Transaction is not signed") }
|
||||||
|
return try serialized(includingSignature: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transactionHash() throws -> [UInt8] {
|
||||||
|
guard signature != nil else { throw XRPError.rpc("Transaction is not signed") }
|
||||||
|
let blob = try serialized(includingSignature: true)
|
||||||
|
return XRPHash.sha512Half([0x54, 0x58, 0x4e, 0x00] + blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Encoding helpers
|
||||||
|
|
||||||
|
private func header(type: UInt8, field: UInt8) -> [UInt8] {
|
||||||
|
if field < 16 { return [(type << 4) | field] }
|
||||||
|
return [type << 4, field]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uint16(_ value: UInt16) -> [UInt8] {
|
||||||
|
[UInt8(value >> 8), UInt8(value & 0xff)]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uint32(_ value: UInt32) -> [UInt8] {
|
||||||
|
[
|
||||||
|
UInt8((value >> 24) & 0xff),
|
||||||
|
UInt8((value >> 16) & 0xff),
|
||||||
|
UInt8((value >> 8) & 0xff),
|
||||||
|
UInt8(value & 0xff),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Native (XRP) amount: 8 bytes big-endian drops with the XRP flag bit set.
|
||||||
|
private func amount(_ drops: UInt64) throws -> [UInt8] {
|
||||||
|
guard drops <= XRPAmount.maxDrops else { throw XRPError.rpc("Amount is too large") }
|
||||||
|
var bytes = uint64(drops)
|
||||||
|
bytes[0] |= 0x40
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uint64(_ value: UInt64) -> [UInt8] {
|
||||||
|
var bytes: [UInt8] = []
|
||||||
|
for shift in stride(from: 56, through: 0, by: -8) {
|
||||||
|
bytes.append(UInt8((value >> UInt64(shift)) & 0xff))
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
private func variableLength(_ bytes: [UInt8]) -> [UInt8] {
|
||||||
|
var output: [UInt8] = []
|
||||||
|
let count = bytes.count
|
||||||
|
if count < 192 {
|
||||||
|
output.append(UInt8(count))
|
||||||
|
} else if count <= 12_480 {
|
||||||
|
output.append(UInt8((count >> 8) + 192))
|
||||||
|
output.append(UInt8(count & 0xff))
|
||||||
|
} else {
|
||||||
|
output.append(UInt8((count >> 16) + 240))
|
||||||
|
output.append(UInt8((count >> 8) & 0xff))
|
||||||
|
output.append(UInt8(count & 0xff))
|
||||||
|
}
|
||||||
|
return output + bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Minimal XRP Ledger JSON-RPC client. A couple of public endpoints are tried
|
||||||
|
/// in order so a single outage does not break the wallet.
|
||||||
|
final class XRPClient {
|
||||||
|
static let shared = XRPClient()
|
||||||
|
|
||||||
|
private let endpoints = [
|
||||||
|
"https://s1.ripple.com:51234/",
|
||||||
|
"https://s2.ripple.com:51234/",
|
||||||
|
"https://xrplcluster.com/",
|
||||||
|
]
|
||||||
|
|
||||||
|
private var preferredEndpoint = 0
|
||||||
|
private let session: URLSession
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
let configuration = URLSessionConfiguration.ephemeral
|
||||||
|
configuration.timeoutIntervalForRequest = 20
|
||||||
|
configuration.waitsForConnectivity = false
|
||||||
|
session = URLSession(configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpc(_ method: String, _ params: [String: Any] = [:]) async throws -> [String: Any] {
|
||||||
|
var lastError: Error = XRPError.network("XRP network is unavailable")
|
||||||
|
for attempt in 0..<endpoints.count {
|
||||||
|
let index = (preferredEndpoint + attempt) % endpoints.count
|
||||||
|
guard let url = URL(string: endpoints[index]) else { continue }
|
||||||
|
do {
|
||||||
|
let result = try await send(method, params, to: url)
|
||||||
|
preferredEndpoint = index
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
lastError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
private func send(_ method: String, _ params: [String: Any], to url: URL) async throws -> [String: Any] {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
let body: [String: Any] = ["method": method, "params": [params], "id": 1]
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
|
||||||
|
throw XRPError.network("Ledger request failed (\(method))")
|
||||||
|
}
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
throw XRPError.network("Malformed ledger response")
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = json["result"] as? [String: Any] ?? [:]
|
||||||
|
if let error = result["error"] as? String {
|
||||||
|
let message = result["error_message"] as? String ?? error
|
||||||
|
throw XRPError.rpc(message)
|
||||||
|
}
|
||||||
|
if let error = json["error"] as? String {
|
||||||
|
throw XRPError.rpc(json["error_message"] as? String ?? error)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isAccountNotFound(_ error: Error) -> Bool {
|
||||||
|
if case XRPError.rpc(let message) = error {
|
||||||
|
return message == "actNotFound" || message.contains("Account not found")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum XRPAlgorithm: String {
|
||||||
|
case ed25519
|
||||||
|
case secp256k1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set by the app target so this file stays compilable outside iOS.
|
||||||
|
enum Secp256k1Backend {
|
||||||
|
static var derivePrivateKey: ((Data) -> Data?)?
|
||||||
|
static var publicKey: ((Data) -> Data?)?
|
||||||
|
static var sign: ((Data, Data) -> Data?)?
|
||||||
|
static var addScalar: ((Data, Data) -> Data?)?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct XRPKeypair {
|
||||||
|
let algorithm: XRPAlgorithm
|
||||||
|
/// ed25519: 32-byte seed. secp256k1: 32-byte scalar.
|
||||||
|
let privateKey: [UInt8]
|
||||||
|
/// ed25519: 0xED + 32-byte key. secp256k1: 33-byte compressed point.
|
||||||
|
let publicKey: [UInt8]
|
||||||
|
|
||||||
|
var address: String { XRPKey.address(fromPublicKey: publicKey) }
|
||||||
|
|
||||||
|
var signingPublicKeyHex: String { Hex.encode(publicKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum XRPKey {
|
||||||
|
static let ed25519SeedVersion: [UInt8] = [0x01, 0xe1, 0x4b]
|
||||||
|
static let familySeedVersion: [UInt8] = [0x21]
|
||||||
|
static let accountIdVersion: [UInt8] = [0x00]
|
||||||
|
static let ed25519Prefix: UInt8 = 0xED
|
||||||
|
|
||||||
|
// MARK: - Seeds
|
||||||
|
|
||||||
|
static func generateSeed(algorithm: XRPAlgorithm = .ed25519) -> String {
|
||||||
|
var entropy = [UInt8](repeating: 0, count: 16)
|
||||||
|
for index in entropy.indices { entropy[index] = UInt8.random(in: 0...255) }
|
||||||
|
let version = algorithm == .ed25519 ? ed25519SeedVersion : familySeedVersion
|
||||||
|
return XRPBase58.checkEncode(version: version, payload: entropy)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decodeSeed(_ seed: String) throws -> (entropy: [UInt8], algorithm: XRPAlgorithm) {
|
||||||
|
let decoded = try XRPBase58.decode(seed.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
guard decoded.count > 4 else { throw XRPError.invalidSeed }
|
||||||
|
let body = Array(decoded[0..<(decoded.count - 4)])
|
||||||
|
let checksum = Array(decoded[(decoded.count - 4)...])
|
||||||
|
guard XRPHash.checksum(body) == checksum else { throw XRPError.invalidSeed }
|
||||||
|
|
||||||
|
if body.count == ed25519SeedVersion.count + 16, Array(body.prefix(3)) == ed25519SeedVersion {
|
||||||
|
return (Array(body.dropFirst(3)), .ed25519)
|
||||||
|
}
|
||||||
|
if body.count == familySeedVersion.count + 16, body[0] == familySeedVersion[0] {
|
||||||
|
return (Array(body.dropFirst(1)), .secp256k1)
|
||||||
|
}
|
||||||
|
throw XRPError.unsupportedSeed
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Keys
|
||||||
|
|
||||||
|
/// Recovery phrase for a new wallet: 12 BIP-39 words.
|
||||||
|
static func generateMnemonic() -> String {
|
||||||
|
BIP39.generateMnemonic()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepts either a 12 word BIP-39 recovery phrase or a legacy XRP family
|
||||||
|
/// seed (`sEd…` / `s…`).
|
||||||
|
static func keypair(fromSecret secret: String) throws -> XRPKeypair {
|
||||||
|
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if BIP39.isMnemonic(trimmed) {
|
||||||
|
return try keypair(fromMnemonic: trimmed)
|
||||||
|
}
|
||||||
|
return try keypair(fromSeed: trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives the account at `m/44'/144'/0'/0/0`, the path hardware and mobile
|
||||||
|
/// wallets use for XRP.
|
||||||
|
static func keypair(fromMnemonic mnemonic: String) throws -> XRPKeypair {
|
||||||
|
guard BIP39.entropy(from: mnemonic) != nil else {
|
||||||
|
throw XRPError.invalidSeed
|
||||||
|
}
|
||||||
|
let seed = BIP39.seed(from: mnemonic)
|
||||||
|
guard let master = BIP32.masterKey(fromSeed: seed),
|
||||||
|
let derived = BIP32.derive(BIP32.xrpPath, from: master) else {
|
||||||
|
throw XRPError.unsupportedSeed
|
||||||
|
}
|
||||||
|
return XRPKeypair(algorithm: .secp256k1,
|
||||||
|
privateKey: derived.privateKey,
|
||||||
|
publicKey: derived.publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func keypair(fromSeed seed: String) throws -> XRPKeypair {
|
||||||
|
let (entropy, algorithm) = try decodeSeed(seed)
|
||||||
|
switch algorithm {
|
||||||
|
case .ed25519:
|
||||||
|
let raw = XRPHash.sha512Half(entropy)
|
||||||
|
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(raw))
|
||||||
|
let publicKey = [ed25519Prefix] + Array(privateKey.publicKey.rawRepresentation)
|
||||||
|
return XRPKeypair(algorithm: .ed25519, privateKey: raw, publicKey: publicKey)
|
||||||
|
case .secp256k1:
|
||||||
|
guard let derive = Secp256k1Backend.derivePrivateKey,
|
||||||
|
let toPublic = Secp256k1Backend.publicKey,
|
||||||
|
let privateKey = derive(Data(entropy)),
|
||||||
|
let publicKey = toPublic(privateKey) else {
|
||||||
|
throw XRPError.unsupportedSeed
|
||||||
|
}
|
||||||
|
return XRPKeypair(algorithm: .secp256k1,
|
||||||
|
privateKey: Array(privateKey),
|
||||||
|
publicKey: Array(publicKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func address(fromPublicKey publicKey: [UInt8]) -> String {
|
||||||
|
let accountId = XRPHash.ripemd160(XRPHash.sha256(publicKey))
|
||||||
|
return XRPBase58.checkEncode(version: accountIdVersion, payload: accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isValidAddress(_ address: String) -> Bool {
|
||||||
|
(try? XRPBase58.checkDecode(address, versionLength: 1)) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func accountId(fromAddress address: String) throws -> [UInt8] {
|
||||||
|
let payload = try XRPBase58.checkDecode(address, versionLength: 1)
|
||||||
|
guard payload.count == 20 else { throw XRPError.invalidAddress }
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Signing
|
||||||
|
|
||||||
|
/// ed25519 returns a 64-byte signature; secp256k1 a DER-encoded one, matching
|
||||||
|
/// what the XRP Ledger expects for each signing scheme.
|
||||||
|
static func sign(message: [UInt8], keypair: XRPKeypair) throws -> [UInt8] {
|
||||||
|
switch keypair.algorithm {
|
||||||
|
case .ed25519:
|
||||||
|
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(keypair.privateKey))
|
||||||
|
let signature = try privateKey.signature(for: Data(message))
|
||||||
|
return Array(signature)
|
||||||
|
case .secp256k1:
|
||||||
|
let digest = XRPHash.sha512Half(message)
|
||||||
|
guard let signFn = Secp256k1Backend.sign,
|
||||||
|
let signature = signFn(Data(digest), Data(keypair.privateKey)) else {
|
||||||
|
throw XRPError.unsupportedSeed
|
||||||
|
}
|
||||||
|
return Array(signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// High level XRP operations: wallet creation/import, balances, history, sends.
|
||||||
|
final class XRPWalletService {
|
||||||
|
static let shared = XRPWalletService()
|
||||||
|
|
||||||
|
private let rippleEpoch: TimeInterval = 946_684_800
|
||||||
|
|
||||||
|
// MARK: - Wallet lifecycle
|
||||||
|
|
||||||
|
func createWallet(name: String) throws -> (WalletRecord, String) {
|
||||||
|
let mnemonic = XRPKey.generateMnemonic()
|
||||||
|
let keypair = try XRPKey.keypair(fromMnemonic: mnemonic)
|
||||||
|
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
|
||||||
|
name: name,
|
||||||
|
address: keypair.address,
|
||||||
|
createdAt: Date())
|
||||||
|
WalletStore.saveWallet(record, chain: .xrp)
|
||||||
|
WalletStore.saveSecret(mnemonic, chain: .xrp)
|
||||||
|
return (record, mnemonic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func importWallet(name: String, secret: String) throws -> WalletRecord {
|
||||||
|
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let keypair = try XRPKey.keypair(fromSecret: trimmed)
|
||||||
|
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
|
||||||
|
name: name,
|
||||||
|
address: keypair.address,
|
||||||
|
createdAt: Date())
|
||||||
|
WalletStore.saveWallet(record, chain: .xrp)
|
||||||
|
WalletStore.saveSecret(trimmed, chain: .xrp)
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func address(forSecret secret: String) throws -> String {
|
||||||
|
try XRPKey.keypair(fromSecret: secret.trimmingCharacters(in: .whitespacesAndNewlines)).address
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Reads
|
||||||
|
|
||||||
|
func balance(address: String) async throws -> Balance {
|
||||||
|
do {
|
||||||
|
let result = try await XRPClient.shared.rpc("account_info",
|
||||||
|
["account": address,
|
||||||
|
"ledger_index": "current"])
|
||||||
|
let accountData = result["account_data"] as? [String: Any] ?? [:]
|
||||||
|
let drops = accountData["Balance"] as? String ?? "0"
|
||||||
|
return Balance(total: XRPAmount.xrp(fromDrops: drops), unlocked: "", funded: true)
|
||||||
|
} catch {
|
||||||
|
if XRPClient.isAccountNotFound(error) {
|
||||||
|
return Balance(total: "0", unlocked: "", funded: false)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transactions(address: String) async throws -> [TransactionRecord] {
|
||||||
|
let result = try await XRPClient.shared.rpc("account_tx",
|
||||||
|
["account": address,
|
||||||
|
"ledger_index_min": -1,
|
||||||
|
"ledger_index_max": -1,
|
||||||
|
"limit": 40])
|
||||||
|
let entries = result["transactions"] as? [[String: Any]] ?? []
|
||||||
|
var records: [TransactionRecord] = []
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
guard let tx = entry["tx_json"] as? [String: Any] ?? entry["tx"] as? [String: Any] else { continue }
|
||||||
|
guard (tx["TransactionType"] as? String) == "Payment" else { continue }
|
||||||
|
let meta = entry["meta"] as? [String: Any] ?? entry["metaData"] as? [String: Any] ?? [:]
|
||||||
|
if let txResult = meta["TransactionResult"] as? String, txResult != "tesSUCCESS" { continue }
|
||||||
|
|
||||||
|
// IOU / MPT deliveries carry an object; only native XRP is shown.
|
||||||
|
let delivered = (meta["delivered_amount"] as? String)
|
||||||
|
?? (meta["DeliveredAmount"] as? String)
|
||||||
|
?? (tx["Amount"] as? String)
|
||||||
|
guard let deliveredDrops = delivered else { continue }
|
||||||
|
|
||||||
|
let destination = tx["Destination"] as? String ?? ""
|
||||||
|
let isIncoming = destination == address
|
||||||
|
let date = (tx["date"] as? NSNumber).map { Date(timeIntervalSince1970: $0.doubleValue + rippleEpoch) }
|
||||||
|
|
||||||
|
records.append(TransactionRecord(
|
||||||
|
id: entry["hash"] as? String ?? tx["hash"] as? String ?? UUID().uuidString,
|
||||||
|
amount: XRPAmount.xrp(fromDrops: deliveredDrops),
|
||||||
|
timestamp: date,
|
||||||
|
direction: isIncoming ? .incoming : .outgoing,
|
||||||
|
confirmed: (entry["validated"] as? Bool) ?? false,
|
||||||
|
fee: (tx["Fee"] as? String).map { XRPAmount.xrp(fromDrops: $0) },
|
||||||
|
tag: (tx["DestinationTag"] as? NSNumber)?.intValue,
|
||||||
|
counterparty: isIncoming ? (tx["Account"] as? String) : destination))
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sends
|
||||||
|
|
||||||
|
func send(secret: String,
|
||||||
|
to: String,
|
||||||
|
amountXRP: String,
|
||||||
|
destinationTag: UInt32?) async throws -> (hash: String, fee: String) {
|
||||||
|
let destination = to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard XRPKey.isValidAddress(destination) else {
|
||||||
|
throw XRPError.rpc("Enter a valid XRP address")
|
||||||
|
}
|
||||||
|
|
||||||
|
let keypair = try XRPKey.keypair(fromSeed: secret)
|
||||||
|
let amountDrops = try XRPAmount.drops(fromXRP: amountXRP)
|
||||||
|
|
||||||
|
async let infoCall = XRPClient.shared.rpc("account_info",
|
||||||
|
["account": keypair.address,
|
||||||
|
"ledger_index": "current"])
|
||||||
|
async let feeCall = XRPClient.shared.rpc("fee", [:])
|
||||||
|
async let ledgerCall = XRPClient.shared.rpc("ledger_current", [:])
|
||||||
|
|
||||||
|
let info = try await infoCall
|
||||||
|
let accountData = info["account_data"] as? [String: Any] ?? [:]
|
||||||
|
guard let sequence = (accountData["Sequence"] as? NSNumber)?.uint32Value else {
|
||||||
|
throw XRPError.rpc("Could not read the account sequence")
|
||||||
|
}
|
||||||
|
let balanceDrops = UInt64((accountData["Balance"] as? String) ?? "0") ?? 0
|
||||||
|
|
||||||
|
let feeResult = try await feeCall
|
||||||
|
let feeDrops = (feeResult["drops"] as? [String: Any])
|
||||||
|
.flatMap { $0["open_ledger_fee"] as? String }
|
||||||
|
.flatMap { UInt64($0) } ?? 12
|
||||||
|
|
||||||
|
let ledger = try await ledgerCall
|
||||||
|
let ledgerIndex = (ledger["ledger_current_index"] as? NSNumber)?.uint32Value
|
||||||
|
|
||||||
|
guard amountDrops + feeDrops <= balanceDrops else {
|
||||||
|
throw XRPError.rpc("Insufficient balance to cover the amount and network fee")
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment = XRPPayment(account: keypair.address,
|
||||||
|
destination: destination,
|
||||||
|
amountDrops: amountDrops,
|
||||||
|
feeDrops: feeDrops,
|
||||||
|
sequence: sequence,
|
||||||
|
lastLedgerSequence: ledgerIndex.map { $0 + 20 },
|
||||||
|
destinationTag: destinationTag,
|
||||||
|
signingPublicKey: keypair.publicKey,
|
||||||
|
signature: nil)
|
||||||
|
|
||||||
|
let signature = try XRPKey.sign(message: payment.signingPayload(), keypair: keypair)
|
||||||
|
payment.signature = signature
|
||||||
|
|
||||||
|
let blob = Hex.encode(try payment.txBlob())
|
||||||
|
let result = try await XRPClient.shared.rpc("submit", ["tx_blob": blob])
|
||||||
|
let engineResult = result["engine_result"] as? String ?? ""
|
||||||
|
guard engineResult.hasPrefix("tes") else {
|
||||||
|
let message = result["engine_result_message"] as? String ?? "Transaction failed"
|
||||||
|
throw XRPError.rpc("\(engineResult): \(message)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Hex.encode(try payment.transactionHash()), XRPAmount.xrp(fromDrops: feeDrops))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Monero native library for iOS
|
||||||
|
|
||||||
|
The wallet runs monero-project `wallet2` (plus monero-cpp as the C++ API) on
|
||||||
|
device. Everything is compiled into a single static archive that the app links.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
| File | Architecture | Platform |
|
||||||
|
|---|---|---|
|
||||||
|
| `libs/libkoins_monero_merged.a` | arm64 | iOS simulator |
|
||||||
|
| `libs/libkoins_monero_device.a` | arm64 | iOS device |
|
||||||
|
|
||||||
|
Both are needed: they are the same CPU architecture but different platforms, so
|
||||||
|
they cannot be merged into one fat archive. `project.yml` selects the right one
|
||||||
|
per SDK.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Xcode with the iOS device and simulator SDKs
|
||||||
|
- CMake 3.2x (monero 0.18 is not tested with CMake 4)
|
||||||
|
- GNU make (the builds use the "Unix Makefiles" generator)
|
||||||
|
- Roughly 10 GB of disk and a long coffee break
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- monero-project v0.18.5.1
|
||||||
|
- monero-cpp (woodser), which keeps monero-project at `external/monero-project`
|
||||||
|
|
||||||
|
## Toolchain customisations
|
||||||
|
|
||||||
|
The files under `depends/` are the ones modified from upstream, kept here so the
|
||||||
|
archive can be regenerated:
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `depends/hosts/ios.mk` | new host; `IOS_PLATFORM=SIMULATOR` (default) or `DEVICE` selects the SDK and the target triple (`aarch64-apple-ios[-simulator]`) |
|
||||||
|
| `depends/packages/packages.mk` | `ios_packages := boost openssl zeromq expat unbound sodium` (no libiconv) |
|
||||||
|
| `depends/packages/boost.mk` | boost for the `ios` toolset: darwin toolset, libtool archiver, c++11, no b2 python |
|
||||||
|
| `depends/packages/zeromq.mk` | ios config opts (`--host=aarch64-apple-darwin`), no libsodium, no curve |
|
||||||
|
| `depends/packages/unbound.mk` | drops the autoconf dependency |
|
||||||
|
| `depends/packages/sodium.mk` | drops the autoconf dependency |
|
||||||
|
| `depends/funcs.mk` | create the prefix `lib` directory before extracting dependency archives |
|
||||||
|
|
||||||
|
One source change is also required, in monero-project:
|
||||||
|
|
||||||
|
- `src/cryptonote_basic/miner.cpp` — wrap the IOKit includes and the related
|
||||||
|
code with `defined(TARGET_OS_OSX) && TARGET_OS_OSX`; IOKit is unavailable on
|
||||||
|
iOS. Two locations, around lines 51 and 986.
|
||||||
|
|
||||||
|
Two more steps are needed when targeting a device:
|
||||||
|
|
||||||
|
- Copy monero's patched boost serialisation headers into the dependency prefix,
|
||||||
|
because monero's build otherwise only finds them for the platform it was
|
||||||
|
installed for:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp external/boost/archive/portable_binary_*.hpp \
|
||||||
|
contrib/depends/$HOST/include/boost/archive/
|
||||||
|
```
|
||||||
|
|
||||||
|
- randomx's AArch64 JIT references `__clear_cache`, which libSystem exports on
|
||||||
|
macOS and the simulator but not on iOS. The app supplies it in
|
||||||
|
`Koins/Support/ClearCache.c`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Set `PLATFORM` to `SIMULATOR` or `DEVICE`; the only differences are the depends
|
||||||
|
host flag and the output archive name.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MONERO=/path/to/monero-project
|
||||||
|
PLATFORM=SIMULATOR # or DEVICE
|
||||||
|
HOST=aarch64-apple-ios
|
||||||
|
|
||||||
|
# 1. Cross-compiled dependencies (boost, openssl, zeromq, expat, unbound, sodium)
|
||||||
|
cd "$MONERO/contrib/depends"
|
||||||
|
make HOST=$HOST IOS_PLATFORM=$PLATFORM -j"$(sysctl -n hw.ncpu)"
|
||||||
|
|
||||||
|
# 2. monero-project, static wallet library
|
||||||
|
cd "$MONERO"
|
||||||
|
mkdir -p build/ios-$PLATFORM && cd build/ios-$PLATFORM
|
||||||
|
cmake -G "Unix Makefiles" \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$MONERO/contrib/depends/$HOST/share/toolchain.cmake" \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DSTATIC=ON -DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DBUILD_TESTS=OFF -DBUILD_DOCUMENTATION=OFF -DUSE_DEVICE_TREZOR=OFF \
|
||||||
|
../..
|
||||||
|
make -j"$(sysctl -n hw.ncpu)" wallet # produces lib/libwallet.a
|
||||||
|
|
||||||
|
# 3. monero-cpp
|
||||||
|
cd /path/to/monero-cpp
|
||||||
|
mkdir -p build-ios-$PLATFORM && cd build-ios-$PLATFORM
|
||||||
|
cmake -G "Unix Makefiles" \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$MONERO/contrib/depends/$HOST/share/toolchain.cmake" \
|
||||||
|
../..
|
||||||
|
make -j"$(sysctl -n hw.ncpu)" # produces libmonero-cpp.a
|
||||||
|
|
||||||
|
# 4. Merge into one archive for the app
|
||||||
|
libtool -static -o libkoins_monero_merged.a \
|
||||||
|
libmonero-cpp.a \
|
||||||
|
"$MONERO/build/ios-$PLATFORM/lib/libwallet.a" \
|
||||||
|
"$MONERO/build/ios-$PLATFORM"/src/*/lib*.a \
|
||||||
|
"$MONERO/build/ios-$PLATFORM"/external/*/lib*.a \
|
||||||
|
"$MONERO/build/ios-$PLATFORM"/contrib/epee/src/libepee.a \
|
||||||
|
"$MONERO/contrib/depends/$HOST"/lib/lib*.a
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the result to `libs/`, using `libkoins_monero_merged.a` for the simulator
|
||||||
|
and `libkoins_monero_device.a` for the device.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Only the simulator archive is needed to run in the simulator; the device
|
||||||
|
archive is needed for a real iPhone, and Xcode requires the matching one at
|
||||||
|
link time.
|
||||||
|
- Building the device slice after a simulator build requires moving the
|
||||||
|
simulator depends prefix aside first, since depends installs into
|
||||||
|
`contrib/depends/<host>` and both platforms share the host name.
|
||||||
|
- The app also links the OpenSSL that is embedded in this archive, which is how
|
||||||
|
the XRP wallet gets secp256k1 without an extra dependency.
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
define int_vars
|
||||||
|
#Set defaults for vars which may be overridden per-package
|
||||||
|
$(1)_cc=$($($(1)_type)_CC)
|
||||||
|
$(1)_cxx=$($($(1)_type)_CXX)
|
||||||
|
$(1)_objc=$($($(1)_type)_OBJC)
|
||||||
|
$(1)_objcxx=$($($(1)_type)_OBJCXX)
|
||||||
|
$(1)_ar=$($($(1)_type)_AR)
|
||||||
|
$(1)_ranlib=$($($(1)_type)_RANLIB)
|
||||||
|
$(1)_libtool=$($($(1)_type)_LIBTOOL)
|
||||||
|
$(1)_nm=$($($(1)_type)_NM)
|
||||||
|
$(1)_cflags=$($($(1)_type)_CFLAGS) $($($(1)_type)_$(release_type)_CFLAGS)
|
||||||
|
$(1)_cxxflags=$($($(1)_type)_CXXFLAGS) $($($(1)_type)_$(release_type)_CXXFLAGS)
|
||||||
|
$(1)_arflags=$($($(1)_type)_ARFLAGS) $($($(1)_type)_$(release_type)_ARFLAGS)
|
||||||
|
$(1)_ldflags=$($($(1)_type)_LDFLAGS) $($($(1)_type)_$(release_type)_LDFLAGS) -L$($($(1)_type)_prefix)/lib
|
||||||
|
$(1)_cppflags=$($($(1)_type)_CPPFLAGS) $($($(1)_type)_$(release_type)_CPPFLAGS) -I$($($(1)_type)_prefix)/include
|
||||||
|
$(1)_recipe_hash:=
|
||||||
|
endef
|
||||||
|
|
||||||
|
define int_get_all_dependencies
|
||||||
|
$(sort $(foreach dep,$(2),$(2) $(call int_get_all_dependencies,$(1),$($(dep)_dependencies))))
|
||||||
|
endef
|
||||||
|
|
||||||
|
define fetch_file_inner
|
||||||
|
( mkdir -p $$($(1)_download_dir) && echo Fetching $(3) from $(2) && \
|
||||||
|
$(build_DOWNLOAD) "$$($(1)_download_dir)/$(4).temp" "$(2)/$(3)" && \
|
||||||
|
echo "$(5) $$($(1)_download_dir)/$(4).temp" > $$($(1)_download_dir)/.$(4).hash && \
|
||||||
|
$(build_SHA256SUM) -c $$($(1)_download_dir)/.$(4).hash && \
|
||||||
|
mv $$($(1)_download_dir)/$(4).temp $$($(1)_source_dir)/$(4) && \
|
||||||
|
rm -rf $$($(1)_download_dir) )
|
||||||
|
endef
|
||||||
|
|
||||||
|
define fetch_file
|
||||||
|
( test -f $$($(1)_source_dir)/$(4) || \
|
||||||
|
( $(call fetch_file_inner,$(1),$(2),$(3),$(4),$(5)) || \
|
||||||
|
$(call fetch_file_inner,$(1),$(FALLBACK_DOWNLOAD_PATH),$(4),$(4),$(5))))
|
||||||
|
endef
|
||||||
|
|
||||||
|
define int_get_build_recipe_hash
|
||||||
|
$(eval $(1)_all_file_checksums:=$(shell $(build_SHA256SUM) $(meta_depends) packages/$(1).mk $(addprefix $(PATCHES_PATH)/$(1)/,$($(1)_patches)) | cut -d" " -f1))
|
||||||
|
$(eval $(1)_recipe_hash:=$(shell echo -n "$($(1)_all_file_checksums)" | $(build_SHA256SUM) | cut -d" " -f1))
|
||||||
|
endef
|
||||||
|
|
||||||
|
define int_get_build_id
|
||||||
|
$(eval $(1)_dependencies += $($(1)_$(host_arch)_$(host_os)_dependencies) $($(1)_$(host_os)_dependencies))
|
||||||
|
$(eval $(1)_all_dependencies:=$(call int_get_all_dependencies,$(1),$($($(1)_type)_native_toolchain) $($(1)_dependencies)))
|
||||||
|
$(foreach dep,$($(1)_all_dependencies),$(eval $(1)_build_id_deps+=$(dep)-$($(dep)_version)-$($(dep)_recipe_hash)))
|
||||||
|
$(eval $(1)_build_id_long:=$(1)-$($(1)_version)-$($(1)_recipe_hash)-$(release_type) $($(1)_build_id_deps) $($($(1)_type)_id_string))
|
||||||
|
$(eval $(1)_build_id:=$(shell echo -n "$($(1)_build_id_long)" | $(build_SHA256SUM) | cut -c-$(HASH_LENGTH)))
|
||||||
|
final_build_id_long+=$($(package)_build_id_long)
|
||||||
|
|
||||||
|
#compute package-specific paths
|
||||||
|
$(1)_build_subdir?=.
|
||||||
|
$(1)_download_file?=$($(1)_file_name)
|
||||||
|
$(1)_source_dir:=$(SOURCES_PATH)
|
||||||
|
$(1)_source:=$$($(1)_source_dir)/$($(1)_file_name)
|
||||||
|
$(1)_staging_dir=$(base_staging_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)
|
||||||
|
$(1)_staging_prefix_dir:=$$($(1)_staging_dir)$($($(1)_type)_prefix)
|
||||||
|
$(1)_extract_dir:=$(base_build_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)
|
||||||
|
$(1)_download_dir:=$(base_download_dir)/$(1)-$($(1)_version)
|
||||||
|
$(1)_build_dir:=$$($(1)_extract_dir)/$$($(1)_build_subdir)
|
||||||
|
$(1)_cached_checksum:=$(BASE_CACHE)/$(host)/$(1)/$(1)-$($(1)_version)-$($(1)_build_id).tar.gz.hash
|
||||||
|
$(1)_patch_dir:=$(base_build_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)/.patches-$($(1)_build_id)
|
||||||
|
$(1)_prefixbin:=$($($(1)_type)_prefix)/bin/
|
||||||
|
$(1)_cached:=$(BASE_CACHE)/$(host)/$(1)/$(1)-$($(1)_version)-$($(1)_build_id).tar.gz
|
||||||
|
$(1)_all_sources=$($(1)_file_name) $($(1)_extra_sources)
|
||||||
|
|
||||||
|
#stamps
|
||||||
|
$(1)_fetched=$(SOURCES_PATH)/download-stamps/.stamp_fetched-$(1)-$($(1)_file_name).hash
|
||||||
|
$(1)_extracted=$$($(1)_extract_dir)/.stamp_extracted
|
||||||
|
$(1)_preprocessed=$$($(1)_extract_dir)/.stamp_preprocessed
|
||||||
|
$(1)_cleaned=$$($(1)_extract_dir)/.stamp_cleaned
|
||||||
|
$(1)_built=$$($(1)_build_dir)/.stamp_built
|
||||||
|
$(1)_configured=$$($(1)_build_dir)/.stamp_configured
|
||||||
|
$(1)_staged=$$($(1)_staging_dir)/.stamp_staged
|
||||||
|
$(1)_postprocessed=$$($(1)_staging_prefix_dir)/.stamp_postprocessed
|
||||||
|
$(1)_download_path_fixed=$(subst :,\:,$$($(1)_download_path))
|
||||||
|
|
||||||
|
|
||||||
|
#default commands
|
||||||
|
$(1)_fetch_cmds ?= $(call fetch_file,$(1),$(subst \:,:,$$($(1)_download_path_fixed)),$$($(1)_download_file),$($(1)_file_name),$($(1)_sha256_hash))
|
||||||
|
$(1)_extract_cmds ?= mkdir -p $$($(1)_extract_dir) && echo "$$($(1)_sha256_hash) $$($(1)_source)" > $$($(1)_extract_dir)/.$$($(1)_file_name).hash && $(build_SHA256SUM) -c $$($(1)_extract_dir)/.$$($(1)_file_name).hash && tar --strip-components=1 -xf $$($(1)_source)
|
||||||
|
$(1)_preprocess_cmds ?=
|
||||||
|
$(1)_build_cmds ?=
|
||||||
|
$(1)_config_cmds ?=
|
||||||
|
$(1)_stage_cmds ?=
|
||||||
|
$(1)_set_vars ?=
|
||||||
|
|
||||||
|
|
||||||
|
all_sources+=$$($(1)_fetched)
|
||||||
|
endef
|
||||||
|
#$(foreach dep_target,$($(1)_all_dependencies),$(eval $(1)_dependency_targets=$($(dep_target)_cached)))
|
||||||
|
|
||||||
|
|
||||||
|
define int_config_attach_build_config
|
||||||
|
$(eval $(call $(1)_set_vars,$(1)))
|
||||||
|
$(1)_cflags+=$($(1)_cflags_$(release_type))
|
||||||
|
$(1)_cflags+=$($(1)_cflags_$(host_arch)) $($(1)_cflags_$(host_arch)_$(release_type))
|
||||||
|
$(1)_cflags+=$($(1)_cflags_$(host_os)) $($(1)_cflags_$(host_os)_$(release_type))
|
||||||
|
$(1)_cflags+=$($(1)_cflags_$(host_arch)_$(host_os)) $($(1)_cflags_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_cxxflags+=$($(1)_cxxflags_$(release_type))
|
||||||
|
$(1)_cxxflags+=$($(1)_cxxflags_$(host_arch)) $($(1)_cxxflags_$(host_arch)_$(release_type))
|
||||||
|
$(1)_cxxflags+=$($(1)_cxxflags_$(host_os)) $($(1)_cxxflags_$(host_os)_$(release_type))
|
||||||
|
$(1)_cxxflags+=$($(1)_cxxflags_$(host_arch)_$(host_os)) $($(1)_cxxflags_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_arflags+=$($(1)_arflags_$(release_type))
|
||||||
|
$(1)_arflags+=$($(1)_arflags_$(host_arch)) $($(1)_arflags_$(host_arch)_$(release_type))
|
||||||
|
$(1)_arflags+=$($(1)_arflags_$(host_os)) $($(1)_arflags_$(host_os)_$(release_type))
|
||||||
|
$(1)_arflags+=$($(1)_arflags_$(host_arch)_$(host_os)) $($(1)_arflags_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_cppflags+=$($(1)_cppflags_$(release_type))
|
||||||
|
$(1)_cppflags+=$($(1)_cppflags_$(host_arch)) $($(1)_cppflags_$(host_arch)_$(release_type))
|
||||||
|
$(1)_cppflags+=$($(1)_cppflags_$(host_os)) $($(1)_cppflags_$(host_os)_$(release_type))
|
||||||
|
$(1)_cppflags+=$($(1)_cppflags_$(host_arch)_$(host_os)) $($(1)_cppflags_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_ldflags+=$($(1)_ldflags_$(release_type))
|
||||||
|
$(1)_ldflags+=$($(1)_ldflags_$(host_arch)) $($(1)_ldflags_$(host_arch)_$(release_type))
|
||||||
|
$(1)_ldflags+=$($(1)_ldflags_$(host_os)) $($(1)_ldflags_$(host_os)_$(release_type))
|
||||||
|
$(1)_ldflags+=$($(1)_ldflags_$(host_arch)_$(host_os)) $($(1)_ldflags_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_build_opts+=$$($(1)_build_opts_$(release_type))
|
||||||
|
$(1)_build_opts+=$$($(1)_build_opts_$(host_arch)) $$($(1)_build_opts_$(host_arch)_$(release_type))
|
||||||
|
$(1)_build_opts+=$$($(1)_build_opts_$(host_os)) $$($(1)_build_opts_$(host_os)_$(release_type))
|
||||||
|
$(1)_build_opts+=$$($(1)_build_opts_$(host_arch)_$(host_os)) $$($(1)_build_opts_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_config_opts+=$$($(1)_config_opts_$(release_type))
|
||||||
|
$(1)_config_opts+=$$($(1)_config_opts_$(host_arch)) $$($(1)_config_opts_$(host_arch)_$(release_type))
|
||||||
|
$(1)_config_opts+=$$($(1)_config_opts_$(host_os)) $$($(1)_config_opts_$(host_os)_$(release_type))
|
||||||
|
$(1)_config_opts+=$$($(1)_config_opts_$(host_arch)_$(host_os)) $$($(1)_config_opts_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_config_env+=$$($(1)_config_env_$(release_type))
|
||||||
|
$(1)_config_env+=$($(1)_config_env_$(host_arch)) $($(1)_config_env_$(host_arch)_$(release_type))
|
||||||
|
$(1)_config_env+=$($(1)_config_env_$(host_os)) $($(1)_config_env_$(host_os)_$(release_type))
|
||||||
|
$(1)_config_env+=$($(1)_config_env_$(host_arch)_$(host_os)) $($(1)_config_env_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_build_env+=$$($(1)_build_env_$(release_type))
|
||||||
|
$(1)_build_env+=$($(1)_build_env_$(host_arch)) $($(1)_build_env_$(host_arch)_$(release_type))
|
||||||
|
$(1)_build_env+=$($(1)_build_env_$(host_os)) $($(1)_build_env_$(host_os)_$(release_type))
|
||||||
|
$(1)_build_env+=$($(1)_build_env_$(host_arch)_$(host_os)) $($(1)_build_env_$(host_arch)_$(host_os)_$(release_type))
|
||||||
|
|
||||||
|
$(1)_config_env+=PKG_CONFIG_LIBDIR=$($($(1)_type)_prefix)/lib/pkgconfig
|
||||||
|
$(1)_config_env+=PKG_CONFIG_PATH=$($($(1)_type)_prefix)/share/pkgconfig
|
||||||
|
$(1)_config_env+=PATH="$(build_prefix)/bin:$(PATH)"
|
||||||
|
$(1)_build_env+=PATH="$(build_prefix)/bin:$(PATH)"
|
||||||
|
$(1)_stage_env+=PATH="$(build_prefix)/bin:$(PATH)"
|
||||||
|
$(1)_autoconf=./configure --build=$(BUILD) --host=$($($(1)_type)_host) --prefix=$($($(1)_type)_prefix) $$($(1)_config_opts) CC="$$($(1)_cc)" CXX="$$($(1)_cxx)"
|
||||||
|
|
||||||
|
ifeq ($(filter $(1),libusb unbound),)
|
||||||
|
$(1)_autoconf += --disable-dependency-tracking
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_nm),)
|
||||||
|
$(1)_autoconf += NM="$$($(1)_nm)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_ranlib),)
|
||||||
|
$(1)_autoconf += RANLIB="$$($(1)_ranlib)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_ar),)
|
||||||
|
$(1)_autoconf += AR="$$($(1)_ar)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_arflags),)
|
||||||
|
$(1)_autoconf += ARFLAGS="$$($(1)_arflags)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_cflags),)
|
||||||
|
$(1)_autoconf += CFLAGS="$$($(1)_cflags)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_cxxflags),)
|
||||||
|
$(1)_autoconf += CXXFLAGS="$$($(1)_cxxflags)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_cppflags),)
|
||||||
|
$(1)_autoconf += CPPFLAGS="$$($(1)_cppflags)"
|
||||||
|
endif
|
||||||
|
ifneq ($($(1)_ldflags),)
|
||||||
|
$(1)_autoconf += LDFLAGS="$$($(1)_ldflags)"
|
||||||
|
endif
|
||||||
|
endef
|
||||||
|
|
||||||
|
define int_add_cmds
|
||||||
|
$($(1)_fetched):
|
||||||
|
$(AT)mkdir -p $$(@D) $(SOURCES_PATH)
|
||||||
|
$(AT)rm -f $$@
|
||||||
|
$(AT)touch $$@
|
||||||
|
$(AT)cd $$(@D); $(call $(1)_fetch_cmds,$(1))
|
||||||
|
$(AT)cd $($(1)_source_dir); $(foreach source,$($(1)_all_sources),$(build_SHA256SUM) $(source) >> $$(@);)
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_extracted): | $($(1)_fetched)
|
||||||
|
$(AT)echo Extracting $(1)...
|
||||||
|
$(AT)mkdir -p $$(@D)
|
||||||
|
$(AT)cd $$(@D); $(call $(1)_extract_cmds,$(1))
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_preprocessed): | $($(1)_dependencies) $($(1)_extracted)
|
||||||
|
$(AT)echo Preprocessing $(1)...
|
||||||
|
$(AT)mkdir -p $$(@D) $($(1)_patch_dir)
|
||||||
|
$(AT)$(foreach patch,$($(1)_patches),cd $(PATCHES_PATH)/$(1); cp $(patch) $($(1)_patch_dir) ;)
|
||||||
|
$(AT)cd $$(@D); $(call $(1)_preprocess_cmds, $(1))
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_configured): | $($(1)_preprocessed)
|
||||||
|
$(AT)echo Configuring $(1)...
|
||||||
|
$(AT)rm -rf $(host_prefix); mkdir -p $(host_prefix)/lib; cd $(host_prefix); $(foreach package,$($(1)_all_dependencies), $(if $(strip $($(package)_cached)), tar xf $($(package)_cached);,))
|
||||||
|
$(AT)mkdir -p $$(@D)
|
||||||
|
$(AT)+cd $$(@D); $($(1)_config_env) $(call $(1)_config_cmds, $(1))
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_built): | $($(1)_configured)
|
||||||
|
$(AT)echo Building $(1)...
|
||||||
|
$(AT)mkdir -p $$(@D)
|
||||||
|
$(AT)+cd $$(@D); $($(1)_build_env) $(call $(1)_build_cmds, $(1))
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_staged): | $($(1)_built)
|
||||||
|
$(AT)echo Staging $(1)...
|
||||||
|
$(AT)mkdir -p $($(1)_staging_dir)/$(host_prefix)
|
||||||
|
$(AT)cd $($(1)_build_dir); $($(1)_stage_env) $(call $(1)_stage_cmds, $(1))
|
||||||
|
$(AT)rm -rf $($(1)_extract_dir)
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_postprocessed): | $($(1)_staged)
|
||||||
|
$(AT)echo Postprocessing $(1)...
|
||||||
|
$(AT)cd $($(1)_staging_prefix_dir); $(call $(1)_postprocess_cmds)
|
||||||
|
$(AT)touch $$@
|
||||||
|
$($(1)_cached): | $($(1)_dependencies) $($(1)_postprocessed)
|
||||||
|
$(AT)echo Caching $(1)...
|
||||||
|
$(AT)cd $$($(1)_staging_dir)/$(host_prefix); find . | sort | tar --no-recursion -czf $$($(1)_staging_dir)/$$(@F) -T -
|
||||||
|
$(AT)mkdir -p $$(@D)
|
||||||
|
$(AT)rm -rf $$(@D) && mkdir -p $$(@D)
|
||||||
|
$(AT)mv $$($(1)_staging_dir)/$$(@F) $$(@)
|
||||||
|
$(AT)rm -rf $($(1)_staging_dir)
|
||||||
|
$($(1)_cached_checksum): $($(1)_cached)
|
||||||
|
$(AT)cd $$(@D); $(build_SHA256SUM) $$(<F) > $$(@)
|
||||||
|
|
||||||
|
.PHONY: $(1)
|
||||||
|
$(1): | $($(1)_cached_checksum)
|
||||||
|
.SECONDARY: $($(1)_cached) $($(1)_postprocessed) $($(1)_staged) $($(1)_built) $($(1)_configured) $($(1)_preprocessed) $($(1)_extracted) $($(1)_fetched)
|
||||||
|
|
||||||
|
endef
|
||||||
|
|
||||||
|
stages = fetched extracted preprocessed configured built staged postprocessed cached cached_checksum
|
||||||
|
|
||||||
|
define ext_add_stages
|
||||||
|
$(foreach stage,$(stages),
|
||||||
|
$(1)_$(stage): $($(1)_$(stage))
|
||||||
|
.PHONY: $(1)_$(stage))
|
||||||
|
endef
|
||||||
|
|
||||||
|
# These functions create the build targets for each package. They must be
|
||||||
|
# broken down into small steps so that each part is done for all packages
|
||||||
|
# before moving on to the next step. Otherwise, a package's info
|
||||||
|
# (build-id for example) would only be available to another package if it
|
||||||
|
# happened to be computed already.
|
||||||
|
|
||||||
|
#set the type for host/build packages.
|
||||||
|
$(foreach native_package,$(native_packages),$(eval $(native_package)_type=build))
|
||||||
|
$(foreach package,$(packages),$(eval $(package)_type=$(host_arch)_$(host_os)))
|
||||||
|
|
||||||
|
#set overridable defaults
|
||||||
|
$(foreach package,$(all_packages),$(eval $(call int_vars,$(package))))
|
||||||
|
|
||||||
|
#include package files
|
||||||
|
$(foreach package,$(all_packages),$(eval include packages/$(package).mk))
|
||||||
|
|
||||||
|
#compute a hash of all files that comprise this package's build recipe
|
||||||
|
$(foreach package,$(all_packages),$(eval $(call int_get_build_recipe_hash,$(package))))
|
||||||
|
|
||||||
|
#generate a unique id for this package, incorporating its dependencies as well
|
||||||
|
$(foreach package,$(all_packages),$(eval $(call int_get_build_id,$(package))))
|
||||||
|
|
||||||
|
#compute final vars after reading package vars
|
||||||
|
$(foreach package,$(all_packages),$(eval $(call int_config_attach_build_config,$(package))))
|
||||||
|
|
||||||
|
#create build targets
|
||||||
|
$(foreach package,$(all_packages),$(eval $(call int_add_cmds,$(package))))
|
||||||
|
|
||||||
|
#special exception: if a toolchain package exists, all non-native packages depend on it
|
||||||
|
$(foreach package,$(packages),$(eval $($(package)_unpacked): |$($($(host_arch)_$(host_os)_native_toolchain)_cached) ))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# iOS host for monero depends system (arm64 simulator for dev).
|
||||||
|
# Targets modern iOS (14+) on Apple Silicon simulators / devices.
|
||||||
|
|
||||||
|
IOS_MIN_VERSION=13.0
|
||||||
|
LD64_VERSION=609
|
||||||
|
|
||||||
|
ifndef IOS_PLATFORM
|
||||||
|
IOS_PLATFORM=SIMULATOR
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(IOS_PLATFORM),SIMULATOR)
|
||||||
|
IOS_SDK_ROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
|
||||||
|
CC_target=aarch64-apple-ios-simulator
|
||||||
|
else
|
||||||
|
IOS_SDK_ROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
|
||||||
|
CC_target=aarch64-apple-ios
|
||||||
|
endif
|
||||||
|
|
||||||
|
ios_CC=clang -target $(CC_target) -miphoneos-version-min=$(IOS_MIN_VERSION) --sysroot $(IOS_SDK_ROOT) -mlinker-version=$(LD64_VERSION)
|
||||||
|
ios_CXX=clang++ -target $(CC_target) -miphoneos-version-min=$(IOS_MIN_VERSION) --sysroot $(IOS_SDK_ROOT) -mlinker-version=$(LD64_VERSION) -stdlib=libc++
|
||||||
|
|
||||||
|
ios_CFLAGS=-pipe -isysroot $(IOS_SDK_ROOT)
|
||||||
|
ios_CXXFLAGS=-pipe -isysroot $(IOS_SDK_ROOT) -std=c++14 -stdlib=libc++
|
||||||
|
ios_ARFLAGS=cr
|
||||||
|
|
||||||
|
ios_release_CFLAGS=-O1
|
||||||
|
ios_release_CXXFLAGS=$(ios_release_CFLAGS)
|
||||||
|
|
||||||
|
ios_debug_CFLAGS=-O1
|
||||||
|
ios_debug_CXXFLAGS=$(ios_debug_CFLAGS)
|
||||||
|
|
||||||
|
ios_native_toolchain=native_cctools darwin_sdk
|
||||||
|
|
||||||
|
# iOS uses the system (macOS) ar/ranlib/strip/nm for cross-compiled archives.
|
||||||
|
ios_AR=ar
|
||||||
|
ios_RANLIB=ranlib
|
||||||
|
ios_STRIP=strip
|
||||||
|
ios_NM=nm
|
||||||
|
ios_LIBTOOL=libtool
|
||||||
|
ios_OTOOL=otool
|
||||||
|
ios_INSTALL_NAME_TOOL=install_name_tool
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package=boost
|
||||||
|
$(package)_version=1.76.0
|
||||||
|
$(package)_download_path=https://archives.boost.io/release/$($(package)_version)/source/
|
||||||
|
$(package)_file_name=$(package)_$(subst .,_,$($(package)_version)).tar.gz
|
||||||
|
$(package)_sha256_hash=7bd7ddceec1a1dfdcbdb3e609b60d01739c38390a5f956385a12f3122049f0ca
|
||||||
|
$(package)_dependencies=
|
||||||
|
$(package)_patches=fix_integral_wrapper.patch
|
||||||
|
|
||||||
|
define $(package)_set_vars
|
||||||
|
$(package)_config_opts_release=variant=release
|
||||||
|
$(package)_config_opts_debug=variant=debug
|
||||||
|
$(package)_config_opts+=--layout=system --user-config=user-config.jam
|
||||||
|
$(package)_config_opts+=threading=multi link=static -sNO_BZIP2=1 -sNO_ZLIB=1
|
||||||
|
$(package)_config_opts_linux=threadapi=pthread runtime-link=shared
|
||||||
|
$(package)_config_opts_android=threadapi=pthread runtime-link=static target-os=android
|
||||||
|
$(package)_config_opts_darwin=--toolset=darwin runtime-link=shared
|
||||||
|
$(package)_config_opts_mingw32=binary-format=pe target-os=windows threadapi=win32 runtime-link=static
|
||||||
|
$(package)_config_opts_x86_64_mingw32=address-model=64
|
||||||
|
$(package)_config_opts_i686_mingw32=address-model=32
|
||||||
|
$(package)_config_opts_i686_linux=address-model=32 architecture=x86
|
||||||
|
$(package)_toolset_$(host_os)=gcc
|
||||||
|
$(package)_archiver_$(host_os)=$($(package)_ar)
|
||||||
|
$(package)_toolset_darwin=darwin
|
||||||
|
$(package)_archiver_darwin=$($(package)_libtool)
|
||||||
|
$(package)_config_libraries=chrono,filesystem,program_options,system,thread,test,date_time,regex,serialization,locale
|
||||||
|
$(package)_toolset_ios=darwin
|
||||||
|
$(package)_archiver_ios=libtool
|
||||||
|
$(package)_arflags_ios=
|
||||||
|
$(package)_cxxflags_ios=-fPIC -std=c++11 -stdlib=libc++
|
||||||
|
$(package)_cxxflags=-std=c++11
|
||||||
|
$(package)_cxxflags_linux=-fPIC
|
||||||
|
$(package)_cxxflags_freebsd=-fPIC
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_preprocess_cmds
|
||||||
|
patch -p1 < $($(package)_patch_dir)/fix_integral_wrapper.patch &&\
|
||||||
|
echo "using $(boost_toolset_$(host_os)) : : $($(package)_cxx) : <root>\"$(IOS_SDK_ROOT)\" <cxxflags>\"$($(package)_cxxflags) $($(package)_cppflags) -isysroot $(IOS_SDK_ROOT)\" <linkflags>\"$($(package)_ldflags) -isysroot $(IOS_SDK_ROOT)\" <archiver>\"$(boost_archiver_$(host_os))\" <arflags>\"$($(package)_arflags)\" <striper>\"$(host_STRIP)\" <ranlib>\"$(host_RANLIB)\" <rc>\"$(host_WINDRES)\" : ;" > user-config.jam
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_config_cmds
|
||||||
|
./bootstrap.sh --with-libraries=$(boost_config_libraries)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_stage_cmds
|
||||||
|
./b2 -d0 -j4 --prefix=$($(package)_staging_prefix_dir) $($(package)_config_opts) install
|
||||||
|
endef
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
packages:=boost openssl zeromq expat unbound
|
||||||
|
|
||||||
|
# iOS: iconv is provided by the system; libiconv-1.15 fails with modern clang.
|
||||||
|
ios_packages := boost openssl zeromq expat unbound sodium
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ccache is useless in gitian builds
|
||||||
|
ifneq ($(GITIAN),1)
|
||||||
|
native_packages := native_ccache
|
||||||
|
endif
|
||||||
|
|
||||||
|
hardware_packages := hidapi protobuf libusb
|
||||||
|
hardware_native_packages := native_protobuf
|
||||||
|
|
||||||
|
android_native_packages = android_ndk
|
||||||
|
android_packages = ncurses readline sodium
|
||||||
|
|
||||||
|
darwin_native_packages = $(hardware_native_packages)
|
||||||
|
darwin_packages = ncurses readline sodium $(hardware_packages)
|
||||||
|
|
||||||
|
# not really native...
|
||||||
|
freebsd_native_packages = freebsd_base
|
||||||
|
freebsd_packages = ncurses readline sodium
|
||||||
|
|
||||||
|
linux_packages = eudev ncurses readline sodium $(hardware_packages)
|
||||||
|
linux_native_packages = $(hardware_native_packages)
|
||||||
|
|
||||||
|
ifeq ($(build_tests),ON)
|
||||||
|
packages += gtest
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifneq ($(host_arch),riscv64)
|
||||||
|
linux_packages += unwind
|
||||||
|
endif
|
||||||
|
|
||||||
|
mingw32_packages = icu4c sodium $(hardware_packages)
|
||||||
|
mingw32_native_packages = $(hardware_native_packages)
|
||||||
|
|
||||||
|
ifneq ($(build_os),darwin)
|
||||||
|
darwin_native_packages += darwin_sdk native_clang native_cctools native_libtapi
|
||||||
|
endif
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package=sodium
|
||||||
|
$(package)_version=1.0.18
|
||||||
|
$(package)_download_path=https://download.libsodium.org/libsodium/releases/
|
||||||
|
$(package)_file_name=libsodium-$($(package)_version).tar.gz
|
||||||
|
$(package)_sha256_hash=6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1
|
||||||
|
$(package)_patches=disable-glibc-getrandom-getentropy.patch fix-whitespace.patch
|
||||||
|
|
||||||
|
define $(package)_set_vars
|
||||||
|
$(package)_config_opts=--enable-static --disable-shared
|
||||||
|
$(package)_config_opts+=--prefix=$(host_prefix)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_preprocess_cmds
|
||||||
|
patch -p1 < $($(package)_patch_dir)/fix-whitespace.patch
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_config_cmds
|
||||||
|
$($(package)_autoconf) AR_FLAGS=$($(package)_arflags)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_build_cmds
|
||||||
|
$(MAKE)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_stage_cmds
|
||||||
|
$(MAKE) DESTDIR=$($(package)_staging_dir) install
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_postprocess_cmds
|
||||||
|
rm lib/*.la
|
||||||
|
endef
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package=unbound
|
||||||
|
$(package)_version=1.25.1
|
||||||
|
$(package)_download_path=https://www.nlnetlabs.nl/downloads/$(package)/
|
||||||
|
$(package)_file_name=$(package)-$($(package)_version).tar.gz
|
||||||
|
$(package)_sha256_hash=0fe8b6277b0959cfd17562debac0aa5f71e0b02dc4ffa9c60271c583edab586f
|
||||||
|
$(package)_dependencies=openssl expat
|
||||||
|
$(package)_patches=disable-glibc-reallocarray.patch hardcode_config_date.patch
|
||||||
|
|
||||||
|
|
||||||
|
define $(package)_set_vars
|
||||||
|
$(package)_config_opts=--disable-shared --enable-static --without-pyunbound --prefix=$(host_prefix)
|
||||||
|
$(package)_config_opts+=--with-libexpat=$(host_prefix) --with-ssl=$(host_prefix) --with-libevent=no
|
||||||
|
$(package)_config_opts+=--without-pythonmodule --disable-flto --with-pthreads --with-libunbound-only
|
||||||
|
$(package)_config_opts_linux=--with-pic
|
||||||
|
$(package)_config_opts_w64=--enable-static-exe --sysconfdir=/etc --prefix=$(host_prefix) --target=$(host_prefix)
|
||||||
|
$(package)_config_opts_x86_64_darwin=ac_cv_func_SHA384_Init=yes
|
||||||
|
$(package)_build_opts_mingw32=LDFLAGS="$($(package)_ldflags) -lpthread"
|
||||||
|
$(package)_cflags_mingw32+="-D_WIN32_WINNT=0x600"
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_preprocess_cmds
|
||||||
|
rm doc/*.odp doc/*.pdf contrib/*.tar.gz contrib/*.tar.bz2 &&\
|
||||||
|
rm -rf testdata dnscrypt/testdata
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_config_cmds
|
||||||
|
$($(package)_autoconf) ac_cv_func_getentropy=no
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_build_cmds
|
||||||
|
$(MAKE) $($(package)_build_opts)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_stage_cmds
|
||||||
|
$(MAKE) DESTDIR=$($(package)_staging_dir) install
|
||||||
|
endef
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package=zeromq
|
||||||
|
$(package)_version=4.3.4
|
||||||
|
$(package)_download_path=https://github.com/zeromq/libzmq/releases/download/v$($(package)_version)/
|
||||||
|
$(package)_file_name=$(package)-$($(package)_version).tar.gz
|
||||||
|
$(package)_sha256_hash=c593001a89f5a85dd2ddf564805deb860e02471171b3f204944857336295c3e5
|
||||||
|
$(package)_patches=06aba27b04c5822cb88a69677382a0f053367143.patch
|
||||||
|
|
||||||
|
define $(package)_set_vars
|
||||||
|
$(package)_config_opts=--without-documentation --disable-shared --without-libsodium --disable-curve
|
||||||
|
$(package)_config_opts_linux=--with-pic
|
||||||
|
$(package)_config_opts_freebsd=--with-pic
|
||||||
|
$(package)_config_opts_ios=--host=aarch64-apple-darwin --with-pic
|
||||||
|
$(package)_cxxflags=-std=c++11
|
||||||
|
$(package)_cxxflags_ios=-std=c++11 -isysroot $(IOS_SDK_ROOT) -stdlib=libc++ -Wno-deprecated-declarations
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_preprocess_cmds
|
||||||
|
patch -p1 < $($(package)_patch_dir)/06aba27b04c5822cb88a69677382a0f053367143.patch
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_config_cmds
|
||||||
|
$($(package)_autoconf) AR_FLAGS=$($(package)_arflags)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_build_cmds
|
||||||
|
$(MAKE) src/libzmq.la
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_stage_cmds
|
||||||
|
$(MAKE) DESTDIR=$($(package)_staging_dir) install-libLTLIBRARIES install-includeHEADERS install-pkgconfigDATA
|
||||||
|
endef
|
||||||
|
|
||||||
|
define $(package)_postprocess_cmds
|
||||||
|
rm -rf bin share &&\
|
||||||
|
rm lib/*.la
|
||||||
|
endef
|
||||||
|
|
||||||