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 @@ -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;
Expand Down Expand Up @@ -30,6 +31,8 @@ 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>(
new ApiClientWithHttpClientRetryOptions
Expand Down
152 changes: 152 additions & 0 deletions src/Trakx.Fireblocks.ApiClient/TravelRule/BinanceTravelRuleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
namespace Trakx.Fireblocks.ApiClient.TravelRule;

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

/// <inheritdoc />
public async Task<ExtraParameters> BuildWithdrawalExtraParametersAsync(CancellationToken cancellationToken = default)
{
return await BuildExtraParametersAsync(
partyKey: "beneficiary",
transactionDirection: "withdraw",
vaspSection: new Dictionary<string, object>
{
["beneficiaryVASP"] = new Dictionary<string, object> { ["vaspCode"] = "BINANCE" },
["originatingVASP"] = new Dictionary<string, object> { ["vaspCountry"] = "FR" }
},
cancellationToken);
}

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

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>
{
[partyKey] = new Dictionary<string, object>
{
["participantRelationshipType"] = encryptionService.Encrypt("FirstParty", pem),
["entityType"] = encryptionService.Encrypt("Business", pem)
},
["transactionData"] = new Dictionary<string, object>
{
[transactionDirection] = new Dictionary<string, object>
{
["isAddressVerified"] = encryptionService.Encrypt(true, pem)
}
}
};

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",
["typeVersion"] = "1.0.0",
["data"] = data
};

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

/// <inheritdoc />
public async Task<CreateTransactionResponse> 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;
}

/// <inheritdoc />
public async Task<CreateTransactionResponse> 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<string> GetPublicKeyAsync(CancellationToken cancellationToken)
{
return _cachedPublicKeyPem ??= (await exchangeAccountsClient
.GetExchangeAccountsCredentialsPublicKeyAsync(cancellationToken))
.Content.PublicKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Trakx.Fireblocks.ApiClient.TravelRule;

/// <summary>
/// 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
{
/// <summary>
/// Builds the travel rule extra parameters required for Binance withdrawals (exchange to custodian).
/// </summary>
Task<ExtraParameters> BuildWithdrawalExtraParametersAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Builds the travel rule extra parameters required for Binance deposits (custodian to exchange).
/// </summary>
Task<ExtraParameters> BuildDepositExtraParametersAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Withdraws from Binance exchange account to a vault account, with travel rule compliance.
/// </summary>
Task<CreateTransactionResponse> WithdrawFromBinanceAsync(
string exchangeAccountId, string vaultAccountId, string assetId, string amount,
string? note = null, string? customerRefId = null, CancellationToken cancellationToken = default);

/// <summary>
/// Deposits from a vault account to Binance exchange account, with travel rule compliance.
/// </summary>
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
@@ -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;
Expand Down Expand Up @@ -31,19 +32,26 @@ public class FireblocksApiFixture : IDisposable

public FireblocksApiFixture()
{
var apiConfiguration = AwsConfigurationHelper.GetConfigurationFromAws<FireblocksApiConfiguration>()
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<FireblocksApiConfiguration>();
}
catch
{
return AwsConfigurationHelper.GetConfigurationFromAws<FireblocksApiConfiguration>("Production")
with { BaseUrl = new Uri("https://api.fireblocks.io/v1") };
}
}

protected virtual void Dispose(bool disposing)
{
if (!disposing) return;
Expand All @@ -55,4 +63,4 @@ public void Dispose()
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Loading
Loading