Update: sell-only book, and JIT inbound at release
1. The book is sell-only
A post offers CKB. The poster is the seller. The taker is the buyer. There is no buy post.
The last build had both sides: sell (offering CKB) and buy (bidding for CKB). A buy post made the taker the seller. take had no invoice. Both sides got need-invoice, and the buyer sent payout-invoice before any hold existed.
That path is gone.
new-orderwithsideother thansellis refused.- Taking a leftover buy post is refused.
takealways carries the buyer’s payout invoice. The hold is created there. Noneed-invoiceround trip on take.- The app no longer has Buy / Sell tabs. Posts is one list of sell offers. A buy post on the relay is hidden.
- New post has no side picker. It always sells. Confirm is Sell CKB?
You buy CKB by taking someone else’s sell. You sell by posting.
sequenceDiagram
autonumber
participant BN as Buyer node
participant B as Buyer
participant D as Daemon
participant O as Operator node
participant S as Seller
S->>D: new-order (sell)
BN->>BN: mint S_b, invoice under H(S_b)
B->>D: take with that invoice
D->>O: hold under H(S_b), no preimage
D-->>S: pay-invoice
D-->>B: pay-invoice
Note over O: no S_b, so no settle and no force-close claim
S->>O: pays the hold
O-->>D: Received
D-->>B: waiting-fiat
D-->>S: waiting-fiat
B->>S: fiat, outside Twine
B->>D: fiat-sent
S->>D: release
D->>D: record the hash before paying
D->>O: send_payment
O->>BN: pays H(S_b)
O-->>D: payment_preimage is S_b
D->>O: create_preimage, then settle
D-->>B: settled
D-->>S: settled
2. JIT inbound at release
A Fiber payment needs inbound on the receiver. Capacity is not inbound. A channel the buyer funds alone cannot receive. Asking every buyer to dual-fund a channel before they take a small trade is a bad first step, especially on a laptop with no public IP.
On release, if Twine cannot already pay the buyer, the daemon opens a private one-way channel to them, funded from the Twine Fiber node.
That is JIT inbound. It happens at payout, not at take.
sequenceDiagram
autonumber
participant S as Seller
participant D as Daemon
participant O as Twine Fiber
participant B as Buyer node
S->>D: release
D->>O: list_channels toward buyer
alt outbound already covers the trade
D->>O: send_payment
else no inbound
D->>O: connect_peer if needed
D->>O: open_channel one_way, private, trade + reserve, min 100 CKB
O-->>B: channel confirms
D->>O: send_payment
end
O-->>D: payment_preimage is S_b
D->>O: create_preimage, then settle
If the buyer already has inbound, Twine pays and does not open. If they are already peered, it does not dial again. If the node is offline, it does not hang on the hold. It asks for a new invoice. The buyer reconnects, stays online, and retries. A pending open is not paid twice.
The hub now publishes its listen addresses on kind 31421. Take can include a public Fiber listen address. Loopback and LAN addresses are refused. A laptop connects outbound to the hub. It does not need a public IP.
What you do
| Role | Before the trade | At payout |
|---|---|---|
| Seller | Post the offer. Open and fund a channel to the Twine hub so you can lock the hold. | Release, same as before. |
| Buyer | Take that sell. Connect outbound to the hub. Leave auto-accept on. Keep about 99 CKB on-chain. | Stay online. Twine opens inbound to you if you cannot receive yet. |
The app says this on Post, Take, and Trade. Post and Take show a channel checklist. Seller has to confirm outbound before they can post or take. Buyer is told to stay online so Twine can open inbound at payout.
What this spends
The Twine node now spends on-chain CKB to fund those channels. Keep spare CKB on it. The RPC biscuit grew: read / write on channels and peers, on top of invoices, payments, node, and watchtower.
read("node");
read("invoices"); write("invoices");
read("payments"); write("payments");
read("channels"); write("channels");
read("peers"); write("peers");
write("watchtower");
node("<this fnn pubkey>");
A one-way channel only lets the opener send. Twine is the opener. A buyer-funded one-way still cannot receive.
3. Deploy
There is a VPS install path: DEPLOY.md. Fiber 0.9.1, port 8228 public, RPC on localhost, systemd for the daemon. The README covers the CKB key, the biscuit, and announced_addrs for wallets that are not on the same machine.
What this does not change
- Fiat is still off the protocol.
- The daemon still stores no preimage at take.
- A failed payout still does not settle. The buyer can still send another invoice with the same payment hash.
- The 36-hour hold, the 15-minute payment window, and the 30-minute safety margin are the same.
- Disputes are the same.
Tests (cargo test in the daemon, mock Fiber)
a_buy_post_cannot_be_created_or_takena_post_must_be_sellpayout_skips_jit_when_the_buyer_already_has_inboundpayout_opens_a_one_way_channel_when_the_buyer_has_no_inboundpayout_skips_connect_when_the_buyer_already_peeredpayout_asks_for_a_new_invoice_when_the_buyer_node_is_offlinea_buyer_funded_one_way_channel_cannot_receiveoperator_funded_one_way_can_pay
The app tests that Buy / Sell tabs are gone, that a buy post is hidden, and that the buyer checklist is JIT inbound at payout.
Code
Ask for review
@ArthurZhang @quake — please look at this JIT path. The hold hash is still the buyer’s. The new piece is that the operator node now opens a private one-way channel at release when there is no route.
I want blunt notes on:
- Opening at release, not at take. The buyer has to stay online through an on-chain channel open while the hold is already
Received. - Funding with trade + 99 CKB reserve, floored at 100 CKB so a default Fiber node auto-accepts.
one_way: true,public: false. - The biscuit now includes
write("channels")andwrite("peers"). - If the open is slow, the 30-minute safety margin can still refund the seller after fiat was marked sent.
- Anything I am missing on Fiber auto-accept, one-way channels, or a laptop that only connected outbound.