-
Notifications
You must be signed in to change notification settings - Fork 9
fix(trades): handle takes left Canceled by the old optimistic cancel #448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
897bbba
c0aa804
5e26223
5e8e5d2
f88ecdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,12 +131,61 @@ final tradeStatusLookupProvider = | |
| }, | ||
| ); | ||
|
|
||
| /// Reads the local user's role in a trade through the bridge; injectable so | ||
| /// screens that must know whether they already participate can be tested | ||
| /// without the Rust side. | ||
| final tradeRoleLookupProvider = Provider<Future<TradeRole?> Function(String)>( | ||
| (ref) => (orderId) => orders_api.getTradeRole(orderId: orderId), | ||
| ); | ||
| /// The local user's role in an order they still take part in | ||
| /// ([participatingRole]), over the rows [tradeListReaderProvider] reads. | ||
| /// Screens that must know whether they already participate override that | ||
| /// reader, so this composition is what their tests exercise. | ||
| /// | ||
| /// Null when the rows cannot be read at all, as the `get_trade_role` bridge | ||
| /// call this replaced did with a database error: an unreadable store is no | ||
| /// proof of participation, and the take screen calls this where a thrown | ||
| /// future would strand it — unawaited in `initState`, and before the Take | ||
| /// button leaves its idle state. A take the user does hold is still refused | ||
| /// by the daemon, which the screen reports. | ||
| final tradeRoleLookupProvider = Provider<Future<TradeRole?> Function(String)>(( | ||
| ref, | ||
| ) { | ||
| final readTrades = ref.watch(tradeListReaderProvider); | ||
| return (orderId) async { | ||
| try { | ||
| return participatingRole(await readTrades(), orderId); | ||
| } catch (e, st) { | ||
| debugPrint('[tradeRoleLookup] reading the trades failed: $e\n$st'); | ||
| return null; | ||
| } | ||
| }; | ||
| }); | ||
|
|
||
| /// The role of the user's trade on [orderId] among [trades], or null when | ||
| /// they no longer take part in it: no row at all, or only a take that has | ||
| /// ended ([isEndedTake]). | ||
| /// | ||
| /// Every row for the order is read, not just one: a database from before | ||
| /// takes replaced their order's earlier row can hold two, and a live one | ||
| /// among them still makes the user a participant. | ||
| TradeRole? participatingRole(Iterable<TradeInfo> trades, String orderId) { | ||
| for (final trade in trades) { | ||
| if (trade.order.id == orderId && !isEndedTake(trade)) return trade.role; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /// Whether [trade] is a take whose row has ended, which leaves the user | ||
| /// nothing to follow on its order. | ||
| /// | ||
| /// A trade that truly ended leaves its order in a status mostrod never takes | ||
| /// it out of: a take needs `Pending`, and only a waiting state goes back to | ||
| /// it. So once the order can be taken again, such a row is what a take that | ||
| /// never went active left behind: older builds marked it `Canceled` as soon | ||
| /// as its cancel went out. Holding on to it sent the user to that dead trade | ||
| /// instead of letting them take the order again (#434). Rust already takes | ||
| /// over such a row: the confirmed take replaces every earlier row of its | ||
| /// order. | ||
| /// | ||
| /// Never a maker's row: its order is theirs, and the take screen is not | ||
| /// where they manage it. | ||
| bool isEndedTake(TradeInfo trade) => | ||
| !trade.order.isMine && isTerminalTradeStatus(trade.order.status); | ||
|
|
||
| /// Takes an order through the bridge; injectable so the take screen's | ||
| /// outcomes (loading, already taken, rejected) can be tested without Rust. | ||
|
|
@@ -184,7 +233,7 @@ final tradeStatusProvider = StreamProvider.family | |
| ref, | ||
| orderId, | ||
| read: () => lookup(orderId), | ||
| isFinal: (status) => status != null && _isTerminal(status), | ||
| isFinal: (status) => status != null && isTerminalTradeStatus(status), | ||
| ).where((status) => status != null).cast<OrderStatus>(); | ||
| }); | ||
|
|
||
|
|
@@ -205,8 +254,9 @@ final tradeUpdatesProvider = StreamProvider.autoDispose<TradeUpdate>(( | |
| } | ||
| }); | ||
|
|
||
| /// Whether the UI can stop polling. Escrow settlement still awaits payout. | ||
| bool _isTerminal(OrderStatus s) => const { | ||
| /// Whether a trade in [s] has ended: nothing moves it out again, so the UI | ||
| /// can stop polling. Escrow settlement still awaits payout. | ||
| bool isTerminalTradeStatus(OrderStatus s) => const { | ||
| OrderStatus.success, | ||
| OrderStatus.settledByAdmin, | ||
| OrderStatus.completedByAdmin, | ||
|
|
@@ -216,6 +266,33 @@ bool _isTerminal(OrderStatus s) => const { | |
| OrderStatus.canceledByAdmin, | ||
| }.contains(s); | ||
|
|
||
| /// The status a trade shows: its [row]'s persisted one, or the [live] one | ||
| /// from [tradeStatusProvider], which reads the order book first. | ||
| /// | ||
| /// The row wins in two cases: | ||
| /// * **It has ended** ([isTerminalTradeStatus]). Whatever the book says about | ||
| /// the order later is no longer this trade. The one way such a row can be | ||
| /// wrong is the cancel's optimistic write on an active trade: a cooperative | ||
| /// cancel the peer never accepts, on a trade that then completes. | ||
| /// * **It is a take ([isTake]) and the book says `pending`.** A public | ||
| /// `pending` means nobody holds the order, so it is never a take's status. | ||
| /// Older builds marked a take `Canceled` as soon as its cancel went out, | ||
| /// even before it went active; once the daemon put the order back in the | ||
| /// book, that `pending` read as the user's own order, with a Cancel the | ||
| /// daemon refuses (`IsNotYourOrder`). A take parked at `WaitingTakerBond` | ||
| /// is another: publicly its order is still `pending`. | ||
| /// | ||
| /// Otherwise the live status, or the row's while there is none yet. | ||
| OrderStatus shownTradeStatus({ | ||
| required OrderStatus row, | ||
| required OrderStatus? live, | ||
| required bool isTake, | ||
| }) { | ||
| if (live == null || isTerminalTradeStatus(row)) return row; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: rg -n -i 'optimistic|cooperative|cooperativelyCanceled|canceled|cancel.*status|status.*cancel' lib test specs
sed -n '120,185p' lib/features/order/providers/trade_state_provider.dart
sed -n '245,290p' lib/features/order/providers/trade_state_provider.dartRepository: MostroP2P/app Length of output: 42181 Do not treat every terminal row as authoritative. An older client can persist Restrict terminal-row precedence to rows known to represent an ended pre-active take, or migrate ambiguous legacy rows before applying the override. Add a regression test for a stale 🤖 Prompt for AI Agents |
||
| if (isTake && live == OrderStatus.pending) return row; | ||
| return live; | ||
| } | ||
|
|
||
| /// Loads the buyer/seller role for a trade from the persistent DB. | ||
| /// | ||
| /// Returns `true` when the local user is the buyer, `false` for seller, or | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚡ Performance | 🟡 Minor
Every take-screen lookup now deserialises the user's whole trade history — twice per take.
tradeRoleLookupProviderruns oninitStateand again on the Take tap.list_tradesreads everytradesrow,serde_json-decodes each blob, sorts them, and FRB copies the fullVec<TradeInfo>into Dart, just to filter oneorder.id.get_trade_rolehit theidx_trades_order_idexpression index instead. For a long-lived account (hundreds of closed trades, each withbond/ peer snapshots) that is a noticeable delay right when the user taps Take.Not blocking for the fix, but since the rule lives in Dart only because Rust has no "all rows for an order" query, consider a narrow bridge call instead (e.g.
list_trades_for_order(order_id)using the existingjson_extract(data, '$.order.id') = ?index) and keepparticipatingRoleas the pure rule over its result.