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
36 changes: 19 additions & 17 deletions crates/ghostkey-server/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,25 +435,28 @@ async fn enqueue_alarm_escalation(
let amount_sat = crate::lightning::heartbeat_amount_sat();
let one_tap_block = match mint_or_reuse_one_tap_token(state, vault_id, now_iso).await? {
Some(token) => format!(
"Tap this link and pay the tiny {amount_sat}-sat Lightning \
invoice to check in:\n\n\
"Tap this link and pay {amount_sat} sats from any Lightning \
wallet to check in:\n\n\
{base}/#/checkin-link/{vault_id}/{token}\n\n"
),
None => String::new(),
};
// "1 days" reads like a robot wrote it, at the exact moment the
// email most needs to be taken seriously.
let days_word = if days_left == 1 { "day" } else { "days" };

let (subject, lead) = match count_so_far {
0 => (
format!("You missed a check-in. {days_left} days until your heir is notified"),
format!("You missed a check-in. {days_left} {days_word} until your heir is notified"),
"This is the first daily reminder.".to_string(),
),
n if n < 7 => (
format!("{days_left} days left to check in before your heir is contacted"),
format!("{days_left} {days_word} left to check in before your heir is contacted"),
format!("You've now missed {} daily reminders.", n + 1),
),
_ => (
format!(
"Last few days: {days_left} days until your heir is notified about {display_label}"
"Last few days: {days_left} {days_word} until your heir is notified about {display_label}"
),
"This is one of the final reminders.".to_string(),
),
Expand All @@ -465,7 +468,7 @@ async fn enqueue_alarm_escalation(
{one_tap_block}\
You can also open the dashboard on any device:\n\n\
{base}/#/checkin\n\n\
If we don't hear from you within {days_left} day(s), your heir \
If we don't hear from you within {days_left} {days_word}, your heir \
will receive a claim link for this vault. You can stop that \
instantly by checking in.\n\n\
From GhostKey\n"
Expand Down Expand Up @@ -756,17 +759,16 @@ async fn enqueue_pre_deadline_reminder(
"Hello,\n\n\
A quick reminder that {display_label} needs a check-in by \
{deadline_friendly}. That's about 24 hours from now.\n\n\
Tap this link and pay the tiny {amount_sat}-sat Lightning \
invoice. The payment is your proof you're still here, and it \
Tap this link and pay {amount_sat} sats from any Lightning \
wallet. The payment is your proof you're still here, and it \
resets the countdown.\n\n\
{one_tap_url}\n\n\
You can pay from any Lightning wallet. If you'd rather not from \
this email, open the dashboard on any device and check in \
there:\n\n\
If you'd rather not pay from this email, open the dashboard \
on any device and check in there:\n\n\
{base}/#/checkin\n\n\
If we don't hear from you by the deadline, you'll get one more \
email, and then your heir will be contacted after the \
grace period.\n\n\
If we don't hear from you by the deadline, we'll keep \
reminding you every day through the grace period. Only after \
that would your heir be contacted.\n\n\
If this email reached you by mistake, you can ignore it.\n\n\
From GhostKey\n"
);
Expand All @@ -789,7 +791,7 @@ async fn enqueue_pre_deadline_reminder(
"title": title,
"body": format!(
"{display_label} needs a check-in by {deadline_friendly}. \
Pay a tiny {amount_sat}-sat Lightning invoice to check in."
Pay {amount_sat} sats from any Lightning wallet to check in."
),
"url": one_tap_url,
})
Expand Down Expand Up @@ -1092,8 +1094,8 @@ async fn enqueue_alarm_owner(
let amount_sat = crate::lightning::heartbeat_amount_sat();
let one_tap_block = match &one_tap_url {
Some(url) => format!(
"Tap this link and pay the tiny {amount_sat}-sat Lightning \
invoice to check in. The payment is your proof you're still \
"Tap this link and pay {amount_sat} sats from any Lightning \
wallet to check in. The payment is your proof you're still \
here and resets the clock.\n\n\
{url}\n\n"
),
Expand Down
11 changes: 5 additions & 6 deletions ghostkey-web/src/CheckinPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,20 +198,19 @@ export function CheckinPortal({ initialId }: { initialId?: string }) {
{state.kind === "no-credentials" && (
<div className="mt-6">
<InlineAlert tone="warning">
This device doesn't have the credentials for vault{" "}
This device doesn't remember vault{" "}
<span className="font-mono">{state.id.slice(0, 8)}…</span>.
<br />
If you set this vault up with a password, sign in with
If you set your vault up with a password, sign in with
your email and password instead.{" "}
<a
href="#/checkin"
className="underline hover:text-[var(--text)]"
>
go to sign in
Go to sign in
</a>
. For legacy vaults, check in from the browser you used
to set the vault up; the owner token only lives on that
device.
. For older vaults, check in from the browser you used at
setup; that's the only device that can open this one.
</InlineAlert>
</div>
)}
Expand Down
104 changes: 57 additions & 47 deletions ghostkey-web/src/ClaimPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1040,26 +1040,31 @@ function GuardianClaim({ view, token }: { view: ClaimView; token: string }) {
</Field>
</div>

<div className="mt-4">
<Field
label="Fee rate in sat/vB (optional)"
hint={
feeRate.trim() && !feeRateValid
? "Enter a whole number between 1 and 1000, or leave blank."
: "Leave blank to use 2 sat/vB."
}
>
<input
type="text"
inputMode="numeric"
value={feeRate}
onChange={(e) => setFeeRate(e.target.value)}
placeholder="2"
className="input"
disabled={submitting || confirming}
/>
</Field>
</div>
<details className="mt-3">
<summary className="cursor-pointer text-xs text-muted">
Advanced: change the network fee
</summary>
<div className="mt-2">
<Field
label="Fee rate in sat/vB (optional)"
hint={
feeRate.trim() && !feeRateValid
? "Enter a whole number between 1 and 1000, or leave blank."
: "Leave blank to use 2 sat/vB."
}
>
<input
type="text"
inputMode="numeric"
value={feeRate}
onChange={(e) => setFeeRate(e.target.value)}
placeholder="2"
className="input"
disabled={submitting || confirming}
/>
</Field>
</div>
</details>

{!confirming ? (
<>
Expand Down Expand Up @@ -1260,9 +1265,9 @@ function PasswordVaultClaim({
</h2>
<p className="mt-2 text-sm text-soft">
Open any Bitcoin wallet and tap <strong>Receive</strong>. A
Lightning wallet works too. Apps like Wallet of Satoshi, Bitnob,
Cash App, or Phoenix each give you a Bitcoin address that adds the
money to your balance. Copy the long address that starts with{" "}
Lightning wallet works too. Apps like Blink, Bitnob, or Wallet of
Satoshi each give you a Bitcoin address that adds the money to
your balance. Copy the long address that starts with{" "}
<code className="font-mono">{bech32PrefixFor(sealed.network)}</code>{" "}
and paste it below.
</p>
Expand Down Expand Up @@ -1291,26 +1296,31 @@ function PasswordVaultClaim({
</Field>
</div>

<div className="mt-4">
<Field
label="Fee rate in sat/vB (optional)"
hint={
feeRate.trim() && !feeRateValid
? "Enter a whole number between 1 and 1000, or leave blank."
: "Leave blank to use 2 sat/vB. Raise it if you need the transaction to confirm faster."
}
>
<input
type="text"
inputMode="numeric"
value={feeRate}
onChange={(e) => setFeeRate(e.target.value)}
placeholder="2"
className="input"
disabled={submitting || confirming}
/>
</Field>
</div>
<details className="mt-3">
<summary className="cursor-pointer text-xs text-muted">
Advanced: change the network fee
</summary>
<div className="mt-2">
<Field
label="Fee rate in sat/vB (optional)"
hint={
feeRate.trim() && !feeRateValid
? "Enter a whole number between 1 and 1000, or leave blank."
: "Leave blank to use 2 sat/vB. Raise it if you need the transaction to confirm faster."
}
>
<input
type="text"
inputMode="numeric"
value={feeRate}
onChange={(e) => setFeeRate(e.target.value)}
placeholder="2"
className="input"
disabled={submitting || confirming}
/>
</Field>
</div>
</details>

{!confirming ? (
<>
Expand Down Expand Up @@ -1529,9 +1539,9 @@ function DerivedHeirClaim({
</h2>
<p className="mt-2 text-sm text-soft">
Paste any Bitcoin address you control on the {networkLabel(params.network)}.{" "}
A Lightning wallet works too. Apps like Wallet of Satoshi, Bitnob,
Cash App, or Phoenix each give you a Bitcoin address that adds the
money to your balance. {walletExamplesInline(params.network)}.
A Lightning wallet works too. Apps like Blink, Bitnob, or Wallet of
Satoshi each give you a Bitcoin address that adds the money to your
balance. {walletExamplesInline(params.network)}.
</p>

<div className="mt-4">
Expand Down Expand Up @@ -1992,7 +2002,7 @@ function BroadcastSuccess({ result }: { result: BroadcastClaimResponse }) {
rel="noreferrer noopener"
className="font-display text-sm font-bold tracking-tight text-accent underline underline-offset-2"
>
Watch it confirm on mempool.space
Watch it arrive
</a>
</div>
</div>
Expand Down
8 changes: 5 additions & 3 deletions ghostkey-web/src/HeirVideoMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,15 @@ export function HeirVideoMessage({ token }: { token: string }) {
{state.verified ? (
<p className="text-ok mt-3 flex items-center gap-2 text-sm font-medium">
<span aria-hidden>✓</span>
Verified. Recorded by the owner of this vault, and untampered.
Verified. Recorded by the owner of this vault, and it hasn't been
changed since.
</p>
) : (
<div className="mt-3">
<InlineAlert tone="warning">
This message played, but its signature did not match the vault
owner's key. It may have been altered. Treat it with caution.
This message played, but we could not confirm it really came from
the person who set up this vault. It may have been changed. Treat
it with caution.
</InlineAlert>
</div>
)}
Expand Down
10 changes: 5 additions & 5 deletions ghostkey-web/src/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function Hero({ onNavigate }: Props) {
function TrustRow() {
const items = [
{ strong: "0", sub: "Third parties" },
{ strong: "Your key", sub: "Never stored" },
{ strong: "Your key", sub: "Locked by your password" },
{ strong: "On-chain", sub: "No company can freeze it" },
];
return (
Expand Down Expand Up @@ -249,7 +249,7 @@ const REASONS: Reason[] = [
{
tag: "Precise",
title: "On-chain timelocks",
body: "Waiting periods are enforced by Bitcoin itself using OP_CSV. Once set, no one can move funds before the timer runs out. Not us, not them, not anyone.",
body: "Waiting periods are enforced by Bitcoin itself. Once set, no one can move the funds to your heir before the timer runs out. Not us, not them, not anyone.",
},
{
tag: "Trustless",
Expand Down Expand Up @@ -478,7 +478,7 @@ const FAQS = [
},
{
q: "Can I change my heir after creating a vault?",
a: "Yes, while you're still checking in. From the dashboard you can change the heir, the waiting period, or how often you check in. The change takes effect the next time you update the vault.",
a: "You can add another heir at any time, each with their own share. To change who inherits an existing share, or to change its waiting period, send that share back to your own wallet from the dashboard (one transaction) and set up a fresh vault with the new details. While you keep checking in, nothing ever moves without you.",
},
{
q: "What does a check-in cost?",
Expand All @@ -494,7 +494,7 @@ const FAQS = [
},
{
q: "What kinds of Bitcoin can I deposit?",
a: "Any Bitcoin you already hold. You don't send it to us. It stays in your wallet, with the inheritance rules attached to it.",
a: "Any Bitcoin you already hold. You send it from your wallet to your vault's own Bitcoin address, one whose rules already include your heir. We never hold it and can't spend it.",
},
{
q: "Is this a legal will?",
Expand All @@ -506,7 +506,7 @@ const FAQS = [
},
{
q: "What if GhostKey shuts down?",
a: "Your heir can still claim. When you set up, you download a recovery kit: one file with everything needed to claim the Bitcoin straight from the Bitcoin network. If GhostKey ever disappears, your heir, or anyone helping them, uses that file to unlock the funds without us. Keep it with your important papers.",
a: "Your heir can still claim. At setup you save two files: your own spare key, which opens with your password, and a file for your heir, which opens with a short code kept beside it. If GhostKey ever disappears, your heir opens their file and it walks them through claiming straight from the Bitcoin network, no GhostKey needed. Keep both with your important papers.",
},
];

Expand Down
2 changes: 1 addition & 1 deletion ghostkey-web/src/LightningCheckin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function LightningCheckin({

<div className="mt-6 min-h-[200px]">
{state.kind === "minting" && (
<p className="text-sm text-muted">Minting invoice…</p>
<p className="text-sm text-muted">Getting your check-in ready…</p>
)}

{state.kind === "error" && (
Expand Down
Loading
Loading