resolveCoinBalanceIntents function

Future<void> resolveCoinBalanceIntents(
  1. TransactionBlockDataBuilder data,
  2. TransactionBuilderClient? client
)

Implementation

Future<void> resolveCoinBalanceIntents(
    TransactionBlockDataBuilder data, TransactionBuilderClient? client) async {
  if (!data.commands.any(_isCwbIntent)) return;
  if (data.sender == null) {
    throw ArgumentError('Sender must be set to resolve CoinWithBalance');
  }
  if (client == null) {
    throw ArgumentError('Client must be provided to build transactions '
        'with CoinWithBalance intents');
  }

  final totalByType = <String, BigInt>{};
  final intentsByType = <String, List<BigInt>>{};

  // First pass: sum required balances per coin type and resolve any
  // zero-balance intents in place (they need no coins).
  for (var i = 0; i < data.commands.length; i++) {
    final cmd = data.commands[i];
    if (!_isCwbIntent(cmd)) continue;
    final d = cmd[r'$Intent']['data'] as Map;
    final type = d['type'] as String;
    final balance = BigInt.parse(d['balance'].toString());
    final outputKind = (d['outputKind'] ?? 'coin') as String;
    if (outputKind != 'coin') {
      throw UnsupportedError(
          'CoinWithBalance outputKind "$outputKind" (address-balance) is not '
          'supported');
    }
    if (balance == BigInt.zero) {
      final coinType = type == 'gas' ? _suiType : type;
      _replaceCommand(data, i, [_moveCall('$_coinPackage::coin::zero', [
        coinType
      ], const [])]);
      continue;
    }
    totalByType[type] = (totalByType[type] ?? BigInt.zero) + balance;
    (intentsByType[type] ??= <BigInt>[]).add(balance);
  }

  if (totalByType.containsKey('gas') && totalByType.containsKey(_suiType)) {
    throw ArgumentError('Cannot mix SUI CoinWithBalance intents that use the '
        'gas coin with ones that do not (useGasCoin: false).');
  }

  // Object ids already used by other inputs must not be re-selected as coins.
  final usedIds = <String>{};
  for (final input in data.inputs) {
    final owned = input['Object']?['ImmOrOwnedObject']?['objectId'];
    if (owned != null) usedIds.add(normalizeSuiAddress(owned));
    final unresolved = input['UnresolvedObject']?['objectId'];
    if (unresolved != null) usedIds.add(normalizeSuiAddress(unresolved));
  }

  final coinsByType = <String, List<CoinStruct>>{};
  for (final entry in totalByType.entries) {
    if (entry.key == 'gas') continue;
    coinsByType[entry.key] = await _selectCoins(
        client, data.sender!, entry.key, entry.value, usedIds);
  }

  // Second pass: replace each intent with the real commands. All intents of a
  // type share one merge+split, produced when the first is encountered.
  final typeResults = <String, List<dynamic>>{};
  final typeNext = <String, int>{};
  var index = 0;
  while (index < data.commands.length) {
    final cmd = data.commands[index];
    if (!_isCwbIntent(cmd)) {
      index++;
      continue;
    }
    final type = cmd[r'$Intent']['data']['type'] as String;
    final commands = <Map<String, dynamic>>[];

    if (!typeResults.containsKey(type)) {
      final intents = intentsByType[type]!;
      final sources = <dynamic>[];
      if (type == 'gas') {
        sources.add({r'$kind': 'GasCoin', 'GasCoin': true});
      } else {
        for (final coin in coinsByType[type]!) {
          sources.add(data.addInput(
              'object',
              Inputs.objectRef(
                  SuiObjectRef(coin.digest, coin.coinObjectId, coin.version))));
        }
      }
      final baseCoin = sources.first;
      final rest = sources.sublist(1);
      for (var i = 0; i < rest.length; i += 500) {
        commands.add(_mergeCoins(baseCoin, rest.sublist(i, min(i + 500, rest.length))));
      }
      final splitCmdIndex = index + commands.length;
      commands.add(_splitCoins(
          baseCoin,
          intents
              .map((b) =>
                  data.addInput('pure', Inputs.pure(SuiBcs.U64.serialize(b))))
              .toList()));
      typeResults[type] = [
        for (var i = 0; i < intents.length; i++)
          {r'$kind': 'NestedResult', 'NestedResult': [splitCmdIndex, i]}
      ];
      typeNext[type] = 0;
    }

    final intentResult = typeResults[type]![typeNext[type]!];
    typeNext[type] = typeNext[type]! + 1;
    _replaceCommand(data, index, commands, intentResult);
    index += commands.length;
  }
}