Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
path: type:
(builtins.match ".*nginx.conf.template$" path != null)
|| (builtins.match ".*\\.mmdb$" path != null)
|| (builtins.match ".*\\.html$" path != null)
|| (craneLibVersions.msrv.filterCargoSources path type);
name = "source";
};
Expand Down
17 changes: 17 additions & 0 deletions payjoin-mailroom/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::process::Command;

fn main() {
// Emit the short git commit hash at build time.
let commit = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "unknown".into());

println!("cargo:rustc-env=GIT_COMMIT={commit}");

// Re-run if HEAD changes (new commit).
println!("cargo:rerun-if-changed=../.git/HEAD");
}
78 changes: 21 additions & 57 deletions payjoin-mailroom/src/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,64 +433,19 @@ fn handle_peek<E: SendableError>(
}
}

fn landing_page_html() -> String {
const TEMPLATE: &str = include_str!("../static/index.html");
const VERSION: &str = env!("CARGO_PKG_VERSION");
const COMMIT: &str = env!("GIT_COMMIT");
TEMPLATE.replace("{{VERSION}}", VERSION).replace("{{COMMIT}}", COMMIT)
}

async fn handle_directory_home_path() -> Result<Response<Body>, HandlerError> {
let mut res = Response::new(empty());
*res.status_mut() = StatusCode::OK;
res.headers_mut().insert(CONTENT_TYPE, HeaderValue::from_static("text/html"));

let html = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Payjoin Directory</title>
<style>
body {
background-color: #0f0f0f;
color: #eaeaea;
font-family: Manrope, sans-serif;
padding: 2rem;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 0 10px rgba(0, 170, 255, 0.2);
text-align: center;
}
h1 {
color: black;
background-color: #C71585;
margin-bottom: 1rem;
padding: 0.5rem;
border-radius: 4px;
}
p {
color: #ccc;
}
a{
color: #F75394;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Payjoin Directory</h1>
<p>The Payjoin Directory provides a rendezvous point for sender and receiver to meet. The directory stores Payjoin payloads to support asynchronous communication.</p>
<p>Learn more about how asynchronous payjoin works here: <a href="https://payjoin.org/docs/how-it-works/payjoin-v2-bip-77">Payjoin V2</a></p>
</div>
</body>
</html>
"#;

*res.body_mut() = full(html);
Ok(res)
let html = landing_page_html();
Ok(Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "text/html")
.body(full(html))?)
}

#[derive(Debug)]
Expand Down Expand Up @@ -838,4 +793,13 @@ mod tests {
assert_eq!(status, StatusCode::OK);
assert_eq!(body, r#"{"versions":[1,2]}"#);
}

// Landing page

#[test]
fn landing_page_contains_version() {
let html = landing_page_html();
assert!(!html.contains("{{VERSION}}"));
assert!(!html.contains("{{COMMIT}}"));
}
}
255 changes: 255 additions & 0 deletions payjoin-mailroom/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payjoin Mailroom</title>
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml,%3Csvg viewBox='0 0 36 36' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18 36C27.9411 36 36 27.9411 36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36ZM3.79975 9.77472C2.32541 12.2837 1.58824 15.0254 1.58824 18C1.58824 20.9616 2.31894 23.7034 3.78035 26.2253C5.25469 28.7343 7.24635 30.7259 9.75532 32.2003C12.2772 33.6746 15.0254 34.4118 18 34.4118C20.9616 34.4118 23.6969 33.6811 26.2059 32.2197C28.7278 30.7453 30.7259 28.7537 32.2003 26.2447C33.6746 23.7228 34.4118 20.9746 34.4118 18C34.4118 15.0384 33.6746 12.3031 32.2003 9.79412C30.7388 7.27222 28.7472 5.27409 26.2253 3.79975C23.7163 2.32541 20.9746 1.58824 18 1.58824C15.0384 1.58824 12.2966 2.32541 9.77472 3.79975C7.26575 5.26116 5.27409 7.25282 3.79975 9.77472ZM15.8079 15.7691C15.213 16.364 14.9155 17.1076 14.9155 18C14.9155 18.8794 15.213 19.6166 15.8079 20.2115C16.4157 20.8064 17.1464 21.1039 18 21.1039C18.8665 21.1039 19.5972 20.8064 20.1921 20.2115C20.8 19.6037 21.1039 18.8665 21.1039 18C21.1039 17.1335 20.8064 16.3963 20.2115 15.7885C19.6166 15.1677 18.8794 14.8573 18 14.8573C17.1335 14.8573 16.4028 15.1612 15.8079 15.7691ZM5.56508 25.1777C4.2718 22.9533 3.62516 20.5607 3.62516 18C3.62516 15.4652 4.2524 13.0985 5.50688 10.8999C6.7743 8.68836 8.52023 6.92303 10.7447 5.60388C12.9691 4.28473 15.3876 3.62516 18 3.62516C20.5866 3.62516 22.9921 4.27826 25.2165 5.58448C27.4539 6.8907 29.2063 8.64956 30.4737 10.8611C31.7541 13.0597 32.3942 15.4393 32.3942 18C32.3942 20.5866 31.7411 22.9856 30.4349 25.1971C29.1416 27.4086 27.3892 29.161 25.1777 30.4543C22.9662 31.7347 20.5736 32.3748 18 32.3748C15.4522 32.3748 13.0661 31.7282 10.8417 30.4349C8.61723 29.1416 6.85836 27.3892 5.56508 25.1777Z' fill='%23f75394'/%3E%3C/svg%3E"
/>
<style>
:root {
--magenta: #f75390;
--magenta-dark: #c93265;
--bg: #0e0e10;
--surface: #18181c;
--surface-border: #28282e;
--text: #e4e4e8;
--text-muted: #9a9aa0;
--text-dim: #6e6e76;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: var(--bg);
color: var(--text);
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}

header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1.25rem 2rem;
border-bottom: 1px solid var(--surface-border);
}

header svg {
width: 24px;
height: 24px;
flex-shrink: 0;
}

header span {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
}

main {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}

.card {
background: var(--surface);
border: 1px solid var(--surface-border);
border-radius: 12px;
padding: 2.5rem;
max-width: 640px;
width: 100%;
}

h1 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.75rem;
}

.lead {
font-size: 1rem;
color: var(--text-muted);
margin-bottom: 1.75rem;
}

.components {
display: grid;
gap: 1rem;
margin-bottom: 1.75rem;
}

.component {
background: var(--bg);
border: 1px solid var(--surface-border);
border-radius: 8px;
padding: 1rem 1.25rem;
}

.component h2 {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.25rem;
}

.component p {
font-size: 0.85rem;
color: var(--text-muted);
}

.notice {
font-size: 0.8rem;
color: var(--text-dim);
border-left: 2px solid var(--surface-border);
padding-left: 0.75rem;
margin-bottom: 1.75rem;
}

.links {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}

.links a {
color: var(--magenta);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
padding: 0.35rem 0.65rem;
border: 1px solid var(--magenta);
border-radius: 6px;
transition:
background 0.15s,
color 0.15s;
white-space: nowrap;
}

.links a:hover {
background: var(--magenta);
color: var(--bg);
}

a {
color: var(--magenta);
text-decoration: none;
transition: color 0.15s;
}

a:hover {
color: var(--magenta-dark);
}

footer {
border-top: 1px solid var(--surface-border);
padding: 1rem 2rem;
display: flex;
justify-content: center;
gap: 1.5rem;
font-size: 0.75rem;
color: var(--text-dim);
}

footer code {
font-family:
ui-monospace, "Cascadia Code", "Source Code Pro", monospace;
}
</style>
</head>

<body>
<header>
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18 36C27.9411 36 36 27.9411 36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36ZM3.79975 9.77472C2.32541 12.2837 1.58824 15.0254 1.58824 18C1.58824 20.9616 2.31894 23.7034 3.78035 26.2253C5.25469 28.7343 7.24635 30.7259 9.75532 32.2003C12.2772 33.6746 15.0254 34.4118 18 34.4118C20.9616 34.4118 23.6969 33.6811 26.2059 32.2197C28.7278 30.7453 30.7259 28.7537 32.2003 26.2447C33.6746 23.7228 34.4118 20.9746 34.4118 18C34.4118 15.0384 33.6746 12.3031 32.2003 9.79412C30.7388 7.27222 28.7472 5.27409 26.2253 3.79975C23.7163 2.32541 20.9746 1.58824 18 1.58824C15.0384 1.58824 12.2966 2.32541 9.77472 3.79975C7.26575 5.26116 5.27409 7.25282 3.79975 9.77472ZM15.8079 15.7691C15.213 16.364 14.9155 17.1076 14.9155 18C14.9155 18.8794 15.213 19.6166 15.8079 20.2115C16.4157 20.8064 17.1464 21.1039 18 21.1039C18.8665 21.1039 19.5972 20.8064 20.1921 20.2115C20.8 19.6037 21.1039 18.8665 21.1039 18C21.1039 17.1335 20.8064 16.3963 20.2115 15.7885C19.6166 15.1677 18.8794 14.8573 18 14.8573C17.1335 14.8573 16.4028 15.1612 15.8079 15.7691ZM5.56508 25.1777C4.2718 22.9533 3.62516 20.5607 3.62516 18C3.62516 15.4652 4.2524 13.0985 5.50688 10.8999C6.7743 8.68836 8.52023 6.92303 10.7447 5.60388C12.9691 4.28473 15.3876 3.62516 18 3.62516C20.5866 3.62516 22.9921 4.27826 25.2165 5.58448C27.4539 6.8907 29.2063 8.64956 30.4737 10.8611C31.7541 13.0597 32.3942 15.4393 32.3942 18C32.3942 20.5866 31.7411 22.9856 30.4349 25.1971C29.1416 27.4086 27.3892 29.161 25.1777 30.4543C22.9662 31.7347 20.5736 32.3748 18 32.3748C15.4522 32.3748 13.0661 31.7282 10.8417 30.4349C8.61723 29.1416 6.85836 27.3892 5.56508 25.1777Z"
fill="#f75394"
/>
</svg>
<span>Payjoin Mailroom</span>
</header>

<main>
<div class="card">
<h1>What is this?</h1>
<p class="lead">
The payjoin mailroom is a lightweight binary that bundles the two
server-side roles required by BIP 77 Async Payjoin: a directory and an
OHTTP relay. Together, they let sender and receiver complete a payjoin
without being online at the same time, while keeping network
identities private.
</p>

<div class="components">
<div class="component">
<h2>Payjoin Directory</h2>
<p>
A store-and-forward mailbox that holds small, ephemeral,
end-to-end encrypted payloads for asynchronous payjoin.
</p>
</div>
<div class="component">
<h2>OHTTP Relay</h2>
<p>
An Oblivious HTTP proxy that separates client IP addresses from
the directory, preventing it from correlating users with their
network identity.
</p>
</div>
</div>

<p class="notice">
OHTTP privacy requires that these two roles be operated by different
parties. The mailroom enforces self-loop detection: requests where the
relay and directory resolve to the same mailroom are rejected. Wallets
should always pick distinct mailrooms for their relay and directory.
</p>

<div class="links">
<a href="https://payjoin.org/docs/how-it-works/payjoin-v2-bip-77"
>How Async Payjoin works</a
>
<a
href="https://github.com/payjoin/rust-payjoin/tree/master/payjoin-mailroom#payjoin-mailroom"
>Run your own mailroom</a
>
<a href="https://payjoindevkit.org/introduction/"
>Add Payjoin to your wallet</a
>
</div>
</div>
</main>

<footer>
<span><code>payjoin-mailroom-v{{VERSION}} @ {{COMMIT}}</code></span>
<span
>Copyright &copy;
<script>
document.write(new Date().getFullYear());
</script>
The Payjoin Developers
</span>
</footer>
</body>
</html>
Loading