diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c550f26..3e2efc2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, macos-latest]
+ os: [ubuntu-latest, macos-latest, windows-latest]
dotnet-version: ['9.0.x']
fail-fast: false
diff --git a/native/linux-x64/libbitcoinkernel.so b/native/linux-x64/libbitcoinkernel.so
index 3b6cb11..e877220 100755
Binary files a/native/linux-x64/libbitcoinkernel.so and b/native/linux-x64/libbitcoinkernel.so differ
diff --git a/native/osx-x64/libbitcoinkernel.dylib b/native/osx-x64/libbitcoinkernel.dylib
index 8e7369f..e2a995f 100755
Binary files a/native/osx-x64/libbitcoinkernel.dylib and b/native/osx-x64/libbitcoinkernel.dylib differ
diff --git a/native/win-x64/bitcoinkernel.dll b/native/win-x64/bitcoinkernel.dll
new file mode 100755
index 0000000..3f8d039
Binary files /dev/null and b/native/win-x64/bitcoinkernel.dll differ
diff --git a/src/BitcoinKernel.Interop/BitcoinKernel.Interop.csproj b/src/BitcoinKernel.Interop/BitcoinKernel.Interop.csproj
index ca9447e..9154e69 100644
--- a/src/BitcoinKernel.Interop/BitcoinKernel.Interop.csproj
+++ b/src/BitcoinKernel.Interop/BitcoinKernel.Interop.csproj
@@ -25,7 +25,13 @@
true
runtimes/linux-x64/native
-
+
+
+ PreserveNewest
+ true
+ runtimes/win-x64/native
+
+
diff --git a/src/BitcoinKernel.Interop/NativeMethods.cs b/src/BitcoinKernel.Interop/NativeMethods.cs
index 1b5c770..375b86f 100644
--- a/src/BitcoinKernel.Interop/NativeMethods.cs
+++ b/src/BitcoinKernel.Interop/NativeMethods.cs
@@ -488,6 +488,12 @@ public static extern int TransactionToBytes(
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_get_input_at")]
public static extern IntPtr TransactionGetInputAt(IntPtr transaction, nuint index);
+ ///
+ /// Gets the nLockTime value of a transaction.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_get_locktime")]
+ public static extern uint TransactionGetLocktime(IntPtr transaction);
+
///
/// Destroys a transaction.
///
@@ -861,6 +867,12 @@ public static extern IntPtr TransactionSpentOutputsGetCoinAt(
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_out_point")]
public static extern IntPtr TransactionInputGetOutPoint(IntPtr transaction_input);
+ ///
+ /// Gets the nSequence value from a transaction input.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_sequence")]
+ public static extern uint TransactionInputGetSequence(IntPtr transaction_input);
+
///
/// Destroys a transaction input.
///
diff --git a/src/BitcoinKernel/Primitives/Transaction.cs b/src/BitcoinKernel/Primitives/Transaction.cs
index d560a41..c5d7d66 100644
--- a/src/BitcoinKernel/Primitives/Transaction.cs
+++ b/src/BitcoinKernel/Primitives/Transaction.cs
@@ -89,6 +89,11 @@ internal Transaction(IntPtr handle, bool ownsHandle = true)
/// The number of outputs as an integer.
public int OutputCount => (int)NativeMethods.TransactionCountOutputs(_handle);
+ ///
+ /// Gets the nLockTime value of this transaction.
+ ///
+ public uint LockTime => NativeMethods.TransactionGetLocktime(_handle);
+
///
/// Gets the transaction ID (txid) as bytes.
///
@@ -118,7 +123,7 @@ public string GetTxidHex()
/// Thrown when index is out of range.
/// Thrown when input retrieval fails.
- public IntPtr GetInputAt(int index)
+ public TxIn GetInputAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, InputCount);
@@ -127,7 +132,7 @@ public IntPtr GetInputAt(int index)
if (inputPtr == IntPtr.Zero)
throw new TransactionException($"Failed to get input at index {index}");
- return inputPtr;
+ return new TxIn(inputPtr, ownsHandle: false);
}
/// The TxOut at the specified index.
diff --git a/src/BitcoinKernel/Primitives/TxIn.cs b/src/BitcoinKernel/Primitives/TxIn.cs
new file mode 100644
index 0000000..d4645b4
--- /dev/null
+++ b/src/BitcoinKernel/Primitives/TxIn.cs
@@ -0,0 +1,69 @@
+using System;
+using BitcoinKernel.Exceptions;
+using BitcoinKernel.Interop;
+
+namespace BitcoinKernel.Primitives;
+
+///
+/// Managed wrapper for a Bitcoin transaction input.
+///
+public sealed class TxIn : IDisposable
+{
+ private IntPtr _handle;
+ private bool _disposed;
+ private readonly bool _ownsHandle;
+
+ internal TxIn(IntPtr handle, bool ownsHandle = true)
+ {
+ _handle = handle;
+ _ownsHandle = ownsHandle;
+ }
+
+ ///
+ /// Gets the nSequence value of this input.
+ ///
+ public uint Sequence => NativeMethods.TransactionInputGetSequence(_handle);
+
+ ///
+ /// Gets the out point of this input. The returned out point is not owned and
+ /// depends on the lifetime of the transaction.
+ ///
+ public IntPtr GetOutPoint()
+ {
+ IntPtr outPointPtr = NativeMethods.TransactionInputGetOutPoint(_handle);
+ if (outPointPtr == IntPtr.Zero)
+ throw new TransactionException("Failed to get out point from transaction input");
+
+ return outPointPtr;
+ }
+
+ ///
+ /// Creates a copy of this transaction input.
+ ///
+ public TxIn Copy()
+ {
+ IntPtr copyHandle = NativeMethods.TransactionInputCopy(_handle);
+ if (copyHandle == IntPtr.Zero)
+ throw new TransactionException("Failed to copy transaction input");
+
+ return new TxIn(copyHandle, ownsHandle: true);
+ }
+
+ internal IntPtr Handle => _handle;
+
+ public void Dispose()
+ {
+ if (!_disposed && _handle != IntPtr.Zero && _ownsHandle)
+ {
+ NativeMethods.TransactionInputDestroy(_handle);
+ _handle = IntPtr.Zero;
+ _disposed = true;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ ~TxIn()
+ {
+ Dispose();
+ }
+}
diff --git a/tests/BitcoinKernel.Tests/TransactionTests.cs b/tests/BitcoinKernel.Tests/TransactionTests.cs
new file mode 100644
index 0000000..f2e445b
--- /dev/null
+++ b/tests/BitcoinKernel.Tests/TransactionTests.cs
@@ -0,0 +1,94 @@
+using BitcoinKernel.Primitives;
+using Xunit;
+
+namespace BitcoinKernel.Tests;
+
+public class TransactionTests
+{
+ // Legacy P2PKH transaction — version 2, locktime 510826, sequence 0xFFFFFFFE
+ private const string LegacyTxHex =
+ "02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95" +
+ "000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5" +
+ "f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe0" +
+ "9ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617" +
+ "feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb0" +
+ "88ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700";
+
+ // Segwit P2SH transaction — version 1, locktime 0, sequence 0xFFFFFFFF
+ private const string SegwitTxHex =
+ "01000000000101d9fd94d0ff0026d307c994d0003180a5f248146efb6371d040c5973f5f66d9" +
+ "df0400000017160014b31b31a6cb654cfab3c50567bcf124f48a0beaecffffffff012cbd1c00" +
+ "0000000017a914233b74bf0823fa58bbbd26dfc3bb4ae715547167870247304402206f60569c" +
+ "ac136c114a58aedd80f6fa1c51b49093e7af883e605c212bdafcd8d202200e91a55f408a021a" +
+ "d2631bc29a67bd6915b2d7e9ef0265627eabd7f7234455f6012103e7e802f50344303c76d12c" +
+ "089c8724c1b230e3b745693bbe16aad536293d15e300000000";
+
+ [Fact]
+ public void LockTime_LegacyTransaction_ReturnsCorrectValue()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ Assert.Equal(510826u, tx.LockTime);
+ }
+
+ [Fact]
+ public void LockTime_SegwitTransaction_ReturnsZero()
+ {
+ using var tx = Transaction.FromHex(SegwitTxHex);
+ Assert.Equal(0u, tx.LockTime);
+ }
+
+ [Fact]
+ public void GetInputAt_ReturnsTxIn()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ using var input = tx.GetInputAt(0);
+ Assert.NotNull(input);
+ Assert.IsType(input);
+ }
+
+ [Fact]
+ public void GetInputAt_OutOfRange_Throws()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ Assert.Throws(() => tx.GetInputAt(-1));
+ Assert.Throws(() => tx.GetInputAt(tx.InputCount));
+ }
+
+ [Fact]
+ public void TxIn_Sequence_LegacyTransaction_ReturnsCorrectValue()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ using var input = tx.GetInputAt(0);
+ // Sequence 0xFFFFFFFE = 4294967294 (RBF signalling)
+ Assert.Equal(0xFFFFFFFEu, input.Sequence);
+ }
+
+ [Fact]
+ public void TxIn_Sequence_SegwitTransaction_ReturnsMaxValue()
+ {
+ using var tx = Transaction.FromHex(SegwitTxHex);
+ using var input = tx.GetInputAt(0);
+ // Sequence 0xFFFFFFFF = final (no RBF, no relative locktime)
+ Assert.Equal(0xFFFFFFFFu, input.Sequence);
+ }
+
+ [Fact]
+ public void TxIn_GetOutPoint_ReturnsNonNullPointer()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ using var input = tx.GetInputAt(0);
+ var outPoint = input.GetOutPoint();
+ Assert.NotEqual(IntPtr.Zero, outPoint);
+ }
+
+ [Fact]
+ public void TxIn_Copy_IsIndependent()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ using var input = tx.GetInputAt(0);
+ using var copy = input.Copy();
+
+ Assert.NotNull(copy);
+ Assert.Equal(input.Sequence, copy.Sequence);
+ }
+}