Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af6251669a | ||
|
|
6eec1f998d | ||
|
|
d9ca8a0047 |
@@ -1,9 +1,10 @@
|
|||||||
# Instructions for AI assistants
|
# Instructions for AI assistants
|
||||||
|
|
||||||
- Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command.
|
- Never commit changes unless explicitly told to do so. Always ask first or wait for the user's command.
|
||||||
- "Apply the changes" / "apply it" / "do it" does NOT mean commit. It means make the file changes. Before any commit (main repo, tap repo, anywhere), stop and ask for explicit permission, and list which files will be committed. Wait for a clear "yes" / "commit it" before running `git commit`.
|
- "Apply the changes" / "apply it" / "do it" does NOT mean commit. It means make the file changes. Before any commit (main repo, tap repo, anywhere), stop and ask for explicit permission, and list which files will be committed. Wait for a clear "yes" / "commit it" / "commit and push" before running `git commit`.
|
||||||
- Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location.
|
- Never manually edit `.router/router.ts` — sv-router auto-generates it from the filesystem. Just create the route file in the correct filesystem location.
|
||||||
- Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message.
|
- Only commit when explicitly told to do so. When asked to commit, always write a single simple line commit message.
|
||||||
- When asked to publish, always rebuild the DMG first (`bun run build:stable` from `apps/desktop`), wait for notarization to complete, verify with `xcrun stapler validate`, then publish the fresh DMG. Never publish a stale build.
|
- When asked to publish, always rebuild the DMG first (`bun run build:stable` from `apps/desktop`), wait for notarization to complete, verify with `xcrun stapler validate`, then publish the fresh DMG. Never publish a stale build.
|
||||||
- Never publish without explicit permission. Before any publish action — running `bun run build:stable`, `gh release upload`, updating the Homebrew cask, or pushing the tap repo — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed — the affirmative applies to the action you described, and you must still pause before each subsequent publish step (e.g. upload, cask update) to ask for permission again.
|
- Never publish without explicit permission. Before any publish action — running `bun run build:stable`, `gh release upload`, updating the Homebrew cask, or pushing the tap repo — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed through the whole pipeline without pausing between steps: build, notarize, verify, upload to the GitHub release, update the Homebrew cask, and push the tap repo. One permission covers the entire publish flow. You must still get a separate permission before starting each new publish cycle.
|
||||||
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending."
|
- Permission to commit ("commit", "commit and push", or similar) authorizes `git commit` and also `git push` of the committed changes. Only push the branches/files described in the permission.
|
||||||
|
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending." List each pending item as a numbered entry (`1.`, `2.`, …) so the user can approve or act on them by number (e.g. "go ahead with 1, 2").
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
app: {
|
app: {
|
||||||
name: 'koins',
|
name: 'koins',
|
||||||
identifier: 'koins.tzgyn.com',
|
identifier: 'koins.tzgyn.com',
|
||||||
version: '0.0.3',
|
version: '0.0.5',
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// Vite builds to dist/, we copy from there
|
// Vite builds to dist/, we copy from there
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import {
|
import { MoneroWalletRpc, MoneroRpcConnection, connectToWalletRpc, connectToDaemonRpc } from 'monero-ts'
|
||||||
MoneroWalletRpc,
|
|
||||||
MoneroRpcConnection,
|
|
||||||
connectToWalletRpc,
|
|
||||||
connectToDaemonRpc,
|
|
||||||
} from 'monero-ts'
|
|
||||||
import { getBinaryPath, getWalletDir } from './binary'
|
import { getBinaryPath, getWalletDir } from './binary'
|
||||||
import { existsSync, readdirSync } from 'fs'
|
import { existsSync, readdirSync } from 'fs'
|
||||||
|
import { log, logError } from '../logger'
|
||||||
|
|
||||||
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
|
const DEFAULT_DAEMON = 'xmr-node.cakewallet.com:18081'
|
||||||
|
|
||||||
@@ -65,20 +61,13 @@ 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 conn.sendJsonRequest('get_version', {})
|
||||||
port: state.rpcPort,
|
if (res?.result?.version) {
|
||||||
socket: {
|
|
||||||
open(s) {
|
|
||||||
s.end()
|
|
||||||
},
|
|
||||||
close() {},
|
|
||||||
data() {},
|
|
||||||
error() {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
console.log(`[monero] RPC ready after ${Date.now() - start}ms`)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
lastErr = JSON.stringify(res)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = String(e)
|
lastErr = String(e)
|
||||||
}
|
}
|
||||||
@@ -89,6 +78,28 @@ async function waitForRpc(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function connectClient(
|
||||||
|
state: MoneroWalletState,
|
||||||
|
): Promise<MoneroWalletRpc> {
|
||||||
|
let lastErr = ''
|
||||||
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
|
try {
|
||||||
|
const client = (await connectToWalletRpc(
|
||||||
|
rpcUrl(state),
|
||||||
|
state.rpcUser,
|
||||||
|
state.rpcPassword,
|
||||||
|
)) as MoneroWalletRpc
|
||||||
|
console.log(`[monero] MoneroWalletRpc connected (attempt ${attempt})`)
|
||||||
|
return client
|
||||||
|
} catch (e) {
|
||||||
|
lastErr = String(e)
|
||||||
|
console.log(`[monero] connectToWalletRpc attempt ${attempt} failed:`, e)
|
||||||
|
if (attempt < 3) await new Promise((r) => setTimeout(r, 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to connect to monero-wallet-rpc: ${lastErr}`)
|
||||||
|
}
|
||||||
|
|
||||||
export async function start(
|
export async function start(
|
||||||
state: MoneroWalletState,
|
state: MoneroWalletState,
|
||||||
daemonAddress: string = DEFAULT_DAEMON,
|
daemonAddress: string = DEFAULT_DAEMON,
|
||||||
@@ -169,6 +180,9 @@ export async function start(
|
|||||||
`[monero] wallet-rpc process exited with code ${exitCode}`,
|
`[monero] wallet-rpc process exited with code ${exitCode}`,
|
||||||
error?.message ?? '',
|
error?.message ?? '',
|
||||||
)
|
)
|
||||||
|
log(
|
||||||
|
`[monero] wallet-rpc process exited with code ${exitCode} ${error?.message ?? ''}`,
|
||||||
|
)
|
||||||
if (state.saveTimer) {
|
if (state.saveTimer) {
|
||||||
clearInterval(state.saveTimer)
|
clearInterval(state.saveTimer)
|
||||||
state.saveTimer = null
|
state.saveTimer = null
|
||||||
@@ -222,12 +236,9 @@ export async function start(
|
|||||||
console.log(`[monero] RPC server is ready`)
|
console.log(`[monero] RPC server is ready`)
|
||||||
|
|
||||||
console.log(`[monero] connecting MoneroWalletRpc client...`)
|
console.log(`[monero] connecting MoneroWalletRpc client...`)
|
||||||
state.wallet = (await connectToWalletRpc(
|
state.wallet = await connectClient(state)
|
||||||
rpcUrl(state),
|
|
||||||
state.rpcUser,
|
|
||||||
state.rpcPassword,
|
|
||||||
)) as MoneroWalletRpc
|
|
||||||
console.log(`[monero] MoneroWalletRpc connected successfully`)
|
console.log(`[monero] MoneroWalletRpc connected successfully`)
|
||||||
|
log(`[monero] wallet-rpc started, RPC ready`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stop(state: MoneroWalletState) {
|
export async function stop(state: MoneroWalletState) {
|
||||||
@@ -241,21 +252,21 @@ export async function stop(state: MoneroWalletState) {
|
|||||||
if (state.wallet) {
|
if (state.wallet) {
|
||||||
await rawRpc(state, 'store')
|
await rawRpc(state, 'store')
|
||||||
console.log(`[monero] wallet saved before stop`)
|
console.log(`[monero] wallet saved before stop`)
|
||||||
|
log(`[monero] wallet saved before stop`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[monero] wallet save before stop failed:`, e)
|
console.log(`[monero] wallet save before stop failed:`, e)
|
||||||
}
|
}
|
||||||
if (state.process) {
|
if (state.process) {
|
||||||
console.log(
|
const proc = state.process
|
||||||
`[monero] sending SIGTERM to wallet-rpc (pid ${state.process.pid})`,
|
console.log(`[monero] sending SIGTERM to wallet-rpc (pid ${proc.pid})`)
|
||||||
)
|
proc.kill()
|
||||||
state.process.kill()
|
await Promise.race([proc.exited, sleep(3000)])
|
||||||
await Promise.race([state.process.exited, sleep(3000)])
|
if (proc.exitCode === null) {
|
||||||
if (state.process.exitCode === null) {
|
|
||||||
console.log(
|
console.log(
|
||||||
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
|
`[monero] wallet-rpc did not exit gracefully, sending SIGKILL`,
|
||||||
)
|
)
|
||||||
state.process.kill('SIGKILL')
|
proc.kill('SIGKILL')
|
||||||
}
|
}
|
||||||
state.process = null
|
state.process = null
|
||||||
}
|
}
|
||||||
@@ -310,9 +321,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
|
||||||
|
// wallet-rpc process is still running (e.g. stale-process cleanup).
|
||||||
|
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 state.wallet.openWallet(name, password)
|
await state.wallet.openWallet(name, password)
|
||||||
console.log(`[monero] wallet opened: ${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) {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
@@ -129,7 +132,13 @@ 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')
|
||||||
|
|||||||
@@ -95,12 +95,17 @@ export const MoneroWallet = () => {
|
|||||||
if (loginInFlight) return loginInFlight
|
if (loginInFlight) return loginInFlight
|
||||||
loginInFlight = (async () => {
|
loginInFlight = (async () => {
|
||||||
accountType = 'monero'
|
accountType = 'monero'
|
||||||
|
error = ''
|
||||||
beginOpening()
|
beginOpening()
|
||||||
try {
|
try {
|
||||||
if (installed && !running) {
|
if (installed && !running) {
|
||||||
await start()
|
await start()
|
||||||
await checkStatus()
|
await checkStatus()
|
||||||
}
|
}
|
||||||
|
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])
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
let xrpBalances = $state<Record<string, { balance: number; funded: boolean }>>({})
|
let xrpBalances = $state<Record<string, { balance: number; funded: boolean }>>({})
|
||||||
let loadingXrp = $state(true)
|
let loadingXrp = $state(true)
|
||||||
let loadingXmr = $state(true)
|
let loadingXmr = $state(true)
|
||||||
|
let fetchXrpInFlight = false
|
||||||
|
|
||||||
let feError = $state<string | null>(null)
|
let feError = $state<string | null>(null)
|
||||||
|
|
||||||
@@ -70,10 +71,13 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function fetchXrp() {
|
async function fetchXrp() {
|
||||||
|
if (fetchXrpInFlight) return
|
||||||
|
fetchXrpInFlight = true
|
||||||
loadingXrp = true
|
loadingXrp = true
|
||||||
const rpc = electrobun.rpc
|
const rpc = electrobun.rpc
|
||||||
if (!rpc) {
|
if (!rpc) {
|
||||||
loadingXrp = false
|
loadingXrp = false
|
||||||
|
fetchXrpInFlight = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -92,6 +96,7 @@
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
loadingXrp = false
|
loadingXrp = false
|
||||||
|
fetchXrpInFlight = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchXmr() {
|
async function fetchXmr() {
|
||||||
@@ -111,12 +116,31 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
frontendLog('dashboard mounted')
|
frontendLog('dashboard mounted')
|
||||||
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
||||||
xrpWallet.init().then(async () => {
|
xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e))
|
||||||
await fetchXrp()
|
|
||||||
})
|
|
||||||
fetchXmr()
|
fetchXmr()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Wait for the XRP wallet list to load, then fetch balances.
|
||||||
|
$effect(() => {
|
||||||
|
if (xrpWallet.ready && xrpWallet.wallets.length > 0) {
|
||||||
|
fetchXrp()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Self-heal: if the monero wallet list never got loaded (e.g. the server
|
||||||
|
// was starting while the dashboard rendered), retry login once.
|
||||||
|
$effect(() => {
|
||||||
|
if (
|
||||||
|
moneroWallet.ready &&
|
||||||
|
!moneroWallet.walletOpen &&
|
||||||
|
!moneroWallet.opening &&
|
||||||
|
moneroWallet.wallets.length === 0 &&
|
||||||
|
!moneroWallet.error
|
||||||
|
) {
|
||||||
|
void moneroWallet.login()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (moneroWallet.walletOpen) {
|
if (moneroWallet.walletOpen) {
|
||||||
fetchXmr()
|
fetchXmr()
|
||||||
@@ -205,7 +229,7 @@
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex flex-1 flex-col justify-center">
|
<CardContent class="flex flex-1 flex-col justify-center">
|
||||||
{#if loadingXrp}
|
{#if !xrpWallet.ready || loadingXrp || (xrpWallet.wallets.length > 0 && Object.keys(xrpBalances).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>
|
||||||
|
|||||||
Reference in New Issue
Block a user