Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file modified native/linux-x64/libbitcoinkernel.so
Binary file not shown.
Binary file modified native/osx-x64/libbitcoinkernel.dylib
Binary file not shown.
Binary file added native/win-x64/bitcoinkernel.dll
Binary file not shown.
8 changes: 7 additions & 1 deletion src/BitcoinKernel.Interop/BitcoinKernel.Interop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@
<Pack>true</Pack>
<PackagePath>runtimes/linux-x64/native</PackagePath>
</None>

<!-- Include native libraries for Windows -->
<None Include="..\..\native\win-x64\bitcoinkernel.dll" Link="runtimes\win-x64\native\bitcoinkernel.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Pack>true</Pack>
<PackagePath>runtimes/win-x64/native</PackagePath>
</None>

</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/BitcoinKernel.Interop/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// Gets the nLockTime value of a transaction.
/// </summary>
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_get_locktime")]
public static extern uint TransactionGetLocktime(IntPtr transaction);

/// <summary>
/// Destroys a transaction.
/// </summary>
Expand Down Expand Up @@ -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);

/// <summary>
/// Gets the nSequence value from a transaction input.
/// </summary>
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_sequence")]
public static extern uint TransactionInputGetSequence(IntPtr transaction_input);

/// <summary>
/// Destroys a transaction input.
/// </summary>
Expand Down
9 changes: 7 additions & 2 deletions src/BitcoinKernel/Primitives/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ internal Transaction(IntPtr handle, bool ownsHandle = true)
/// <returns>The number of outputs as an integer.</returns>
public int OutputCount => (int)NativeMethods.TransactionCountOutputs(_handle);

/// <summary>
/// Gets the nLockTime value of this transaction.
/// </summary>
public uint LockTime => NativeMethods.TransactionGetLocktime(_handle);

/// <summary>
/// Gets the transaction ID (txid) as bytes.
/// </summary>
Expand Down Expand Up @@ -118,7 +123,7 @@ public string GetTxidHex()

/// <exception cref="ArgumentOutOfRangeException">Thrown when index is out of range.</exception>
/// <exception cref="TransactionException">Thrown when input retrieval fails.</exception>
public IntPtr GetInputAt(int index)
public TxIn GetInputAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, InputCount);
Expand All @@ -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);
}

/// <returns>The TxOut at the specified index.</returns>
Expand Down
69 changes: 69 additions & 0 deletions src/BitcoinKernel/Primitives/TxIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using BitcoinKernel.Exceptions;
using BitcoinKernel.Interop;

namespace BitcoinKernel.Primitives;

/// <summary>
/// Managed wrapper for a Bitcoin transaction input.
/// </summary>
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;
}

/// <summary>
/// Gets the nSequence value of this input.
/// </summary>
public uint Sequence => NativeMethods.TransactionInputGetSequence(_handle);

/// <summary>
/// Gets the out point of this input. The returned out point is not owned and
/// depends on the lifetime of the transaction.
/// </summary>
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;
}

/// <summary>
/// Creates a copy of this transaction input.
/// </summary>
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();
}
}
94 changes: 94 additions & 0 deletions tests/BitcoinKernel.Tests/TransactionTests.cs
Original file line number Diff line number Diff line change
@@ -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<TxIn>(input);
}

[Fact]
public void GetInputAt_OutOfRange_Throws()
{
using var tx = Transaction.FromHex(LegacyTxHex);
Assert.Throws<ArgumentOutOfRangeException>(() => tx.GetInputAt(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => 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);
}
}
Loading