Skip to content

Commit

Permalink
fix: guard ship purchases against insufficient funds
Browse files Browse the repository at this point in the history
  • Loading branch information
eseidel committed Oct 1, 2023
1 parent ea5d779 commit cb979cb
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -679,4 +679,4 @@ ESEIDEL-4E: null
Orbiting X1-AV52-62175B ASTEROID_FIELD EXCAVATOR 1/60
MOUNT_SURVEYOR_II 1 x 17,478c = 17,478c

null in this case is a surveyor.
null in this case is a surveyor.
8 changes: 5 additions & 3 deletions packages/cli/lib/behavior/behavior.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ void jobAssert(
}
}

/// Throw a job exception.
Never failJob(String message, Duration timeout) =>
throw JobException(message, timeout);

/// Exception thrown from a Job if the condition is not met.
T assertNotNull<T>(
T? value,
Expand Down Expand Up @@ -175,11 +179,9 @@ class MultiJob {
return result.waitTime;
}
}
jobAssert(
false,
failJob(
'Too many $name job iterations',
const Duration(hours: 1),
);
return null;
}
}
40 changes: 28 additions & 12 deletions packages/cli/lib/behavior/buy_ship.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:cli/logger.dart';
import 'package:cli/nav/navigation.dart';
import 'package:cli/nav/route.dart';
import 'package:cli/net/actions.dart';
import 'package:cli/net/exceptions.dart';
import 'package:cli/net/queries.dart';
import 'package:cli/trading.dart';
import 'package:db/db.dart';
Expand Down Expand Up @@ -118,21 +119,36 @@ Future<DateTime?> advanceBuyShip(
recordShipyardDataAndLog(caches.shipyardPrices, shipyard, ship);

// TODO(eseidel): Catch exceptions about insufficient credits.
final result = await purchaseShipAndLog(
api,
db,
caches.ships,
caches.agent,
ship,
shipyard.waypointSymbol,
shipType,
);
final PurchaseShip201ResponseData result;
try {
result = await purchaseShipAndLog(
api,
db,
caches.ships,
caches.agent,
ship,
shipyard.waypointSymbol,
shipType,
);
} on ApiException catch (e) {
// ApiException 400: {"error":{"message":"Failed to purchase ship.
// Agent has insufficient funds.","code":4216,
// "data":{"creditsAvailable":116103,"creditsNeeded":172355}}}
final neededCredits = neededCreditsFromPurchaseShipException(e);
if (neededCredits == null) {
// Was not an insufficient credits exception.
rethrow;
}
failJob(
'Failed to purchase ship ${caches.agent.agent.credits} < $neededCredits',
const Duration(minutes: 10),
);
}

// Record our success!
state.isComplete = true;
jobAssert(
false,
failJob(
'Purchased ${result.ship.symbol} ($shipType)!',
const Duration(minutes: 10),
);
return null;
}
3 changes: 1 addition & 2 deletions packages/cli/lib/behavior/jobs/mount_job.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,5 @@ Future<JobResult> doMountJob(
);
// We're done.
state.isComplete = true;
jobAssert(false, 'Mounting complete!', const Duration(hours: 1));
return JobResult.complete();
failJob('Mounting complete!', const Duration(hours: 1));
}
30 changes: 30 additions & 0 deletions packages/cli/lib/net/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,33 @@ bool isInsufficientCreditsException(ApiException e) {
bool isShipNotInOrbitException(ApiException e) {
return isAPIExceptionWithCode(e, 4236);
}

// ApiException 400: {"error":{"message":"Failed to purchase ship.
// Agent has insufficient funds.","code":4216,
// "data":{"creditsAvailable":116103,"creditsNeeded":172355}}}
// bool isInsufficientCreditsToPurchaseShipException(ApiException e) {
// return isAPIExceptionWithCode(e, 4216);
// }

int? neededCreditsFromPurchaseShipException(ApiException e) {
final jsonString = e.message;
if (jsonString != null) {
Map<String, dynamic> exceptionJson;
try {
exceptionJson = jsonDecode(jsonString) as Map<String, dynamic>;
} on FormatException catch (e) {
// Catch any json decode errors, so the original exception can be
// rethrown by the caller instead of a json decode error.
logger.warn('Failed to parse exception json: $e');
return null;
}
final error = mapCastOfType<String, dynamic>(exceptionJson, 'error');
final code = mapValueOfType<int>(error, 'code');
if (code != 4216) {
return null;
}
final errorData = mapCastOfType<String, dynamic>(error, 'data');
return mapValueOfType<int>(errorData, 'creditsNeeded');
}
return null;
}

0 comments on commit cb979cb

Please sign in to comment.