Add Level-B for SideShift: the server creates a SideShift variable shift via the authenticated API and shows the deposit address in-wallet; BCH settles to the user's OWN address, so self-custody is preserved end-to-end (funds never touch the wallet operator). Other providers stay link-out. - src/onramp/sideshift-shift.ts: createVariableShift / getShiftStatus / verifySideShiftSecret (injected fetch, typed errors). Endpoints verified against docs.sideshift.ai; live secret check passes GET /v2/account. - web: OnrampContext.sideshiftSecret/affiliate from SIDESHIFT_SECRET / SIDESHIFT_AFFILIATE env; handlers + routes GET /api/onramp/config, POST /api/onramp/shift, GET /api/onramp/shift?id=. Secret stays server-side (a test asserts it never appears in the response). - ui: SideShift card gets "Swap in wallet" (deposit address + range + 6s status poll) when shiftEnabled, with an "Open site" link-out fallback. - guard: mainnet-only — a bchtest:/bchreg: settle address is rejected with a switch-to-mainnet message (SideShift settles mainnet BCH). - scripts/deviant-web.mjs auto-loads .env; .env.example documents the keys. - ADR-0036 amended: Tier-3 admitted narrowly for non-custodial API swap providers only. Full suite 478 pass / 0 fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 lines
4.5 KiB
JavaScript
111 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* deviant-web — local web UI for the Deviant wallet.
|
|
*
|
|
* Starts a localhost HTTP server that serves the SPA in `public/` and a
|
|
* small JSON API backed by a real DeviantWallet (same core the CLI uses).
|
|
*
|
|
* Run: node --import tsx scripts/deviant-web.mjs
|
|
* npm: npm run deviant:web
|
|
*
|
|
* Flags (same defaults as the CLI):
|
|
* --keys <path> WIF keys file (default .chipnet-keys.json)
|
|
* --network <net> chipnet | testnet4 | mainnet (default chipnet)
|
|
* --tokens <a,b> token JSON files (default asm + asm2)
|
|
* --port <n> listen port (default 8088)
|
|
*
|
|
* Binds to 127.0.0.1 only — never expose this off the machine; it can sign
|
|
* and broadcast with the wallet's key.
|
|
*/
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TestNetWallet, Wallet } from 'mainnet-js';
|
|
import { InMemoryTokenRegistry } from '../src/token/registry.js';
|
|
import { tokenToPortable } from '../src/token/portable.js';
|
|
import { connectWallet, tokenFileToTierA } from '../src/wallet/runtime.js';
|
|
import { registerToken } from '../src/wallet/register.js';
|
|
import { startWebServer } from '../src/web/server.js';
|
|
import { parseArgs } from '../src/cli/wallet-cli.js';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const TOKENS_DIR = join(ROOT, '.deviant', 'tokens');
|
|
|
|
// Load `.env` from the project root if present, so on-ramp keys/referral IDs
|
|
// (KYCNOT_API_KEY, TROCADOR_API_KEY, SIDESHIFT_AFFILIATE, …) reach process.env.
|
|
// Uses Node's built-in loader (v20.12+/v21.7+); silently skipped if absent.
|
|
const ENV_FILE = join(ROOT, '.env');
|
|
if (existsSync(ENV_FILE) && typeof process.loadEnvFile === 'function') {
|
|
try { process.loadEnvFile(ENV_FILE); } catch { /* malformed .env — ignore */ }
|
|
}
|
|
|
|
async function main() {
|
|
// deviant-web takes only flags (no command). Prepend a placeholder so the
|
|
// flag parser doesn't consume the first `--flag` as the command slot.
|
|
const { flags } = parseArgs(['web', ...process.argv.slice(2)]);
|
|
const network = typeof flags.network === 'string' ? flags.network : 'chipnet';
|
|
const port = typeof flags.port === 'string' ? Number(flags.port) : 8088;
|
|
const keysPath = typeof flags.keys === 'string' ? flags.keys : join(ROOT, '.chipnet-keys.json');
|
|
const tokenPaths = typeof flags.tokens === 'string'
|
|
? flags.tokens.split(',').map((p) => p.trim())
|
|
: ['.chipnet-asm-token.json', '.chipnet-asm2-token.json'].map((f) => join(ROOT, f));
|
|
|
|
const registry = new InMemoryTokenRegistry();
|
|
for (const p of tokenPaths) {
|
|
const abs = p.startsWith('/') || /^[A-Za-z]:/.test(p) ? p : join(ROOT, p);
|
|
if (existsSync(abs)) registry.add(tokenFileToTierA(JSON.parse(readFileSync(abs, 'utf8'))), 'replace');
|
|
}
|
|
// Persistent registered tokens (same dir the CLI's `register` writes to).
|
|
if (existsSync(TOKENS_DIR)) {
|
|
for (const f of readdirSync(TOKENS_DIR)) {
|
|
if (!f.endsWith('.json')) continue;
|
|
try {
|
|
registerToken(JSON.parse(readFileSync(join(TOKENS_DIR, f), 'utf8')), registry, { policy: 'replace' });
|
|
} catch (err) {
|
|
console.error(`[warn] skipping ${f}: ${err?.message ?? err}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Persist newly-registered tokens as portable JSON so they reload next run.
|
|
const persistToken = (token) => {
|
|
mkdirSync(TOKENS_DIR, { recursive: true });
|
|
writeFileSync(join(TOKENS_DIR, `${token.id}.json`), JSON.stringify(tokenToPortable(token), null, 2));
|
|
};
|
|
// Drop a forgotten token's persisted file ("Convert to BCH" for Tier B).
|
|
const forgetToken = (id) => {
|
|
const f = join(TOKENS_DIR, `${id}.json`);
|
|
if (existsSync(f)) rmSync(f);
|
|
};
|
|
|
|
const { wif } = JSON.parse(readFileSync(keysPath, 'utf8'));
|
|
const w = network === 'mainnet' ? await Wallet.fromWIF(wif) : await TestNetWallet.fromWIF(wif);
|
|
|
|
const wallet = connectWallet({
|
|
registry,
|
|
network,
|
|
privateKey: w.privateKey,
|
|
cashaddr: w.cashaddr,
|
|
tokenAddress: w.tokenaddr,
|
|
mainnetWallet: w, // enables asset creation (genesis)
|
|
});
|
|
|
|
startWebServer(
|
|
{
|
|
ctx: { wallet, registry, ownTokenAddress: w.tokenaddr },
|
|
publicDir: join(ROOT, 'public'),
|
|
walletInfo: { network, cashaddr: w.cashaddr, tokenAddress: w.tokenaddr },
|
|
persistToken,
|
|
forgetToken,
|
|
},
|
|
port,
|
|
);
|
|
|
|
console.log(`Deviant web UI → http://127.0.0.1:${port} (${network}, ${w.cashaddr})`);
|
|
console.log('Ctrl-C to stop.');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('FAILED:', err?.message ?? err);
|
|
if (err?.stack) console.error(err.stack);
|
|
process.exit(1);
|
|
});
|