From 2d0ee9dca34f2fd14d20116384a84e92d7012712 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 5 May 2026 15:21:23 +0800 Subject: [PATCH 01/14] feat: update to new graphql - comply with hasura syntax - change to new graphql url in constants - update data model construction from json to comply new hasura return value --- .../lib/src/constants/app_constants.dart | 2 +- quantus_sdk/lib/src/models/account_stats.dart | 2 +- .../lib/src/models/miner_reward_event.dart | 2 +- .../src/models/pending_transfer_event.dart | 4 +- .../lib/src/models/transaction_event.dart | 6 +- .../services/account_discovery_service.dart | 2 +- .../src/services/chain_history_service.dart | 126 ++++++++++-------- .../lib/src/services/taskmaster_service.dart | 8 +- .../src/services/wormhole_utxo_service.dart | 30 ++--- 9 files changed, 96 insertions(+), 86 deletions(-) diff --git a/quantus_sdk/lib/src/constants/app_constants.dart b/quantus_sdk/lib/src/constants/app_constants.dart index 16452ecac..a8a0d3df5 100644 --- a/quantus_sdk/lib/src/constants/app_constants.dart +++ b/quantus_sdk/lib/src/constants/app_constants.dart @@ -17,7 +17,7 @@ class AppConstants { 'https://a2-planck.quantus.cat', 'https://matcha-latte.quantus.com', ]; - static const List graphQlEndpoints = ['https://subsquid.quantus.com/blue/graphql']; + static const List graphQlEndpoints = ['https://sub2.quantus.com/v1/graphql']; // local test android use special ip // static const String taskMasterEndpoint = 'http://10.0.2.2:3000/api'; diff --git a/quantus_sdk/lib/src/models/account_stats.dart b/quantus_sdk/lib/src/models/account_stats.dart index 90db4b80f..9aea729b6 100644 --- a/quantus_sdk/lib/src/models/account_stats.dart +++ b/quantus_sdk/lib/src/models/account_stats.dart @@ -22,7 +22,7 @@ class AccountStats { sendCount: json['data']['immediate_txs'] as int, reversalCount: json['data']['reversible_txs'] as int, miningCount: json['data']['mining_events'] as int, - miningRewards: BigInt.parse(json['data']['mining_rewards'] as String), + miningRewards: BigInt.from(json['data']['mining_rewards']), ); } } diff --git a/quantus_sdk/lib/src/models/miner_reward_event.dart b/quantus_sdk/lib/src/models/miner_reward_event.dart index f014b5ccb..e8cedbc82 100644 --- a/quantus_sdk/lib/src/models/miner_reward_event.dart +++ b/quantus_sdk/lib/src/models/miner_reward_event.dart @@ -29,7 +29,7 @@ class MinerRewardEvent extends TransactionEvent { return MinerRewardEvent( id: json['id'] as String, miner: json['miner']?['id'] as String? ?? '', - reward: BigInt.parse(json['reward'] as String), + reward: BigInt.from(json['reward']), timestamp: DateTime.parse(json['timestamp'] as String), blockNumber: blockHeight, blockHash: blockHash, diff --git a/quantus_sdk/lib/src/models/pending_transfer_event.dart b/quantus_sdk/lib/src/models/pending_transfer_event.dart index 570fa3e7c..0dbd98a1b 100644 --- a/quantus_sdk/lib/src/models/pending_transfer_event.dart +++ b/quantus_sdk/lib/src/models/pending_transfer_event.dart @@ -37,7 +37,7 @@ class PendingTransactionEvent extends TransactionEvent { tempId: json['id'] as String, from: json['from'] as String, to: json['to'] as String, - amount: BigInt.parse(json['amount'].toString()), + amount: BigInt.from(json['amount']), timestamp: DateTime.parse(json['timestamp']), blockHash: json['blockHash'] as String?, transactionState: TransactionState.values.firstWhere( @@ -52,7 +52,7 @@ class PendingTransactionEvent extends TransactionEvent { ), scheduledAtTime: json['scheduledAtTime'] != null ? DateTime.tryParse(json['scheduledAtTime']) : null, delaySeconds: json['delaySeconds'] ?? 0, - fee: json['fee'] != null ? BigInt.parse(json['fee'].toString()) : null, + fee: json['fee'] != null ? BigInt.from(json['fee']) : null, extrinsicHash: json['extrinsicHash'] as String?, blockNumber: json['blockNumber'] ?? 0, error: json['error'] as String?, diff --git a/quantus_sdk/lib/src/models/transaction_event.dart b/quantus_sdk/lib/src/models/transaction_event.dart index c844c1941..efe1c2dda 100644 --- a/quantus_sdk/lib/src/models/transaction_event.dart +++ b/quantus_sdk/lib/src/models/transaction_event.dart @@ -52,9 +52,9 @@ class TransferEvent extends TransactionEvent { id: json['id'] as String, from: json['from']?['id'] as String? ?? '', to: json['to']?['id'] as String? ?? '', - amount: BigInt.parse(json['amount'] as String), + amount: BigInt.from(json['amount']), timestamp: DateTime.parse(json['timestamp'] as String), - fee: json['fee'] != null ? BigInt.parse(json['fee'] as String) : BigInt.zero, + fee: json['fee'] != null ? BigInt.from(json['fee']) : BigInt.zero, extrinsicHash: json['extrinsic']?['id'] as String?, blockNumber: blockHeight, blockHash: blockHash, @@ -110,7 +110,7 @@ class ReversibleTransferEvent extends TransactionEvent { id: json['id'] as String, from: transfer['from']?['id'] as String? ?? '', to: transfer['to']?['id'] as String? ?? '', - amount: BigInt.parse(transfer['amount'] as String), + amount: BigInt.from(transfer['amount']), timestamp: DateTime.parse(json['timestamp'] as String), txId: json['txId'] as String, status: status, diff --git a/quantus_sdk/lib/src/services/account_discovery_service.dart b/quantus_sdk/lib/src/services/account_discovery_service.dart index 4c3d69272..e631a4bef 100644 --- a/quantus_sdk/lib/src/services/account_discovery_service.dart +++ b/quantus_sdk/lib/src/services/account_discovery_service.dart @@ -10,7 +10,7 @@ class AccountDiscoveryService { static const String _accountsQuery = r''' query AccountsQuery($ids: [String!]) { - accounts(where: {id_in: $ids}) { + accounts: account(where: {id: {_in: $ids}}) { id } } diff --git a/quantus_sdk/lib/src/services/chain_history_service.dart b/quantus_sdk/lib/src/services/chain_history_service.dart index 2815ffe96..ed15f3c27 100644 --- a/quantus_sdk/lib/src/services/chain_history_service.dart +++ b/quantus_sdk/lib/src/services/chain_history_service.dart @@ -22,27 +22,31 @@ class ChainHistoryService { ChainHistoryService(); - String _buildScheduledReversibleTransfersQuery(TransactionFilter filter) { - final String directionCondition; +String _buildScheduledReversibleTransfersQuery(TransactionFilter filter) { + final String whereClause; + switch (filter) { case TransactionFilter.send: - directionCondition = - 'account: {id_in: \$accounts}, scheduledReversibleTransfer: {from: {id_in: \$accounts}, scheduledAt_gt: \$after}'; + whereClause = + '{_and: [{account_id: {_in: \$accounts}}, {scheduled_reversible_transfer_id: {_is_null: false}}, {scheduledReversibleTransfer: {from_id: {_in: \$accounts}, scheduled_at: {_gt: \$after}}}]}'; + break; case TransactionFilter.receive: - directionCondition = - 'account: {id_in: \$accounts}, scheduledReversibleTransfer: {to: {id_in: \$accounts}, scheduledAt_gt: \$after}'; + whereClause = + '{_and: [{account_id: {_in: \$accounts}}, {scheduled_reversible_transfer_id: {_is_null: false}}, {scheduledReversibleTransfer: {to_id: {_in: \$accounts}, scheduled_at: {_gt: \$after}}}]}'; + break; case TransactionFilter.all: - directionCondition = 'account: {id_in: \$accounts}, scheduledReversibleTransfer: {scheduledAt_gt: \$after}'; + whereClause = + '{_and: [{account_id: {_in: \$accounts}}, {scheduled_reversible_transfer_id: {_is_null: false}}, {scheduledReversibleTransfer: {scheduled_at: {_gt: \$after}}}]}'; + break; } return ''' -query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!, \$after: DateTime!) { - accountEvents(limit: \$limit, +query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!, \$after: timestamptz!) { + accountEvents: account_event( + limit: \$limit, offset: \$offset, - where: { - scheduledReversibleTransfer_isNull: false, - $directionCondition - }, orderBy: timestamp_DESC + where: $whereClause, + order_by: {timestamp: desc} ) { id scheduledReversibleTransfer { @@ -55,8 +59,8 @@ query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: In to { id } - txId - scheduledAt + txId: tx_id + scheduledAt: scheduled_at block { height hash @@ -64,7 +68,6 @@ query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: In extrinsic { id } - timestamp } } } @@ -80,11 +83,13 @@ query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: In /// Mining rewards are always a "receive", so they are excluded when the /// filter is [TransactionFilter.send] and included otherwise. String _buildAccountEventsQuery(TransactionFilter filter) { - // The base condition that applies to every variant - const String baseCondition = 'balanceEvent_isNull: true, scheduledReversibleTransfer_isNull: true'; + // The base condition that applies to every variant. + // Using Hasura's direct foreign key field is cleaner. + const String baseCondition = '{scheduled_reversible_transfer_id: {_is_null: true}}'; - // Transfer extrinsic guard — only include on-chain transfers - const String transferGuard = '{OR: [{transfer_isNull: true}, {transfer: {extrinsic_isNull: false}}]}'; + // Transfer extrinsic guard — only include on-chain transfers. + // Using direct `transfer_id` and `extrinsic_id` relation fields. + const String transferGuard = '{_or: [{transfer_id: {_is_null: true}}, {transfer: {extrinsic_id: {_is_null: false}}}]}'; // Whether to include the minerReward field in the response final bool includeMinerReward = filter != TransactionFilter.send; @@ -109,18 +114,23 @@ query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: In switch (filter) { case TransactionFilter.send: + // Properly formatted Hasura boolean expression with colons and balanced brackets whereClause = - '{AND: [{account: {id_in: \$accounts}, $baseCondition}, $transferGuard, {OR: [{transfer: {from: {id_in: \$accounts}}}, {executedReversibleTransfer: {scheduledTransfer: {from: {id_in: \$accounts}}}}, {cancelledReversibleTransfer: {scheduledTransfer: {from: {id_in: \$accounts}}}}]}]}'; + '{_and: [{account_id: {_in: \$accounts}}, $baseCondition, $transferGuard, {_or: [{transfer: {from_id: {_in: \$accounts}}}, {executedReversibleTransfer: {scheduledTransfer: {from_id: {_in: \$accounts}}}}, {cancelledReversibleTransfer: {scheduledTransfer: {from_id: {_in: \$accounts}}}}]}]}'; + break; case TransactionFilter.receive: + // Properly formatted Hasura boolean expression with colons and balanced brackets whereClause = - '{AND: [{account: {id_in: \$accounts}, $baseCondition}, $transferGuard, {OR: [{transfer: {to: {id_in: \$accounts}}}, {executedReversibleTransfer: {scheduledTransfer: {to: {id_in: \$accounts}}}}, {cancelledReversibleTransfer: {scheduledTransfer: {to: {id_in: \$accounts}}}}, {minerReward_isNull: false}]}]}'; + '{_and: [{account_id: {_in: \$accounts}}, $baseCondition, $transferGuard, {_or: [{transfer: {to_id: {_in: \$accounts}}}, {executedReversibleTransfer: {scheduledTransfer: {to_id: {_in: \$accounts}}}}, {cancelledReversibleTransfer: {scheduledTransfer: {to_id: {_in: \$accounts}}}}, {miner_reward_id: {_is_null: false}}]}]}'; + break; case TransactionFilter.all: - whereClause = '{AND: [{account: {id_in: \$accounts}, $baseCondition}, $transferGuard]}'; + whereClause = '{_and: [{account_id: {_in: \$accounts}}, $baseCondition, $transferGuard]}'; + break; } return ''' query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { - accountEvents(limit: \$limit, offset: \$offset, where: $whereClause, orderBy: timestamp_DESC) { + accountEvents: account_event(limit: \$limit, offset: \$offset, where: $whereClause, order_by: {timestamp: desc}) { id transfer { id @@ -139,10 +149,9 @@ query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { extrinsic { id } - timestamp fee executedBy { - txId + txId: tx_id } } executedReversibleTransfer { @@ -150,7 +159,7 @@ query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { height hash } - txId + txId: tx_id timestamp id scheduledTransfer { @@ -161,7 +170,7 @@ query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { to { id } - scheduledAt + scheduledAt: scheduled_at } } cancelledReversibleTransfer { @@ -169,7 +178,7 @@ query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { height hash } - txId + txId: tx_id timestamp id extrinsic { @@ -183,23 +192,23 @@ query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { to { id } - scheduledAt + scheduledAt: scheduled_at } }$minerRewardField } } '''; - } +} // GraphQL query to fetch transactions by their hash final String _executedTransactionByTxId = r''' query ExecutedReversibleTransferByTxId($txId: String!) { - executedReversibleTransfers(where: {txId_eq: $txId}) { + executedReversibleTransfers: executed_reversible_transfer(where: {tx_id: {_eq: $txId}}) { block { height hash } - txId + txId: tx_id timestamp id scheduledTransfer { @@ -210,7 +219,7 @@ query ExecutedReversibleTransferByTxId($txId: String!) { to { id } - scheduledAt + scheduledAt: scheduled_at } } } @@ -227,20 +236,20 @@ query SearchPendingTransaction( $amount: BigInt!, $blockHeightAfter: Int!, ) { - events( + events: event( limit: 1 where: { transfer: { - from: { id_eq: $from }, - to: { id_eq: $to }, - amount_eq: $amount, - extrinsic_isNull: false, + from: { id: {_eq: $from } }, + to: { id: {_eq: $to } }, + amount: {_eq: $amount }, + extrinsic: {id: {_is_null: false}}, block: { - height_gt: $blockHeightAfter + height: {_gt: $blockHeightAfter} } } } - orderBy: timestamp_DESC + order_by: {timestamp: desc} ) { id timestamp @@ -271,20 +280,20 @@ query SearchPendingTransaction( $amount: BigInt!, $blockHeightAfter: Int!, ) { - events( + events: event( limit: 1 where: { scheduledReversibleTransfer: { - from: { id_eq: $from }, - to: { id_eq: $to }, - amount_eq: $amount, - extrinsic_isNull: false, + from: { id: {_eq: $from } }, + to: { id: {_eq: $to } }, + amount: {_eq: $amount }, + extrinsic: {id: {_is_null: false}}, block: { - height_gt: $blockHeightAfter + height: {_gt: $blockHeightAfter} } } } - orderBy: timestamp_DESC + order_by: {timestamp: desc} ) { id timestamp @@ -297,8 +306,8 @@ query SearchPendingTransaction( timestamp from { id } to { id } - txId - scheduledAt + txId: tx_id + scheduledAt: scheduled_at block { height hash } extrinsic { id @@ -311,10 +320,10 @@ query SearchPendingTransaction( final String _searchByExtrinsicHashTransferQuery = r''' query SearchByExtrinsicHash($extrinsicHash: String!) { - events( + events: event( limit: 1 - where: { transfer: { extrinsic: { id_eq: $extrinsicHash } } } - orderBy: timestamp_DESC + where: { transfer: { extrinsic: { id: {_eq: $extrinsicHash } } } } + order_by: {timestamp: desc} ) { id timestamp @@ -336,10 +345,10 @@ query SearchByExtrinsicHash($extrinsicHash: String!) { final String _searchByExtrinsicHashReversibleQuery = r''' query SearchByExtrinsicHash($extrinsicHash: String!) { - events( + events: event( limit: 1 - where: { scheduledReversibleTransfer: { extrinsic: { id_eq: $extrinsicHash } } } - orderBy: timestamp_DESC + where: { scheduledReversibleTransfer: { extrinsic: { id: {_eq: $extrinsicHash } } } } + order_by: {timestamp: desc} ) { id timestamp @@ -350,8 +359,8 @@ query SearchByExtrinsicHash($extrinsicHash: String!) { timestamp from { id } to { id } - txId - scheduledAt + txId: tx_id + scheduledAt: scheduled_at block { height hash } extrinsic { id } timestamp @@ -526,6 +535,7 @@ query SearchByExtrinsicHash($extrinsicHash: String!) { } final List? events = responseBody['data']?['accountEvents']; + print('events: $events'); final page = _pageFromEvents(events, limit, _parseOtherTransferEvent); return OtherTransfersResult(transfers: page.items, hasMore: page.hasMore); } catch (e, stackTrace) { diff --git a/quantus_sdk/lib/src/services/taskmaster_service.dart b/quantus_sdk/lib/src/services/taskmaster_service.dart index 1331c4748..4bb16e549 100644 --- a/quantus_sdk/lib/src/services/taskmaster_service.dart +++ b/quantus_sdk/lib/src/services/taskmaster_service.dart @@ -136,9 +136,9 @@ class TaskmasterService { final String _minerStatsQuery = r''' query MinerStats($ids: [String!]!) { - minerStats(where: {id_in: $ids}) { - totalMinedBlocks - totalRewards + minerStats: account_stats(where: {id: {_in: $ids}}) { + totalMinedBlocks: total_mined_blocks + totalRewards: total_rewards id } } @@ -579,7 +579,7 @@ class TaskmasterService { for (final stats in minerStatsList) { totalMinedBlocks += stats['totalMinedBlocks'] as int; - totalRewards += BigInt.parse(stats['totalRewards'] as String); + totalRewards += BigInt.from(stats['totalRewards']); } return MinerStats(totalMinedBlocks: totalMinedBlocks, totalRewards: totalRewards); diff --git a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart index 13c488128..364700b9b 100644 --- a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart +++ b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart @@ -34,9 +34,9 @@ class WormholeTransfer { id: json['id'] as String, wormholeAddress: json['to']?['id'] as String? ?? '', fromAddress: json['from']?['id'] as String? ?? '', - amount: BigInt.parse(json['amount'] as String), - transferCount: BigInt.parse(json['transferCount'] as String), - leafIndex: BigInt.parse(json['leafIndex'] as String), + amount: BigInt.from(json['amount']), + transferCount: BigInt.from(json['transferCount']), + leafIndex: BigInt.from(json['leafIndex']), blockNumber: block?['height'] as int? ?? 0, blockHash: block?['hash'] as String? ?? '', timestamp: DateTime.parse(json['timestamp'] as String), @@ -55,20 +55,20 @@ class WormholeUtxoService { static const String _transfersToWormholeQuery = r''' query WormholeTransfers($wormholeAddress: String!, $limit: Int!, $offset: Int!) { - transfers( + transfers: transfer( limit: $limit offset: $offset where: { - to: { id_eq: $wormholeAddress } + to: { id: {_eq: $wormholeAddress } } } - orderBy: timestamp_DESC + order_by: {timestamp: desc} ) { id from { id } to { id } amount - leafIndex - transferCount + leafIndex: leaf_index + transferCount: transfer_count timestamp block { height @@ -79,20 +79,20 @@ query WormholeTransfers($wormholeAddress: String!, $limit: Int!, $offset: Int!) static const String _transfersToMultipleQuery = r''' query WormholeTransfersMultiple($wormholeAddresses: [String!]!, $limit: Int!, $offset: Int!) { - transfers( + transfers: transfer( limit: $limit offset: $offset where: { - to: { id_in: $wormholeAddresses } + to: { id: {_in: $wormholeAddresses } } } - orderBy: timestamp_DESC + order_by: {timestamp: desc} ) { id from { id } to { id } amount - leafIndex - transferCount + leafIndex: leaf_index + transferCount: transfer_count timestamp block { height @@ -103,8 +103,8 @@ query WormholeTransfersMultiple($wormholeAddresses: [String!]!, $limit: Int!, $o static const String _nullifiersQuery = r''' query CheckNullifiers($nullifiers: [String!]!) { - wormholeNullifiers( - where: { nullifier_in: $nullifiers } + wormholeNullifiers: wormhole_nullifier( + where: { nullifier: {_in: $nullifiers } } ) { nullifier } From 5765020f5e636241549056affd4ac9371cefc4ca Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 6 May 2026 13:00:07 +0800 Subject: [PATCH 02/14] fix: revert to parse string We have made hasura return string for number value. --- quantus_sdk/lib/src/models/account_stats.dart | 2 +- quantus_sdk/lib/src/models/miner_reward_event.dart | 2 +- quantus_sdk/lib/src/models/pending_transfer_event.dart | 4 ++-- quantus_sdk/lib/src/models/transaction_event.dart | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/quantus_sdk/lib/src/models/account_stats.dart b/quantus_sdk/lib/src/models/account_stats.dart index 9aea729b6..90db4b80f 100644 --- a/quantus_sdk/lib/src/models/account_stats.dart +++ b/quantus_sdk/lib/src/models/account_stats.dart @@ -22,7 +22,7 @@ class AccountStats { sendCount: json['data']['immediate_txs'] as int, reversalCount: json['data']['reversible_txs'] as int, miningCount: json['data']['mining_events'] as int, - miningRewards: BigInt.from(json['data']['mining_rewards']), + miningRewards: BigInt.parse(json['data']['mining_rewards'] as String), ); } } diff --git a/quantus_sdk/lib/src/models/miner_reward_event.dart b/quantus_sdk/lib/src/models/miner_reward_event.dart index e8cedbc82..f014b5ccb 100644 --- a/quantus_sdk/lib/src/models/miner_reward_event.dart +++ b/quantus_sdk/lib/src/models/miner_reward_event.dart @@ -29,7 +29,7 @@ class MinerRewardEvent extends TransactionEvent { return MinerRewardEvent( id: json['id'] as String, miner: json['miner']?['id'] as String? ?? '', - reward: BigInt.from(json['reward']), + reward: BigInt.parse(json['reward'] as String), timestamp: DateTime.parse(json['timestamp'] as String), blockNumber: blockHeight, blockHash: blockHash, diff --git a/quantus_sdk/lib/src/models/pending_transfer_event.dart b/quantus_sdk/lib/src/models/pending_transfer_event.dart index 0dbd98a1b..52117ce69 100644 --- a/quantus_sdk/lib/src/models/pending_transfer_event.dart +++ b/quantus_sdk/lib/src/models/pending_transfer_event.dart @@ -37,7 +37,7 @@ class PendingTransactionEvent extends TransactionEvent { tempId: json['id'] as String, from: json['from'] as String, to: json['to'] as String, - amount: BigInt.from(json['amount']), + amount: BigInt.parse(json['amount'] as String), timestamp: DateTime.parse(json['timestamp']), blockHash: json['blockHash'] as String?, transactionState: TransactionState.values.firstWhere( @@ -52,7 +52,7 @@ class PendingTransactionEvent extends TransactionEvent { ), scheduledAtTime: json['scheduledAtTime'] != null ? DateTime.tryParse(json['scheduledAtTime']) : null, delaySeconds: json['delaySeconds'] ?? 0, - fee: json['fee'] != null ? BigInt.from(json['fee']) : null, + fee: json['fee'] != null ? BigInt.parse(json['fee'] as String) : null, extrinsicHash: json['extrinsicHash'] as String?, blockNumber: json['blockNumber'] ?? 0, error: json['error'] as String?, diff --git a/quantus_sdk/lib/src/models/transaction_event.dart b/quantus_sdk/lib/src/models/transaction_event.dart index efe1c2dda..c844c1941 100644 --- a/quantus_sdk/lib/src/models/transaction_event.dart +++ b/quantus_sdk/lib/src/models/transaction_event.dart @@ -52,9 +52,9 @@ class TransferEvent extends TransactionEvent { id: json['id'] as String, from: json['from']?['id'] as String? ?? '', to: json['to']?['id'] as String? ?? '', - amount: BigInt.from(json['amount']), + amount: BigInt.parse(json['amount'] as String), timestamp: DateTime.parse(json['timestamp'] as String), - fee: json['fee'] != null ? BigInt.from(json['fee']) : BigInt.zero, + fee: json['fee'] != null ? BigInt.parse(json['fee'] as String) : BigInt.zero, extrinsicHash: json['extrinsic']?['id'] as String?, blockNumber: blockHeight, blockHash: blockHash, @@ -110,7 +110,7 @@ class ReversibleTransferEvent extends TransactionEvent { id: json['id'] as String, from: transfer['from']?['id'] as String? ?? '', to: transfer['to']?['id'] as String? ?? '', - amount: BigInt.from(transfer['amount']), + amount: BigInt.parse(transfer['amount'] as String), timestamp: DateTime.parse(json['timestamp'] as String), txId: json['txId'] as String, status: status, From 9775189187819d2dfbd172c2fd2ce0c4a669fb2a Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 6 May 2026 13:01:53 +0800 Subject: [PATCH 03/14] chore: formatting --- quantus_sdk/lib/src/services/chain_history_service.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quantus_sdk/lib/src/services/chain_history_service.dart b/quantus_sdk/lib/src/services/chain_history_service.dart index ed15f3c27..529130707 100644 --- a/quantus_sdk/lib/src/services/chain_history_service.dart +++ b/quantus_sdk/lib/src/services/chain_history_service.dart @@ -22,7 +22,7 @@ class ChainHistoryService { ChainHistoryService(); -String _buildScheduledReversibleTransfersQuery(TransactionFilter filter) { + String _buildScheduledReversibleTransfersQuery(TransactionFilter filter) { final String whereClause; switch (filter) { @@ -89,7 +89,8 @@ query ScheduledReversibleTransfersByAccounts(\$accounts: [String!]!, \$limit: In // Transfer extrinsic guard — only include on-chain transfers. // Using direct `transfer_id` and `extrinsic_id` relation fields. - const String transferGuard = '{_or: [{transfer_id: {_is_null: true}}, {transfer: {extrinsic_id: {_is_null: false}}}]}'; + const String transferGuard = + '{_or: [{transfer_id: {_is_null: true}}, {transfer: {extrinsic_id: {_is_null: false}}}]}'; // Whether to include the minerReward field in the response final bool includeMinerReward = filter != TransactionFilter.send; @@ -198,7 +199,7 @@ query AccountEvents(\$accounts: [String!]!, \$limit: Int!, \$offset: Int!) { } } '''; -} + } // GraphQL query to fetch transactions by their hash final String _executedTransactionByTxId = r''' From 639aa0afafac7a4ec2ab59570cba8bb9d2e1655e Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 6 May 2026 15:15:40 +0800 Subject: [PATCH 04/14] feat: finish mining reward screen --- mobile-app/assets/v2/axe.svg | 3 + .../lib/features/components/get_started.dart | 17 +- .../lib/features/components/link_text.dart | 4 +- .../reversible_transaction_action_sheet.dart | 4 +- .../transaction_details_action_sheet.dart | 6 +- .../lib/services/mining_rewards_service.dart | 8 +- .../lib/shared/utils/open_external_url.dart | 13 + .../accounts/account_ready_screen.dart | 3 +- .../activity/transaction_detail_sheet.dart | 4 +- .../lib/v2/screens/home/activity_section.dart | 4 +- .../settings/about_quantus_screen.dart | 4 +- .../settings/help_and_support_screen.dart | 6 +- .../settings/mining_rewards_screen.dart | 331 ++++++++++++++++++ .../v2/screens/settings/settings_screen.dart | 49 ++- mobile-app/lib/v2/theme/app_colors.dart | 6 + mobile-app/lib/v2/theme/app_text_styles.dart | 17 + mobile-app/pubspec.yaml | 1 + .../lib/src/constants/app_constants.dart | 2 + 18 files changed, 439 insertions(+), 43 deletions(-) create mode 100644 mobile-app/assets/v2/axe.svg create mode 100644 mobile-app/lib/shared/utils/open_external_url.dart create mode 100644 mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart diff --git a/mobile-app/assets/v2/axe.svg b/mobile-app/assets/v2/axe.svg new file mode 100644 index 000000000..8c267333d --- /dev/null +++ b/mobile-app/assets/v2/axe.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile-app/lib/features/components/get_started.dart b/mobile-app/lib/features/components/get_started.dart index fef34d44f..58e6f1aee 100644 --- a/mobile-app/lib/features/components/get_started.dart +++ b/mobile-app/lib/features/components/get_started.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/utils/url_utils.dart'; -import 'package:url_launcher/url_launcher.dart'; class GetStarted extends StatelessWidget { const GetStarted({super.key}); @@ -33,26 +33,17 @@ class GetStarted extends StatelessWidget { ), const SizedBox(height: 25), GestureDetector( - onTap: () { - final Uri url = Uri.parse(AppConstants.tutorialsAndGuidesUrl); - launchUrl(url); - }, + onTap: () => openUrl(AppConstants.tutorialsAndGuidesUrl), child: Text('Tutorials & Guides →', style: context.themeText.smallParagraph), ), const SizedBox(height: 25), GestureDetector( - onTap: () { - final Uri url = Uri.parse(AppConstants.communityUrl); - launchUrl(url); - }, + onTap: () => openUrl(AppConstants.communityUrl), child: Text('Community →', style: context.themeText.smallParagraph), ), const SizedBox(height: 25), GestureDetector( - onTap: () { - final Uri url = Uri.parse(AppConstants.techSupportUrl); - launchUrl(url); - }, + onTap: () => openUrl(AppConstants.techSupportUrl), child: Text('Tech Support →', style: context.themeText.smallParagraph), ), ], diff --git a/mobile-app/lib/features/components/link_text.dart b/mobile-app/lib/features/components/link_text.dart index e93315130..567f8537b 100644 --- a/mobile-app/lib/features/components/link_text.dart +++ b/mobile-app/lib/features/components/link_text.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; class LinkText extends StatelessWidget { final String label; @@ -19,7 +19,7 @@ class LinkText extends StatelessWidget { child: Text(label, style: effectiveTextStyle), onTap: () { final Uri uri = Uri.parse(url); - launchUrl(uri); + openUrl(uri.toString()); }, ); } diff --git a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart index ef04518f3..822e40289 100644 --- a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart +++ b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart @@ -16,8 +16,8 @@ import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/reversible_transfer_monitoring_service.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/shared/utils/tx_filter_family_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; enum ReversibleTransactionMode { reversible, guardianIntercept } @@ -310,7 +310,7 @@ class _ReversibleTransactionActionSheetState extends ConsumerState openUrl(String urlString, {LaunchMode mode = LaunchMode.platformDefault}) async { + final uri = Uri.parse(urlString); + try { + final launched = await launchUrl(uri, mode: mode); + if (!launched) { + print('launchUrl returned false: $urlString'); + } + } catch (e, st) { + print('launchUrl failed: $urlString error=$e\n$st'); + } +} diff --git a/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart b/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart index 2c173c72d..fc4b1959b 100644 --- a/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart @@ -25,7 +25,6 @@ class AccountReadyScreen extends StatelessWidget { final String checksumPhrase; final String accountId; - static const _galleryLargeTitle = Color(0xFFEBEBEB); static const _successRingSize = 78.0; static const _checkIconSize = 32.0; static const _borderWidth = 2.0; @@ -92,7 +91,7 @@ class AccountReadyScreen extends StatelessWidget { Text( headline, textAlign: TextAlign.center, - style: text.paragraph?.copyWith(fontSize: 32, color: _galleryLargeTitle, height: 1.0), + style: text.paragraph?.copyWith(fontSize: 32, color: colors.textLightGray, height: 1.0), ), ], ), diff --git a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart index fbc0a4c8d..ef5ef693d 100644 --- a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart +++ b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart @@ -5,11 +5,11 @@ import 'package:resonance_network_wallet/features/components/dotted_border.dart' import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/amount_display_with_conversion.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -import 'package:url_launcher/url_launcher.dart'; void showTransactionDetailSheet(BuildContext context, TransactionEvent tx, String activeAccountId) { BottomSheetContainer.show( @@ -213,6 +213,6 @@ class _ExplorerLink extends StatelessWidget { path = '$transactionType/${tx.blockHash}'; } - if (path != null) launchUrl(Uri.parse('${AppConstants.explorerEndpoint}/$path')); + if (path != null) openUrl('${AppConstants.explorerEndpoint}/$path'); } } diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 48033b6f7..0cef11203 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -7,6 +7,7 @@ import 'package:resonance_network_wallet/models/combined_transactions_list.dart' import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/utils/url_utils.dart'; import 'package:resonance_network_wallet/v2/screens/activity/activity_screen.dart'; import 'package:resonance_network_wallet/v2/screens/activity/transaction_detail_sheet.dart'; @@ -14,7 +15,6 @@ import 'package:resonance_network_wallet/v2/screens/activity/tx_item.dart'; import 'package:resonance_network_wallet/v2/screens/settings/testnet_rewards_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -import 'package:url_launcher/url_launcher.dart'; class ActivitySection extends ConsumerStatefulWidget { final AsyncValue txAsync; @@ -179,7 +179,7 @@ class _ActivitySectionState extends ConsumerState { behavior: HitTestBehavior.opaque, onTap: () => links[i].$2 == AppConstants.faucetUrl ? launchXPost(links[i].$2) - : launchUrl(Uri.parse(links[i].$2)), + : openUrl(links[i].$2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart index c0534e644..e9c369b02 100644 --- a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart +++ b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -import 'package:url_launcher/url_launcher.dart'; class AboutQuantusScreenV2 extends StatelessWidget { const AboutQuantusScreenV2({super.key}); @@ -49,7 +49,7 @@ class AboutQuantusScreenV2 extends StatelessWidget { SettingsTappableRow( title: entry.value.title, subtitle: entry.value.subtitle, - onTap: () => launchUrl(_uriForAboutLink(entry.value)), + onTap: () => openUrl(_uriForAboutLink(entry.value).toString()), trailing: SettingsTappableRowUtils.externalLink(colors), ), if (entry.key < _externalLinks.length - 1) const SettingsDivider(), diff --git a/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart b/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart index 3c75102c2..2dc2c32ba 100644 --- a/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart +++ b/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -import 'package:url_launcher/url_launcher.dart'; class HelpAndSupportScreenV2 extends StatelessWidget { const HelpAndSupportScreenV2({super.key}); @@ -22,13 +22,13 @@ class HelpAndSupportScreenV2 extends StatelessWidget { title: 'Email Support', subtitle: AppConstants.emailSupport, colors: colors, - onTap: () => launchUrl(Uri.parse('mailto:${AppConstants.emailSupport}')), + onTap: () => openUrl('mailto:${AppConstants.emailSupport}'), ), _contactBlock( title: 'Telegram', subtitle: AppConstants.telegramHandle, colors: colors, - onTap: () => launchUrl(Uri.parse(AppConstants.communityUrl)), + onTap: () => openUrl(AppConstants.communityUrl), showBottomDivider: false, ), ], diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart new file mode 100644 index 000000000..800746966 --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -0,0 +1,331 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; +import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; +import 'package:resonance_network_wallet/v2/components/loader.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/v2/components/split_card.dart'; +import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class MiningRewardsScreen extends ConsumerWidget { + const MiningRewardsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final miningAsync = ref.watch(miningRewardsProvider); + final colors = context.colors; + final text = context.themeText; + + return ScaffoldBase.refreshable( + appBar: const V2AppBar(title: 'Mining Rewards'), + onRefresh: () async => ref.invalidate(miningRewardsProvider), + slivers: [ + miningAsync.when( + skipLoadingOnRefresh: false, + data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : const _NoRewards(), + loading: () => const SizedBox(height: 200, child: Center(child: Loader(size: 32))), + error: (err, _) => + _ErrorState(colors: colors, text: text, onRetry: () => ref.invalidate(miningRewardsProvider)), + ), + ], + ); + } +} + +class _WithRewards extends StatelessWidget { + final MiningRewardsData data; + + const _WithRewards({required this.data}); + + static const _blockReward = 0.386613134081; + static const _resonanceSince = 'Jul 2025'; + static const _schrodingerSince = 'Oct 2025'; + static const _diracSince = 'Nov 2025'; + static const _planckSince = 'Jan 2026'; + + String get _quanEarned => (data.totalBlocks * _blockReward).toStringAsFixed(1); + + String get _activeSince { + if (data.resonanceBlocks > 0) return _resonanceSince; + if (data.schrodingerBlocks > 0) return _schrodingerSince; + if (data.diracBlocks > 0) return _diracSince; + return _planckSince; + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + + final testnets = [ + _TestnetEntry('Planck', 'Canary network · Active now', data.planckBlocks, isActive: true), + _TestnetEntry('Dirac', _diracSince, data.diracBlocks), + _TestnetEntry('Schrödinger', _schrodingerSince, data.schrodingerBlocks), + _TestnetEntry('Resonance', _resonanceSince, data.resonanceBlocks), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SplitCard( + topChild: _CardTopSection( + totalBlocks: data.totalBlocks, + totalBlocksColor: colors.textLightGray, + statusLabel: 'Mining', + statusColor: colors.success, + ), + bottomChild: Row( + children: [ + _StatColumn(label: 'QUAN EARNED', value: _quanEarned, valueColor: colors.accentOrange), + const SizedBox(width: 64), + _StatColumn(label: 'ACTIVE SINCE', value: _activeSince, valueColor: colors.textPrimary), + ], + ), + ), + const SizedBox(height: 32), + for (var i = 0; i < testnets.length; i++) ...[ + _TestnetRow(entry: testnets[i]), + if (i < testnets.length - 1) Divider(color: colors.toasterBackground, height: 1, thickness: 1), + ], + const SizedBox(height: 48), + Center( + child: _OrangeLinkButton( + label: 'View Telemetry ↗', + text: text, + onTap: () => openUrl(AppConstants.telemetryUrl), + ), + ), + const SizedBox(height: 24), + ], + ); + } +} + +class _NoRewards extends StatelessWidget { + const _NoRewards(); + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SplitCard( + topChild: _CardTopSection( + totalBlocks: 0, + totalBlocksColor: colors.textTertiary, + statusLabel: 'Pending', + statusColor: colors.textTertiary, + ), + bottomChild: _StatColumn(label: 'QUAN EARNED', value: '0.00', valueColor: colors.textTertiary), + ), + const SizedBox(height: 64), + Text( + 'No mining data yet', + style: text.mediumTitle?.copyWith(fontWeight: FontWeight.w400, color: colors.textMuted), + ), + const SizedBox(height: 8), + Text( + 'Set up a Quantus mining node to start earning rewards.', + textAlign: TextAlign.center, + style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault, height: 1.35), + ), + const SizedBox(height: 64), + _OrangeLinkButton( + label: 'Mining Setup Guide ↗', + text: text, + onTap: () => openUrl(AppConstants.miningSetupGuideUrl), + ), + const SizedBox(height: 24), + ], + ); + } +} + +class _CardTopSection extends StatelessWidget { + final int totalBlocks; + final Color totalBlocksColor; + final String statusLabel; + final Color statusColor; + + const _CardTopSection({ + required this.totalBlocks, + required this.totalBlocksColor, + required this.statusLabel, + required this.statusColor, + }); + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('BLOCKS MINED', style: text.receiveLabel?.copyWith(color: colors.textLabel)), + Row( + children: [ + Container( + width: 4, + height: 4, + decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle), + ), + const SizedBox(width: 4), + Text(statusLabel, style: text.smallParagraph?.copyWith(color: statusColor)), + ], + ), + ], + ), + const SizedBox(height: 8), + Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), + const SizedBox(height: 4), + Text('blocks across all testnets', style: text.detail?.copyWith(color: colors.textMuted)), + ], + ); + } +} + +class _StatColumn extends StatelessWidget { + final String label; + final String value; + final Color valueColor; + + const _StatColumn({required this.label, required this.value, required this.valueColor}); + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: text.receiveLabel?.copyWith(color: colors.textLabel)), + const SizedBox(height: 8), + Text(value, style: text.sendSectionLabel?.copyWith(color: valueColor)), + ], + ); + } +} + +class _TestnetRow extends StatelessWidget { + final _TestnetEntry entry; + + const _TestnetRow({required this.entry}); + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + final countColor = entry.isActive ? colors.success : colors.textLightGray; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(entry.name, style: text.smallTitle?.copyWith(fontWeight: FontWeight.w400)), + const SizedBox(height: 8), + Text(entry.subtitle, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${entry.blocks}', + style: text.smallTitle?.copyWith( + fontFamily: AppTextTheme.fontFamilySecondary, + fontWeight: FontWeight.w400, + color: countColor, + ), + ), + const SizedBox(height: 4), + Text('blocks', style: text.detail?.copyWith(color: colors.textMuted)), + ], + ), + ], + ), + ); + } +} + +class _OrangeLinkButton extends StatelessWidget { + final String label; + final AppTextTheme text; + final VoidCallback onTap; + + const _OrangeLinkButton({required this.label, required this.text, required this.onTap}); + + @override + Widget build(BuildContext context) { + final colors = context.colors; + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: colors.accentOrange, width: 1)), + ), + padding: const EdgeInsets.only(bottom: 3), + child: Text(label, style: text.smallParagraph?.copyWith(color: colors.accentOrange)), + ), + ); + } +} + +class _ErrorState extends StatelessWidget { + final AppColorsV2 colors; + final AppTextTheme text; + final VoidCallback onRetry; + + const _ErrorState({required this.colors, required this.text, required this.onRetry}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 200, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Failed to load mining rewards', style: text.paragraph?.copyWith(color: colors.textPrimary)), + const SizedBox(height: 8), + Text('Please check your connection', style: text.detail?.copyWith(color: colors.textTertiary)), + const SizedBox(height: 20), + GestureDetector( + onTap: onRetry, + child: Text( + 'Try Again', + style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), + ), + ), + ], + ), + ), + ); + } +} + +class _TestnetEntry { + final String name; + final String subtitle; + final int blocks; + final bool isActive; + + const _TestnetEntry(this.name, this.subtitle, this.blocks, {this.isActive = false}); +} diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 905f929e2..718bf4ab5 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/about_quantus_screen.dart'; @@ -9,14 +11,24 @@ import 'package:resonance_network_wallet/v2/screens/settings/help_and_support_sc import 'package:resonance_network_wallet/v2/screens/settings/preferences_settings_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/mining_rewards_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/wallet_settings_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class SettingsScreenV2 extends StatelessWidget { +const _miningRewardsTitle = 'Mining Rewards'; + +class SettingsScreenV2 extends ConsumerStatefulWidget { const SettingsScreenV2({super.key}); + @override + ConsumerState createState() => _SettingsScreenV2State(); +} + +class _SettingsScreenV2State extends ConsumerState { @override Widget build(BuildContext context) { + final miningAsync = ref.watch(miningRewardsProvider); + final colors = context.colors; final trailing = SettingsTappableRowUtils.chevron(colors); final entries = _settingsHubItems(colors); @@ -26,19 +38,34 @@ class SettingsScreenV2 extends StatelessWidget { mainContent: ListView( children: [ for (final e in entries.asMap().entries) ...[ - SettingsTappableRow( - leading: e.value.leading, - title: e.value.title, - subtitle: e.value.subtitle, - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => e.value.page)), - trailing: trailing, - ), + if (e.value.title == _miningRewardsTitle) + miningAsync.when( + data: (data) => + _buildTappableRow(e.value, subtitle: '${data.totalBlocks} blocks mined', trailing: trailing), + loading: () => _buildTappableRow(e.value, subtitle: 'Loading...', trailing: trailing), + error: (err, st) { + debugPrint('Error getting mining rewards: ${err.toString()}'); + debugPrint('Stack trace: ${st.toString()}'); + + return _buildTappableRow(e.value, subtitle: 'Error getting mining rewards', trailing: trailing); + }, + ) + else + _buildTappableRow(e.value, trailing: trailing), if (e.key < entries.length - 1) const SettingsDivider(), ], ], ), ); } + + Widget _buildTappableRow(_SettingsHubItem item, {required Widget trailing, String? subtitle}) => SettingsTappableRow( + leading: item.leading, + title: item.title, + subtitle: subtitle ?? item.subtitle, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => item.page)), + trailing: trailing, + ); } class _SettingsHubItem { @@ -64,6 +91,12 @@ List<_SettingsHubItem> _settingsHubItems(AppColorsV2 colors) { subtitle: 'Currency, POS mode, notifications', page: const PreferencesSettingsScreenV2(), ), + _SettingsHubItem( + leading: _settingsHubIcon(colors, svg: SvgPicture.asset('assets/v2/axe.svg', width: 18, height: 18)), + title: _miningRewardsTitle, + subtitle: 'Loading...', + page: const MiningRewardsScreen(), + ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.shield_outlined), title: 'Account Type', diff --git a/mobile-app/lib/v2/theme/app_colors.dart b/mobile-app/lib/v2/theme/app_colors.dart index 89cd2b904..6be68c7d1 100644 --- a/mobile-app/lib/v2/theme/app_colors.dart +++ b/mobile-app/lib/v2/theme/app_colors.dart @@ -17,6 +17,7 @@ class AppColorsV2 extends ThemeExtension { final Color textMuted; final Color textError; final Color textLabel; + final Color textLightGray; // Accents final Color accentOrange; @@ -83,6 +84,7 @@ class AppColorsV2 extends ThemeExtension { required this.textMuted, required this.textError, required this.textLabel, + required this.textLightGray, required this.accentOrange, required this.accentGreen, required this.checksum, @@ -132,6 +134,7 @@ class AppColorsV2 extends ThemeExtension { textMuted: const Color(0xFF888888), textError: const Color(0xFFC0392B), textLabel: const Color(0xFF787878), + textLightGray: const Color(0xFFEBEBEB), accentOrange: const Color(0xFFFF6B35), accentGreen: const Color(0xFF34C759), checksum: const Color(0xFF95A7FB), @@ -173,6 +176,7 @@ class AppColorsV2 extends ThemeExtension { Color? textMuted, Color? textError, Color? textLabel, + Color? textLightGray, Color? accentOrange, Color? accentGreen, Color? checksum, @@ -224,6 +228,7 @@ class AppColorsV2 extends ThemeExtension { textMuted: textMuted ?? this.textMuted, textError: textError ?? this.textError, textLabel: textLabel ?? this.textLabel, + textLightGray: textLightGray ?? this.textLightGray, accentOrange: accentOrange ?? this.accentOrange, accentGreen: accentGreen ?? this.accentGreen, checksum: checksum ?? this.checksum, @@ -275,6 +280,7 @@ class AppColorsV2 extends ThemeExtension { textMuted: Color.lerp(textMuted, other.textMuted, t) ?? textMuted, textError: Color.lerp(textError, other.textError, t) ?? textError, textLabel: Color.lerp(textLabel, other.textLabel, t) ?? textLabel, + textLightGray: Color.lerp(textLightGray, other.textLightGray, t) ?? textLightGray, accentOrange: Color.lerp(accentOrange, other.accentOrange, t) ?? accentOrange, accentGreen: Color.lerp(accentGreen, other.accentGreen, t) ?? accentGreen, checksum: Color.lerp(checksum, other.checksum, t) ?? checksum, diff --git a/mobile-app/lib/v2/theme/app_text_styles.dart b/mobile-app/lib/v2/theme/app_text_styles.dart index 382d9b0f2..441eb64c4 100644 --- a/mobile-app/lib/v2/theme/app_text_styles.dart +++ b/mobile-app/lib/v2/theme/app_text_styles.dart @@ -18,6 +18,7 @@ class AppTextTheme extends ThemeExtension { final TextStyle? timer; final TextStyle? detail; final TextStyle? tiny; + final TextStyle? totalMinedBlocks; final TextStyle? transactionDetailAmountPrimary; final TextStyle? transactionDetailAmountSymbol; final TextStyle? transactionDetailRowLabel; @@ -39,6 +40,7 @@ class AppTextTheme extends ThemeExtension { this.timer, this.detail, this.tiny, + this.totalMinedBlocks, this.transactionDetailAmountPrimary, this.transactionDetailAmountSymbol, this.transactionDetailRowLabel, @@ -62,6 +64,12 @@ class AppTextTheme extends ThemeExtension { timer: const TextStyle(fontSize: 28, fontWeight: FontWeight.w600, fontFamily: fontFamily), detail: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400, fontFamily: fontFamily), tiny: const TextStyle(fontSize: 11, fontWeight: FontWeight.w400, fontFamily: fontFamily), + totalMinedBlocks: const TextStyle( + fontFamily: fontFamilySecondary, + fontSize: 56, + fontWeight: FontWeight.w400, + height: 1.0, + ), transactionDetailAmountPrimary: const TextStyle( fontFamily: fontFamilySecondary, fontSize: 64, @@ -113,6 +121,12 @@ class AppTextTheme extends ThemeExtension { timer: const TextStyle(fontSize: 36, fontWeight: FontWeight.w600, fontFamily: fontFamily), detail: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, fontFamily: fontFamily), tiny: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400, fontFamily: fontFamily), + totalMinedBlocks: const TextStyle( + fontFamily: fontFamilySecondary, + fontSize: 60, + fontWeight: FontWeight.w400, + height: 1.0, + ), transactionDetailAmountPrimary: const TextStyle( fontFamily: fontFamilySecondary, fontSize: 80, @@ -164,6 +178,7 @@ class AppTextTheme extends ThemeExtension { TextStyle? timer, TextStyle? detail, TextStyle? tiny, + TextStyle? totalMinedBlocks, TextStyle? transactionDetailAmountPrimary, TextStyle? transactionDetailAmountSymbol, TextStyle? transactionDetailRowLabel, @@ -185,6 +200,7 @@ class AppTextTheme extends ThemeExtension { timer: timer ?? this.timer, detail: detail ?? this.detail, tiny: tiny ?? this.tiny, + totalMinedBlocks: totalMinedBlocks ?? this.totalMinedBlocks, transactionDetailAmountPrimary: transactionDetailAmountPrimary ?? this.transactionDetailAmountPrimary, transactionDetailAmountSymbol: transactionDetailAmountSymbol ?? this.transactionDetailAmountSymbol, transactionDetailRowLabel: transactionDetailRowLabel ?? this.transactionDetailRowLabel, @@ -211,6 +227,7 @@ class AppTextTheme extends ThemeExtension { timer: TextStyle.lerp(timer, other.timer, t), detail: TextStyle.lerp(detail, other.detail, t), tiny: TextStyle.lerp(tiny, other.tiny, t), + totalMinedBlocks: TextStyle.lerp(totalMinedBlocks, other.totalMinedBlocks, t), transactionDetailAmountPrimary: TextStyle.lerp( transactionDetailAmountPrimary, other.transactionDetailAmountPrimary, diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 849ff8656..bf81af5ee 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -107,6 +107,7 @@ flutter: - assets/v2/action_swap.svg - assets/v2/uppercase_q.svg - assets/v2/uppercase_q.png + - assets/v2/axe.svg - assets/v2/uppercase_q_black_bg.png - assets/v2/swap_arrows_down_up.svg - assets/v2/swap_clock_counter_clockwise.svg diff --git a/quantus_sdk/lib/src/constants/app_constants.dart b/quantus_sdk/lib/src/constants/app_constants.dart index a8a0d3df5..94b75f443 100644 --- a/quantus_sdk/lib/src/constants/app_constants.dart +++ b/quantus_sdk/lib/src/constants/app_constants.dart @@ -39,6 +39,8 @@ class AppConstants { static const String raidQuestsPageUrl = 'https://www.quantus.com/quests/raid'; static const String communityUrl = 'https://t.me/quantusnetwork'; static const String faucetUrl = 'https://x.com/QuantusNetwork/status/2033738875827589221'; + static const String miningSetupGuideUrl = 'https://docs.quantus.com/guides/mining'; + static const String telemetryUrl = 'https://telemetry.quantus.cat'; // Development accounts static const String crystalAlice = '//Crystal Alice'; From 5bda3a06ef91869fc2d304381bad5b972ff1917e Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 6 May 2026 17:26:42 +0800 Subject: [PATCH 05/14] feat: finish proper loading state --- .../lib/features/components/skeleton.dart | 91 ++++++++++------ .../lib/services/mining_rewards_service.dart | 8 +- .../v2/screens/activity/activity_screen.dart | 2 +- .../lib/v2/screens/home/activity_section.dart | 2 +- .../settings/mining_rewards_screen.dart | 100 +++++++++++++----- mobile-app/lib/v2/theme/app_colors.dart | 20 ++-- 6 files changed, 148 insertions(+), 75 deletions(-) diff --git a/mobile-app/lib/features/components/skeleton.dart b/mobile-app/lib/features/components/skeleton.dart index 65ae1bad3..37c5c4313 100644 --- a/mobile-app/lib/features/components/skeleton.dart +++ b/mobile-app/lib/features/components/skeleton.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; -import '../styles/app_colors_theme.dart'; - -const _defaultSkeletonBaseColor = Color(0xFF3D3C44); -const _defaultSkeletonHighlightColor = Color(0xFF5A5A5A); +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; /// A skeleton widget with shimmer animation for loading states class Skeleton extends StatefulWidget { @@ -11,25 +8,15 @@ class Skeleton extends StatefulWidget { final BorderRadius? borderRadius; final Duration duration; - const Skeleton({ - super.key, - this.width, - this.height = 16, - this.borderRadius, - this.duration = const Duration(milliseconds: 1500), - }); + static const defaultDuration = Duration(milliseconds: 1200); + + const Skeleton({super.key, this.width, this.height = 16, this.borderRadius, this.duration = defaultDuration}); /// Creates a circular skeleton (useful for avatars) - const Skeleton.circular({super.key, required double size, this.duration = const Duration(milliseconds: 1500)}) + Skeleton.circular({super.key, required double size, this.duration = defaultDuration}) : width = size, height = size, - borderRadius = null; - - /// Creates a skeleton for a transaction item - const Skeleton.txItem({super.key, this.duration = const Duration(milliseconds: 1500)}) - : width = double.infinity, - height = 40, - borderRadius = null; + borderRadius = BorderRadius.circular(size); @override State createState() => _SkeletonState(); @@ -58,13 +45,7 @@ class _SkeletonState extends State with SingleTickerProviderStateMixin @override Widget build(BuildContext context) { - final themeColors = Theme.of(context).extension(); - final baseColor = themeColors?.skeletonBase ?? _defaultSkeletonBaseColor; - final highlightColor = themeColors?.skeletonHighlight ?? _defaultSkeletonHighlightColor; - - final borderRadius = - widget.borderRadius ?? - (widget.width == widget.height ? BorderRadius.circular(widget.width ?? 0) : BorderRadius.circular(4)); + final borderRadius = widget.borderRadius ?? BorderRadius.circular(4); return AnimatedBuilder( animation: _animation, @@ -72,14 +53,22 @@ class _SkeletonState extends State with SingleTickerProviderStateMixin return Container( width: widget.width, height: widget.height, - decoration: BoxDecoration( - borderRadius: borderRadius, - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [baseColor, highlightColor, baseColor], - stops: const [0.0, 0.5, 1.0], - transform: _SlideGradientTransform(_animation.value), + decoration: BoxDecoration(borderRadius: borderRadius, color: context.colors.skeletonBase), + child: Opacity( + opacity: 0.2, + child: Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + borderRadius: borderRadius, + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [context.colors.skeletonHighlightA, context.colors.skeletonHighlightB, context.colors.skeletonHighlightA], + stops: const [0.0, 0.5, 1.0], + transform: _SlideGradientTransform(_animation.value), + ), + ), ), ), ); @@ -98,3 +87,37 @@ class _SlideGradientTransform extends GradientTransform { return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0); } } + +class TxItemSkeleton extends StatelessWidget { + const TxItemSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + final double txItemHeight = 32.0; + final double txItemDetailHeight = 12.0; + + return Row( + children: [ + Skeleton(width: txItemHeight, height: txItemHeight), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Skeleton(width: 64, height: txItemDetailHeight), + const SizedBox(height: 6), + Skeleton(width: 52, height: txItemDetailHeight), + ], + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Skeleton(width: 100, height: txItemDetailHeight), + const SizedBox(height: 6), + Skeleton(width: 88, height: txItemDetailHeight), + ], + ), + ], + ); + } +} diff --git a/mobile-app/lib/services/mining_rewards_service.dart b/mobile-app/lib/services/mining_rewards_service.dart index 95050f4b1..6bc6b4f14 100644 --- a/mobile-app/lib/services/mining_rewards_service.dart +++ b/mobile-app/lib/services/mining_rewards_service.dart @@ -53,10 +53,10 @@ class MiningRewardsService { print('[MiningRewards] Resonance: $resonance, Schrödinger: $schrodinger, Dirac: $dirac, Planck: $planck'); return MiningRewardsData( - resonanceBlocks: 100,// resonance, - schrodingerBlocks: 100,// schrodinger, - diracBlocks: 100,// dirac, - planckBlocks: 100,// planck, + resonanceBlocks: resonance, + schrodingerBlocks: schrodinger, + diracBlocks: dirac, + planckBlocks: planck, ); } diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index a557eae2d..a83526f7d 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -81,7 +81,7 @@ class _ActivityScreenState extends ConsumerState { const SizedBox(height: 12), for (var j = 0; j < 3; j++) ...[ - const Skeleton.txItem(), + const TxItemSkeleton(), if (j < 2) Divider(color: colors.txItemSeparator, height: 24), ], ], diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 0cef11203..287b16421 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -92,7 +92,7 @@ class _ActivitySectionState extends ConsumerState { _header(colors, text, context), const SizedBox(height: 24), for (var i = 0; i < 3; i++) ...[ - const Skeleton.txItem(), + const TxItemSkeleton(), if (i < 2) Divider(color: colors.txItemSeparator, height: 24), ], ], diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index 800746966..977ec74b9 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; -import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/split_card.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -25,9 +25,8 @@ class MiningRewardsScreen extends ConsumerWidget { onRefresh: () async => ref.invalidate(miningRewardsProvider), slivers: [ miningAsync.when( - skipLoadingOnRefresh: false, data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : const _NoRewards(), - loading: () => const SizedBox(height: 200, child: Center(child: Loader(size: 32))), + loading: () => const _NoRewards(isLoading: true), error: (err, _) => _ErrorState(colors: colors, text: text, onRetry: () => ref.invalidate(miningRewardsProvider)), ), @@ -106,7 +105,9 @@ class _WithRewards extends StatelessWidget { } class _NoRewards extends StatelessWidget { - const _NoRewards(); + final bool isLoading; + + const _NoRewards({this.isLoading = false}); @override Widget build(BuildContext context) { @@ -122,27 +123,59 @@ class _NoRewards extends StatelessWidget { totalBlocksColor: colors.textTertiary, statusLabel: 'Pending', statusColor: colors.textTertiary, + isLoading: isLoading, + ), + bottomChild: _StatColumn( + label: 'QUAN EARNED', + value: '0.00', + valueColor: colors.textTertiary, + isLoading: isLoading, ), - bottomChild: _StatColumn(label: 'QUAN EARNED', value: '0.00', valueColor: colors.textTertiary), - ), - const SizedBox(height: 64), - Text( - 'No mining data yet', - style: text.mediumTitle?.copyWith(fontWeight: FontWeight.w400, color: colors.textMuted), - ), - const SizedBox(height: 8), - Text( - 'Set up a Quantus mining node to start earning rewards.', - textAlign: TextAlign.center, - style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault, height: 1.35), - ), - const SizedBox(height: 64), - _OrangeLinkButton( - label: 'Mining Setup Guide ↗', - text: text, - onTap: () => openUrl(AppConstants.miningSetupGuideUrl), ), - const SizedBox(height: 24), + if (isLoading) ...[ + const SizedBox(height: 32), + for (var i = 0; i < 4; i++) ...[ + const Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Skeleton(width: 100), SizedBox(height: 8), Skeleton(width: 72)], + ), + ), + Spacer(), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [Skeleton(width: 72), SizedBox(height: 8), Skeleton(width: 56)], + ), + ), + ], + ), + const SizedBox(height: 16), + if (i < 3) Divider(color: colors.toasterBackground, height: 1, thickness: 1), + const SizedBox(height: 24), + ], + ] else ...[ + const SizedBox(height: 64), + Text( + 'No mining data yet', + style: text.mediumTitle?.copyWith(fontWeight: FontWeight.w400, color: colors.textMuted), + ), + const SizedBox(height: 8), + Text( + 'Set up a Quantus mining node to start earning rewards.', + textAlign: TextAlign.center, + style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault, height: 1.35), + ), + const SizedBox(height: 64), + _OrangeLinkButton( + label: 'Mining Setup Guide ↗', + text: text, + onTap: () => openUrl(AppConstants.miningSetupGuideUrl), + ), + const SizedBox(height: 24), + ], ], ); } @@ -153,12 +186,14 @@ class _CardTopSection extends StatelessWidget { final Color totalBlocksColor; final String statusLabel; final Color statusColor; + final bool isLoading; const _CardTopSection({ required this.totalBlocks, required this.totalBlocksColor, required this.statusLabel, required this.statusColor, + this.isLoading = false, }); @override @@ -166,7 +201,6 @@ class _CardTopSection extends StatelessWidget { final colors = context.colors; final text = context.themeText; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -182,13 +216,19 @@ class _CardTopSection extends StatelessWidget { decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle), ), const SizedBox(width: 4), - Text(statusLabel, style: text.smallParagraph?.copyWith(color: statusColor)), + if (isLoading) + const Skeleton(width: 100, height: 24) + else + Text(statusLabel, style: text.smallParagraph?.copyWith(color: statusColor)), ], ), ], ), const SizedBox(height: 8), - Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), + if (isLoading) + const Skeleton(width: 100, height: 24) + else + Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), const SizedBox(height: 4), Text('blocks across all testnets', style: text.detail?.copyWith(color: colors.textMuted)), ], @@ -200,8 +240,9 @@ class _StatColumn extends StatelessWidget { final String label; final String value; final Color valueColor; + final bool isLoading; - const _StatColumn({required this.label, required this.value, required this.valueColor}); + const _StatColumn({required this.label, required this.value, required this.valueColor, this.isLoading = false}); @override Widget build(BuildContext context) { @@ -213,7 +254,10 @@ class _StatColumn extends StatelessWidget { children: [ Text(label, style: text.receiveLabel?.copyWith(color: colors.textLabel)), const SizedBox(height: 8), - Text(value, style: text.sendSectionLabel?.copyWith(color: valueColor)), + if (isLoading) + const Skeleton(width: 100, height: 24) + else + Text(value, style: text.sendSectionLabel?.copyWith(color: valueColor)), ], ); } diff --git a/mobile-app/lib/v2/theme/app_colors.dart b/mobile-app/lib/v2/theme/app_colors.dart index 6be68c7d1..4210495df 100644 --- a/mobile-app/lib/v2/theme/app_colors.dart +++ b/mobile-app/lib/v2/theme/app_colors.dart @@ -46,7 +46,8 @@ class AppColorsV2 extends ThemeExtension { final Color buttonDisabled; final Color buttonDanger; final Color skeletonBase; - final Color skeletonHighlight; + final Color skeletonHighlightA; + final Color skeletonHighlightB; final Color toasterBorder; final Color toasterBackground; final Color sheetBackground; @@ -105,7 +106,8 @@ class AppColorsV2 extends ThemeExtension { required this.buttonDisabled, required this.buttonDanger, required this.skeletonBase, - required this.skeletonHighlight, + required this.skeletonHighlightA, + required this.skeletonHighlightB, required this.segmentedControlPill, required this.surfaceDeep, required this.copyButtonCopiedBg, @@ -157,8 +159,9 @@ class AppColorsV2 extends ThemeExtension { copyButtonCopiedBorder: const Color(0xFF1A3226), buttonDisabled: const Color(0xFF3D3C44), buttonDanger: const Color(0x1AFF0000), - skeletonBase: const Color(0xFF3D3C44), - skeletonHighlight: const Color(0xFF5A5A5A), + skeletonBase: const Color(0xFF161616), + skeletonHighlightA: const Color(0xFF000000), + skeletonHighlightB: const Color(0xFF666666), tagGuardian: const Color(0xFF9747FF), tagEntrusted: const Color(0xFFFFD541), tagHighSecurity: const Color(0xFF4CEDE7), @@ -203,7 +206,8 @@ class AppColorsV2 extends ThemeExtension { Color? buttonDanger, Color? borderDanger, Color? skeletonBase, - Color? skeletonHighlight, + Color? skeletonHighlightA, + Color? skeletonHighlightB, Color? segmentedControlPill, Color? surfaceDeep, Color? copyButtonCopiedBg, @@ -250,7 +254,8 @@ class AppColorsV2 extends ThemeExtension { buttonDanger: buttonDanger ?? this.buttonDanger, borderDanger: borderDanger ?? this.borderDanger, skeletonBase: skeletonBase ?? this.skeletonBase, - skeletonHighlight: skeletonHighlight ?? this.skeletonHighlight, + skeletonHighlightA: skeletonHighlightA ?? this.skeletonHighlightA, + skeletonHighlightB: skeletonHighlightB ?? this.skeletonHighlightB, segmentedControlPill: segmentedControlPill ?? this.segmentedControlPill, surfaceDeep: surfaceDeep ?? this.surfaceDeep, copyButtonCopiedBg: copyButtonCopiedBg ?? this.copyButtonCopiedBg, @@ -308,7 +313,8 @@ class AppColorsV2 extends ThemeExtension { buttonDanger: Color.lerp(buttonDanger, other.buttonDanger, t) ?? buttonDanger, borderDanger: Color.lerp(borderDanger, other.borderDanger, t) ?? borderDanger, skeletonBase: Color.lerp(skeletonBase, other.skeletonBase, t) ?? skeletonBase, - skeletonHighlight: Color.lerp(skeletonHighlight, other.skeletonHighlight, t) ?? skeletonHighlight, + skeletonHighlightA: Color.lerp(skeletonHighlightA, other.skeletonHighlightA, t) ?? skeletonHighlightA, + skeletonHighlightB: Color.lerp(skeletonHighlightB, other.skeletonHighlightB, t) ?? skeletonHighlightB, segmentedControlPill: Color.lerp(segmentedControlPill, other.segmentedControlPill, t) ?? segmentedControlPill, surfaceDeep: Color.lerp(surfaceDeep, other.surfaceDeep, t) ?? surfaceDeep, copyButtonCopiedBg: Color.lerp(copyButtonCopiedBg, other.copyButtonCopiedBg, t) ?? copyButtonCopiedBg, From db03006204ac7fba9ebf8956e601727de1403a9a Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 6 May 2026 17:45:18 +0800 Subject: [PATCH 06/14] feat: update empty state in home --- .../lib/v2/screens/home/activity_section.dart | 106 +++--------------- .../lib/v2/screens/home/home_screen.dart | 15 +++ 2 files changed, 28 insertions(+), 93 deletions(-) diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 287b16421..6cf1d9864 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -7,12 +7,9 @@ import 'package:resonance_network_wallet/models/combined_transactions_list.dart' import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; -import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; -import 'package:resonance_network_wallet/utils/url_utils.dart'; import 'package:resonance_network_wallet/v2/screens/activity/activity_screen.dart'; import 'package:resonance_network_wallet/v2/screens/activity/transaction_detail_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/activity/tx_item.dart'; -import 'package:resonance_network_wallet/v2/screens/settings/testnet_rewards_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -28,8 +25,6 @@ class ActivitySection extends ConsumerStatefulWidget { } class _ActivitySectionState extends ConsumerState { - bool _getStartedExpanded = true; - @override Widget build(BuildContext context) { final formatTxAmount = ref.watch(txAmountDisplayProvider); @@ -49,13 +44,7 @@ class _ActivitySectionState extends ConsumerState { if (all.isEmpty) { return Column( - children: [ - const SizedBox(height: 40), - _header(colors, text, context), - _emptyState(text, colors), - const SizedBox(height: 40), - _getStartedSection(text, colors), - ], + children: [const SizedBox(height: 40), _header(colors, text, context), _emptyState(text, colors)], ); } @@ -130,90 +119,21 @@ class _ActivitySectionState extends ConsumerState { padding: const EdgeInsets.symmetric(vertical: 24), child: Column( children: [ - Text('No Transactions Yet', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), - const SizedBox(height: 8), - Text('Your activity will appear here', style: text.detail?.copyWith(color: colors.textSecondary)), - ], - ), - ); - } - - Widget _getStartedSection(AppTextTheme text, AppColorsV2 colors) { - const links = [ - ('Get Testnet Tokens', AppConstants.faucetUrl), - ('Community', AppConstants.communityUrl), - // ('Tech Support', AppConstants.techSupportUrl), - ]; - - return Column( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => _getStartedExpanded = !_getStartedExpanded), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Get Started', style: text.smallTitle), - Icon( - _getStartedExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, - color: colors.textSecondary, - size: 16, - ), - ], + Text( + 'No Transactions Yet', + style: text.mediumTitle?.copyWith(color: colors.textMuted, fontWeight: FontWeight.w400), ), - ), - AnimatedCrossFade( - firstChild: Padding( - padding: const EdgeInsets.only(top: 24), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(14), - ), - child: Column( - children: [ - for (var i = 0; i < links.length; i++) ...[ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => links[i].$2 == AppConstants.faucetUrl - ? launchXPost(links[i].$2) - : openUrl(links[i].$2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(links[i].$1, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), - Icon(Icons.arrow_outward, color: colors.textPrimary, size: 20), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Divider(color: colors.separator, height: 0), - ), - ], - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => - Navigator.push(context, MaterialPageRoute(builder: (_) => const TestnetRewardsScreen())), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Testnet Rewards', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), - Icon(Icons.chevron_right, color: colors.textPrimary, size: 20), - ], - ), - ), - ], - ), + const SizedBox(height: 8), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 240), + child: Text( + 'Your activity will appear here once you send or receive QUAN.', + textAlign: TextAlign.center, + style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault), ), ), - secondChild: const SizedBox.shrink(), - crossFadeState: _getStartedExpanded ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - ), - ], + ], + ), ); } diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 672ba9a05..483cf9142 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -6,10 +6,12 @@ import 'package:resonance_network_wallet/features/components/dotted_border.dart' import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; +import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/amount_display_with_conversion.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/open_accounts_management_button.dart'; import 'package:resonance_network_wallet/v2/screens/receive/receive_screen.dart'; import 'package:resonance_network_wallet/v2/screens/send/input_amount_screen.dart'; @@ -87,6 +89,7 @@ class _HomeScreenState extends ConsumerState { }); final isPosMode = ref.watch(posModeProvider); + final balanceAsync = ref.watch(balanceProvider); final accountAsync = ref.watch(activeAccountProvider); final txAsync = ref.watch(activeAccountTransactionsProvider(TransactionFilter.all)); final colors = context.colors; @@ -110,6 +113,18 @@ class _HomeScreenState extends ConsumerState { ActivitySection(txAsync: txAsync, activeAccount: active.account, onRetry: _refresh), SizedBox(height: isPosMode ? 120 : 58), ], + bottomContent: balanceAsync + .whenData( + (balance) => balance == BigInt.zero + ? ScaffoldBaseBottomContent( + child: QuantusButton.simple( + label: 'Get Testnet Tokens ↗', + onTap: () => openUrl(AppConstants.faucetUrl), + ), + ) + : null, + ) + .value, ); }, ); From 5379fe84afbda5173e1b38ccf1a9d4a4b44affb3 Mon Sep 17 00:00:00 2001 From: Beast Date: Thu, 7 May 2026 13:41:27 +0800 Subject: [PATCH 07/14] feat: update graphql wormhole to comply hasura --- .../lib/src/services/wormhole_utxo_service.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart index 3845bdfa5..e59e9a0c0 100644 --- a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart +++ b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart @@ -196,9 +196,9 @@ class WormholeUtxoService { }) async { const query = r''' query TransfersToAddress($to: String!, $limit: Int!, $offset: Int!, $afterBlock: Int) { - transfers( - where: { to: { id_eq: $to }, block: { height_gt: $afterBlock } } - orderBy: [block_height_ASC] + transfers: transfer( + where: { to: { id: {_eq: $to } }, block: { height: {_gt: $afterBlock } } } + order_by: {block_height: asc} limit: $limit offset: $offset ) { @@ -208,8 +208,8 @@ query TransfersToAddress($to: String!, $limit: Int!, $offset: Int!, $afterBlock: to { id } amount toHash - leafIndex - transferCount + leafIndex: leaf_index + transferCount: transfer_count } }'''; @@ -303,8 +303,8 @@ query TransfersToAddress($to: String!, $limit: Int!, $offset: Int!, $afterBlock: Future> _querySpentNullifierHashes(List nullifierHashes) async { const query = r''' query SpentNullifiers($hashes: [String!]!) { - wormholeNullifiers(where: { nullifierHash_in: $hashes }, limit: 1000) { - nullifierHash + wormholeNullifiers: wormhole_nullifier(where: { nullifier_hash: {_in: $hashes } }, limit: 1000) { + nullifierHash: nullifier_hash block { height } } }'''; From f1776f8ebf47a9bdd6f8323ee2bbed9597cbebe1 Mon Sep 17 00:00:00 2001 From: Beast Date: Thu, 7 May 2026 14:31:41 +0800 Subject: [PATCH 08/14] feat: make planck as the highlighted mined stats and earned --- .../providers/mining_rewards_provider.dart | 2 +- .../lib/services/mining_rewards_service.dart | 12 ++++----- .../lib/v2/screens/home/home_screen.dart | 4 +-- .../settings/mining_rewards_screen.dart | 25 +++++++++---------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/mobile-app/lib/providers/mining_rewards_provider.dart b/mobile-app/lib/providers/mining_rewards_provider.dart index c75a978d1..e177f4e81 100644 --- a/mobile-app/lib/providers/mining_rewards_provider.dart +++ b/mobile-app/lib/providers/mining_rewards_provider.dart @@ -9,7 +9,7 @@ final miningRewardsProvider = FutureProvider((ref) async { final service = ref.watch(miningRewardsServiceProvider); final accounts = ref.watch(accountsProvider).value; if (accounts == null || accounts.isEmpty) { - return const MiningRewardsData(resonanceBlocks: 0, schrodingerBlocks: 0, diracBlocks: 0, planckBlocks: 0); + return MiningRewardsData(resonanceBlocks: 0, schrodingerBlocks: 0, diracBlocks: 0, planckBlocks: 0, planckRewards: BigInt.zero); } final oldMiningAccountId = await TaskmasterService().getOldMiningAccountId(); final accountsList = accounts.map((a) => a.accountId).toList(); diff --git a/mobile-app/lib/services/mining_rewards_service.dart b/mobile-app/lib/services/mining_rewards_service.dart index 6bc6b4f14..1ca26ee12 100644 --- a/mobile-app/lib/services/mining_rewards_service.dart +++ b/mobile-app/lib/services/mining_rewards_service.dart @@ -9,12 +9,14 @@ class MiningRewardsData { final int schrodingerBlocks; final int diracBlocks; final int planckBlocks; + final BigInt planckRewards; const MiningRewardsData({ required this.resonanceBlocks, required this.schrodingerBlocks, required this.diracBlocks, required this.planckBlocks, + required this.planckRewards, }); int get totalBlocks => resonanceBlocks + schrodingerBlocks + diracBlocks + planckBlocks; @@ -49,22 +51,18 @@ class MiningRewardsService { final resonance = _countBlocks('resonance', miners['resonance']!, allAccountIds); final schrodinger = _countBlocks('schrodinger', miners['schrodinger']!, allAccountIds); final dirac = _countBlocks('dirac', miners['dirac']!, allAccountIds); - final planck = await _fetchPlanckBlocks(allAccountIds); + final planck = await TaskmasterService().getMinerStats(); print('[MiningRewards] Resonance: $resonance, Schrödinger: $schrodinger, Dirac: $dirac, Planck: $planck'); return MiningRewardsData( resonanceBlocks: resonance, schrodingerBlocks: schrodinger, diracBlocks: dirac, - planckBlocks: planck, + planckBlocks: planck.totalMinedBlocks, + planckRewards: planck.totalRewards, ); } - Future _fetchPlanckBlocks(Set accountIds) async { - final minerStats = await TaskmasterService().getMinerStats(); - return minerStats.totalMinedBlocks; - } - List<_MinerEntry> _parseMiners(String jsonStr) { final decoded = jsonDecode(jsonStr); final stats = decoded['data']['minerStats'] as List; diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 483cf9142..0df0ed2a2 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -6,7 +6,7 @@ import 'package:resonance_network_wallet/features/components/dotted_border.dart' import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; -import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; +import 'package:resonance_network_wallet/utils/url_utils.dart'; import 'package:resonance_network_wallet/v2/components/amount_display_with_conversion.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -119,7 +119,7 @@ class _HomeScreenState extends ConsumerState { ? ScaffoldBaseBottomContent( child: QuantusButton.simple( label: 'Get Testnet Tokens ↗', - onTap: () => openUrl(AppConstants.faucetUrl), + onTap: () => launchXPost(AppConstants.faucetUrl), ), ) : null, diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index 977ec74b9..56171298c 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -35,19 +36,16 @@ class MiningRewardsScreen extends ConsumerWidget { } } -class _WithRewards extends StatelessWidget { +class _WithRewards extends ConsumerWidget { final MiningRewardsData data; const _WithRewards({required this.data}); - static const _blockReward = 0.386613134081; static const _resonanceSince = 'Jul 2025'; static const _schrodingerSince = 'Oct 2025'; static const _diracSince = 'Nov 2025'; static const _planckSince = 'Jan 2026'; - String get _quanEarned => (data.totalBlocks * _blockReward).toStringAsFixed(1); - String get _activeSince { if (data.resonanceBlocks > 0) return _resonanceSince; if (data.schrodingerBlocks > 0) return _schrodingerSince; @@ -56,12 +54,14 @@ class _WithRewards extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final numberFmt = ref.watch(numberFormattingServiceProvider); + final quanEarned = numberFmt.formatBalance(data.planckRewards, maxDecimals: 1); + final colors = context.colors; final text = context.themeText; final testnets = [ - _TestnetEntry('Planck', 'Canary network · Active now', data.planckBlocks, isActive: true), _TestnetEntry('Dirac', _diracSince, data.diracBlocks), _TestnetEntry('Schrödinger', _schrodingerSince, data.schrodingerBlocks), _TestnetEntry('Resonance', _resonanceSince, data.resonanceBlocks), @@ -72,14 +72,14 @@ class _WithRewards extends StatelessWidget { children: [ SplitCard( topChild: _CardTopSection( - totalBlocks: data.totalBlocks, - totalBlocksColor: colors.textLightGray, + totalBlocks: data.planckBlocks, + totalBlocksColor: colors.success, statusLabel: 'Mining', statusColor: colors.success, ), bottomChild: Row( children: [ - _StatColumn(label: 'QUAN EARNED', value: _quanEarned, valueColor: colors.accentOrange), + _StatColumn(label: 'QUAN EARNED', value: quanEarned, valueColor: colors.accentOrange), const SizedBox(width: 64), _StatColumn(label: 'ACTIVE SINCE', value: _activeSince, valueColor: colors.textPrimary), ], @@ -230,7 +230,7 @@ class _CardTopSection extends StatelessWidget { else Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), const SizedBox(height: 4), - Text('blocks across all testnets', style: text.detail?.copyWith(color: colors.textMuted)), + Text('on the Planck testnet', style: text.detail?.copyWith(color: colors.textMuted)), ], ); } @@ -272,7 +272,7 @@ class _TestnetRow extends StatelessWidget { Widget build(BuildContext context) { final colors = context.colors; final text = context.themeText; - final countColor = entry.isActive ? colors.success : colors.textLightGray; + final countColor = colors.textLightGray; return Padding( padding: const EdgeInsets.symmetric(vertical: 16), @@ -369,7 +369,6 @@ class _TestnetEntry { final String name; final String subtitle; final int blocks; - final bool isActive; - const _TestnetEntry(this.name, this.subtitle, this.blocks, {this.isActive = false}); + const _TestnetEntry(this.name, this.subtitle, this.blocks); } From 43b3ddae8c12e8b585efe3d29d408c8735beea3c Mon Sep 17 00:00:00 2001 From: Beast Date: Thu, 7 May 2026 14:33:40 +0800 Subject: [PATCH 09/14] fix: query for transfer change toHash to to_hash --- quantus_sdk/lib/src/services/wormhole_utxo_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart index e59e9a0c0..04cb3624b 100644 --- a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart +++ b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart @@ -207,7 +207,7 @@ query TransfersToAddress($to: String!, $limit: Int!, $offset: Int!, $afterBlock: from { id } to { id } amount - toHash + toHash: to_hash leafIndex: leaf_index transferCount: transfer_count } From 2da531489f4cb7a73f3700acba25ad022b64dc9a Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 8 May 2026 13:39:13 +0800 Subject: [PATCH 10/14] feat: show current testnet details and redeem button --- .../lib/features/components/skeleton.dart | 6 +- .../providers/mining_rewards_provider.dart | 19 ++++- .../lib/providers/wallet_providers.dart | 8 ++ .../lib/services/mining_rewards_service.dart | 22 +++-- .../settings/mining_rewards_screen.dart | 80 +++++++++++++++---- .../lib/src/services/taskmaster_service.dart | 2 - .../src/services/wormhole_utxo_service.dart | 2 +- 7 files changed, 111 insertions(+), 28 deletions(-) diff --git a/mobile-app/lib/features/components/skeleton.dart b/mobile-app/lib/features/components/skeleton.dart index 37c5c4313..87662a5ba 100644 --- a/mobile-app/lib/features/components/skeleton.dart +++ b/mobile-app/lib/features/components/skeleton.dart @@ -64,7 +64,11 @@ class _SkeletonState extends State with SingleTickerProviderStateMixin gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, - colors: [context.colors.skeletonHighlightA, context.colors.skeletonHighlightB, context.colors.skeletonHighlightA], + colors: [ + context.colors.skeletonHighlightA, + context.colors.skeletonHighlightB, + context.colors.skeletonHighlightA, + ], stops: const [0.0, 0.5, 1.0], transform: _SlideGradientTransform(_animation.value), ), diff --git a/mobile-app/lib/providers/mining_rewards_provider.dart b/mobile-app/lib/providers/mining_rewards_provider.dart index e177f4e81..aff42438e 100644 --- a/mobile-app/lib/providers/mining_rewards_provider.dart +++ b/mobile-app/lib/providers/mining_rewards_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; final miningRewardsServiceProvider = Provider((ref) => MiningRewardsService()); @@ -8,11 +9,25 @@ final miningRewardsServiceProvider = Provider((ref) => Min final miningRewardsProvider = FutureProvider((ref) async { final service = ref.watch(miningRewardsServiceProvider); final accounts = ref.watch(accountsProvider).value; + if (accounts == null || accounts.isEmpty) { - return MiningRewardsData(resonanceBlocks: 0, schrodingerBlocks: 0, diracBlocks: 0, planckBlocks: 0, planckRewards: BigInt.zero); + return MiningRewardsData( + resonanceBlocks: 0, + schrodingerBlocks: 0, + diracBlocks: 0, + planckBlocks: 0, + planckRewards: BigInt.zero, + redeemedRewards: BigInt.zero, + redeemableRewards: BigInt.zero, + ); } + + final mnemonic = await ref.watch(settingsServiceProvider).getMnemonic(0); + final keyPair = ref.watch(hdWalletServiceProvider).deriveWormholeKeyPair(mnemonic: mnemonic!); + final oldMiningAccountId = await TaskmasterService().getOldMiningAccountId(); final accountsList = accounts.map((a) => a.accountId).toList(); accountsList.add(oldMiningAccountId); - return service.getMiningRewards(accountsList); + + return service.getMiningRewards(ref, keyPair, accountsList); }); diff --git a/mobile-app/lib/providers/wallet_providers.dart b/mobile-app/lib/providers/wallet_providers.dart index 7ee0fd05c..f04e57b92 100644 --- a/mobile-app/lib/providers/wallet_providers.dart +++ b/mobile-app/lib/providers/wallet_providers.dart @@ -53,6 +53,14 @@ final highSecurityServiceProvider = Provider((ref) { return HighSecurityService(); }); +final hdWalletServiceProvider = Provider((ref) { + return HdWalletService(); +}); + +final wormholeUtxoServiceProvider = Provider((ref) { + return WormholeUtxoService(); +}); + final isHighSecurityProvider = FutureProvider.family((ref, account) async { final highSecurityService = ref.watch(highSecurityServiceProvider); return await highSecurityService.isHighSecurity(account); diff --git a/mobile-app/lib/services/mining_rewards_service.dart b/mobile-app/lib/services/mining_rewards_service.dart index 1ca26ee12..074f17bf2 100644 --- a/mobile-app/lib/services/mining_rewards_service.dart +++ b/mobile-app/lib/services/mining_rewards_service.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/utils/env_utils.dart'; class MiningRewardsData { @@ -10,6 +12,8 @@ class MiningRewardsData { final int diracBlocks; final int planckBlocks; final BigInt planckRewards; + final BigInt redeemedRewards; + final BigInt redeemableRewards; const MiningRewardsData({ required this.resonanceBlocks, @@ -17,6 +21,8 @@ class MiningRewardsData { required this.diracBlocks, required this.planckBlocks, required this.planckRewards, + required this.redeemedRewards, + required this.redeemableRewards, }); int get totalBlocks => resonanceBlocks + schrodingerBlocks + diracBlocks + planckBlocks; @@ -35,8 +41,9 @@ class MiningRewardsService { _cachedAccountIds = null; } - Future getMiningRewards(List currentAccountIds) async { + Future getMiningRewards(Ref ref, WormholeKeyPair keyPair, List currentAccountIds) async { print('[MiningRewards] Current account IDs: $currentAccountIds'); + final wormholeUtxoService = ref.read(wormholeUtxoServiceProvider); final miners = >{}; for (final entry in _assets.entries) { @@ -51,15 +58,20 @@ class MiningRewardsService { final resonance = _countBlocks('resonance', miners['resonance']!, allAccountIds); final schrodinger = _countBlocks('schrodinger', miners['schrodinger']!, allAccountIds); final dirac = _countBlocks('dirac', miners['dirac']!, allAccountIds); - final planck = await TaskmasterService().getMinerStats(); + final (planckStats, redeemableRewards) = await ( + TaskmasterService().getMinerStats(), + wormholeUtxoService.getUnspentBalance(wormholeAddress: keyPair.address, secretHex: keyPair.secretHex), + ).wait; + final redeemedRewards = planckStats.totalRewards - redeemableRewards; - print('[MiningRewards] Resonance: $resonance, Schrödinger: $schrodinger, Dirac: $dirac, Planck: $planck'); return MiningRewardsData( resonanceBlocks: resonance, schrodingerBlocks: schrodinger, diracBlocks: dirac, - planckBlocks: planck.totalMinedBlocks, - planckRewards: planck.totalRewards, + planckBlocks: planckStats.totalMinedBlocks, + planckRewards: planckStats.totalRewards, + redeemedRewards: redeemedRewards, + redeemableRewards: redeemableRewards, ); } diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index 56171298c..fcff75328 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -6,7 +6,9 @@ import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart' import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; +import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart'; import 'package:resonance_network_wallet/v2/components/split_card.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; @@ -30,8 +32,15 @@ class MiningRewardsScreen extends ConsumerWidget { loading: () => const _NoRewards(isLoading: true), error: (err, _) => _ErrorState(colors: colors, text: text, onRetry: () => ref.invalidate(miningRewardsProvider)), - ), + ), ], + bottomContent: miningAsync.when( + data: (data) => data.totalBlocks > 0 + ? const ScaffoldBaseBottomContent(child: QuantusButton.simple(label: 'Redeem', onTap: null)) + : null, + loading: () => null, + error: (err, _) => null, + ), ); } } @@ -44,19 +53,13 @@ class _WithRewards extends ConsumerWidget { static const _resonanceSince = 'Jul 2025'; static const _schrodingerSince = 'Oct 2025'; static const _diracSince = 'Nov 2025'; - static const _planckSince = 'Jan 2026'; - - String get _activeSince { - if (data.resonanceBlocks > 0) return _resonanceSince; - if (data.schrodingerBlocks > 0) return _schrodingerSince; - if (data.diracBlocks > 0) return _diracSince; - return _planckSince; - } @override Widget build(BuildContext context, WidgetRef ref) { final numberFmt = ref.watch(numberFormattingServiceProvider); - final quanEarned = numberFmt.formatBalance(data.planckRewards, maxDecimals: 1); + final quanEarned = numberFmt.formatBalance(data.planckRewards, addSymbol: true); + final redeemedRewards = numberFmt.formatBalance(data.redeemedRewards, addSymbol: true); + final redeemableRewards = numberFmt.formatBalance(data.redeemableRewards, addSymbol: true); final colors = context.colors; final text = context.themeText; @@ -67,21 +70,34 @@ class _WithRewards extends ConsumerWidget { _TestnetEntry('Resonance', _resonanceSince, data.resonanceBlocks), ]; + final miningSummaryPairRows = [ + _StatPairRow( + left: _MiningStatCell(label: 'TESTNET BLOCKS', value: '${data.totalBlocks}', valueColor: colors.textLightGray), + right: _MiningStatCell(label: 'TESTNET REWARDS', value: quanEarned, valueColor: colors.accentOrange), + ), + _StatPairRow( + left: _MiningStatCell(label: 'REDEEMED', value: redeemedRewards, valueColor: colors.textLightGray), + right: _MiningStatCell(label: 'REDEEMABLE', value: redeemableRewards, valueColor: colors.success), + ), + ]; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SplitCard( topChild: _CardTopSection( - totalBlocks: data.planckBlocks, - totalBlocksColor: colors.success, + totalBlocks: data.totalBlocks, + totalBlocksColor: colors.textLightGray, statusLabel: 'Mining', statusColor: colors.success, ), - bottomChild: Row( + bottomChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _StatColumn(label: 'QUAN EARNED', value: quanEarned, valueColor: colors.accentOrange), - const SizedBox(width: 64), - _StatColumn(label: 'ACTIVE SINCE', value: _activeSince, valueColor: colors.textPrimary), + for (var i = 0; i < miningSummaryPairRows.length; i++) ...[ + if (i > 0) const SizedBox(height: 24), + miningSummaryPairRows[i], + ], ], ), ), @@ -230,7 +246,37 @@ class _CardTopSection extends StatelessWidget { else Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), const SizedBox(height: 4), - Text('on the Planck testnet', style: text.detail?.copyWith(color: colors.textMuted)), + Text('blocks across all testnets', style: text.detail?.copyWith(color: colors.textMuted)), + ], + ); + } +} + +class _MiningStatCell { + const _MiningStatCell({required this.label, required this.value, required this.valueColor}); + + final String label; + final String value; + final Color valueColor; +} + +class _StatPairRow extends StatelessWidget { + const _StatPairRow({required this.left, required this.right}); + + final _MiningStatCell left; + final _MiningStatCell right; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: _StatColumn(label: left.label, value: left.value, valueColor: left.valueColor), + ), + const SizedBox(width: 12), + Expanded( + child: _StatColumn(label: right.label, value: right.value, valueColor: right.valueColor), + ), ], ); } diff --git a/quantus_sdk/lib/src/services/taskmaster_service.dart b/quantus_sdk/lib/src/services/taskmaster_service.dart index 4bb16e549..394ac8b82 100644 --- a/quantus_sdk/lib/src/services/taskmaster_service.dart +++ b/quantus_sdk/lib/src/services/taskmaster_service.dart @@ -566,8 +566,6 @@ class TaskmasterService { final Map data = responseBody['data']; - print('data $data'); - final List? minerStatsList = data['minerStats']; if (minerStatsList == null || minerStatsList.isEmpty) { return MinerStats(totalMinedBlocks: 0, totalRewards: BigInt.zero); diff --git a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart index 04cb3624b..376f24bd1 100644 --- a/quantus_sdk/lib/src/services/wormhole_utxo_service.dart +++ b/quantus_sdk/lib/src/services/wormhole_utxo_service.dart @@ -198,7 +198,7 @@ class WormholeUtxoService { query TransfersToAddress($to: String!, $limit: Int!, $offset: Int!, $afterBlock: Int) { transfers: transfer( where: { to: { id: {_eq: $to } }, block: { height: {_gt: $afterBlock } } } - order_by: {block_height: asc} + order_by: {block: {height: asc}} limit: $limit offset: $offset ) { From 6f1a51030f13a2c60eb8ffe9a407fabc5c082452 Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 8 May 2026 15:58:45 +0800 Subject: [PATCH 11/14] revert: remove skeleton widget changes (moved to feat/skeleton-widget-update) Co-authored-by: Cursor --- .../lib/features/components/skeleton.dart | 95 +++++++------------ mobile-app/lib/v2/theme/app_colors.dart | 20 ++-- 2 files changed, 41 insertions(+), 74 deletions(-) diff --git a/mobile-app/lib/features/components/skeleton.dart b/mobile-app/lib/features/components/skeleton.dart index 87662a5ba..65ae1bad3 100644 --- a/mobile-app/lib/features/components/skeleton.dart +++ b/mobile-app/lib/features/components/skeleton.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import '../styles/app_colors_theme.dart'; + +const _defaultSkeletonBaseColor = Color(0xFF3D3C44); +const _defaultSkeletonHighlightColor = Color(0xFF5A5A5A); /// A skeleton widget with shimmer animation for loading states class Skeleton extends StatefulWidget { @@ -8,15 +11,25 @@ class Skeleton extends StatefulWidget { final BorderRadius? borderRadius; final Duration duration; - static const defaultDuration = Duration(milliseconds: 1200); - - const Skeleton({super.key, this.width, this.height = 16, this.borderRadius, this.duration = defaultDuration}); + const Skeleton({ + super.key, + this.width, + this.height = 16, + this.borderRadius, + this.duration = const Duration(milliseconds: 1500), + }); /// Creates a circular skeleton (useful for avatars) - Skeleton.circular({super.key, required double size, this.duration = defaultDuration}) + const Skeleton.circular({super.key, required double size, this.duration = const Duration(milliseconds: 1500)}) : width = size, height = size, - borderRadius = BorderRadius.circular(size); + borderRadius = null; + + /// Creates a skeleton for a transaction item + const Skeleton.txItem({super.key, this.duration = const Duration(milliseconds: 1500)}) + : width = double.infinity, + height = 40, + borderRadius = null; @override State createState() => _SkeletonState(); @@ -45,7 +58,13 @@ class _SkeletonState extends State with SingleTickerProviderStateMixin @override Widget build(BuildContext context) { - final borderRadius = widget.borderRadius ?? BorderRadius.circular(4); + final themeColors = Theme.of(context).extension(); + final baseColor = themeColors?.skeletonBase ?? _defaultSkeletonBaseColor; + final highlightColor = themeColors?.skeletonHighlight ?? _defaultSkeletonHighlightColor; + + final borderRadius = + widget.borderRadius ?? + (widget.width == widget.height ? BorderRadius.circular(widget.width ?? 0) : BorderRadius.circular(4)); return AnimatedBuilder( animation: _animation, @@ -53,26 +72,14 @@ class _SkeletonState extends State with SingleTickerProviderStateMixin return Container( width: widget.width, height: widget.height, - decoration: BoxDecoration(borderRadius: borderRadius, color: context.colors.skeletonBase), - child: Opacity( - opacity: 0.2, - child: Container( - width: widget.width, - height: widget.height, - decoration: BoxDecoration( - borderRadius: borderRadius, - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - context.colors.skeletonHighlightA, - context.colors.skeletonHighlightB, - context.colors.skeletonHighlightA, - ], - stops: const [0.0, 0.5, 1.0], - transform: _SlideGradientTransform(_animation.value), - ), - ), + decoration: BoxDecoration( + borderRadius: borderRadius, + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [baseColor, highlightColor, baseColor], + stops: const [0.0, 0.5, 1.0], + transform: _SlideGradientTransform(_animation.value), ), ), ); @@ -91,37 +98,3 @@ class _SlideGradientTransform extends GradientTransform { return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0); } } - -class TxItemSkeleton extends StatelessWidget { - const TxItemSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - final double txItemHeight = 32.0; - final double txItemDetailHeight = 12.0; - - return Row( - children: [ - Skeleton(width: txItemHeight, height: txItemHeight), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Skeleton(width: 64, height: txItemDetailHeight), - const SizedBox(height: 6), - Skeleton(width: 52, height: txItemDetailHeight), - ], - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Skeleton(width: 100, height: txItemDetailHeight), - const SizedBox(height: 6), - Skeleton(width: 88, height: txItemDetailHeight), - ], - ), - ], - ); - } -} diff --git a/mobile-app/lib/v2/theme/app_colors.dart b/mobile-app/lib/v2/theme/app_colors.dart index 4210495df..6be68c7d1 100644 --- a/mobile-app/lib/v2/theme/app_colors.dart +++ b/mobile-app/lib/v2/theme/app_colors.dart @@ -46,8 +46,7 @@ class AppColorsV2 extends ThemeExtension { final Color buttonDisabled; final Color buttonDanger; final Color skeletonBase; - final Color skeletonHighlightA; - final Color skeletonHighlightB; + final Color skeletonHighlight; final Color toasterBorder; final Color toasterBackground; final Color sheetBackground; @@ -106,8 +105,7 @@ class AppColorsV2 extends ThemeExtension { required this.buttonDisabled, required this.buttonDanger, required this.skeletonBase, - required this.skeletonHighlightA, - required this.skeletonHighlightB, + required this.skeletonHighlight, required this.segmentedControlPill, required this.surfaceDeep, required this.copyButtonCopiedBg, @@ -159,9 +157,8 @@ class AppColorsV2 extends ThemeExtension { copyButtonCopiedBorder: const Color(0xFF1A3226), buttonDisabled: const Color(0xFF3D3C44), buttonDanger: const Color(0x1AFF0000), - skeletonBase: const Color(0xFF161616), - skeletonHighlightA: const Color(0xFF000000), - skeletonHighlightB: const Color(0xFF666666), + skeletonBase: const Color(0xFF3D3C44), + skeletonHighlight: const Color(0xFF5A5A5A), tagGuardian: const Color(0xFF9747FF), tagEntrusted: const Color(0xFFFFD541), tagHighSecurity: const Color(0xFF4CEDE7), @@ -206,8 +203,7 @@ class AppColorsV2 extends ThemeExtension { Color? buttonDanger, Color? borderDanger, Color? skeletonBase, - Color? skeletonHighlightA, - Color? skeletonHighlightB, + Color? skeletonHighlight, Color? segmentedControlPill, Color? surfaceDeep, Color? copyButtonCopiedBg, @@ -254,8 +250,7 @@ class AppColorsV2 extends ThemeExtension { buttonDanger: buttonDanger ?? this.buttonDanger, borderDanger: borderDanger ?? this.borderDanger, skeletonBase: skeletonBase ?? this.skeletonBase, - skeletonHighlightA: skeletonHighlightA ?? this.skeletonHighlightA, - skeletonHighlightB: skeletonHighlightB ?? this.skeletonHighlightB, + skeletonHighlight: skeletonHighlight ?? this.skeletonHighlight, segmentedControlPill: segmentedControlPill ?? this.segmentedControlPill, surfaceDeep: surfaceDeep ?? this.surfaceDeep, copyButtonCopiedBg: copyButtonCopiedBg ?? this.copyButtonCopiedBg, @@ -313,8 +308,7 @@ class AppColorsV2 extends ThemeExtension { buttonDanger: Color.lerp(buttonDanger, other.buttonDanger, t) ?? buttonDanger, borderDanger: Color.lerp(borderDanger, other.borderDanger, t) ?? borderDanger, skeletonBase: Color.lerp(skeletonBase, other.skeletonBase, t) ?? skeletonBase, - skeletonHighlightA: Color.lerp(skeletonHighlightA, other.skeletonHighlightA, t) ?? skeletonHighlightA, - skeletonHighlightB: Color.lerp(skeletonHighlightB, other.skeletonHighlightB, t) ?? skeletonHighlightB, + skeletonHighlight: Color.lerp(skeletonHighlight, other.skeletonHighlight, t) ?? skeletonHighlight, segmentedControlPill: Color.lerp(segmentedControlPill, other.segmentedControlPill, t) ?? segmentedControlPill, surfaceDeep: Color.lerp(surfaceDeep, other.surfaceDeep, t) ?? surfaceDeep, copyButtonCopiedBg: Color.lerp(copyButtonCopiedBg, other.copyButtonCopiedBg, t) ?? copyButtonCopiedBg, From 368ef8f6fa356f3854d3d8af4b906cb8622e1709 Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 8 May 2026 16:03:26 +0800 Subject: [PATCH 12/14] fix: runtime error --- mobile-app/lib/providers/mining_rewards_provider.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile-app/lib/providers/mining_rewards_provider.dart b/mobile-app/lib/providers/mining_rewards_provider.dart index aff42438e..d23a6263a 100644 --- a/mobile-app/lib/providers/mining_rewards_provider.dart +++ b/mobile-app/lib/providers/mining_rewards_provider.dart @@ -23,7 +23,10 @@ final miningRewardsProvider = FutureProvider((ref) async { } final mnemonic = await ref.watch(settingsServiceProvider).getMnemonic(0); - final keyPair = ref.watch(hdWalletServiceProvider).deriveWormholeKeyPair(mnemonic: mnemonic!); + if (mnemonic == null) { + throw Exception('Mnemonic not found!'); + } + final keyPair = ref.watch(hdWalletServiceProvider).deriveWormholeKeyPair(mnemonic: mnemonic); final oldMiningAccountId = await TaskmasterService().getOldMiningAccountId(); final accountsList = accounts.map((a) => a.accountId).toList(); From 3ff16e1e62bc3177f8b7e3145cb261c9904bb18b Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 8 May 2026 16:05:03 +0800 Subject: [PATCH 13/14] chore: formatting --- mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart | 2 +- quantus_sdk/lib/src/services/number_formatting_service.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index fcff75328..a00f8645d 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -32,7 +32,7 @@ class MiningRewardsScreen extends ConsumerWidget { loading: () => const _NoRewards(isLoading: true), error: (err, _) => _ErrorState(colors: colors, text: text, onRetry: () => ref.invalidate(miningRewardsProvider)), - ), + ), ], bottomContent: miningAsync.when( data: (data) => data.totalBlocks > 0 diff --git a/quantus_sdk/lib/src/services/number_formatting_service.dart b/quantus_sdk/lib/src/services/number_formatting_service.dart index 3a3cb721c..d30b66712 100644 --- a/quantus_sdk/lib/src/services/number_formatting_service.dart +++ b/quantus_sdk/lib/src/services/number_formatting_service.dart @@ -9,7 +9,8 @@ class NumberFormattingService { final LocaleNumberConfig _localeConfig; - NumberFormattingService({LocaleNumberConfig? localeConfig}) : _localeConfig = localeConfig ?? LocaleNumberConfig.fromDefaultLocale(); + NumberFormattingService({LocaleNumberConfig? localeConfig}) + : _localeConfig = localeConfig ?? LocaleNumberConfig.fromDefaultLocale(); /// Formats a raw BigInt balance (representing the smallest unit) into a /// user-readable string with a specified number of decimal places. From c2e26140f3525259719ca2a3018a55fcd4f6a5ae Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 8 May 2026 16:06:40 +0800 Subject: [PATCH 14/14] fix: revert back skeleton constructor call --- mobile-app/lib/v2/screens/activity/activity_screen.dart | 2 +- mobile-app/lib/v2/screens/home/activity_section.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index a83526f7d..a557eae2d 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -81,7 +81,7 @@ class _ActivityScreenState extends ConsumerState { const SizedBox(height: 12), for (var j = 0; j < 3; j++) ...[ - const TxItemSkeleton(), + const Skeleton.txItem(), if (j < 2) Divider(color: colors.txItemSeparator, height: 24), ], ], diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 6cf1d9864..275479c0e 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -81,7 +81,7 @@ class _ActivitySectionState extends ConsumerState { _header(colors, text, context), const SizedBox(height: 24), for (var i = 0; i < 3; i++) ...[ - const TxItemSkeleton(), + const Skeleton.txItem(), if (i < 2) Divider(color: colors.txItemSeparator, height: 24), ], ],