Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static IServiceCollection AddFireblocksClient(this IServiceCollection ser
services.AddSingleton<IFireblocksCredentialsProvider, ApiKeyCredentialsProvider>();
services.AddSingleton<IClientConfigurator, ClientConfigurator>();
services.AddSingleton<IFireblocksApiClientsFactory, FireblocksApiClientsFactory>();
services.AddSingleton<IRsaEncryptionService, RsaEncryptionService>();
services.AddSingleton<IBinanceTravelRuleService, BinanceTravelRuleService>();

services.AddApiClientsOfBaseTypeWithHttpClient<IFireblocksApiClientBase>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,71 @@ namespace Trakx.Fireblocks.ApiClient.TravelRule;

/// <inheritdoc />
internal class BinanceTravelRuleService(
ITransactionsClient transactionsClient) : IBinanceTravelRuleService
IExchange_accountsClient exchangeAccountsClient,
ITransactionsClient transactionsClient,
IRsaEncryptionService encryptionService) : IBinanceTravelRuleService
{
private string? _cachedPublicKeyPem;

/// <inheritdoc />
public Task<ExtraParameters> BuildWithdrawalExtraParametersAsync(CancellationToken cancellationToken = default)
public async Task<ExtraParameters> BuildWithdrawalExtraParametersAsync(CancellationToken cancellationToken = default)
{
var data = new Dictionary<string, object>
{
["beneficiary"] = new Dictionary<string, object>
return await BuildExtraParametersAsync(
partyKey: "beneficiary",
transactionDirection: "withdraw",
vaspSection: new Dictionary<string, object>
{
["participantRelationshipType"] = "FirstParty",
["entityType"] = "Business"
["originatingVASP"] = new Dictionary<string, object> { ["vaspCountry"] = "FR" }
},
["transactionData"] = new Dictionary<string, object>
cancellationToken);
}

/// <inheritdoc />
public async Task<ExtraParameters> BuildDepositExtraParametersAsync(CancellationToken cancellationToken = default)
{
return await BuildExtraParametersAsync(
partyKey: "originator",
transactionDirection: "deposit",
vaspSection: new Dictionary<string, object>
{
["withdraw"] = new Dictionary<string, object>
{
["isAddressVerified"] = true
}
["originatingVASP"] = new Dictionary<string, object> { ["vaspName"] = "Binance" }
},
["originatingVASP"] = new Dictionary<string, object>
{
["vaspCountry"] = "FR"
}
};

var piiData = new Dictionary<string, object>
{
["type"] = "exchange-service-travel-rule",
["typeVersion"] = "1.0.0",
["data"] = data
};

var extraParameters = new ExtraParameters();
extraParameters.AdditionalProperties["piiData"] = piiData;
return Task.FromResult(extraParameters);
cancellationToken);
}

/// <inheritdoc />
public Task<ExtraParameters> BuildDepositExtraParametersAsync(CancellationToken cancellationToken = default)
private async Task<ExtraParameters> BuildExtraParametersAsync(
string partyKey,
string transactionDirection,
Dictionary<string, object> vaspSection,
CancellationToken cancellationToken)
{
var pem = await GetPublicKeyAsync(cancellationToken);

var data = new Dictionary<string, object>
{
["originator"] = new Dictionary<string, object>
[partyKey] = new Dictionary<string, object>
{
["participantRelationshipType"] = "FirstParty",
["entityType"] = "Business"
["participantRelationshipType"] = encryptionService.Encrypt("FirstParty", pem),
["entityType"] = encryptionService.Encrypt("Business", pem)
},
["transactionData"] = new Dictionary<string, object>
{
["deposit"] = new Dictionary<string, object>
[transactionDirection] = new Dictionary<string, object>
{
["isAddressVerified"] = true
["isAddressVerified"] = encryptionService.Encrypt(true, pem)
}
},
["originatingVASP"] = new Dictionary<string, object>
{
["vaspName"] = "Binance"
}
};

foreach (var (key, value) in vaspSection)
{
var plainValues = (Dictionary<string, object>)value;
var encrypted = new Dictionary<string, object>();
foreach (var (fieldKey, fieldValue) in plainValues)
encrypted[fieldKey] = encryptionService.Encrypt((string)fieldValue, pem);
data[key] = encrypted;
}

var piiData = new Dictionary<string, object>
{
["type"] = "exchange-service-travel-rule",
Expand All @@ -71,7 +76,7 @@ public Task<ExtraParameters> BuildDepositExtraParametersAsync(CancellationToken

var extraParameters = new ExtraParameters();
extraParameters.AdditionalProperties["piiData"] = piiData;
return Task.FromResult(extraParameters);
return extraParameters;
}

/// <inheritdoc />
Expand Down Expand Up @@ -135,4 +140,11 @@ public async Task<CreateTransactionResponse> DepositToBinanceAsync(
var response = await transactionsClient.CreateTransactionAsync(body: request, cancellationToken: cancellationToken);
return response.Content;
}

private async Task<string> GetPublicKeyAsync(CancellationToken cancellationToken)
{
return _cachedPublicKeyPem ??= (await exchangeAccountsClient
.GetExchangeAccountsCredentialsPublicKeyAsync(cancellationToken))
.Content.PublicKey;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Trakx.Fireblocks.ApiClient.TravelRule;

/// <summary>
/// Service for building PII data required by Binance travel rule compliance.
/// 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
/// </summary>
public interface IBinanceTravelRuleService
Expand Down Expand Up @@ -29,4 +29,4 @@ Task<CreateTransactionResponse> WithdrawFromBinanceAsync(
Task<CreateTransactionResponse> DepositToBinanceAsync(
string exchangeAccountId, string vaultAccountId, string assetId, string amount,
string? note = null, string? customerRefId = null, CancellationToken cancellationToken = default);
}
}
24 changes: 24 additions & 0 deletions src/Trakx.Fireblocks.ApiClient/TravelRule/IRsaEncryptionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Trakx.Fireblocks.ApiClient.TravelRule;

/// <summary>
/// Service for encrypting PII data using RSA-OAEP with SHA-256.
/// </summary>
public interface IRsaEncryptionService
{
/// <summary>
/// Encrypts a string value using RSA-OAEP with SHA-256.
/// </summary>
/// <param name="plainText">The plain text value to encrypt.</param>
/// <param name="publicKeyPem">The RSA public key in PEM format.</param>
/// <returns>Base64-encoded encrypted value.</returns>
string Encrypt(string plainText, string publicKeyPem);

/// <summary>
/// Encrypts a boolean value using RSA-OAEP with SHA-256.
/// The boolean is converted to lowercase JSON representation ("true" or "false") before encryption.
/// </summary>
/// <param name="plainValue">The boolean value to encrypt.</param>
/// <param name="publicKeyPem">The RSA public key in PEM format.</param>
/// <returns>Base64-encoded encrypted value.</returns>
string Encrypt(bool plainValue, string publicKeyPem);
}
38 changes: 38 additions & 0 deletions src/Trakx.Fireblocks.ApiClient/TravelRule/RsaEncryptionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Security.Cryptography;
using System.Text;

namespace Trakx.Fireblocks.ApiClient.TravelRule;

/// <inheritdoc />
internal class RsaEncryptionService : IRsaEncryptionService
{
/// <inheritdoc />
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);
}

/// <inheritdoc />
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);
}
}
Original file line number Diff line number Diff line change
@@ -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<IBinanceTravelRuleService>();
var transactionsClient = _serviceProvider.GetRequiredService<ITransactionsClient>();

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<IBinanceTravelRuleService>();
var transactionsClient = _serviceProvider.GetRequiredService<ITransactionsClient>();

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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Trakx.Fireblocks.ApiClient.Tests.Integration;

public class TransactionsClientTests : FireblocksClientTestsBase
public partial class TransactionsClientTests : FireblocksClientTestsBase
{
private readonly ITransactionsClient _transactionsClient;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public void Should_be_able_to_resolve_common_dependencies()
_serviceProvider.CheckServiceRegistration<IFireblocksCredentialsProvider, ApiKeyCredentialsProvider>();
_serviceProvider.CheckServiceRegistration<IClientConfigurator, ClientConfigurator>();
_serviceProvider.CheckServiceRegistration<IFireblocksApiClientsFactory, FireblocksApiClientsFactory>();
_serviceProvider.CheckServiceRegistration<IRsaEncryptionService, RsaEncryptionService>();
_serviceProvider.CheckServiceRegistration<IBinanceTravelRuleService, BinanceTravelRuleService>();
}

Expand Down
Loading