Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bc54930
fix: clamp sync transaction parse batch size to prevent zero-size bat…
vilenarios Jul 3, 2026
abbc68d
fix: make wallet balance fetch best-effort during login
vilenarios Jul 3, 2026
c749c4d
perf: bound concurrent drive syncs to 5 at a time
vilenarios Jul 3, 2026
4fbdcfe
perf: reuse a single MetadataCache instance across sync batches
vilenarios Jul 3, 2026
8206d3b
fix: collect successfully imported files in bulk import result
vilenarios Jul 3, 2026
49a1da9
perf: fetch pin transaction info in batches of 100 instead of 5
vilenarios Jul 3, 2026
8218128
nit: remove unnecessary dart:async import in worker pool test
vilenarios Jul 3, 2026
a66fb87
chore: pin equatable below 2.1.0 in ardrive_ui to fix package analyze
vilenarios Jul 5, 2026
cd8ba4a
chore: point graphql at turbo-gateway.com directly, goldsky as fallback
vilenarios Jul 9, 2026
84048cf
chore: pin equatable below 2.1.0 in ardrive_ui to fix package analyze
vilenarios Jul 5, 2026
31718fb
perf: remove ArNS record fetching from the sync pipeline
vilenarios Jul 9, 2026
00634f6
perf: persist the GAR gateway list and refresh only on demand
vilenarios Jul 9, 2026
7300fef
chore: pin equatable below 2.1.0 in ardrive_ui to fix package analyze
vilenarios Jul 5, 2026
fe3d465
test: await FutureOr getString in gateway fallback tests
vilenarios Jul 9, 2026
878c2f3
Merge remote-tracking branch 'origin/fix/gql-turbo-gateway' into fix/…
vilenarios Jul 9, 2026
0215ebc
Merge remote-tracking branch 'origin/perf/minimize-solana-rpc' into f…
vilenarios Jul 9, 2026
dadcfdd
fix: time out gateway refresh instead of hanging on a stalled RPC
vilenarios Jul 9, 2026
67e12aa
perf: paginate drive history at 1000 per page with endpoint-sticky fa…
vilenarios Jul 10, 2026
8d68b87
fix: catch pagination-phase errors with await-for instead of yield*
vilenarios Jul 10, 2026
5b0389f
fix: harden sync pagination and address adversarial review findings
vilenarios Jul 10, 2026
6b714e4
nit: const GraphQLResponse in strategy test
vilenarios Jul 10, 2026
f689a21
test: no-version config migration also preserves custom gateways
vilenarios Jul 10, 2026
8b8f137
perf: skip verification pages for tiny tails, raise drive concurrency…
vilenarios Jul 10, 2026
9511d13
perf: default drive-history page size back to 100
vilenarios Jul 15, 2026
5e45fad
fix: restore full retry budget at safe page size, sync-scoped owner memo
vilenarios Jul 15, 2026
5f6a3d1
revert: remove the drive-history pagination experiment
vilenarios Jul 15, 2026
c2fa83e
perf: raise default concurrent drive syncs to 50
vilenarios Jul 15, 2026
dca6be8
Merge remote-tracking branch 'origin/dev' into fix/sync-quick-wins
Jul 29, 2026
916216e
perf: size the tx-parse batch by actual concurrency, not total drives…
Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions assets/config/dev.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"configVersion": 2,
"configVersion": 3,
"defaultArweaveGatewayUrl": "https://ar-io.dev",
"defaultArweaveGatewayForDataRequest": {
"label": "AR.IO Testnet",
Expand All @@ -12,5 +12,6 @@
"allowedDataItemSizeForTurbo": 100000,
"stripePublishableKey": "pk_test_51JUAtwC8apPOWkDLh2FPZkQkiKZEkTo6wqgLCtQoClL6S4l2jlbbc5MgOdwOUdU9Tn93NNvqAGbu115lkJChMikG00XUfTmo2z",
"uploadThumbnails": true,
"autoSync": false
"autoSync": false,
"maxConcurrentDriveSyncs": 50
}
7 changes: 4 additions & 3 deletions assets/config/prod.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"configVersion": 2,
"defaultArweaveGatewayUrl": "https://ardrive.net",
"configVersion": 3,
"defaultArweaveGatewayUrl": "https://turbo-gateway.com",
"defaultArweaveGatewayForDataRequest": {
"label": "Turbo Gateway",
"url": "https://turbo-gateway.com"
Expand All @@ -12,5 +12,6 @@
"allowedDataItemSizeForTurbo": 100000,
"stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj",
"uploadThumbnails": true,
"autoSync": false
"autoSync": false,
"maxConcurrentDriveSyncs": 50
}
7 changes: 4 additions & 3 deletions assets/config/staging.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"configVersion": 2,
"defaultArweaveGatewayUrl": "https://ardrive.net",
"configVersion": 3,
"defaultArweaveGatewayUrl": "https://turbo-gateway.com",
"defaultArweaveGatewayForDataRequest": {
"label": "Turbo Gateway",
"url": "https://turbo-gateway.com"
Expand All @@ -12,5 +12,6 @@
"allowedDataItemSizeForTurbo": 100000,
"stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj",
"uploadThumbnails": true,
"autoSync": false
"autoSync": false,
"maxConcurrentDriveSyncs": 50
}
12 changes: 11 additions & 1 deletion lib/authentication/ardrive_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,17 @@ class ArDriveAuthImpl implements ArDriveAuth {

@override
Future<void> refreshBalance() async {
_updateBalance();
// Await the fetch so callers (e.g. ProfileCubit.refreshBalance, which
// re-emits currentUser right after) observe the refreshed value instead
// of the stale one.
try {
final balance = await _userRepository.getBalance(currentUser.wallet);
_currentUser = _currentUser!.copyWith(walletBalance: balance);
_userStreamController.add(_currentUser);
} catch (e) {
logger.e('Error refreshing wallet balance', e);
// Keep the previous value on error.
}
}
}

Expand Down
16 changes: 13 additions & 3 deletions lib/core/arfs/use_cases/bulk_import_files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -419,17 +419,23 @@ class BulkImportFiles {
: 5,
taskQueue: fileEntries,
onWorkerError: (file, error) {
// The WorkerPool passes both the failed TASK and its exception.
// Preserve the exception (and its originalError) so payment
// rejections stay detectable, and record every failed file so
// BulkImportResult stays truthful (imported + failures == total).
logger.e('Bulk import worker error', error, StackTrace.current);
// Record the failure so BulkImportResult carries it (with its
// originalError) — the pool otherwise swallows task exceptions.
final manifestFile =
file.dataTxId != null ? fileDataTxIdToFile[file.dataTxId] : null;
final path = manifestFile?.path ?? file.name ?? 'unknown';
failures.add(error is FileImportFailure
? error
: FileImportFailure(
path: file.name ?? '',
path: path,
dataTxId: file.dataTxId ?? '',
error: error.toString(),
originalError: error,
));
onFileFailure?.call(path);
},
execute: (file) async {
if (_isCancelled) {
Expand All @@ -452,6 +458,10 @@ class BulkImportFiles {
originalOwnerAddress: originalOwnerAddress,
);

// WorkerPool discards execute's return value, so successful
// imports must be collected here for BulkImportResult.
importedFiles.add(fileEntry);

onFileUploadSuccess?.call(file.name!);

return fileEntry;
Expand Down
14 changes: 13 additions & 1 deletion lib/gar/domain/repositories/gar_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:collection/collection.dart';

abstract class GarRepository {
Future<List<Gateway>> getGateways();
Future<List<Gateway>> refreshGateways();
List<Gateway> searchGateways(String query);
Future<Gateway> getSelectedGateway();
Future<void> updateGateway(Gateway gateway);
Expand Down Expand Up @@ -35,10 +36,21 @@ class GarRepositoryImpl implements GarRepository {

final List<Gateway> _gateways = [];

/// Serves the persisted/session gateway list; the network is hit at most
/// once ever (on the very first use). Use [refreshGateways] for an
/// explicit, user-initiated re-fetch.
@override
Future<List<Gateway>> getGateways() async {
_gateways.clear();
_gateways.addAll(await arioSDK.getGateways());
_gateways.addAll(await arweave.gatewayFallback.getGatewaysCached());

return _gateways;
}

@override
Future<List<Gateway>> refreshGateways() async {
_gateways.clear();
_gateways.addAll(await arweave.gatewayFallback.refreshGateways());

return _gateways;
}
Expand Down
19 changes: 19 additions & 0 deletions lib/gar/presentation/bloc/gar_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ class GarBloc extends Bloc<GarEvent, GarState> {
}
});

on<RefreshGateways>((event, emit) async {
try {
emit(LoadingGateways());

final gateways = await garRepository.refreshGateways();
final currentGateway = await garRepository.getSelectedGateway();

emit(
GatewaysLoaded(
gateways: gateways,
currentGateway: currentGateway,
),
);
} catch (e) {
logger.e('Failed to refresh gateways from AR.IO', e);
emit(const GatewaysError());
}
});

on<SelectGateway>((event, emit) async {
emit(VerifyingGateway());

Expand Down
3 changes: 3 additions & 0 deletions lib/gar/presentation/bloc/gar_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ abstract class GarEvent extends Equatable {

final class GetGateways extends GarEvent {}

/// User-initiated refresh of the gateway list from the network.
final class RefreshGateways extends GarEvent {}

final class SelectGateway extends GarEvent {
final Gateway gateway;

Expand Down
6 changes: 6 additions & 0 deletions lib/gar/presentation/widgets/gar_modal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ class _ArIOGatewaySelectorModalContentState extends State<_ArIOGatewaySelectorMo
),
),
actions: [
ModalAction(
action: () {
garBloc.add(RefreshGateways());
},
title: 'Refresh list',
),
ModalAction(
action: () {
Navigator.of(context).pop();
Expand Down
1 change: 0 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,6 @@ class AppState extends State<App> {
configService: configService,
arioSDK: ArioSDKFactory().create(),
),
arnsRepository: _.read<ARNSRepository>(),
userPreferencesRepository: _.read<UserPreferencesRepository>(),
),
),
Expand Down
22 changes: 18 additions & 4 deletions lib/services/arweave/arweave_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ class ArweaveService {
/// Cache for drive signatures (immutable on-chain, never change).
final Map<String, DriveSignatureEntity?> _cachedDriveSignatures = {};

/// Single [MetadataCache] instance reused across all sync batches.
/// Previously a new cache was rebuilt from SharedPreferences for every
/// parsed batch of every drive during sync.
MetadataCache? _metadataCache;

Future<MetadataCache> _getMetadataCache() async {
_metadataCache ??= await MetadataCache.fromCacheStore(
await newSharedPreferencesCacheStore(),
);
return _metadataCache!;
}

/// Clears the cached result of [getUniqueUserDriveEntityTxs] and entity data.
/// Call after creating/updating a drive or after a full sync completes.
void clearUserDriveTxsCache() {
Expand Down Expand Up @@ -545,9 +557,7 @@ class ArweaveService {
);
}

final metadataCache = await MetadataCache.fromCacheStore(
await newSharedPreferencesCacheStore(),
);
final metadataCache = await _getMetadataCache();

final blockHistory = <BlockEntities>[];

Expand Down Expand Up @@ -1735,9 +1745,13 @@ class ArweaveService {

/// Fetches transaction info for multiple transactions in batches.
/// Returns a stream of transaction info batches.
///
/// The batch size matches the query's `first: 100` page size (the same
/// pattern as [getTransactionConfirmations] and [getLicenseAssertions]),
/// so each batch resolves in a single request.
Stream<Map<String, TxInfo>> getInfoOfTxsToBePinned(
List<String> transactionIds, {
int batchSize = 5,
int batchSize = 100,
}) async* {
for (var i = 0; i < transactionIds.length; i += batchSize) {
final end = (i + batchSize < transactionIds.length)
Expand Down
119 changes: 106 additions & 13 deletions lib/services/arweave/data_gateway_fallback.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:async';
import 'dart:convert';

import 'package:ardrive/download/download_exceptions.dart';
import 'package:ardrive/services/arweave/arweave_service.dart';
import 'package:ardrive/utils/key_value_store.dart';
import 'package:ardrive/utils/local_key_value_store.dart';
import 'package:ardrive/utils/logger.dart';
import 'package:ardrive_http/ardrive_http.dart';
import 'package:ario_sdk/ario_sdk.dart';
Expand Down Expand Up @@ -33,9 +36,109 @@ class DataGatewayFallback {
/// SnapshotValidationService) to avoid duplicate Solana RPC calls.
List<Gateway>? cachedGateways;

static const _garCacheKey = 'gar_gateways_cache_v1';

KeyValueStore? _store;

DataGatewayFallback({
required ArioSDK arioSDK,
}) : _arioSDK = arioSDK;
KeyValueStore? store,
}) : _arioSDK = arioSDK,
_store = store;

Future<KeyValueStore?> _getStore() async {
if (_store != null) return _store;
try {
_store = await LocalKeyValueStore.getInstance();
} catch (e) {
logger.w('GAR cache store unavailable: $e');
}
return _store;
}

/// Returns the gateway list while fetching from the network at most once
/// ever: memory cache → persisted cache → single SDK fetch (persisted on
/// success). The gateway registry rarely changes, so we avoid hitting the
/// Solana RPC on every session; explicit refreshes go through
/// [refreshGateways] (e.g. from the gateway settings screen).
Future<List<Gateway>>? _getGatewaysFuture;

Future<List<Gateway>> getGatewaysCached() {
// Memoize the in-flight future: concurrent first callers (e.g. several
// drive syncs validating snapshots at once) must share one fetch.
return _getGatewaysFuture ??= _getGatewaysCachedImpl();
}

Future<List<Gateway>> _getGatewaysCachedImpl() async {
if (cachedGateways != null) return cachedGateways!;

final persisted = await _loadPersistedGateways();
if (persisted != null) {
cachedGateways = persisted;
return persisted;
}

try {
final fetched = await _arioSDK
.getGateways()
.timeout(_garListTimeout, onTimeout: () => <Gateway>[]);
cachedGateways = fetched;
if (fetched.isNotEmpty) {
await _persistGateways(fetched);
}
} catch (e) {
// RPC failed — cache empty list in memory so we don't retry every call
logger.w('GAR list unavailable, will not retry this session: $e');
cachedGateways = [];
}
return cachedGateways!;
}

/// Force-refreshes the gateway list from the network and persists the
/// result. User-initiated only (refresh action in gateway settings).
///
/// Unlike [getGatewaysCached], a stalled RPC throws ([TimeoutException])
/// instead of returning an empty list, so the caller can surface an error
/// state with a retry affordance rather than silently showing no gateways.
/// The existing cache and persisted list are left untouched on failure.
Future<List<Gateway>> refreshGateways() async {
final fetched = await _arioSDK.getGateways().timeout(_garListTimeout);
cachedGateways = fetched;
_getGatewaysFuture = null; // next cached read observes the refresh
if (fetched.isNotEmpty) {
await _persistGateways(fetched);
}
return fetched;
}

Future<List<Gateway>?> _loadPersistedGateways() async {
try {
final store = await _getStore();
final raw = await store?.getString(_garCacheKey);
if (raw == null) return null;
final decoded = (json.decode(raw) as List)
.map((e) => Gateway.fromJson(e as Map<String, dynamic>))
.toList();
// An empty persisted list carries no value; treat as not cached so the
// next session retries the fetch.
return decoded.isEmpty ? null : decoded;
} catch (e) {
logger.w('Failed to load persisted GAR list, refetching: $e');
return null;
}
}

Future<void> _persistGateways(List<Gateway> gateways) async {
try {
final store = await _getStore();
await store?.putString(
_garCacheKey,
json.encode(gateways.map((g) => g.toJson()).toList()),
);
} catch (e) {
logger.w('Failed to persist GAR list: $e');
}
}

/// Fetch transaction data with serial gateway fallback.
///
Expand Down Expand Up @@ -193,20 +296,10 @@ class DataGatewayFallback {
final primaryHost = primaryClient.api.gatewayUrl.host;

try {
if (cachedGateways == null) {
try {
cachedGateways = await _arioSDK
.getGateways()
.timeout(_garListTimeout, onTimeout: () => <Gateway>[]);
} catch (e) {
// Solana RPC failed — cache empty list so we don't retry every call
logger.w('GAR list unavailable for fallback, will not retry: $e');
cachedGateways = [];
}
}
final gateways = await getGatewaysCached();

var added = 0;
for (final gw in cachedGateways!) {
for (final gw in gateways) {
if (added >= _maxGarFallbacks) break;
if (gw.settings.fqdn == primaryHost) continue;
clients.add(_getOrCreateClient(gw.settings.fqdn));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query InfoOfTransactionsToBePinned($transactionIds: [ID!]) {
transactions(ids: $transactionIds) {
transactions(first: 100, ids: $transactionIds) {
edges {
node {
id
Expand Down
Loading
Loading