Skip to content

Add unlock feature#1232

Open
AhmedHanafy725 wants to merge 2 commits into
developmentfrom
development_unlock
Open

Add unlock feature#1232
AhmedHanafy725 wants to merge 2 commits into
developmentfrom
development_unlock

Conversation

@AhmedHanafy725

Copy link
Copy Markdown
Contributor

No description provided.

@zaelgohary zaelgohary left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review below. On-device check: built on a physical device with the locked-tokens flag forced on and the card pointed at a real address holding 15 time-locked TFT escrows (~1,254,188.75 TFT) — the listing renders correctly. The unlock action was not exercised (irreversible mainnet), so unlock-path notes are code-review only. Severity assumes the flag-gated, disabled-by-default rollout.

Verified correct (not flagged): preauth-tx submission without an extra signature, minTime units (seconds), ChangeTrust('0') trustline removal, the fee/sequence model, and the flag wiring (safe false fallback).

if (lockedToken.amount > 0) {
builder.addOperation(
PaymentOperationBuilder(
keyPair.accountId, asset, lockedToken.amount.toStringAsFixed(7))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Amount precision (unlock path). amount is a double (parsed at L65 via double.parse) re-serialized here with toStringAsFixed(7). Stellar amounts are 7-dp fixed point; a double represents every stroop exactly only up to 2^53 = 900,719,925.47 TFT. Above that this string can differ from the escrow's real balance → leftover dust makes the later ChangeTrust('0') fail, or overpay makes the Payment op_underfunded; either way the whole drain tx reverts and the balance can't be unlocked. Carry the exact balance string end-to-end (never double) and re-read the live balance at drain time.

);

builder.addOperation(
AccountMergeOperationBuilder(keyPair.accountId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 AccountMerge needs the escrow's HIGH threshold. All three ops are sourced from the escrow, but the tx is signed only by the main key (L223). Payment/ChangeTrust are medium-threshold; AccountMerge is high. Nothing verifies the main key's signer weight on the escrow meets the high threshold → op_bad_auth reverts the whole drain. AccountMerge also requires zero remaining sub-entries, so any extra signer/trustline left on the escrow blocks it. Worth validating against the real escrow setup before enabling unlock.

if (lockedToken.unlockHash != null) {
final submitted = await _submitUnlockTransaction(lockedToken);
if (!submitted) continue;
lockedToken.unlockHash = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Two-phase unlock isn't atomic and the error is swallowed. TX1 (_submitUnlockTransaction) is irreversible — it consumes the preauth_tx signer; if TX2 (_transferLockedBalance, L160) then throws, the catch … continue at L164 drops the token, so the UI reports failure while on-chain the escrow is unlocked-but-undrained. Recoverable on retry, but misleading. Distinguish 'couldn't unlock' from 'unlocked but transfer failed — retry to claim', and report per-escrow outcomes instead of swallowing.

}

final data = jsonDecode(response.body);
final String xdr = data['transaction_xdr'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 A token-service hiccup hides real funds. No null check on data['transaction_xdr'], and any non-200/transport error throws → _getLockedTokenDetails returns null (L112) → the escrow is silently dropped from the list (the card may even render empty). A transient tokenservices.threefold.io outage makes locked balances disappear with no error shown. Prefer surfacing the token as 'locked, unlock time unknown' rather than dropping it.

amount: amount,
unlockHash: unlockHash,
unlockFrom: minTime,
canBeUnlocked: minTime != null && nowSeconds >= minTime,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Preauth tx with no minTime shows as permanently locked. canBeUnlocked = minTime != null && … → if the unlock tx has no lower time bound (claimable immediately), this is false forever and the button never enables. The immediate-claim path is only handled when there is no preauth signer at all (the unlockHash == null branch).

'https://tokenservices.threefold.io/threefoldfoundation';

/// Asset codes we consider when looking for locked balances.
const List<String> _allowedAssetCodes = ['TFT', 'TFTA', 'FreeTFT'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Only TFT is supported — narrow this to ['TFT']. Keeping TFTA/FreeTFT means the scanner can pick them up, and an escrow holding TFT plus another allowed asset would leave a second trustline so AccountMerge (L217) fails (the multi-asset hazard). Restricting to ['TFT'] removes that and makes the hardcoded 'Locked TFT' label and _totalLocked sum correct.

}
}
logger.i(
'[LockedTokens] Escrow ${account.accountId}: ${balance.balance} ${balance.assetCode}, '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 PII in logs. This (plus L23 and L178) logs the wallet's Stellar address, each escrow accountId, and balances at logger.i — these ship in release logs and expose holdings/identity. Drop to debug or redact. (Good: the secret is correctly never logged.)

/// Submits the stored unlock transaction to the Stellar network. Returns `false`
/// when the time-lock has not expired yet.
Future<bool> _submitUnlockTransaction(LockedToken lockedToken) async {
final unlockTx = await _fetchUnlockTransaction(lockedToken.unlockHash!);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Redundant network fetch on the sensitive path. This re-fetches the unlock tx already retrieved during listing (_getLockedTokenDetails_fetchUnlockTransaction); minTime/unlockFrom is already on the model. Reuse it instead of a second POST per escrow.

'[LockedTokens] Escrow ${account.accountId}: ${balance.balance} ${balance.assetCode}, '
'unlockHash=${unlockHash ?? 'none'}');

final detailed = await _getLockedTokenDetails(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 N+1, sequential. One token-service POST per time-locked escrow, awaited inside this loop (which runs from the card's initState). With many escrows the Assets screen blocks for forSigner + K serial round-trips, and one slow escrow stalls the rest. Run them concurrently with Future.wait.

Widget build(BuildContext context) {
// Hide the section entirely when there is nothing locked.
if (!_loading && _lockedTokens.isEmpty) {
return const SizedBox.shrink();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 'Locked' section flashes on every wallet open. build() only short-circuits when !_loading && isEmpty, so while loading it always renders the Divider + 'Locked' header + spinner — every wallet, every open — then collapses for the common no-locked case (layout jump). Gate the header on loaded and non-empty, and consider caching so it doesn't re-fetch on every entry.

- Drain escrow using its exact live balance string instead of a re-serialized
  double, avoiding stroop drift that could revert the drain transaction
- Surface escrows as "locked, unlock time unknown" on token-service failures
  instead of silently dropping them; validate transaction_xdr in the response
- Treat a preauth tx with no minTime as claimable immediately
- Paginate forSigner so wallets signing >200 accounts are not truncated
- Restrict locked assets to TFT to avoid multi-asset AccountMerge failures
- Log address/escrow/balance details at debug level to keep PII out of release
- Resolve escrow unlock details concurrently instead of serially
- Report per-escrow unlock outcomes so an unlocked-but-undrained escrow tells
  the user to retry rather than reporting an outright failure
- Hide the Locked section until loaded and non-empty to stop header flashing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants