aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark/receive-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-03 14:46:36 -0700
committerAdam Soltys <[email protected]>2026-07-03 14:46:36 -0700
commit5b7f8ba40cbf5c8189147b6a37aadc1b230ac4e9 (patch)
treec2de49506f15aad217e5df02e5ec03b19ce1491c /tools/ark/receive-test.js
parent8029e4bd43cc425d31f50286d7ae4e94b4206ace (diff)
downloadcoinosv3-5b7f8ba40cbf5c8189147b6a37aadc1b230ac4e9.tar.gz
coinosv3-5b7f8ba40cbf5c8189147b6a37aadc1b230ac4e9.zip
Ark: native-JS protocol client + persistent ArkManager
Speaks Second's bark/captaind Ark protocol directly from the browser — gRPC-web over fetch with hand-rolled protobuf, no wasm, no new deps (crypto via the existing @scure/@noble stack incl. btc-signer's BIP-327 musig2 module). src/ark: - proto.js transport, ark address codec, mailbox blinding/auth, VTXO ProtocolEncoding v2 decode - send.js arkoor send: checkpoint+arkoor txs, BIP-341 sighashes, MuSig2 cosign ceremony, signed-VTXO assembly - board.js onchain -> ark: funding taproot, exit-tx cosign - refresh.js delegated round (hArk): participation, tx-chain reconstruction, hash-locked leaf cosign, forfeit swap - validate.js receive-side verification: chain must land on the vtxo point, anchor checked onchain, every transition sig verified - manager.js persistent wallet layer: storage-checkpointed step-machine actions (send/board/refresh) that survive crashes, mailbox sync with validation, multi-input consolidation refresh tools/ark: end-to-end regtest tests (receive, send, board, refresh, manager incl. crash-mid-send + resume), all passing against captaind with the bark CLI as counterparty. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
Diffstat (limited to 'tools/ark/receive-test.js')
-rw-r--r--tools/ark/receive-test.js106
1 files changed, 106 insertions, 0 deletions
diff --git a/tools/ark/receive-test.js b/tools/ark/receive-test.js
new file mode 100644
index 0000000..1e6adbe
--- /dev/null
+++ b/tools/ark/receive-test.js
@@ -0,0 +1,106 @@
+// Regtest end-to-end test for the native-JS Ark receive path.
+//
+// 1. Derive vtxo + mailbox keys from a mnemonic (hal-style @scure stack)
+// 2. Encode an ark address with a blinded mailbox delivery
+// 3. Pay it from the bark CLI wallet (alice)
+// 4. Read the mailbox over gRPC-web, decode the VTXO, validate it
+//
+// Usage: bun tools/ark/receive-test.js (from the halwallet repo root)
+
+import { execSync } from 'node:child_process';
+import { mnemonicToSeedSync } from '@scure/bip39';
+import { HDKey } from '@scure/bip32';
+import { hex } from '@scure/base';
+import { secp256k1 } from '@noble/curves/secp256k1';
+
+import {
+ getArkInfo, handshake, encodeAddress, decodeAddress,
+ blindMailboxId, readMailbox,
+} from '../../src/ark/proto.js';
+
+const ARK = process.env.ARK_URL || 'http://127.0.0.1:3535';
+const ESPLORA = process.env.ESPLORA_URL || 'http://127.0.0.1:30002';
+const BARK = `${process.env.HOME}/bark/target/debug/bark`;
+const ALICE = `${BARK} --datadir ${process.env.HOME}/ark-regtest/alice -q`;
+const AMOUNT_SAT = 12345;
+
+const MNEMONIC = 'rug rebuild abstract kiwi rifle vast food robust rifle spirit dilemma stumble';
+
+// --- keys: our own derivation scheme (the ASP doesn't care) ---
+const seed = mnemonicToSeedSync(MNEMONIC);
+const master = HDKey.fromMasterSeed(seed);
+const vtxoKey = master.derive("m/350'/0'/0'");
+const mailboxKey = master.derive("m/350'/1'/0'");
+const keys = {
+ vtxo: { privkey: vtxoKey.privateKey, pubkey: secp256k1.getPublicKey(vtxoKey.privateKey, true) },
+ mailbox: { privkey: mailboxKey.privateKey, pubkey: secp256k1.getPublicKey(mailboxKey.privateKey, true) },
+};
+
+// --- 1. ark info + handshake ---
+const info = await getArkInfo(ARK);
+console.log('ark info:', info);
+const hs = await handshake(ARK);
+console.log('handshake:', hs);
+
+// --- 2. build our address ---
+// NB the blinding DH partner is the *mailbox service* pubkey from ArkInfo,
+// not the server identity pubkey.
+const blindedId = blindMailboxId(keys.mailbox.pubkey, hex.decode(info.mailboxPubkey), keys.vtxo.privkey);
+const address = encodeAddress({
+ testnet: info.network !== 'bitcoin',
+ serverPubkey: hex.decode(info.serverPubkey),
+ userPubkey: keys.vtxo.pubkey,
+ blindedMailboxId: blindedId,
+});
+console.log('\nour ark address:', address);
+console.log('round-trip decode:', decodeAddress(address));
+
+// --- 3. pay it from the bark CLI ---
+console.log(`\npaying ${AMOUNT_SAT} sat from alice (bark CLI)...`);
+execSync(`${ALICE} send ${address} "${AMOUNT_SAT} sat"`, { stdio: 'inherit' });
+
+// --- 4. read mailbox, decode, validate ---
+let received = null;
+for (let attempt = 0; attempt < 15 && !received; attempt++) {
+ const { messages } = await readMailbox(ARK, keys.mailbox);
+ for (const m of messages) {
+ if (m.kind === 'arkoor' && m.vtxos.length) received = m;
+ }
+ if (!received) await new Promise((res) => setTimeout(res, 2000));
+}
+if (!received) {
+ console.error('❌ no arkoor message in mailbox after 30s');
+ process.exit(1);
+}
+
+console.log(`\nmailbox message (checkpoint ${received.checkpoint}), ${received.vtxos.length} vtxo(s):`);
+const ourPub = hex.encode(keys.vtxo.pubkey);
+let ok = true;
+const check = (name, cond, detail = '') => {
+ console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ' — ' + detail : ''}`);
+ if (!cond) ok = false;
+};
+
+for (const v of received.vtxos) {
+ console.log(`\nvtxo ${v.id}`);
+ console.log(` amount: ${v.amountSat} sat, expiry height: ${v.expiryHeight}, exit delta: ${v.exitDelta}`);
+ console.log(` anchor: ${v.anchorPoint.txid}:${v.anchorPoint.vout}, genesis depth: ${v.genesis.length}`);
+ console.log(` transitions: ${v.genesis.map((g) => g.transition.type).join(' -> ')}`);
+
+ check('policy is pubkey', v.policy.type === 'pubkey', v.policy.type);
+ check('paid to our key', v.policy.userPubkey === ourPub, v.policy.userPubkey);
+ check('amount matches', v.amountSat === AMOUNT_SAT, String(v.amountSat));
+ check('server pubkey matches ark info', v.serverPubkey === info.serverPubkey);
+ check('all genesis transitions signed', v.genesis.every((g) => g.transition.signature));
+
+ const st = await fetch(`${ESPLORA}/tx/${v.anchorPoint.txid}/status`).then((r) => r.json());
+ check('chain anchor tx confirmed onchain', st.confirmed === true, `esplora height ${st.block_height}`);
+
+ const tip = await fetch(`${ESPLORA}/blocks/tip/height`).then((r) => r.text());
+ check('not expired', v.expiryHeight > Number(tip), `expires ${v.expiryHeight}, tip ${tip}`);
+}
+
+console.log(ok
+ ? `\n✅ SUCCESS: native-JS client received + decoded + validated an off-chain Ark payment (balance: ${received.vtxos.reduce((n, v) => n + v.amountSat, 0)} sat)`
+ : '\n❌ some checks failed');
+process.exit(ok ? 0 : 1);