From fc2d4b17ed8e76ce2b69e43a466eb7ecb7cd4b12 Mon Sep 17 00:00:00 2001 From: monsieurleberre Date: Wed, 11 Feb 2026 06:36:38 -0500 Subject: [PATCH 1/3] add Binance Travel Rule support with PII encryption --- .../Registration/DependencyInjection.cs | 3 + .../TravelRule/BinanceTravelRuleService.cs | 152 +++++++++++++++ .../TravelRule/IBinanceTravelRuleService.cs | 32 ++++ .../TravelRule/IRsaEncryptionService.cs | 24 +++ .../TravelRule/RsaEncryptionService.cs | 38 ++++ .../Base/FireblocksClientTestsBase.cs | 24 ++- .../ExchangeAccountsClientTests.cs | 149 ++++++++++++++- .../Integration/VaultClientTests.cs | 38 +++- .../Unit/DependencyInjectionTests.cs | 3 + .../BinanceTravelRuleServiceTests.cs | 179 ++++++++++++++++++ .../TravelRule/RsaEncryptionServiceTests.cs | 74 ++++++++ 11 files changed, 704 insertions(+), 12 deletions(-) create mode 100644 src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs create mode 100644 src/Trakx.Fireblocks.ApiClient/TravelRule/IBinanceTravelRuleService.cs create mode 100644 src/Trakx.Fireblocks.ApiClient/TravelRule/IRsaEncryptionService.cs create mode 100644 src/Trakx.Fireblocks.ApiClient/TravelRule/RsaEncryptionService.cs create mode 100644 tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs create mode 100644 tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/RsaEncryptionServiceTests.cs diff --git a/src/Trakx.Fireblocks.ApiClient/Registration/DependencyInjection.cs b/src/Trakx.Fireblocks.ApiClient/Registration/DependencyInjection.cs index 9cc7e9e..07a3ec4 100644 --- a/src/Trakx.Fireblocks.ApiClient/Registration/DependencyInjection.cs +++ b/src/Trakx.Fireblocks.ApiClient/Registration/DependencyInjection.cs @@ -3,6 +3,7 @@ using Trakx.Common.ApiClient; using Trakx.Common.Configuration; using Trakx.Common.DateAndTime; +using Trakx.Fireblocks.ApiClient.TravelRule; using Trakx.Fireblocks.ApiClient.Utils; namespace Trakx.Fireblocks.ApiClient; @@ -30,6 +31,8 @@ public static IServiceCollection AddFireblocksClient(this IServiceCollection ser services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddApiClientsOfBaseTypeWithHttpClient( new ApiClientWithHttpClientRetryOptions diff --git a/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs b/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs new file mode 100644 index 0000000..6a9f3bd --- /dev/null +++ b/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs @@ -0,0 +1,152 @@ +namespace Trakx.Fireblocks.ApiClient.TravelRule; + +/// +internal class BinanceTravelRuleService( + IExchange_accountsClient exchangeAccountsClient, + ITransactionsClient transactionsClient, + IRsaEncryptionService encryptionService) : IBinanceTravelRuleService +{ + private string? _cachedPublicKeyPem; + + /// + public async Task BuildWithdrawalExtraParametersAsync(CancellationToken cancellationToken = default) + { + return await BuildExtraParametersAsync( + partyKey: "beneficiary", + transactionDirection: "withdraw", + vaspSection: new Dictionary + { + ["beneficiaryVASP"] = new Dictionary { ["vaspCode"] = "BINANCE" }, + ["originatingVASP"] = new Dictionary { ["vaspCountry"] = "FR" } + }, + cancellationToken); + } + + /// + public async Task BuildDepositExtraParametersAsync(CancellationToken cancellationToken = default) + { + return await BuildExtraParametersAsync( + partyKey: "originator", + transactionDirection: "deposit", + vaspSection: new Dictionary + { + ["originatingVASP"] = new Dictionary { ["vaspName"] = "Binance" }, + ["beneficiaryVASP"] = new Dictionary { ["vaspCountry"] = "FR" } + }, + cancellationToken); + } + + private async Task BuildExtraParametersAsync( + string partyKey, + string transactionDirection, + Dictionary vaspSection, + CancellationToken cancellationToken) + { + var pem = await GetPublicKeyAsync(cancellationToken); + + var data = new Dictionary + { + [partyKey] = new Dictionary + { + ["participantRelationshipType"] = encryptionService.Encrypt("FirstParty", pem), + ["entityType"] = encryptionService.Encrypt("Business", pem) + }, + ["transactionData"] = new Dictionary + { + [transactionDirection] = new Dictionary + { + ["isAddressVerified"] = encryptionService.Encrypt(true, pem) + } + } + }; + + foreach (var (key, value) in vaspSection) + { + var plainValues = (Dictionary)value; + var encrypted = new Dictionary(); + foreach (var (fieldKey, fieldValue) in plainValues) + encrypted[fieldKey] = encryptionService.Encrypt((string)fieldValue, pem); + data[key] = encrypted; + } + + var piiData = new Dictionary + { + ["type"] = "exchange-service-travel-rule", + ["typeVersion"] = "1.0.0", + ["data"] = data + }; + + var extraParameters = new ExtraParameters(); + extraParameters.AdditionalProperties["piiData"] = piiData; + return extraParameters; + } + + /// + public async Task WithdrawFromBinanceAsync( + string exchangeAccountId, string vaultAccountId, string assetId, string amount, + string? note = null, string? customerRefId = null, CancellationToken cancellationToken = default) + { + var extraParameters = await BuildWithdrawalExtraParametersAsync(cancellationToken); + + var request = new TransactionRequest + { + Operation = TransactionOperation.TRANSFER, + AssetId = assetId, + Amount = amount, + Note = note, + CustomerRefId = customerRefId, + Source = new TransferPeerPath + { + Type = TransferPeerPathType.EXCHANGE_ACCOUNT, + Id = exchangeAccountId, + }, + Destination = new DestinationTransferPeerPath + { + Type = TransferPeerPathType.VAULT_ACCOUNT, + Id = vaultAccountId, + }, + ExtraParameters = extraParameters, + }; + + var response = await transactionsClient.CreateTransactionAsync(body: request, cancellationToken: cancellationToken); + return response.Content; + } + + /// + public async Task DepositToBinanceAsync( + string exchangeAccountId, string vaultAccountId, string assetId, string amount, + string? note = null, string? customerRefId = null, CancellationToken cancellationToken = default) + { + var extraParameters = await BuildDepositExtraParametersAsync(cancellationToken); + + var request = new TransactionRequest + { + Operation = TransactionOperation.TRANSFER, + AssetId = assetId, + Amount = amount, + Note = note, + CustomerRefId = customerRefId, + Source = new TransferPeerPath + { + Type = TransferPeerPathType.VAULT_ACCOUNT, + Id = vaultAccountId, + }, + Destination = new DestinationTransferPeerPath + { + Type = TransferPeerPathType.EXCHANGE_ACCOUNT, + Id = exchangeAccountId, + }, + ExtraParameters = extraParameters, + }; + + var response = await transactionsClient.CreateTransactionAsync(body: request, cancellationToken: cancellationToken); + return response.Content; + } + + private async Task GetPublicKeyAsync(CancellationToken cancellationToken) + { + return _cachedPublicKeyPem ??= (await exchangeAccountsClient + .GetExchangeAccountsCredentialsPublicKeyAsync(cancellationToken)) + .Content.PublicKey; + } +} diff --git a/src/Trakx.Fireblocks.ApiClient/TravelRule/IBinanceTravelRuleService.cs b/src/Trakx.Fireblocks.ApiClient/TravelRule/IBinanceTravelRuleService.cs new file mode 100644 index 0000000..37ea43f --- /dev/null +++ b/src/Trakx.Fireblocks.ApiClient/TravelRule/IBinanceTravelRuleService.cs @@ -0,0 +1,32 @@ +namespace Trakx.Fireblocks.ApiClient.TravelRule; + +/// +/// Service for building encrypted PII data required by Binance travel rule compliance. +/// See: https://developers.fireblocks.com/docs/a-developers-guide-to-constructing-encrypted-pii-messages-for-binance-via-fireblocks +/// +public interface IBinanceTravelRuleService +{ + /// + /// Builds the travel rule extra parameters required for Binance withdrawals (exchange to custodian). + /// + Task BuildWithdrawalExtraParametersAsync(CancellationToken cancellationToken = default); + + /// + /// Builds the travel rule extra parameters required for Binance deposits (custodian to exchange). + /// + Task BuildDepositExtraParametersAsync(CancellationToken cancellationToken = default); + + /// + /// Withdraws from Binance exchange account to a vault account, with travel rule compliance. + /// + Task WithdrawFromBinanceAsync( + string exchangeAccountId, string vaultAccountId, string assetId, string amount, + string? note = null, string? customerRefId = null, CancellationToken cancellationToken = default); + + /// + /// Deposits from a vault account to Binance exchange account, with travel rule compliance. + /// + Task DepositToBinanceAsync( + string exchangeAccountId, string vaultAccountId, string assetId, string amount, + string? note = null, string? customerRefId = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Trakx.Fireblocks.ApiClient/TravelRule/IRsaEncryptionService.cs b/src/Trakx.Fireblocks.ApiClient/TravelRule/IRsaEncryptionService.cs new file mode 100644 index 0000000..3a86a0f --- /dev/null +++ b/src/Trakx.Fireblocks.ApiClient/TravelRule/IRsaEncryptionService.cs @@ -0,0 +1,24 @@ +namespace Trakx.Fireblocks.ApiClient.TravelRule; + +/// +/// Service for encrypting PII data using RSA-OAEP with SHA-256. +/// +public interface IRsaEncryptionService +{ + /// + /// Encrypts a string value using RSA-OAEP with SHA-256. + /// + /// The plain text value to encrypt. + /// The RSA public key in PEM format. + /// Base64-encoded encrypted value. + string Encrypt(string plainText, string publicKeyPem); + + /// + /// Encrypts a boolean value using RSA-OAEP with SHA-256. + /// The boolean is converted to lowercase JSON representation ("true" or "false") before encryption. + /// + /// The boolean value to encrypt. + /// The RSA public key in PEM format. + /// Base64-encoded encrypted value. + string Encrypt(bool plainValue, string publicKeyPem); +} diff --git a/src/Trakx.Fireblocks.ApiClient/TravelRule/RsaEncryptionService.cs b/src/Trakx.Fireblocks.ApiClient/TravelRule/RsaEncryptionService.cs new file mode 100644 index 0000000..fa0dce9 --- /dev/null +++ b/src/Trakx.Fireblocks.ApiClient/TravelRule/RsaEncryptionService.cs @@ -0,0 +1,38 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Trakx.Fireblocks.ApiClient.TravelRule; + +/// +internal class RsaEncryptionService : IRsaEncryptionService +{ + /// + public string Encrypt(string plainText, string publicKeyPem) + { + ArgumentException.ThrowIfNullOrEmpty(plainText); + ArgumentException.ThrowIfNullOrEmpty(publicKeyPem); + + using var rsa = RSA.Create(); + rsa.ImportFromPem(publicKeyPem); + + var plainBytes = Encoding.UTF8.GetBytes(plainText); + var encryptedBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256); + + return Convert.ToBase64String(encryptedBytes); + } + + /// + public string Encrypt(bool plainValue, string publicKeyPem) + { + ArgumentException.ThrowIfNullOrEmpty(publicKeyPem); + + var boolBytes = BitConverter.GetBytes(plainValue); + + using var rsa = RSA.Create(); + rsa.ImportFromPem(publicKeyPem); + + var encryptedBytes = rsa.Encrypt(boolBytes, RSAEncryptionPadding.OaepSHA256); + + return Convert.ToBase64String(encryptedBytes); + } +} diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs index 9d02afe..2dcf8d3 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Serilog; +using Trakx.Common.Infrastructure.Environment.Env; using Trakx.Common.Testing.Configuration; namespace Trakx.Fireblocks.ApiClient.Tests.Integration.Base; @@ -31,19 +32,26 @@ public class FireblocksApiFixture : IDisposable public FireblocksApiFixture() { - var apiConfiguration = AwsConfigurationHelper.GetConfigurationFromAws() - with - { - BaseUrl = new Uri("https://api.fireblocks.io/v1") - }; + var apiConfiguration = GetConfiguration(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddFireblocksClient(apiConfiguration); - ServiceProvider = serviceCollection.BuildServiceProvider(); } + private static FireblocksApiConfiguration GetConfiguration() + { + try + { + return EnvConfigurationHelper.GetConfigurationFromEnv(); + } + catch + { + return AwsConfigurationHelper.GetConfigurationFromAws("Production") + with { BaseUrl = new Uri("https://api.fireblocks.io/v1") }; + } + } + protected virtual void Dispose(bool disposing) { if (!disposing) return; @@ -55,4 +63,4 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs index 041d71b..193df8f 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs @@ -1,5 +1,7 @@ +using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Trakx.Fireblocks.ApiClient.Tests.Integration.Base; +using Trakx.Fireblocks.ApiClient.TravelRule; namespace Trakx.Fireblocks.ApiClient.Tests.Integration; @@ -17,7 +19,7 @@ public ExchangeAccountsClientTests(FireblocksApiFixture apiFixture, ITestOutputH public async Task Exchange_accountsAllAsync_should_return_all_accounts() { var response = await _exchangeAccountsClient.GetPagedExchangeAccountsAsync(5); - response.Content.Exchanges.Should().BeEmpty(); + response.Content.Exchanges.Should().NotBeEmpty(); } [Fact] @@ -28,4 +30,147 @@ public async Task GetExchangeAccountsCredentialsPublicKeyAsync_should_return_pub response.Content.PublicKey.Should().NotBeNullOrEmpty(); response.Content.PublicKey.Should().StartWith("-----BEGIN PUBLIC KEY-----"); } -} \ No newline at end of file + + [Fact] + public async Task List_exchange_accounts_with_details() + { + string? after = null; + + do + { + var response = await _exchangeAccountsClient.GetPagedExchangeAccountsAsync(5, after: after); + var page = response.Content; + + foreach (var exchange in page.Exchanges) + { + _logger.Information("Exchange: Id={Id}, Name={Name}, Type={Type}, Status={Status}, IsSubaccount={IsSubaccount}", + exchange.Id, exchange.Name, exchange.Type, exchange.Status, exchange.IsSubaccount); + + if (exchange.Assets is { Count: > 0 }) + { + foreach (var asset in exchange.Assets) + { + _logger.Information(" Asset: Id={AssetId}, Balance={Balance}, Available={Available}, Total={Total}", + asset.Id, asset.Balance, asset.Available, asset.Total); + } + } + + if (exchange.TradingAccounts is not { Count: > 0 }) continue; + foreach (var tradingAccount in exchange.TradingAccounts) + { + _logger.Information(" TradingAccount: Name={Name}, Type={Type}", tradingAccount.Name, tradingAccount.Type); + } + } + + after = page.Paging?.After; + } while (!string.IsNullOrEmpty(after)); + } + + [Fact] + public async Task BuildWithdrawalExtraParameters_should_produce_valid_encrypted_pii_data() + { + var binanceTravelRuleService = _serviceProvider.GetRequiredService(); + + var result = await binanceTravelRuleService.BuildWithdrawalExtraParametersAsync(); + + result.Should().NotBeNull(); + result.AdditionalProperties.Should().ContainKey("piiData"); + + var piiData = result.AdditionalProperties["piiData"] as Dictionary; + piiData.Should().NotBeNull(); + piiData!["type"].Should().Be("exchange-service-travel-rule"); + piiData["typeVersion"].Should().Be("1.0.0"); + + var data = piiData["data"] as Dictionary; + data.Should().NotBeNull(); + data.Should().ContainKey("beneficiary"); + data.Should().ContainKey("transactionData"); + data.Should().ContainKey("beneficiaryVASP"); + data.Should().ContainKey("originatingVASP"); + + var beneficiary = data!["beneficiary"] as Dictionary; + AssertBase64Encoded(beneficiary!["participantRelationshipType"]); + AssertBase64Encoded(beneficiary["entityType"]); + + _logger.Information("Withdrawal piiData: {PiiData}", + JsonSerializer.Serialize(piiData, new JsonSerializerOptions { WriteIndented = true })); + } + + [Fact] + public async Task BuildDepositExtraParameters_should_produce_valid_encrypted_pii_data() + { + var binanceTravelRuleService = _serviceProvider.GetRequiredService(); + + var result = await binanceTravelRuleService.BuildDepositExtraParametersAsync(); + + result.Should().NotBeNull(); + result.AdditionalProperties.Should().ContainKey("piiData"); + + var piiData = result.AdditionalProperties["piiData"] as Dictionary; + piiData.Should().NotBeNull(); + piiData!["type"].Should().Be("exchange-service-travel-rule"); + piiData["typeVersion"].Should().Be("1.0.0"); + + var data = piiData["data"] as Dictionary; + data.Should().NotBeNull(); + data.Should().ContainKey("originator"); + data.Should().ContainKey("originatingVASP"); + data.Should().ContainKey("transactionData"); + data.Should().ContainKey("beneficiaryVASP"); + + var originator = data!["originator"] as Dictionary; + AssertBase64Encoded(originator!["participantRelationshipType"]); + AssertBase64Encoded(originator["entityType"]); + + _logger.Information("Deposit piiData: {PiiData}", + JsonSerializer.Serialize(piiData, new JsonSerializerOptions { WriteIndented = true })); + } + + [Fact] + public async Task WithdrawFromBinance_should_transfer_JUP_to_vault() + { + var binanceTravelRuleService = _serviceProvider.GetRequiredService(); + var transactionsClient = _serviceProvider.GetRequiredService(); + + var extraParameters = await binanceTravelRuleService.BuildWithdrawalExtraParametersAsync(); + + var request = new TransactionRequest + { + Operation = TransactionOperation.TRANSFER, + AssetId = "JUP_SOL", + Amount = "10", + Note = "test travel rule compliance", + CustomerRefId = Guid.NewGuid().ToString(), + Source = new TransferPeerPath + { + Type = TransferPeerPathType.EXCHANGE_ACCOUNT, + Id = "7b29fd51-6098-4c45-8471-4195cdcbdd70", + }, + Destination = new DestinationTransferPeerPath + { + Type = TransferPeerPathType.VAULT_ACCOUNT, + Id = "0", + }, + ExtraParameters = extraParameters, + }; + + var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.Indented); + _logger.Information("TransactionRequest payload:\n{Payload}", serialized); + + var response = await transactionsClient.CreateTransactionAsync(body: request); + + response.Content.Should().NotBeNull(); + response.Content.Id.Should().NotBeNullOrEmpty(); + + _logger.Information("Withdrawal transaction created: Id={Id}, Status={Status}", + response.Content.Id, response.Content.Status); + } + + private static void AssertBase64Encoded(object value) + { + var str = value as string; + str.Should().NotBeNullOrEmpty(); + var action = () => Convert.FromBase64String(str); + action.Should().NotThrow(); + } +} diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/VaultClientTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/VaultClientTests.cs index c041991..7edbecd 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/VaultClientTests.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/VaultClientTests.cs @@ -1,9 +1,10 @@ +using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using Trakx.Fireblocks.ApiClient.Tests.Integration.Base; namespace Trakx.Fireblocks.ApiClient.Tests.Integration; -public class VaultClientTests : FireblocksClientTestsBase +public partial class VaultClientTests : FireblocksClientTestsBase { private readonly IVaultsClient _vaultClient; @@ -23,6 +24,36 @@ public async Task GetVaultAccountsAsync_should_return_all_vault_accounts() accounts.Should().Contain(x => x.Assets.Any(x => x.Id == "BTC_TEST")); } + [Fact] + public async Task List_vault_accounts_with_details() + { + var userIdPattern = UserIdVaultNameRegex(); + string? after = null; + + do + { + var response = await _vaultClient.GetPagedVaultAccountsAsync(after: after, limit: 200); + var page = response.Content; + + foreach (var vault in page.Accounts) + { + if (userIdPattern.IsMatch(vault.Name ?? "")) continue; + + _logger.Information("Vault: Id={Id}, Name={Name}, AutoFuel={AutoFuel}, Hidden={Hidden}", + vault.Id, vault.Name, vault.AutoFuel, vault.HiddenOnUI); + + if (vault.Assets is not { Count: > 0 }) continue; + foreach (var asset in vault.Assets) + { + _logger.Information(" Asset: Id={AssetId}, Total={Total}, Available={Available}, Pending={Pending}, Frozen={Frozen}", + asset.Id, asset.Total, asset.Available, asset.Pending, asset.Frozen); + } + } + + after = page.Paging?.After; + } while (!string.IsNullOrEmpty(after)); + } + [Fact] public async Task GetVaultAccountsAsync_is_case_insensitive() { @@ -32,4 +63,7 @@ public async Task GetVaultAccountsAsync_is_case_insensitive() accounts.Should().NotBeNullOrEmpty(); accounts.Should().Contain(x => x.Name == "Exchange Warm Wallet"); } -} \ No newline at end of file + + [GeneratedRegex(@"^user__ID[0-9A-Fa-f]{10}$")] + private static partial Regex UserIdVaultNameRegex(); +} diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/DependencyInjectionTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/DependencyInjectionTests.cs index decf46b..86f8fdc 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/DependencyInjectionTests.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/DependencyInjectionTests.cs @@ -2,6 +2,7 @@ using Trakx.Common.DateAndTime; using Trakx.Common.Testing.Configuration; using Trakx.Common.Testing.Extensions; +using Trakx.Fireblocks.ApiClient.TravelRule; using Trakx.Fireblocks.ApiClient.Utils; namespace Trakx.Fireblocks.ApiClient.Tests.Unit; @@ -26,6 +27,8 @@ public void Should_be_able_to_resolve_common_dependencies() _serviceProvider.CheckServiceRegistration(); _serviceProvider.CheckServiceRegistration(); _serviceProvider.CheckServiceRegistration(); + _serviceProvider.CheckServiceRegistration(); + _serviceProvider.CheckServiceRegistration(); } [Fact] diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs new file mode 100644 index 0000000..2200efb --- /dev/null +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs @@ -0,0 +1,179 @@ +using System.Security.Cryptography; +using System.Text; +using Trakx.Common.ApiClient; +using Trakx.Fireblocks.ApiClient.TravelRule; + +namespace Trakx.Fireblocks.ApiClient.Tests.Unit.TravelRule; + +public class BinanceTravelRuleServiceTests : IDisposable +{ + private readonly BinanceTravelRuleService _sut; + private readonly ITransactionsClient _transactionsClient; + private readonly RSA _rsa; + + public BinanceTravelRuleServiceTests() + { + _rsa = RSA.Create(2048); + var publicKeyPem = _rsa.ExportSubjectPublicKeyInfoPem(); + + var exchangeAccountsClient = Substitute.For(); + exchangeAccountsClient.GetExchangeAccountsCredentialsPublicKeyAsync(Arg.Any()) + .Returns(new Response( + 200, + new Dictionary>(), + new ExchangeCredentialsPublicKeyResponse { PublicKey = publicKeyPem })); + + _transactionsClient = Substitute.For(); + _transactionsClient.CreateTransactionAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => new Response( + 200, + new Dictionary>(), + new CreateTransactionResponse { Id = "tx-123", Status = "SUBMITTED" })); + + var encryptionService = new RsaEncryptionService(); + _sut = new BinanceTravelRuleService(exchangeAccountsClient, _transactionsClient, encryptionService); + } + + [Fact] + public async Task BuildWithdrawalExtraParametersAsync_should_return_valid_pii_data_structure() + { + var result = await _sut.BuildWithdrawalExtraParametersAsync(); + + result.Should().NotBeNull(); + result.AdditionalProperties.Should().ContainKey("piiData"); + + var piiData = result.AdditionalProperties["piiData"] as Dictionary; + piiData.Should().NotBeNull(); + piiData!["type"].Should().Be("exchange-service-travel-rule"); + piiData["typeVersion"].Should().Be("1.0.0"); + + var data = piiData["data"] as Dictionary; + data.Should().NotBeNull(); + + var beneficiary = data!["beneficiary"] as Dictionary; + beneficiary.Should().NotBeNull(); + DecryptValue(beneficiary!["participantRelationshipType"]).Should().Be("FirstParty"); + DecryptValue(beneficiary["entityType"]).Should().Be("Business"); + + var transactionData = data["transactionData"] as Dictionary; + var withdraw = (transactionData!["withdraw"] as Dictionary)!; + DecryptBool(withdraw["isAddressVerified"]).Should().BeTrue(); + + var beneficiaryVasp = data["beneficiaryVASP"] as Dictionary; + DecryptValue(beneficiaryVasp!["vaspCode"]).Should().Be("BINANCE"); + + var originatingVasp = data["originatingVASP"] as Dictionary; + DecryptValue(originatingVasp!["vaspCountry"]).Should().Be("FR"); + } + + [Fact] + public async Task BuildDepositExtraParametersAsync_should_return_valid_pii_data_structure() + { + var result = await _sut.BuildDepositExtraParametersAsync(); + + result.Should().NotBeNull(); + result.AdditionalProperties.Should().ContainKey("piiData"); + + var piiData = result.AdditionalProperties["piiData"] as Dictionary; + piiData.Should().NotBeNull(); + piiData!["type"].Should().Be("exchange-service-travel-rule"); + piiData["typeVersion"].Should().Be("1.0.0"); + + var data = piiData["data"] as Dictionary; + data.Should().NotBeNull(); + + var originator = data!["originator"] as Dictionary; + originator.Should().NotBeNull(); + DecryptValue(originator!["participantRelationshipType"]).Should().Be("FirstParty"); + DecryptValue(originator["entityType"]).Should().Be("Business"); + + var originatingVasp = data["originatingVASP"] as Dictionary; + DecryptValue(originatingVasp!["vaspName"]).Should().Be("Binance"); + + var transactionData = data["transactionData"] as Dictionary; + var deposit = (transactionData!["deposit"] as Dictionary)!; + DecryptBool(deposit["isAddressVerified"]).Should().BeTrue(); + + var beneficiaryVasp = data["beneficiaryVASP"] as Dictionary; + DecryptValue(beneficiaryVasp!["vaspCountry"]).Should().Be("FR"); + } + + [Fact] + public async Task BuildWithdrawalExtraParametersAsync_should_produce_base64_encrypted_values() + { + var result = await _sut.BuildWithdrawalExtraParametersAsync(); + + var piiData = result.AdditionalProperties["piiData"] as Dictionary; + var data = piiData!["data"] as Dictionary; + var beneficiary = data!["beneficiary"] as Dictionary; + + // All encrypted values should be valid base64 strings + var encryptedValue = beneficiary!["participantRelationshipType"] as string; + encryptedValue.Should().NotBeNullOrEmpty(); + var action = () => Convert.FromBase64String(encryptedValue!); + action.Should().NotThrow(); + } + + [Fact] + public async Task WithdrawFromBinanceAsync_should_create_transaction_with_correct_peer_paths() + { + var result = await _sut.WithdrawFromBinanceAsync("exchange-1", "vault-2", "BTC", "0.5", "test note"); + + result.Id.Should().Be("tx-123"); + + await _transactionsClient.Received(1).CreateTransactionAsync( + Arg.Any(), + Arg.Any(), + Arg.Is(r => + r.Source.Type == TransferPeerPathType.EXCHANGE_ACCOUNT && + r.Source.Id == "exchange-1" && + r.Destination.Type == TransferPeerPathType.VAULT_ACCOUNT && + r.Destination.Id == "vault-2" && + r.AssetId == "BTC" && + r.Amount == "0.5" && + r.Note == "test note" && + r.ExtraParameters != null), + Arg.Any()); + } + + [Fact] + public async Task DepositToBinanceAsync_should_create_transaction_with_correct_peer_paths() + { + var result = await _sut.DepositToBinanceAsync("exchange-1", "vault-2", "ETH", "1.0"); + + result.Id.Should().Be("tx-123"); + + await _transactionsClient.Received(1).CreateTransactionAsync( + Arg.Any(), + Arg.Any(), + Arg.Is(r => + r.Source.Type == TransferPeerPathType.VAULT_ACCOUNT && + r.Source.Id == "vault-2" && + r.Destination.Type == TransferPeerPathType.EXCHANGE_ACCOUNT && + r.Destination.Id == "exchange-1" && + r.AssetId == "ETH" && + r.Amount == "1.0" && + r.ExtraParameters != null), + Arg.Any()); + } + + private string DecryptValue(object encryptedBase64) + { + var encryptedBytes = Convert.FromBase64String((string)encryptedBase64); + var decryptedBytes = _rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256); + return Encoding.UTF8.GetString(decryptedBytes); + } + + private bool DecryptBool(object encryptedBase64) + { + var encryptedBytes = Convert.FromBase64String((string)encryptedBase64); + var decryptedBytes = _rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256); + return BitConverter.ToBoolean(decryptedBytes); + } + + public void Dispose() + { + _rsa.Dispose(); + } +} diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/RsaEncryptionServiceTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/RsaEncryptionServiceTests.cs new file mode 100644 index 0000000..17c45f7 --- /dev/null +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/RsaEncryptionServiceTests.cs @@ -0,0 +1,74 @@ +using System.Security.Cryptography; +using System.Text; +using Trakx.Fireblocks.ApiClient.TravelRule; + +namespace Trakx.Fireblocks.ApiClient.Tests.Unit.TravelRule; + +public class RsaEncryptionServiceTests : IDisposable +{ + private readonly RsaEncryptionService _sut; + private readonly RSA _rsa; + private readonly string _publicKeyPem; + + public RsaEncryptionServiceTests() + { + _sut = new RsaEncryptionService(); + _rsa = RSA.Create(2048); + _publicKeyPem = _rsa.ExportSubjectPublicKeyInfoPem(); + } + + [Fact] + public void Encrypt_string_should_produce_base64_that_can_be_decrypted() + { + var plainText = "Hello World"; + + var encrypted = _sut.Encrypt(plainText, _publicKeyPem); + + encrypted.Should().NotBeNullOrEmpty(); + var encryptedBytes = Convert.FromBase64String(encrypted); + var decryptedBytes = _rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256); + var decrypted = Encoding.UTF8.GetString(decryptedBytes); + decrypted.Should().Be(plainText); + } + + [Fact] + public void Encrypt_bool_true_should_encrypt_as_raw_byte() + { + var encrypted = _sut.Encrypt(true, _publicKeyPem); + + encrypted.Should().NotBeNullOrEmpty(); + var encryptedBytes = Convert.FromBase64String(encrypted); + var decryptedBytes = _rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256); + BitConverter.ToBoolean(decryptedBytes).Should().BeTrue(); + } + + [Fact] + public void Encrypt_bool_false_should_encrypt_as_raw_byte() + { + var encrypted = _sut.Encrypt(false, _publicKeyPem); + + encrypted.Should().NotBeNullOrEmpty(); + var encryptedBytes = Convert.FromBase64String(encrypted); + var decryptedBytes = _rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256); + BitConverter.ToBoolean(decryptedBytes).Should().BeFalse(); + } + + [Fact] + public void Encrypt_should_throw_when_plainText_is_null_or_empty() + { + var action = () => _sut.Encrypt(string.Empty, _publicKeyPem); + action.Should().Throw(); + } + + [Fact] + public void Encrypt_should_throw_when_publicKeyPem_is_null_or_empty() + { + var action = () => _sut.Encrypt("test", string.Empty); + action.Should().Throw(); + } + + public void Dispose() + { + _rsa.Dispose(); + } +} From a6599b23037cb31376870c36a31b3a9bfd6b248d Mon Sep 17 00:00:00 2001 From: Monsieur Le Berre Date: Wed, 18 Feb 2026 05:24:37 -0500 Subject: [PATCH 2/3] refactor: remove beneficiaryVASP and consolidate travel rule tests --- .../TravelRule/BinanceTravelRuleService.cs | 4 +- .../Base/FireblocksClientTestsBase.cs | 3 +- .../Integration/BinanceTravelRuleTests.cs | 88 +++++++++++ .../ExchangeAccountsClientTests.cs | 147 +----------------- .../Integration/TransactionsClientTests.cs | 2 +- .../BinanceTravelRuleServiceTests.cs | 26 +--- 6 files changed, 98 insertions(+), 172 deletions(-) create mode 100644 tests/Trakx.Fireblocks.ApiClient.Tests/Integration/BinanceTravelRuleTests.cs diff --git a/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs b/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs index 6a9f3bd..b5a7e73 100644 --- a/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs +++ b/src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs @@ -16,7 +16,6 @@ public async Task BuildWithdrawalExtraParametersAsync(Cancellat transactionDirection: "withdraw", vaspSection: new Dictionary { - ["beneficiaryVASP"] = new Dictionary { ["vaspCode"] = "BINANCE" }, ["originatingVASP"] = new Dictionary { ["vaspCountry"] = "FR" } }, cancellationToken); @@ -30,8 +29,7 @@ public async Task BuildDepositExtraParametersAsync(Cancellation transactionDirection: "deposit", vaspSection: new Dictionary { - ["originatingVASP"] = new Dictionary { ["vaspName"] = "Binance" }, - ["beneficiaryVASP"] = new Dictionary { ["vaspCountry"] = "FR" } + ["originatingVASP"] = new Dictionary { ["vaspName"] = "Binance" } }, cancellationToken); } diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs index 2dcf8d3..ebc04e4 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Serilog; -using Trakx.Common.Infrastructure.Environment.Env; using Trakx.Common.Testing.Configuration; namespace Trakx.Fireblocks.ApiClient.Tests.Integration.Base; @@ -63,4 +62,4 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } -} +} \ No newline at end of file diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/BinanceTravelRuleTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/BinanceTravelRuleTests.cs new file mode 100644 index 0000000..153f5ba --- /dev/null +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/BinanceTravelRuleTests.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Trakx.Fireblocks.ApiClient.TravelRule; + +namespace Trakx.Fireblocks.ApiClient.Tests.Integration; + +public partial class TransactionsClientTests +{ + [Fact(Skip = "I hope we will never have to test this again 🙈")] + public async Task WithdrawFromBinance_should_transfer_JUP_to_vault() + { + var binanceTravelRuleService = _serviceProvider.GetRequiredService(); + var transactionsClient = _serviceProvider.GetRequiredService(); + + var extraParameters = await binanceTravelRuleService.BuildWithdrawalExtraParametersAsync(); + + var request = new TransactionRequest + { + Operation = TransactionOperation.TRANSFER, + AssetId = "JUP_SOL", + Amount = "10", + Note = "test travel rule compliance", + CustomerRefId = Guid.NewGuid().ToString(), + Source = new TransferPeerPath + { + Type = TransferPeerPathType.EXCHANGE_ACCOUNT, + Id = "7b29fd51-6098-4c45-8471-4195cdcbdd70", + }, + Destination = new DestinationTransferPeerPath + { + Type = TransferPeerPathType.VAULT_ACCOUNT, + Id = "0", + }, + ExtraParameters = extraParameters, + }; + + var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.Indented); + _logger.Information("TransactionRequest payload:\n{Payload}", serialized); + + var response = await transactionsClient.CreateTransactionAsync(body: request); + + response.Content.Should().NotBeNull(); + response.Content.Id.Should().NotBeNullOrEmpty(); + + _logger.Information("Withdrawal transaction created: Id={Id}, Status={Status}", + response.Content.Id, response.Content.Status); + } + + [Fact(Skip = "I hope we will never have to test this again 🙈")] + public async Task DepositToBinance_should_transfer_JUP_from_vault() + { + var binanceTravelRuleService = _serviceProvider.GetRequiredService(); + var transactionsClient = _serviceProvider.GetRequiredService(); + + var extraParameters = await binanceTravelRuleService.BuildDepositExtraParametersAsync(); + + var request = new TransactionRequest + { + Operation = TransactionOperation.TRANSFER, + AssetId = "JUP_SOL", + Amount = "10", + Note = "test travel rule compliance", + CustomerRefId = Guid.NewGuid().ToString(), + Source = new TransferPeerPath + { + Type = TransferPeerPathType.VAULT_ACCOUNT, + Id = "0", + }, + Destination = new DestinationTransferPeerPath + { + Type = TransferPeerPathType.EXCHANGE_ACCOUNT, + Id = "7b29fd51-6098-4c45-8471-4195cdcbdd70", + }, + ExtraParameters = extraParameters, + }; + + var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.Indented); + _logger.Information("TransactionRequest payload:\n{Payload}", serialized); + + var response = await transactionsClient.CreateTransactionAsync(body: request); + + response.Content.Should().NotBeNull(); + response.Content.Id.Should().NotBeNullOrEmpty(); + + _logger.Information("Deposit transaction created: Id={Id}, Status={Status}", + response.Content.Id, response.Content.Status); + } +} \ No newline at end of file diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs index 193df8f..f90b21f 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/ExchangeAccountsClientTests.cs @@ -19,7 +19,7 @@ public ExchangeAccountsClientTests(FireblocksApiFixture apiFixture, ITestOutputH public async Task Exchange_accountsAllAsync_should_return_all_accounts() { var response = await _exchangeAccountsClient.GetPagedExchangeAccountsAsync(5); - response.Content.Exchanges.Should().NotBeEmpty(); + response.Content.Exchanges.Should().BeEmpty(); } [Fact] @@ -30,147 +30,4 @@ public async Task GetExchangeAccountsCredentialsPublicKeyAsync_should_return_pub response.Content.PublicKey.Should().NotBeNullOrEmpty(); response.Content.PublicKey.Should().StartWith("-----BEGIN PUBLIC KEY-----"); } - - [Fact] - public async Task List_exchange_accounts_with_details() - { - string? after = null; - - do - { - var response = await _exchangeAccountsClient.GetPagedExchangeAccountsAsync(5, after: after); - var page = response.Content; - - foreach (var exchange in page.Exchanges) - { - _logger.Information("Exchange: Id={Id}, Name={Name}, Type={Type}, Status={Status}, IsSubaccount={IsSubaccount}", - exchange.Id, exchange.Name, exchange.Type, exchange.Status, exchange.IsSubaccount); - - if (exchange.Assets is { Count: > 0 }) - { - foreach (var asset in exchange.Assets) - { - _logger.Information(" Asset: Id={AssetId}, Balance={Balance}, Available={Available}, Total={Total}", - asset.Id, asset.Balance, asset.Available, asset.Total); - } - } - - if (exchange.TradingAccounts is not { Count: > 0 }) continue; - foreach (var tradingAccount in exchange.TradingAccounts) - { - _logger.Information(" TradingAccount: Name={Name}, Type={Type}", tradingAccount.Name, tradingAccount.Type); - } - } - - after = page.Paging?.After; - } while (!string.IsNullOrEmpty(after)); - } - - [Fact] - public async Task BuildWithdrawalExtraParameters_should_produce_valid_encrypted_pii_data() - { - var binanceTravelRuleService = _serviceProvider.GetRequiredService(); - - var result = await binanceTravelRuleService.BuildWithdrawalExtraParametersAsync(); - - result.Should().NotBeNull(); - result.AdditionalProperties.Should().ContainKey("piiData"); - - var piiData = result.AdditionalProperties["piiData"] as Dictionary; - piiData.Should().NotBeNull(); - piiData!["type"].Should().Be("exchange-service-travel-rule"); - piiData["typeVersion"].Should().Be("1.0.0"); - - var data = piiData["data"] as Dictionary; - data.Should().NotBeNull(); - data.Should().ContainKey("beneficiary"); - data.Should().ContainKey("transactionData"); - data.Should().ContainKey("beneficiaryVASP"); - data.Should().ContainKey("originatingVASP"); - - var beneficiary = data!["beneficiary"] as Dictionary; - AssertBase64Encoded(beneficiary!["participantRelationshipType"]); - AssertBase64Encoded(beneficiary["entityType"]); - - _logger.Information("Withdrawal piiData: {PiiData}", - JsonSerializer.Serialize(piiData, new JsonSerializerOptions { WriteIndented = true })); - } - - [Fact] - public async Task BuildDepositExtraParameters_should_produce_valid_encrypted_pii_data() - { - var binanceTravelRuleService = _serviceProvider.GetRequiredService(); - - var result = await binanceTravelRuleService.BuildDepositExtraParametersAsync(); - - result.Should().NotBeNull(); - result.AdditionalProperties.Should().ContainKey("piiData"); - - var piiData = result.AdditionalProperties["piiData"] as Dictionary; - piiData.Should().NotBeNull(); - piiData!["type"].Should().Be("exchange-service-travel-rule"); - piiData["typeVersion"].Should().Be("1.0.0"); - - var data = piiData["data"] as Dictionary; - data.Should().NotBeNull(); - data.Should().ContainKey("originator"); - data.Should().ContainKey("originatingVASP"); - data.Should().ContainKey("transactionData"); - data.Should().ContainKey("beneficiaryVASP"); - - var originator = data!["originator"] as Dictionary; - AssertBase64Encoded(originator!["participantRelationshipType"]); - AssertBase64Encoded(originator["entityType"]); - - _logger.Information("Deposit piiData: {PiiData}", - JsonSerializer.Serialize(piiData, new JsonSerializerOptions { WriteIndented = true })); - } - - [Fact] - public async Task WithdrawFromBinance_should_transfer_JUP_to_vault() - { - var binanceTravelRuleService = _serviceProvider.GetRequiredService(); - var transactionsClient = _serviceProvider.GetRequiredService(); - - var extraParameters = await binanceTravelRuleService.BuildWithdrawalExtraParametersAsync(); - - var request = new TransactionRequest - { - Operation = TransactionOperation.TRANSFER, - AssetId = "JUP_SOL", - Amount = "10", - Note = "test travel rule compliance", - CustomerRefId = Guid.NewGuid().ToString(), - Source = new TransferPeerPath - { - Type = TransferPeerPathType.EXCHANGE_ACCOUNT, - Id = "7b29fd51-6098-4c45-8471-4195cdcbdd70", - }, - Destination = new DestinationTransferPeerPath - { - Type = TransferPeerPathType.VAULT_ACCOUNT, - Id = "0", - }, - ExtraParameters = extraParameters, - }; - - var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.Indented); - _logger.Information("TransactionRequest payload:\n{Payload}", serialized); - - var response = await transactionsClient.CreateTransactionAsync(body: request); - - response.Content.Should().NotBeNull(); - response.Content.Id.Should().NotBeNullOrEmpty(); - - _logger.Information("Withdrawal transaction created: Id={Id}, Status={Status}", - response.Content.Id, response.Content.Status); - } - - private static void AssertBase64Encoded(object value) - { - var str = value as string; - str.Should().NotBeNullOrEmpty(); - var action = () => Convert.FromBase64String(str); - action.Should().NotThrow(); - } -} +} \ No newline at end of file diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/TransactionsClientTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/TransactionsClientTests.cs index 7c28b4d..d77bb79 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/TransactionsClientTests.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/TransactionsClientTests.cs @@ -4,7 +4,7 @@ namespace Trakx.Fireblocks.ApiClient.Tests.Integration; -public class TransactionsClientTests : FireblocksClientTestsBase +public partial class TransactionsClientTests : FireblocksClientTestsBase { private readonly ITransactionsClient _transactionsClient; diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs index 2200efb..555f792 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Unit/TravelRule/BinanceTravelRuleServiceTests.cs @@ -60,8 +60,8 @@ public async Task BuildWithdrawalExtraParametersAsync_should_return_valid_pii_da var withdraw = (transactionData!["withdraw"] as Dictionary)!; DecryptBool(withdraw["isAddressVerified"]).Should().BeTrue(); - var beneficiaryVasp = data["beneficiaryVASP"] as Dictionary; - DecryptValue(beneficiaryVasp!["vaspCode"]).Should().Be("BINANCE"); + //explicitly diverge from Fireblocks documentation, the doc is wrong. + data.Should().NotContainKey("beneficiaryVASP"); var originatingVasp = data["originatingVASP"] as Dictionary; DecryptValue(originatingVasp!["vaspCountry"]).Should().Be("FR"); @@ -95,24 +95,8 @@ public async Task BuildDepositExtraParametersAsync_should_return_valid_pii_data_ var deposit = (transactionData!["deposit"] as Dictionary)!; DecryptBool(deposit["isAddressVerified"]).Should().BeTrue(); - var beneficiaryVasp = data["beneficiaryVASP"] as Dictionary; - DecryptValue(beneficiaryVasp!["vaspCountry"]).Should().Be("FR"); - } - - [Fact] - public async Task BuildWithdrawalExtraParametersAsync_should_produce_base64_encrypted_values() - { - var result = await _sut.BuildWithdrawalExtraParametersAsync(); - - var piiData = result.AdditionalProperties["piiData"] as Dictionary; - var data = piiData!["data"] as Dictionary; - var beneficiary = data!["beneficiary"] as Dictionary; - - // All encrypted values should be valid base64 strings - var encryptedValue = beneficiary!["participantRelationshipType"] as string; - encryptedValue.Should().NotBeNullOrEmpty(); - var action = () => Convert.FromBase64String(encryptedValue!); - action.Should().NotThrow(); + //explicitly diverge from Fireblocks documentation, the doc is wrong. + data.Should().NotContainKey("beneficiaryVASP"); } [Fact] @@ -176,4 +160,4 @@ public void Dispose() { _rsa.Dispose(); } -} +} \ No newline at end of file From df33057fca918b5d39a8050a6e2a878404d3902a Mon Sep 17 00:00:00 2001 From: Monsieur Le Berre Date: Wed, 18 Feb 2026 05:59:55 -0500 Subject: [PATCH 3/3] Sorry I missed this --- .../Base/FireblocksClientTestsBase.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs index ebc04e4..12966e5 100644 --- a/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs +++ b/tests/Trakx.Fireblocks.ApiClient.Tests/Integration/Base/FireblocksClientTestsBase.cs @@ -31,24 +31,17 @@ public class FireblocksApiFixture : IDisposable public FireblocksApiFixture() { - var apiConfiguration = GetConfiguration(); + var apiConfiguration = AwsConfigurationHelper.GetConfigurationFromAws() + with + { + BaseUrl = new Uri("https://api.fireblocks.io/v1") + }; var serviceCollection = new ServiceCollection(); + serviceCollection.AddFireblocksClient(apiConfiguration); - ServiceProvider = serviceCollection.BuildServiceProvider(); - } - private static FireblocksApiConfiguration GetConfiguration() - { - try - { - return EnvConfigurationHelper.GetConfigurationFromEnv(); - } - catch - { - return AwsConfigurationHelper.GetConfigurationFromAws("Production") - with { BaseUrl = new Uri("https://api.fireblocks.io/v1") }; - } + ServiceProvider = serviceCollection.BuildServiceProvider(); } protected virtual void Dispose(bool disposing)