diff --git a/src/shared/Angor.Shared/Models/TransactionInfo.cs b/src/shared/Angor.Shared/Models/TransactionInfo.cs
index fc542de51..588c99ed3 100644
--- a/src/shared/Angor.Shared/Models/TransactionInfo.cs
+++ b/src/shared/Angor.Shared/Models/TransactionInfo.cs
@@ -6,5 +6,16 @@ public class TransactionInfo
{
public Transaction Transaction { get; set; }
public long TransactionFee { get; set; }
-
+
+ ///
+ /// Network-specific transaction hex. On Bitcoin this is the same as Transaction.ToHex().
+ /// On Liquid, this holds the Elements-format transaction hex.
+ ///
+ public string? TransactionHex { get; set; }
+
+ ///
+ /// Network-specific transaction ID. On Bitcoin this is the same as Transaction.GetHash().
+ /// On Liquid, this holds the Elements transaction hash.
+ ///
+ public string? TransactionId { get; set; }
}
\ No newline at end of file
diff --git a/src/shared/Angor.Shared/Protocol/BitcoinTransactionTransformer.cs b/src/shared/Angor.Shared/Protocol/BitcoinTransactionTransformer.cs
new file mode 100644
index 000000000..a1f75c100
--- /dev/null
+++ b/src/shared/Angor.Shared/Protocol/BitcoinTransactionTransformer.cs
@@ -0,0 +1,57 @@
+using Angor.Shared.Models;
+using NBitcoin;
+using Transaction = NBitcoin.Transaction;
+
+namespace Angor.Shared.Protocol;
+
+///
+/// No-op transaction transformer for Bitcoin networks.
+/// All methods return their inputs unchanged since no transformation is needed.
+///
+public class BitcoinTransactionTransformer : ITransactionTransformer
+{
+ public TransactionInfo WrapP2WPKH(
+ TransactionInfo bitcoinTxInfo,
+ List coins,
+ List keys)
+ {
+ return bitcoinTxInfo;
+ }
+
+ public TransactionInfo WrapTaproot(
+ TransactionInfo bitcoinTxInfo,
+ Key nbitcoinKey,
+ TxOut[] spentOutputs)
+ {
+ return bitcoinTxInfo;
+ }
+
+ public SignatureInfo WrapTaprootSignatures(
+ SignatureInfo signatureInfo,
+ Transaction recoveryTx,
+ TxOut[] spentOutputs,
+ Key nbitcoinKey,
+ Func scriptPerStage)
+ {
+ return signatureInfo;
+ }
+
+ public TransactionInfo WrapP2WSH(
+ TransactionInfo bitcoinTxInfo,
+ Key nbitcoinKey,
+ Script spendingScript,
+ Transaction recoveryTransaction)
+ {
+ return bitcoinTxInfo;
+ }
+
+ public Transaction GetSighashTransaction(Transaction bitcoinTx)
+ {
+ return bitcoinTx;
+ }
+
+ public TxOut[] GetSighashSpentOutputs(TxOut[] spentOutputs)
+ {
+ return spentOutputs;
+ }
+}
diff --git a/src/shared/Angor.Shared/Protocol/ITransactionTransformer.cs b/src/shared/Angor.Shared/Protocol/ITransactionTransformer.cs
new file mode 100644
index 000000000..4211a5ec7
--- /dev/null
+++ b/src/shared/Angor.Shared/Protocol/ITransactionTransformer.cs
@@ -0,0 +1,62 @@
+using Angor.Shared.Models;
+using NBitcoin;
+using Transaction = NBitcoin.Transaction;
+
+namespace Angor.Shared.Protocol;
+
+///
+/// Transforms transactions for the target network (Bitcoin or Liquid).
+/// On Bitcoin, all methods are no-ops returning their inputs unchanged.
+/// On Liquid, methods rebuild transactions as Elements format with explicit L-BTC asset tags.
+///
+public interface ITransactionTransformer
+{
+ ///
+ /// Wraps a fully-signed P2WPKH transaction for the target network.
+ ///
+ TransactionInfo WrapP2WPKH(
+ TransactionInfo bitcoinTxInfo,
+ List coins,
+ List keys);
+
+ ///
+ /// Wraps a fully-signed Taproot script-path transaction for the target network.
+ ///
+ TransactionInfo WrapTaproot(
+ TransactionInfo bitcoinTxInfo,
+ Key nbitcoinKey,
+ TxOut[] spentOutputs);
+
+ ///
+ /// Wraps Taproot signature strings for the target network.
+ /// Re-computes sighash on the network-appropriate version of the recovery transaction
+ /// and re-signs each stage.
+ ///
+ SignatureInfo WrapTaprootSignatures(
+ SignatureInfo signatureInfo,
+ Transaction recoveryTx,
+ TxOut[] spentOutputs,
+ Key nbitcoinKey,
+ Func scriptPerStage);
+
+ ///
+ /// Wraps a fully-signed P2WSH transaction for the target network.
+ ///
+ TransactionInfo WrapP2WSH(
+ TransactionInfo bitcoinTxInfo,
+ Key nbitcoinKey,
+ Script spendingScript,
+ Transaction recoveryTransaction);
+
+ ///
+ /// Returns the transaction to use for sighash computation.
+ /// On Bitcoin, returns the input unchanged. On Liquid, returns an Elements transaction.
+ ///
+ Transaction GetSighashTransaction(Transaction bitcoinTx);
+
+ ///
+ /// Returns the spent outputs to use for Taproot sighash computation.
+ /// On Bitcoin, returns the input unchanged. On Liquid, returns Elements-format outputs.
+ ///
+ TxOut[] GetSighashSpentOutputs(TxOut[] spentOutputs);
+}
diff --git a/src/shared/Angor.Shared/Protocol/LiquidTransactionTransformer.cs b/src/shared/Angor.Shared/Protocol/LiquidTransactionTransformer.cs
new file mode 100644
index 000000000..1691bd6b3
--- /dev/null
+++ b/src/shared/Angor.Shared/Protocol/LiquidTransactionTransformer.cs
@@ -0,0 +1,228 @@
+using NBitcoin;
+using NBitcoin.Altcoins;
+using NBitcoin.Altcoins.Elements;
+using Angor.Shared.Models;
+
+namespace Angor.Shared.Protocol;
+
+///
+/// Transaction transformer for Liquid (Elements) networks.
+/// Wraps fully-signed Bitcoin transactions and re-creates them as valid Liquid transactions
+/// with explicit L-BTC asset tags and an explicit fee output, then re-signs.
+///
+public class LiquidTransactionTransformer : ITransactionTransformer
+{
+ private static readonly uint256 LBtcAssetId = ElementsParams.PeggedAssetId;
+ private static readonly Network LiquidNetwork = Liquid.Instance.Mainnet;
+
+ private readonly Blockcore.Networks.Network blockcoreNetwork;
+
+ public LiquidTransactionTransformer(Blockcore.Networks.Network blockcoreNetwork)
+ {
+ this.blockcoreNetwork = blockcoreNetwork;
+ }
+
+ public TransactionInfo WrapP2WPKH(
+ TransactionInfo bitcoinTxInfo,
+ List coins,
+ List keys)
+ {
+ var fee = bitcoinTxInfo.TransactionFee;
+ var bitcoinTx = Transaction.Parse(bitcoinTxInfo.Transaction.ToHex(), Network.Main);
+ var elementsTx = BuildElementsTransaction(bitcoinTx, Money.Satoshis(fee));
+
+ var builder = LiquidNetwork.CreateTransactionBuilder();
+
+ foreach (var coin in coins)
+ {
+ var elemCoin = new Coin(
+ new OutPoint(new uint256(coin.Outpoint.Hash.ToString()), coin.Outpoint.N),
+ CreateExplicitOutput(
+ Money.Satoshis(coin.Amount.Satoshi),
+ new Script(coin.ScriptPubKey.ToBytes())));
+ builder.AddCoins(elemCoin);
+ }
+
+ foreach (var blockcoreKey in keys)
+ {
+ builder.AddKeys(BlockcoreKeyToNBitcoinKey(blockcoreKey));
+ }
+
+ builder.SignTransactionInPlace(elementsTx);
+
+ return CreateTransactionInfo(elementsTx, fee, bitcoinTxInfo);
+ }
+
+ public TransactionInfo WrapTaproot(
+ TransactionInfo bitcoinTxInfo,
+ Key nbitcoinKey,
+ TxOut[] spentOutputs)
+ {
+ var fee = bitcoinTxInfo.TransactionFee;
+ var bitcoinTx = Transaction.Parse(bitcoinTxInfo.Transaction.ToHex(), Network.Main);
+ var elementsTx = BuildElementsTransaction(bitcoinTx, Money.Satoshis(fee));
+ var elemSpentOutputs = TransformSpentOutputs(spentOutputs);
+ var trxData = elementsTx.PrecomputeTransactionData(elemSpentOutputs);
+
+ const TaprootSigHash sigHash = TaprootSigHash.All;
+
+ for (int i = 0; i < elementsTx.Inputs.Count; i++)
+ {
+ var bitcoinWit = bitcoinTx.Inputs[i].WitScript;
+ var scriptToExecute = new Script(bitcoinWit[bitcoinWit.PushCount - 2]);
+ var controlBlockBytes = bitcoinWit[bitcoinWit.PushCount - 1];
+
+ var leafHash = scriptToExecute.ToTapScript(TapLeafVersion.C0).LeafHash;
+ var hash = elementsTx.GetSignatureHashTaproot(trxData,
+ new TaprootExecutionData(i, leafHash) { SigHash = sigHash });
+
+ var sig = nbitcoinKey.SignTaprootKeySpend(hash, sigHash);
+
+ var witOps = new List();
+ witOps.Add(sig.ToBytes());
+ for (int p = 1; p < bitcoinWit.PushCount - 2; p++)
+ {
+ witOps.Add(bitcoinWit[p]);
+ }
+ witOps.Add(scriptToExecute.ToBytes());
+ witOps.Add(controlBlockBytes);
+
+ elementsTx.Inputs[i].WitScript = new WitScript(
+ witOps.Select(Op.GetPushOp).ToArray());
+ }
+
+ return CreateTransactionInfo(elementsTx, fee, bitcoinTxInfo);
+ }
+
+ public SignatureInfo WrapTaprootSignatures(
+ SignatureInfo signatureInfo,
+ Transaction recoveryTx,
+ TxOut[] spentOutputs,
+ Key nbitcoinKey,
+ Func scriptPerStage)
+ {
+ var elementsTx = BuildElementsTransaction(recoveryTx, Money.Zero);
+ var elemSpentOutputs = TransformSpentOutputs(spentOutputs);
+
+ const TaprootSigHash sigHash = TaprootSigHash.Single | TaprootSigHash.AnyoneCanPay;
+
+ foreach (var sigItem in signatureInfo.Signatures)
+ {
+ var tapScript = scriptPerStage(sigItem.StageIndex).ToTapScript(TapLeafVersion.C0);
+ var execData = new TaprootExecutionData(sigItem.StageIndex, tapScript.LeafHash) { SigHash = sigHash };
+ var hash = elementsTx.GetSignatureHashTaproot(elemSpentOutputs, execData);
+
+ var sig = nbitcoinKey.SignTaprootKeySpend(hash, sigHash);
+ sigItem.Signature = sig.ToString();
+ }
+
+ return signatureInfo;
+ }
+
+ public TransactionInfo WrapP2WSH(
+ TransactionInfo bitcoinTxInfo,
+ Key nbitcoinKey,
+ Script spendingScript,
+ Transaction recoveryTransaction)
+ {
+ var fee = bitcoinTxInfo.TransactionFee;
+ var bitcoinTx = Transaction.Parse(bitcoinTxInfo.Transaction.ToHex(), Network.Main);
+ var elementsTx = BuildElementsTransaction(bitcoinTx, Money.Satoshis(fee));
+
+ var builder = LiquidNetwork.CreateTransactionBuilder();
+
+ foreach (var input in elementsTx.Inputs)
+ {
+ var spendingOutput = recoveryTransaction.Outputs.AsIndexedOutputs()
+ .First(f => f.N == input.PrevOut.N);
+
+ var elemOutput = CreateExplicitOutput(spendingOutput.TxOut.Value, spendingOutput.TxOut.ScriptPubKey);
+ var coin = new Coin(input.PrevOut, elemOutput);
+ var scriptCoin = coin.ToScriptCoin(spendingScript);
+ builder.AddCoins(scriptCoin);
+ }
+
+ builder.AddKeys(nbitcoinKey);
+ builder.SignTransactionInPlace(elementsTx);
+
+ return CreateTransactionInfo(elementsTx, fee, bitcoinTxInfo);
+ }
+
+ public Transaction GetSighashTransaction(Transaction bitcoinTx)
+ {
+ return BuildElementsTransaction(bitcoinTx, Money.Zero);
+ }
+
+ public TxOut[] GetSighashSpentOutputs(TxOut[] spentOutputs)
+ {
+ return TransformSpentOutputs(spentOutputs);
+ }
+
+ private Transaction BuildElementsTransaction(Transaction bitcoinTx, Money fee)
+ {
+ var elementsTx = LiquidNetwork.CreateTransaction();
+
+ elementsTx.Version = bitcoinTx.Version;
+ elementsTx.LockTime = bitcoinTx.LockTime;
+
+ foreach (var input in bitcoinTx.Inputs)
+ {
+ var elemInput = (ElementsTxIn)LiquidNetwork.Consensus.ConsensusFactory.CreateTxIn();
+ elemInput.PrevOut = input.PrevOut;
+ elemInput.Sequence = input.Sequence;
+ if (input.ScriptSig != null && input.ScriptSig != Script.Empty)
+ {
+ elemInput.ScriptSig = input.ScriptSig;
+ }
+
+ elementsTx.Inputs.Add(elemInput);
+ }
+
+ foreach (var output in bitcoinTx.Outputs)
+ {
+ elementsTx.Outputs.Add(CreateExplicitOutput(output.Value, output.ScriptPubKey));
+ }
+
+ if (fee > Money.Zero)
+ {
+ elementsTx.Outputs.Add(CreateExplicitOutput(fee, Script.Empty));
+ }
+
+ return elementsTx;
+ }
+
+ private static ElementsTxOut CreateExplicitOutput(Money amount, Script scriptPubKey)
+ {
+ var elemOutput = new ElementsTxOut();
+ elemOutput.Value = amount;
+ elemOutput.ScriptPubKey = scriptPubKey;
+ elemOutput.Asset = new ConfidentialAsset(LBtcAssetId);
+ elemOutput.Nonce = new ConfidentialNonce();
+ return elemOutput;
+ }
+
+ private static TxOut[] TransformSpentOutputs(TxOut[] spentOutputs)
+ {
+ return spentOutputs.Select(o => (TxOut)CreateExplicitOutput(o.Value, o.ScriptPubKey)).ToArray();
+ }
+
+ private Key BlockcoreKeyToNBitcoinKey(Blockcore.NBitcoin.Key blockcoreKey)
+ {
+ var wif = blockcoreKey.GetBitcoinSecret(blockcoreNetwork).ToString();
+ return Key.Parse(wif, LiquidNetwork);
+ }
+
+ private static TransactionInfo CreateTransactionInfo(
+ Transaction signedElementsTx,
+ long fee,
+ TransactionInfo originalBitcoinTxInfo)
+ {
+ return new TransactionInfo
+ {
+ Transaction = originalBitcoinTxInfo.Transaction,
+ TransactionHex = signedElementsTx.ToHex(),
+ TransactionId = signedElementsTx.GetHash().ToString(),
+ TransactionFee = fee
+ };
+ }
+}