// DragonX Wallet — regtest chat smoke driver (dev tool, EXCLUDE_FROM_ALL; never shipped). // // Does the CHAT-side work only — identity derivation, outgoing-memo compose, and // harvest extract + decrypt — linking the real chat library. The daemon transport // (z_sendmany / generate / z_listreceivedbyaddress) is driven by the surrounding // regtest runner script, which pipes memos in and out of this tool. Together they // verify the one thing unit tests cannot: that the real dragonxd accepts a multi- // output 0-value chat tx and round-trips its memos verbatim so a chunked (tier-2) // message and a self-addressed Control memo reassemble end to end. // // Modes (memos for `extract` arrive on stdin, one per line): // pubkey // compose // control <mnemonic> <selfZaddr> <cid> <op> <label> // extract <mnemonic> // // SECRET-SAFE: never prints the mnemonic or any key; only public pubkeys, memos, and // recovered plaintext (which the runner feeds back the same message it sent). #include "chat/chat_crypto.h" #include "chat/chat_identity.h" #include "chat/chat_outgoing.h" #include "chat/chat_protocol.h" #include <cstdio> #include <iostream> #include <string> using namespace dragonx::chat; namespace { bool derive(const char* mnemonic, ChatKeyPair& keys, std::string& pubHex) { auto r = deriveChatIdentityFromSecret(mnemonic, keys, /*featureEnabled=*/true); if (r.status != ChatIdentityStatus::Ready) { std::fprintf(stderr, "[chat-smoke] identity derive failed: %s\n", r.error_name); return false; } pubHex = r.public_key_hex; return true; } // Strip the fixed-512 memo's trailing NUL padding (and any stray whitespace) that the // daemon's memoStr carries back, so the header JSON / chunk hex parses cleanly. std::string trimMemo(std::string s) { while (!s.empty() && (s.back() == '\0' || s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back(); return s; } void emit(const OutgoingChatMemos& memos) { std::printf("RECIP %s\n", memos.recipientZaddr.c_str()); std::printf("HEADER %s\n", memos.headerMemo.c_str()); for (const auto& p : memos.payloadMemos) std::printf("PAYLOAD %s\n", p.c_str()); std::fprintf(stderr, "[chat-smoke] composed %zu payload output(s)\n", memos.payloadMemos.size()); } } // namespace int main(int argc, char** argv) { if (argc < 2) { std::fprintf(stderr, "usage: chat_regtest_smoke <pubkey|compose|control|extract> ...\n"); return 2; } const std::string mode = argv[1]; if (mode == "pubkey") { if (argc < 3) return 2; ChatKeyPair keys; std::string pub; if (!derive(argv[2], keys, pub)) return 1; std::printf("%s\n", pub.c_str()); return 0; } if (mode == "compose") { if (argc < 8) { std::fprintf(stderr, "compose needs 6 args\n"); return 2; } ChatKeyPair keys; std::string pub; if (!derive(argv[2], keys, pub)) return 1; OutgoingChatMemos memos; const auto st = buildOutgoingMessage(keys, pub, /*myReply=*/argv[5], /*peerPub=*/argv[3], /*peerZ=*/argv[4], /*cid=*/argv[6], /*plain=*/argv[7], memos); if (st != ChatComposeStatus::Ok) { std::fprintf(stderr, "[chat-smoke] compose status=%d\n", (int)st); return 1; } emit(memos); return 0; } if (mode == "control") { if (argc < 7) { std::fprintf(stderr, "control needs 5 args\n"); return 2; } ChatKeyPair keys; std::string pub; if (!derive(argv[2], keys, pub)) return 1; OutgoingChatMemos memos; const auto st = buildOutgoingControl(pub, /*selfZ=*/argv[3], /*cid=*/argv[4], /*op=*/argv[5], /*label=*/argv[6], memos); if (st != ChatComposeStatus::Ok) { std::fprintf(stderr, "[chat-smoke] control status=%d\n", (int)st); return 1; } emit(memos); return 0; } if (mode == "extract") { if (argc < 3) return 2; ChatKeyPair keys; std::string pub; if (!derive(argv[2], keys, pub)) return 1; HushChatTransactionInput tx; tx.txid = "regtest-tx"; std::string line; std::size_t pos = 0; while (std::getline(std::cin, line)) { const std::string m = trimMemo(line); if (!m.empty()) tx.outputs.push_back({pos++, m}); } auto ext = extractHushChatTransactionMetadata(tx, /*featureEnabled=*/true); std::fprintf(stderr, "[chat-smoke] %zu memo output(s) in -> %zu metadata, %zu issue(s)\n", tx.outputs.size(), ext.metadata.size(), ext.issues.size()); int recovered = 0; for (const auto& md : ext.metadata) { if (md.type == HushChatHeaderType::Message) { std::string plain; const auto cs = decryptIncoming(keys, md.sender_public_key_hex, md.secretstream_header_hex, md.payload_memo, plain); if (cs == ChatCryptoStatus::Ok) { std::printf("PLAINTEXT %s\n", plain.c_str()); ++recovered; } else std::fprintf(stderr, "[chat-smoke] decrypt failed status=%d\n", (int)cs); } else if (md.type == HushChatHeaderType::Control) { std::printf("CONTROL op=%s cid=%s label=%s\n", md.op.c_str(), md.conversation_id.c_str(), md.payload_memo.c_str()); ++recovered; } } return recovered > 0 ? 0 : 1; } std::fprintf(stderr, "unknown mode: %s\n", mode.c_str()); return 2; }