sign method

  1. @override
GsplTxData sign()
override

Implementation

@override
GsplTxData sign() {
  // Deserialize and identify transaction information, get readable transaction structure
  final tx = btc.Transaction.fromHex(txData.hex);
  final inputAddress = wallet.publicKeyToAddress(wallet.publicKey);
  final payments = txData.toJson()['payments'];
  if (payments == null || payments.isEmpty) {
    throw Exception('No valid output found in transaction outputs');
  }

  final prevOutScript = btc.Address.addressToOutputScript(inputAddress, networkType)!;

  // BIP341 sighash commits to every spent output's scriptPubKey and
  // amount, so gather them for all inputs up front (GSPL inputs all
  // belong to the same wallet address, so the script is shared).
  final allPrevScripts = <Uint8List>[];
  final allValues = <int>[];
  for (final input in txData.inputs) {
    if (input.amount == null) throw Exception('Input amount required for sigHash');
    allPrevScripts.add(prevOutScript);
    allValues.add(input.amount!);
  }

  // Iterate through inputs, use wallet.sign to sign sigHash
  final List<GsplItem> signedInputs = [];
  for (int i = 0; i < txData.inputs.length; i++) {
    final input = txData.inputs[i];
    if (input.path == null) throw Exception('Input path cannot be null');
    if (input.amount == null) throw Exception('Input amount required for sigHash');

    final hashType = _hashTypeFor(input);
    final sigHash =
        _sigHashFor(tx, i, prevOutScript, allPrevScripts, allValues, hashType);

    String sigResult;
    final sigHashHex = dynamicToString(sigHash);
    if (isTaprootInput) {
      sigResult = (wallet as LtcCoin).signWithHashType(sigHashHex, hashType);
    } else if (isLtc) {
      sigResult = (wallet as LtcCoin).signWithHashType(sigHashHex, hashType);
    } else if (isDoge) {
      sigResult = (wallet as DogeCoin).signWithHashType(sigHashHex, hashType);
    } else if (isBch) {
      sigResult = (wallet as BchCoin).signWithHashType(sigHashHex, hashType);
    } else {
      sigResult = wallet.sign(sigHashHex);
    }

    final signatureBytes = dynamicToUint8List(sigResult);
    if (isTaprootInput) {
      if (signatureBytes.length != 64) {
        throw StateError(
          'Taproot signer must return a raw 64-byte signature',
        );
      }
      if (hashType != btc.SIGHASH_DEFAULT &&
          !_isValidTaprootHashType(hashType)) {
        throw ArgumentError('Invalid Taproot sighash type: $hashType');
      }
    }

    // Construct new GsplItem to replace
    signedInputs.add(GsplItem(
      path: input.path,
      amount: input.amount,
      address: inputAddress,
      signHashType: input.signHashType,
      signature: signatureBytes,
    ));
  }

  final transactionSigned = btc.Transaction.fromHex(txData.hex);
  for (int i = 0; i < signedInputs.length; i++) {
    final sig = signedInputs[i].signature;
    if (sig == null) {
      throw Exception('Missing signature for input $i');
    }
    if (isTaprootInput) {
      // P2TR key-path spend: the Schnorr signature goes in the witness
      // stack, and scriptSig stays empty.
      final hashType = signedInputs[i].signHashType ?? btc.SIGHASH_DEFAULT;
      final witnessSig = hashType == btc.SIGHASH_DEFAULT
          ? sig
          : Uint8List.fromList([...sig, hashType]);
      transactionSigned.ins[i].witness = [witnessSig];
      transactionSigned.ins[i].script = btc.EMPTY_SCRIPT;
    } else {
      final pubkey = wallet.publicKey;
      final scriptSig = bscript.compile([sig, pubkey]);
      transactionSigned.ins[i].script = scriptSig;
    }
  }
  final signedHex = transactionSigned.toHex();
  txData.hex = signedHex;
  txData.inputs = signedInputs;
  txData.isSigned = true;
  txData.message = signedHex;
  txData.signature = '';

  return txData;
}