aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-23 09:18:39 -0700
committerAdam Soltys <[email protected]>2026-10-23 09:18:39 -0700
commit0a412c94ef1517e7b471e31ccadb867078826721 (patch)
tree8dc2a4ad334fc5ec11a0cac8f7dfa94c0c1e53df /tools
parent10a4841e1a80ae72b35638953add79489edce9b6 (diff)
downloadcoinosv3-0a412c94ef1517e7b471e31ccadb867078826721.tar.gz
coinosv3-0a412c94ef1517e7b471e31ccadb867078826721.zip
On a server that does not honor expired coins the custody tier in force is "self": renewal stays automatic there, in the app and the worker
"Coinos holds my coins" was meant to exist only on servers that keep cosigning a coin past its timelock (the presets carry honorsExpired), and the custody card hid it elsewhere and showed "self". But the tier the renewal pass and the worker mirror read came from the stored choice or the custodial default regardless of server, so a wallet on Second or a custom captaind (where an expired coin is gone) renewed nothing and warned of nothing since 648a550 (2026-10-22): no backstop orders, no renewal in the last hours, and the worker registering nothing while closed. getCustody() now returns "self" in place of "custodial" whenever the active server does not honor expired coins ("exit" stays "exit"); the mirror carries the same, so the worker follows. The stored choice is left alone for a later switch back to a coinos server, and choosing a tier always writes it. tools/ark/custody-foreign-test.js: a custom server with no honorsExpired, nothing stored or "custodial" stored, reads as "self", the card offers only self/exit, and the renewal pass registers backstops and renews the coin at its timelock. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
Diffstat (limited to 'tools')
-rw-r--r--tools/ark/custody-foreign-test.js102
1 files changed, 102 insertions, 0 deletions
diff --git a/tools/ark/custody-foreign-test.js b/tools/ark/custody-foreign-test.js
new file mode 100644
index 0000000..148622d
--- /dev/null
+++ b/tools/ark/custody-foreign-test.js
@@ -0,0 +1,102 @@
+// On a server that does not honor a coin past its timelock (Second, a custom
+// captaind) "coinos holds my coins" is not a tier: whatever is stored, the
+// tier in force is "self" (or "exit" when chosen), the card offers only those
+// two, and the renewal pass registers backstops and renews what is due.
+// Real app with a local Ark manager stub; nothing is sent anywhere.
+// Run: bun tools/ark/custody-foreign-test.js
+import assert from 'node:assert/strict';
+import puppeteer from '../puppeteer.js';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+
+const bundle = await Bun.build({ entrypoints: ['src/app.js'], target: 'browser', minify: true,
+ plugins: [{ name: 'custody-foreign-test', setup(build) {
+ build.onLoad({ filter: /src\/app\.js$/ }, async ({ path }) => ({ loader: 'js', contents: await Bun.file(path).text() + `
+window.coopApp = { ui, wallet, ctx, render, enterWallet, openWalletSettings, get ready() { return !_bootDeciding; },
+ setSpending: (on = true) => { const a = activeAccount(); if (a) { if (on) a.kind = 'spending'; else delete a.kind; } } };
+` }));
+ build.onLoad({ filter: /src\/features\/ark\.js$/ }, async ({ path }) => ({ loader: 'js', contents:
+ (await Bun.file(path).text()).replace(" return {\n id: 'ark',", `
+ window.coopFeature = { setManager: (mgr) => { ark = mgr; }, getCustody, applyCustody, maybeAutoRefresh, autoRefreshCalls: [] };
+ return {\n id: 'ark',`) }));
+ build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' }));
+ } }] });
+assert(bundle.success, bundle.logs.join('\n'));
+const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>${await Bun.file('src/style.css').text()}</style><div id="app"></div><script type="module">${await bundle.outputs[0].text()}</script>`;
+const server = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).pathname === '/'
+ ? new Response(html, { headers: { 'Content-Type': 'text/html' } }) : new Response(null, { status: 404 }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+try {
+ const page = await browser.newPage(), errors = [];
+ page.on('pageerror', (e) => errors.push(e.message));
+ await page.setViewport({ width: 390, height: 844 });
+ const cdp = await page.createCDPSession();
+ await cdp.send('Network.enable');
+ await cdp.send('Network.setBlockedURLs', { urls: ['wss://*', 'ws://*'] });
+ await page.setRequestInterception(true);
+ page.on('request', (r) => r.url().startsWith(server.url.origin) || r.url().startsWith('data:') ? r.continue() : r.abort());
+ await page.evaluateOnNewDocument(() => {
+ localStorage.setItem('btc-wallet-network', 'regtest');
+ // a custom server: no honorsExpired on its record, like Second's preset
+ localStorage.setItem('btc-wallet-ark-provider:regtest', 'custom');
+ localStorage.setItem('btc-wallet-ark-custom:regtest', JSON.stringify({ ark: 'http://localhost:3535', esplora: 'http://localhost:30002' }));
+ });
+ await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
+ await page.waitForFunction(() => window.coopApp?.ready);
+ await page.evaluate(async (mn) => {
+ await coopApp.enterWallet(mn, '', { fresh: true, generated: true });
+ coopApp.ui.onb = null; localStorage.removeItem('btc-wallet-onb-step');
+ coopApp.wallet.scan = async () => {};
+ const table = [{ thresholdBlocks: 0, ppm: 0 }, { thresholdBlocks: 288, ppm: 2000 }, { thresholdBlocks: 1008, ppm: 4000 }, { thresholdBlocks: 2016, ppm: 5000 }];
+ window.coopManager = {
+ honorsExpired: false,
+ state: { vtxos: [
+ { id: 'coin-0', amountSat: 20000, state: 'spendable', expiryHeight: 973921, depth: 2 }, // weeks left
+ { id: 'coin-1', amountSat: 5000, state: 'spendable', expiryHeight: 969760, depth: 1 }, // 9 blocks from its timelock
+ ], actions: [], movements: [], scheduled: [] },
+ info: { network: 'regtest', vtxoExpiryDelta: 4032, roundIntervalSecs: 3600, refreshFees: { baseFeeSat: 0, ppmExpiryTable: table } }, _tipH: 969751,
+ chain: { tipHeight: async () => 969751 },
+ vtxos() { return this.state.vtxos; },
+ _decoded(v) { return { genesis: new Array(v.depth || 1).fill({}), policy: { type: 'pubkey' } }; },
+ balance() { return { spendableSat: 25000, pendingSat: 0, boardingSat: 0, expiredSat: 0 }; },
+ refreshFee(inputs, tip) {
+ let u = 0;
+ for (const v of inputs) u += v.amountSat * (table.filter((e) => e.thresholdBlocks <= v.expiryHeight - tip).pop()?.ppm ?? 0);
+ return Math.ceil(u / 1e6);
+ },
+ async ensureScheduledRefreshes(margin, max, grid) { coopFeature.autoRefreshCalls.push(['ensure', margin, max, grid]); return 0; },
+ async refresh(ids) { coopFeature.autoRefreshCalls.push(['refresh', ids]); return 'refresh-1'; },
+ unseenReceives() { return []; }, ackReceives() {},
+ address() { return 'test-ark-address'; },
+ };
+ coopFeature.setManager(coopManager);
+ coopApp.setSpending(true);
+ coopApp.openWalletSettings('spending');
+ }, generateMnemonic(wordlist));
+ await page.waitForSelector('.ark-custody');
+
+ // nothing stored: the tier in force is "self", and the card offers no custodial option
+ assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'self');
+ assert.equal(await page.evaluate(() => coopApp.wallet.loadFeatureState('arkCustody', null)), null, 'nothing was written to the record');
+ const opts = await page.$$eval('#ark-custody-select option', (os) => os.map((o) => [o.value, o.selected]));
+ assert.deepEqual(opts, [['self', true], ['exit', false]]);
+ console.log('✓ a server that does not honor expired coins has no "coinos holds my coins" tier; "self" is in force');
+
+ // the stored default from a coinos wallet does not switch renewals off here
+ await page.evaluate(() => coopApp.wallet.saveFeatureState('arkCustody', 'custodial'));
+ assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'self');
+ console.log('✓ a stored "custodial" choice reads as "self" on such a server');
+
+ // the renewal pass runs: backstop orders are registered and the coin at its timelock renews now
+ const calls = await page.evaluate(async () => { coopFeature.autoRefreshCalls.length = 0; await coopFeature.maybeAutoRefresh(coopManager); return coopFeature.autoRefreshCalls; });
+ assert.deepEqual(calls[0], ['ensure', 6, 8, 1], 'backstop orders registered (regtest margin 6)');
+ assert.deepEqual(calls[1], ['refresh', ['coin-1']], 'the coin close to its timelock is renewed now');
+ console.log('✓ automatic renewal runs on such a server with nothing opted into');
+
+ // the exit tier can still be chosen, and is kept
+ await page.evaluate(() => coopFeature.applyCustody('exit'));
+ assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'exit');
+ assert.equal(await page.evaluate(() => coopApp.wallet.loadFeatureState('arkCustody', null)), 'exit');
+ console.log('✓ "exit" can be chosen there and is saved');
+ assert.deepEqual(errors, [], 'no page errors');
+} finally { await browser.close(); server.stop(true); }