Brings the whole project into git (previously only the on-ramp feature and a few parallel files were tracked). Includes: wallet core (src/), CashScript contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text). Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB), transcripts/ (session logs), and machine-local config (.claude/*.local, *.lock). Secret-scanned before commit — no credentials in the tree. Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are legacy (OP_RETURN era) but still imported; committed as-is, flagged for a separate removal once de-referenced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
160 lines
5.7 KiB
JavaScript
160 lines
5.7 KiB
JavaScript
/**
|
|
* Deviant mobile — embedded Node core bridge (ADR-0022).
|
|
*
|
|
* Runs INSIDE the on-device `nodejs-mobile` process (a real Node/V8 runtime), so
|
|
* libauth's WASM crypto and mainnet-js networking work here unmodified — the
|
|
* thing Hermes/JSC cannot do. This file is the Node end of the RN<->Node bridge:
|
|
* it receives `EnvelopedRequest`s, dispatches to the SAME core handlers the web
|
|
* shell uses (`src/web/api.ts`), and posts back one correlated `BridgeResponse`.
|
|
*
|
|
* It rebuilds the `RunContext` exactly as `src/web/server.ts` does — an
|
|
* `InMemoryTokenRegistry` plus a `connectWallet(...)` wallet — but per session,
|
|
* because the mnemonic only arrives when the user unlocks.
|
|
*
|
|
* STATUS — UNVERIFIED / UNBUILT (see shells/mobile/README.md):
|
|
* - The `./core/*` imports below are the COMPILED core (`dist/`), copied/bundled
|
|
* into this `nodejs-project` by the device build step (not yet wired).
|
|
* - On iOS this process runs V8 `--jitless`; whether libauth's WASM instantiates
|
|
* there is the ADR-0022 OPEN RISK and must be device-verified first.
|
|
* - Nothing here can run from the current Windows / no-RN-toolchain environment.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// nodejs-mobile injects this bridge module at runtime.
|
|
const rn_bridge = require('rn-bridge');
|
|
|
|
// --- core imports (compiled `dist/`, supplied by the device build step) -----
|
|
// Mirror of src/web/api.ts + the entrypoint wiring in scripts/deviant-web.mjs.
|
|
const { handleBalance, handleTokens, handleCommand, handleRegister, handleFavorite, handleForget } =
|
|
require('./core/web/api.js');
|
|
const { InMemoryTokenRegistry } = require('./core/token/registry.js');
|
|
const { connectWallet } = require('./core/wallet/runtime.js');
|
|
const { Wallet, TestNetWallet, RegTestWallet } = require('mainnet-js');
|
|
|
|
/** Map our network name to the mainnet-js wallet class used for derivation. */
|
|
function walletClassFor(network) {
|
|
if (network === 'mainnet') return Wallet;
|
|
// chipnet + testnet4 both derive via the testnet class; the chain endpoint is
|
|
// selected by connectWallet's ElectrumNetworkProvider(network).
|
|
return network === 'regtest' ? RegTestWallet : TestNetWallet;
|
|
}
|
|
|
|
let seq = 0;
|
|
/** sessionId -> { ctx: RunContext } */
|
|
const sessions = new Map();
|
|
|
|
function bridgeIdentity(w, network, derivationPath) {
|
|
const owned = [w.cashaddr, w.tokenaddr].filter(Boolean);
|
|
return {
|
|
cashaddr: w.cashaddr,
|
|
tokenAddress: w.tokenaddr,
|
|
ownedAddresses: owned,
|
|
publicKeyHex: Buffer.from(w.publicKeyCompressed).toString('hex'),
|
|
network,
|
|
derivationPath,
|
|
};
|
|
}
|
|
|
|
/** Build a fresh RunContext from a derived mainnet-js wallet. */
|
|
function openContext(w, network) {
|
|
const registry = new InMemoryTokenRegistry();
|
|
const wallet = connectWallet({
|
|
registry,
|
|
network,
|
|
privateKey: w.privateKey,
|
|
cashaddr: w.cashaddr,
|
|
tokenAddress: w.tokenaddr,
|
|
mainnetWallet: w, // enables genesis (Artemision) on this session
|
|
});
|
|
return { wallet, registry, ownTokenAddress: w.tokenaddr };
|
|
}
|
|
|
|
function requireSession(req) {
|
|
const session = sessions.get(req.sessionId);
|
|
if (!session) {
|
|
const err = new Error(`no such session: ${req.sessionId}`);
|
|
err.code = 'no-session';
|
|
throw err;
|
|
}
|
|
return session.ctx;
|
|
}
|
|
|
|
async function dispatch(req) {
|
|
switch (req.kind) {
|
|
case 'createWallet': {
|
|
const WClass = walletClassFor(req.network);
|
|
const w = await WClass.newRandom();
|
|
const ctx = openContext(w, req.network);
|
|
const sessionId = `s${(seq++).toString(36)}-${Date.now().toString(36)}`;
|
|
sessions.set(sessionId, { ctx });
|
|
return {
|
|
sessionId,
|
|
identity: bridgeIdentity(w, req.network, w.derivationPath ?? req.derivationPath),
|
|
privateKeyHex: Buffer.from(w.privateKey).toString('hex'),
|
|
mnemonic: w.mnemonic,
|
|
};
|
|
}
|
|
case 'restoreWallet':
|
|
case 'openSession': {
|
|
const WClass = walletClassFor(req.network);
|
|
let w;
|
|
try {
|
|
w = await WClass.fromSeed(req.mnemonic, req.derivationPath);
|
|
} catch (e) {
|
|
const err = new Error(`invalid mnemonic: ${e && e.message ? e.message : e}`);
|
|
err.code = 'invalid-mnemonic';
|
|
throw err;
|
|
}
|
|
const ctx = openContext(w, req.network);
|
|
const sessionId = `s${(seq++).toString(36)}-${Date.now().toString(36)}`;
|
|
sessions.set(sessionId, { ctx });
|
|
return {
|
|
sessionId,
|
|
identity: bridgeIdentity(w, req.network, req.derivationPath),
|
|
privateKeyHex: Buffer.from(w.privateKey).toString('hex'),
|
|
};
|
|
}
|
|
case 'closeSession': {
|
|
sessions.delete(req.sessionId);
|
|
return {};
|
|
}
|
|
case 'balance':
|
|
return handleBalance(requireSession(req));
|
|
case 'tokens':
|
|
return handleTokens(requireSession(req));
|
|
case 'command':
|
|
return handleCommand(requireSession(req), {
|
|
command: req.command,
|
|
args: req.args,
|
|
flags: req.flags,
|
|
});
|
|
case 'register':
|
|
return handleRegister(requireSession(req), { definition: req.definition });
|
|
case 'favorite':
|
|
return handleFavorite(requireSession(req), { id: req.id, pinned: req.pinned });
|
|
case 'forget':
|
|
return handleForget(requireSession(req), { id: req.id });
|
|
default: {
|
|
const err = new Error(`unknown request kind: ${req && req.kind}`);
|
|
err.code = 'core-error';
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
rn_bridge.channel.on('message', async (req) => {
|
|
if (!req || typeof req.id !== 'string') return; // not one of ours
|
|
try {
|
|
const result = await dispatch(req);
|
|
rn_bridge.channel.send({ id: req.id, ok: true, result });
|
|
} catch (err) {
|
|
rn_bridge.channel.send({
|
|
id: req.id,
|
|
ok: false,
|
|
error: { code: (err && err.code) || 'core-error', message: (err && err.message) || String(err) },
|
|
});
|
|
}
|
|
});
|
|
|
|
rn_bridge.channel.send({ id: '__ready__', ok: true, result: { ready: true } });
|