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 src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Take range buy order](./take_buy_range_order.md)
- [Pay bond invoice](./pay_bond_invoice.md)
- [Bond payout invoice](./add_bond_invoice.md)
- [Bond slashed notification](./bond_slashed.md)
- [Seller pays hold invoice](./seller_pay_hold_invoice.md)
- [Fiat sent](./fiatsent.md)
- [Release](./release.md)
Expand Down
67 changes: 67 additions & 0 deletions src/add_bond_invoice.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,78 @@ The recipient of the request message is the non-slashed counterparty of the trad
- **Treat re-deliveries as idempotent reminders.** The same outstanding request is being repeated; do not re-prompt the user for a fresh invoice on every retry. `slashed_at` is re-emitted unchanged, so the deadline the client shows must not shift across retries.
- **Sign the reply with the non-slashed side's trade key.** See [Keys management](./key_management.md).

## Payout confirmations (Phase 3.5)

After the counterparty submits their payout invoice, Mostro acknowledges receipt and later confirms payment with two dedicated actions.

### `bond-invoice-accepted`

Sent by Mostro to the counterparty immediately after successfully receiving and validating their payout bolt11. This confirms Mostro has the invoice on file and is attempting the Lightning payment.

```json
[
{
"order": {
"version": 1,
"id": "<Order Id>",
"action": "bond-invoice-accepted",
"payload": {
"order": {
"id": "<Order Id>",
"kind": "sell",
"amount": 500,
"fiat_code": "VES",
"fiat_amount": 100,
"payment_method": "face to face",
"premium": 1
}
}
}
},
null
]
```

Clients should surface this as a reassurance: *"Your bond payout invoice has been received and the payment is being processed."*

### `bond-payout-completed`

Sent by Mostro to the counterparty when the Lightning payment to their invoice has been confirmed as settled. The `amount` in the payload reflects the amount paid.

```json
[
{
"order": {
"version": 1,
"id": "<Order Id>",
"action": "bond-payout-completed",
"payload": {
"order": {
"id": "<Order Id>",
"kind": "sell",
"amount": 500,
"fiat_code": "VES",
"fiat_amount": 100,
"payment_method": "face to face",
"premium": 1
}
}
}
},
null
]
```

Clients should surface this as a terminal success: *"Your bond payout of `amount` Sats has been completed!"*

Both actions carry `Payload::Order` (the `SmallOrder` context) and are serde-additive: a client that does not recognise them ignores the message and falls back to the `add-bond-invoice` reply it already handles — no funds at risk.

## Failure modes

- The counterparty never replies before the deadline → no Lightning payment arrives and the share is forfeited; the slashed funds remain with the node.
- A reply arrives after the deadline, or from a sender other than the resolved recipient, or after another reply already won the race → Mostro responds with a `cant-do` action carrying reason `not-allowed-by-status`.
- The bolt11 principal does not match the requested counterparty share, or the invoice is otherwise undecodable / expired → Mostro responds with `cant-do` reason `invalid-invoice`.
- On a node where the operator retains 100% of slashed bonds, **no `add-bond-invoice` message is emitted at all**. Clients should not surface a phantom payout request.
- If `send_payment` retries are exhausted (see `payout_max_retries` in the info event) the counterparty is re-prompted with a fresh `add-bond-invoice` request, provided the claim window has not yet elapsed. If the window has passed, the share is forfeited and no further messages are sent.

On success, the counterparty receives their share as a Lightning payment from the node's wallet. The routing fee is paid separately from that wallet and is not deducted from the principal.
61 changes: 61 additions & 0 deletions src/bond_slashed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Bond slashed notification

The `bond-slashed` action is a notification Mostro sends to a bonded party when their anti-abuse bond has been **settled due to a waiting-state timeout**. It is a forfeiture notice: the bond HTLC has already been claimed into Mostro's wallet by the time this message is sent.

> **Scope.** This action is only emitted on the **timeout slash** path (scheduler-driven, gated by `bond_slash_on_waiting_timeout = "true"` in the Mostro info event — see [Other events published by Mostro](./other_events.md#anti-abuse-bond-policy-tags)). It is **not** sent on the dispute-slash path — when a solver slashes a bond via [`admin-settle`](./admin_settle_order.md) or [`admin-cancel`](./admin_cancel_order.md), the slashed party receives the `admin-settled` / `admin-canceled` confirmation instead.

## Direction and trigger

- **Direction:** Mostro → the party whose bond was slashed.
- **Trigger:** The waiting-state timeout elapsed while the responsible party had not performed their expected trade action (e.g. the seller never paid the hold invoice while in `waiting-payment`, or the buyer never submitted an invoice in `waiting-buyer-invoice`), and the operator has configured `bond_slash_on_waiting_timeout = "true"` in the info event.
- The `amount` in the payload is the **slashed bond amount in satoshis** — not the trade amount.

## Wire format

```json
[
{
"order": {
"version": 1,
"id": "<Order Id>",
"action": "bond-slashed",
"payload": {
"order": {
"id": "<Order Id>",
"kind": "sell",
"status": null,
"amount": 785,
"fiat_code": "VES",
"fiat_amount": 100,
"payment_method": "face to face",
"premium": 1,
"created_at": null
}
}
}
},
null
]
```

The `amount` field in the embedded `SmallOrder` is the **slashed bond amount** (not the original trade amount). For a range-order maker bond, this is the proportional slice amount for the taken sub-order. The `status` field is `null` on this message — the `SmallOrder` carries only the bond context (id, kind, amounts), not an order status; clients should rely on the `bond-slashed` action itself, and on the separate order-status messages that follow (`canceled` for the order, plus a republished NIP-33 event when the order returns to the book).

## What happens next

After `bond-slashed` is sent to the responsible party, Mostro also:

1. **Cancels or republishes the order** depending on which party was responsible:
- **Maker responsible** (e.g. maker-as-seller never paid the hold invoice): the order is **canceled** (since the maker cannot be trusted to fulfil it). The maker receives the `bond-slashed` notice followed by a `canceled` confirmation.
- **Taker responsible** (e.g. taker-as-buyer never submitted their invoice): the order is **republished** to the book as `pending` so the maker can be matched again. The taker receives `bond-slashed` followed by `canceled`. The maker's bond (if any) remains `Locked`.

2. **Asks the winning counterparty for a payout invoice** by sending them an [`add-bond-invoice`](./add_bond_invoice.md) message for their share of the slashed bond (governed by `bond_slash_node_share_pct` — the node retains its share, the rest goes to the counterparty).

## Expected client behaviour

- Surface `bond-slashed` to the user as an explicit forfeiture notice, not as a generic cancellation. Suggested wording: *"Your bond of `amount` Sats has been forfeited due to a waiting-state timeout."*
- This action is distinct from `canceled` — clients should not suppress it or merge it into the cancel flow.
- A `canceled` message will typically follow for the order itself; clients should handle both.

## Note on cancels vs. slashes

A cancel sent by either party **before** the timeout elapses never triggers `bond-slashed`. Bonds are always released (never slashed) on user-initiated cancels. Only the automated scheduler-driven timeout slash path emits this action, and only when `slash_on_waiting_timeout = true` is set by the operator.
21 changes: 20 additions & 1 deletion src/cancel.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,26 @@ Mostro updates the addressable event with `d` tag `<Order Id>` to change the sta

## Bond race during take

A client that sent `take-buy` / `take-sell` and is waiting for [`pay-bond-invoice`](./pay_bond_invoice.md) may receive `Action::Canceled` instead — meaning another user paid their bond first on the same order (whichever bond locks first wins; see [pay bond invoice](./pay_bond_invoice.md)). Surface this clearly to the user, e.g. *"Order was taken by another user before you locked the bond."* Do not silently retry the take — the order may not be available anymore, and the supersede mechanism on the daemon side has already discarded the prior bond request.
Multiple takers may simultaneously attempt to take the same order: each creates their own bond hold invoice (a `Requested` bond row) and whichever HTLC is accepted first by LND wins. When a bond locks, all other concurrent `Requested` bonds on the same order are released and their takers each receive `Action::Canceled`.

A client that sent `take-buy` / `take-sell` and is waiting for [`pay-bond-invoice`](./pay_bond_invoice.md) may therefore receive `canceled` instead — meaning another taker's bond locked first. Surface this clearly to the user, e.g. *"Order was taken by another user before you locked the bond."* Do not silently retry the take — the order may no longer be available.

## Cancel during `waiting-taker-bond`

An order remains in the daemon-internal `waiting-taker-bond` state (NIP-33 `s` tag still `pending`) while one or more taker bonds are outstanding. Cancels during this window are routed differently from cancels on `pending` orders.

### Taker self-cancel

A taker who has a `Requested` (unpaid) bond on an order may cancel it by sending `cancel`. Mostro releases **only that taker's bond** (LND invoice cancelled, funds returned). Other concurrent takers' bonds are unaffected and continue racing.

- If the cancelling taker was the only prospective taker, the order drops back to `pending` and is visible on the order book again.
- If other takers' bonds are still `Requested`, the order remains in `waiting-taker-bond` with those bonds still racing.

The maker is not notified; the NIP-33 order event stays `pending` throughout.

### Maker cancel

The maker can cancel the order at any point before trade flow starts (i.e. while in `pending` or `waiting-taker-bond`). Sending `cancel` releases **all** concurrent taker bonds on the order, notifies each prospective taker with `canceled`, transitions the order to `canceled`, and publishes the updated NIP-33 event.

## Cancel cooperatively

Expand Down
9 changes: 9 additions & 0 deletions src/message_suggestions_for_actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ Below are suggestions for messages that clients can show to users when receiving
- **add-bond-invoice:**
Please send me a Lightning invoice for `amount` Sats — this is your share of a slashed bond on order `id`. You have until `deadline` to submit it, or your share will be forfeited and the entire bond will be retained by the node.

- **bond-invoice-accepted:**
Your bond payout invoice has been received. The payment is being processed — you'll be notified once it completes.

- **bond-payout-completed:**
Your bond payout of `amount` Sats has been sent successfully. The funds should arrive in your Lightning wallet shortly.

- **bond-slashed:**
Your anti-abuse bond of `amount` Sats has been forfeited. This happened because the waiting-state timeout elapsed before you completed your required action on order `id`. The bond has been settled into the node's wallet.

- **add-invoice:**
Please send me an invoice for `amount` satoshis equivalent to `fiat_code` `fiat_amount`. This is where I will send the funds upon trade completion. If you don’t provide the invoice within `expiration_seconds`, the trade will be canceled.

Expand Down
10 changes: 10 additions & 0 deletions src/new_buy_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ The nostr event will look like this:
}
```

## Optional: anti-abuse maker bond

When the Mostro node has bonds enabled and `apply_to` is `"make"` or `"both"`, the maker must lock a bond **before** the order is published. Instead of a `new-order` confirmation, Mostro first responds with a [`pay-bond-invoice`](./pay_bond_invoice.md#maker-bond) message asking the maker to pay a small hold invoice (typically ~1% of the trade amount). The order is **not visible on Nostr** until the bond HTLC is accepted.

Only after the maker pays the bond does Mostro:
1. Publish the order to Nostr with status `pending`.
2. Send the `new-order` confirmation (shown below).

If the maker never pays the bond invoice it expires and no order is created. See [Pay bond invoice — Maker bond](./pay_bond_invoice.md#maker-bond) for details.

## Confirmation message

Mostro will send back a nip59 event as a confirmation message, the message in the rumor looks like the following:
Expand Down
10 changes: 10 additions & 0 deletions src/new_sell_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ The event to send to Mostro would look like this:
}
```

## Optional: anti-abuse maker bond

When the Mostro node has bonds enabled and `apply_to` is `"make"` or `"both"`, the maker must lock a bond **before** the order is published. Instead of a `new-order` confirmation, Mostro first responds with a [`pay-bond-invoice`](./pay_bond_invoice.md#maker-bond) message asking the maker to pay a small hold invoice (typically ~1% of the trade amount). The order is **not visible on Nostr** until the bond HTLC is accepted.

Only after the maker pays the bond does Mostro:
1. Publish the order to Nostr with status `pending`.
2. Send the `new-order` confirmation (shown below).

If the maker never pays the bond invoice it expires and no order is created. See [Pay bond invoice — Maker bond](./pay_bond_invoice.md#maker-bond) for details.

## Confirmation message

Mostro will send back a nip59 event as a confirmation message to the user like the following (unencrypted rumor's content example):
Expand Down
8 changes: 8 additions & 0 deletions src/new_sell_range_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ Here we have two new fields, `min_amount` and `max_amount`, to define the range

When a taker takes the order, the amount will be set on the message.

## Optional: anti-abuse maker bond

When the Mostro node has bonds enabled and `apply_to` is `"make"` or `"both"`, the maker of a range order must lock a bond **before** the order is published — same as non-range orders, with one key difference: **the bond is sized against `max_amount`**, not the minimum.

Mostro responds to the `new-order` with a [`pay-bond-invoice`](./pay_bond_invoice.md#maker-bond) instead of the confirmation below. The order is not published to Nostr until the bond HTLC is accepted.

When a taker takes a slice of the range order, the bond obligation is reduced proportionally (the slice's fiat amount relative to `max_amount`). The bond hold invoice remains `Locked` for the lifetime of the range order and is settled once in full when the range closes; partial slashes are tracked as child bond rows rather than early partial settlements. See [Pay bond invoice — Maker bond](./pay_bond_invoice.md#maker-bond) for details.

## Confirmation message

Mostro will send back a nip59 event as a confirmation message to the user like the following:
Expand Down
2 changes: 1 addition & 1 deletion src/order_event.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Events are [addressable events](https://github.com/nostr-protocol/nips/blob/mast
- `f` < Currency >: The fiat asset being traded, using the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) standard.
- `s` < Status >: `pending`, `canceled`, `in-progress`, `success`, `expired`.

An order with `s = pending` may already be matched to a taker who is in the middle of paying their anti-abuse bond. It remains takeable in this window — another user may attempt the take, and whichever bond locks first wins (the prior taker is notified with `Action::Canceled`, see [Cancel](./cancel.md)). The internal daemon state that tracks this (`waiting-taker-bond`, visible only in DM payload echoes) is not part of NIP-69's four-bucket wire model. Clients **must not** gray out or hide a `pending` order from the local order-book view just because their user has initiated a take.
An order with `s = pending` may already be matched to a taker who is in the middle of paying their anti-abuse bond. It remains takeable in this window — another user may attempt the take, and whichever bond locks first wins (the prior taker is notified with `Action::Canceled`, see [Cancel](./cancel.md)). The internal daemon state that tracks this (`waiting-taker-bond`, kept in the daemon's database only and never emitted on the wire) is not part of NIP-69's four-bucket wire model. Clients **must not** gray out or hide a `pending` order from the local order-book view just because their user has initiated a take.
- `amt` < Amount >: The amount of Bitcoin to be traded, the amount is defined in satoshis, if `0` means that the amount of satoshis will be obtained from a public API after the taker accepts the order.
- `fa` < Fiat amount >: The fiat amount being traded, for range orders two values are expected, the minimum and maximum amount.
- `pm` < Payment method >: The payment method used for the trade, if the order has multiple payment methods, they should be separated by a comma.
Expand Down
Loading