Conversation
|
@greptile review the PR according to the CONTRIBUTION.md |
Greptile SummaryThis PR implements a new "Active Options Positions" table displaying each options contract the user is involved in, with their role (maker/taker), token amounts, expiry, and contract address. The feature works by parsing stored contracts, aggregating token balances, querying locked offer collateral, and filtering by contract validity and activity state. Key concern: The maker role determination relies on outpoint txid matching the contract funding txid. This is fragile — any UTXO consolidation or token movement after becoming the maker will cause the txid to change, silently misidentifying the user as a taker. This is a user-facing correctness issue affecting the core feature. Implementation quality: The code is well-structured with clear helper functions for parsing, aggregating, and filtering contract states. The commit history is clean (2 logical, self-contained commits). Confidence Score: 2/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[run_positions] --> B[fetch BTC price]
A --> C[get_option_tokens_from_wallet]
A --> D[get_grantor_tokens_from_wallet]
C & D --> E[build_active_options_displays]
E --> F[fetch_active_contract_states]
F --> G[list_contracts_by_source OPTION_SOURCE]
F --> H[parse_option_contracts]
H --> I[token_to_contract map]
F --> J[aggregate_token_balances\noption & grantor]
F --> K[query_contract_tokens_in_offers\nlist_contracts_by_source OPTION_OFFER_SOURCE]
K --> L[map_offers_to_contracts]
F --> M{candidate_ids\noption + grantor + offer contracts}
J & L --> M
M --> N[query_collateral_for_candidates]
N --> O[build_and_filter_contract_states\nis_valid + is_active]
O --> P[sort by expiry_time]
P --> Q[display_active_options_table]
A --> R[list_contracts OPTION_SOURCE again\nfor Contract History]
A --> S[list_contracts OPTION_OFFER_SOURCE again\nfor Contract History]
Last reviewed commit: b9586f1 |
| /// Query all option tokens locked in option-offer contracts for multiple asset IDs | ||
| async fn query_all_option_tokens_in_offers( | ||
| wallet: &crate::wallet::Wallet, |
There was a problem hiding this comment.
Incorrect offer token accounting
query_all_option_tokens_in_offers is described/used as summing “option tokens locked in option-offer contracts”, but option-offer contracts lock collateral/premium (and later settlement), not option tokens. In this function you filter and sum using OptionOfferArguments::get_collateral_asset_id() and UtxoFilter::asset_id(collateral_id) (positions.rs:237-248), so option_tokens_in_offers is actually collateral-in-offers keyed by the collateral asset id. Downstream, build_maker_positions treats that map as “unsold option tokens” (option_tokens_in_offers.get(&option_asset_id) at positions.rs:371-372), so Maker “sold of total” will be wrong (typically under/over-counting depending on assets).
Also appears at positions.rs:213-274 and positions.rs:367-383.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/cli/positions.rs
Line: 213:215
Comment:
**Incorrect offer token accounting**
`query_all_option_tokens_in_offers` is described/used as summing “option tokens locked in option-offer contracts”, but option-offer contracts lock *collateral/premium* (and later settlement), not option tokens. In this function you filter and sum using `OptionOfferArguments::get_collateral_asset_id()` and `UtxoFilter::asset_id(collateral_id)` (positions.rs:237-248), so `option_tokens_in_offers` is actually *collateral-in-offers* keyed by the collateral asset id. Downstream, `build_maker_positions` treats that map as “unsold option tokens” (`option_tokens_in_offers.get(&option_asset_id)` at positions.rs:371-372), so Maker “sold of total” will be wrong (typically under/over-counting depending on assets).
Also appears at positions.rs:213-274 and positions.rs:367-383.
How can I resolve this? If you propose a fix, please make it concise.e8d39c9 to
3370cb2
Compare
|
@greptile review the PR according to the CONTRIBUTION.md |
3370cb2 to
9e7fdd6
Compare
|
@greptile review the PR according to the CONTRIBUTION.md |
| let (option_balances, grantor_balances, tokens_in_offers) = tokio::join!( | ||
| async { aggregate_token_balances(option_tokens) }, | ||
| async { aggregate_token_balances(grantor_tokens) }, | ||
| query_option_tokens_in_offers(wallet, &option_asset_ids, network) | ||
| ); |
There was a problem hiding this comment.
Unnecessary async wrapping of sync functions
aggregate_token_balances is a synchronous, CPU-bound function. Wrapping it in async { ... } and passing it to tokio::join! provides no concurrency benefit — the async block completes immediately on first poll. Only query_option_tokens_in_offers is genuinely async here.
Consider either calling the sync functions directly before the async work, or adding a comment explaining the intent:
| let (option_balances, grantor_balances, tokens_in_offers) = tokio::join!( | |
| async { aggregate_token_balances(option_tokens) }, | |
| async { aggregate_token_balances(grantor_tokens) }, | |
| query_option_tokens_in_offers(wallet, &option_asset_ids, network) | |
| ); | |
| let option_balances = aggregate_token_balances(option_tokens); | |
| let grantor_balances = aggregate_token_balances(grantor_tokens); | |
| let tokens_in_offers = query_option_tokens_in_offers(wallet, &option_asset_ids, network).await; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/cli/positions.rs
Line: 453-457
Comment:
**Unnecessary async wrapping of sync functions**
`aggregate_token_balances` is a synchronous, CPU-bound function. Wrapping it in `async { ... }` and passing it to `tokio::join!` provides no concurrency benefit — the `async` block completes immediately on first poll. Only `query_option_tokens_in_offers` is genuinely async here.
Consider either calling the sync functions directly before the async work, or adding a comment explaining the intent:
```suggestion
let option_balances = aggregate_token_balances(option_tokens);
let grantor_balances = aggregate_token_balances(grantor_tokens);
let tokens_in_offers = query_option_tokens_in_offers(wallet, &option_asset_ids, network).await;
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.9e7fdd6 to
56cdd41
Compare
|
@greptile review the PR according to the CONTRIBUTION.md |
| let contract_id = tpg_str.clone(); | ||
| option_asset_to_contract.insert(args.option_token(), contract_id.clone()); |
There was a problem hiding this comment.
Missing grantor token to contract mapping
Only args.option_token() is inserted into option_asset_to_contract, but grantor tokens are never mapped. This means the option_asset_ids set passed into query_option_tokens_in_offers will only contain option token asset IDs, not grantor token asset IDs. If a grantor token is ever used as collateral in an option-offer, it won't be discovered, and locked_in_offers will undercount.
Additionally, the map_offers_to_contracts function relies on this same map to resolve asset IDs back to contract IDs — any offer collateral that happens to be a grantor token would be silently dropped.
Consider also mapping the grantor token via get_grantor_token_ids() alongside the option token.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/cli/positions.rs
Line: 344:345
Comment:
**Missing grantor token to contract mapping**
Only `args.option_token()` is inserted into `option_asset_to_contract`, but grantor tokens are never mapped. This means the `option_asset_ids` set passed into `query_option_tokens_in_offers` will only contain option token asset IDs, not grantor token asset IDs. If a grantor token is ever used as collateral in an option-offer, it won't be discovered, and `locked_in_offers` will undercount.
Additionally, the `map_offers_to_contracts` function relies on this same map to resolve asset IDs back to contract IDs — any offer collateral that happens to be a grantor token would be silently dropped.
Consider also mapping the grantor token via `get_grantor_token_ids()` alongside the option token.
How can I resolve this? If you propose a fix, please make it concise.56cdd41 to
7a54d11
Compare
|
@greptile review the PR according to the CONTRIBUTION.md |
| fn is_active(&self) -> bool { | ||
| let collateral_per_contract = self.args.collateral_per_contract(); | ||
| if collateral_per_contract == 0 { | ||
| return false; | ||
| } | ||
| let total_issued = self.total_collateral / collateral_per_contract; | ||
| let user_share = self | ||
| .user_options | ||
| .saturating_add(self.user_grantors) | ||
| .saturating_add(self.locked_in_offers); | ||
| let held_by_others = total_issued.saturating_mul(2).saturating_sub(user_share); | ||
| held_by_others > 0 | ||
| } |
There was a problem hiding this comment.
Let's checked_* functions instead of saturating_*
| impl ContractState { | ||
| /// A contract is valid if it has not expired and has collateral locked. | ||
| fn is_valid(&self, now: i64) -> bool { | ||
| let expiry = i64::from(self.args.expiry_time()); |
There was a problem hiding this comment.
| let expiry = i64::from(self.args.expiry_time()); | |
| let expiry = <use method from below to get expiry time>; |
7a54d11 to
c529813
Compare
|
@greptile review the PR according to the CONTRIBUTION.md |
| let maker_contract_ids: HashSet<String> = option_tokens | ||
| .iter() | ||
| .chain(grantor_tokens.iter()) | ||
| .filter_map(|entry| { | ||
| let parsed = parsed_contracts.get(&entry.taproot_pubkey_gen_str)?; | ||
| let funding_txid = parsed.funding_txid.as_deref()?; | ||
| (entry.entry.outpoint().txid.to_string() == funding_txid).then(|| entry.taproot_pubkey_gen_str.clone()) | ||
| }) | ||
| .collect(); |
There was a problem hiding this comment.
Maker detection breaks after token UTXO consolidation
The maker identification compares entry.entry.outpoint().txid to parsed.funding_txid. This works only while the maker holds the exact UTXO created during funding. Any on-chain operation that spends that UTXO — such as wallet dust consolidation, token re-transmission, or UTXO merging — produces a new txid, causing the comparison to fail silently. The maker will then be misidentified as a taker in the positions table.
Consider deriving the maker role from a more stable property. Options include:
- Recording a maker flag in contract metadata at funding time and reading it back
- Deriving the role from whether the user's pubkey matches the contract's maker pubkey field in
OptionsArguments
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/cli/positions.rs
Line: 499-507
Comment:
**Maker detection breaks after token UTXO consolidation**
The maker identification compares `entry.entry.outpoint().txid` to `parsed.funding_txid`. This works only while the maker holds the exact UTXO created during funding. Any on-chain operation that spends that UTXO — such as wallet dust consolidation, token re-transmission, or UTXO merging — produces a new txid, causing the comparison to fail silently. The maker will then be misidentified as a `taker` in the positions table.
Consider deriving the maker role from a more stable property. Options include:
- Recording a maker flag in contract metadata at funding time and reading it back
- Deriving the role from whether the user's pubkey matches the contract's maker pubkey field in `OptionsArguments`
How can I resolve this? If you propose a fix, please make it concise.
Implement a basic feature to display users’ active options positions.
Resolve: #41 (comment)
The feature currently displays the following information about active options: