Skip to content
67 changes: 59 additions & 8 deletions csharp/src/AdminAPI/Endpoints/RebalanceEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class RebalanceEndpoints
public static RouteGroupBuilder MapRebalanceEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/rebalance", GetAllAsync)
.Produces(StatusCodes.Status200OK);
.Produces<List<RebalanceEntry>>(StatusCodes.Status200OK);

group.MapPost("/rebalance", RebalanceAsync)
.Produces(StatusCodes.Status200OK);
Expand All @@ -30,17 +30,47 @@ public static RouteGroupBuilder MapRebalanceEndpoints(this RouteGroupBuilder gro
private static async Task<IResult> GetAllAsync(
ITemporalClient temporalClient)
{
var query = $"`WorkflowId` STARTS_WITH \"Rebalance\" AND `ExecutionStatus`=\"Running\"";
var results = new List<string>();
var query = $"`WorkflowId` STARTS_WITH \"Rebalance\"";
var results = new List<RebalanceEntry>();

await foreach (var wf in temporalClient.ListWorkflowsAsync(query))
await foreach (var wf in temporalClient.ListWorkflowsAsync(query, new WorkflowListOptions
{
Limit = 10,
}))
{
var whHandle = temporalClient.GetWorkflowHandle(wf.Id);
var describedWorkflow = await whHandle.DescribeAsync();

if (describedWorkflow.Memo.TryGetValue("Summary", out var summary) && summary != null)
{
results.Add(summary.ToString()!);
var decoded = temporalClient.Options.DataConverter.ToValueAsync<string>(summary.Payload);

try
{
var rebalanceEntry = new RebalanceEntry
{
Id = wf.Id,
Status = wf.Status.ToString(),
Summary = decoded.Result.FromJson<RebalanceSummary>(),
};

if (wf.Status == WorkflowExecutionStatus.Completed)
{
var wfResult = await whHandle.GetResultAsync<TransactionResponse>();

rebalanceEntry.Transaction = new TransactionDto
{
Hash = wfResult.TransactionHash,
Network = wfResult.NetworkName,
Type = TransactionType.Transfer,
};
}

results.Add(rebalanceEntry);
}
catch (Exception)
{
}
}
}

Expand Down Expand Up @@ -76,12 +106,33 @@ private static async Task<IResult> RebalanceAsync(
return Results.NotFound($"Wallet {request.FromAddress} not found on network {request.NetworkName}");
}

string toAddress;
var trustedWallet = await trustedWalletRepository.GetAsync(network.Type, request.ToAddress);

if (trustedWallet is null)
{
return Results.NotFound($"Trusted wallet {request.ToAddress} not found on network {request.NetworkName}");
var toWallet = await walletRepository.GetAsync(network.Type, request.ToAddress);

if (toWallet == null)
{
return Results.BadRequest($"To address {request.ToAddress} is not a trusted wallet, but a regular wallet.");
}

toAddress = toWallet.Address;
}
else
{
toAddress = trustedWallet.Address;
}

var summary = new RebalanceSummary
{
Amount = request.Amount.ToString(),
Network = network.ToExtendedDto(),
Token = token.ToDto(),
From = wallet.Address,
To = toAddress,
};

var workflowId = await temporalClient.StartWorkflowAsync(
TemporalHelper.ResolveProcessor(network.Type), [new TransactionRequest()
Expand All @@ -91,7 +142,7 @@ private static async Task<IResult> RebalanceAsync(
Amount = request.Amount,
Asset = token.Asset,
FromAddress = wallet.Address,
ToAddress = trustedWallet.Address,
ToAddress = toAddress,
}.ToJson(),
Type = TransactionType.Transfer,
Network = network.ToDetailedDto(),
Expand All @@ -105,7 +156,7 @@ private static async Task<IResult> RebalanceAsync(
IdReusePolicy = WorkflowIdReusePolicy.TerminateIfRunning,
Memo = new Dictionary<string, object>
{
{ "Summary", $"Rebalancing { TokenUnitHelper.FromBaseUnits(request.Amount, token.Decimals) } {token.Asset} in {network.DisplayName} from {wallet.Address} to {trustedWallet.Address}" },
{ "Summary", summary.ToJson()},
}
});

Expand Down
12 changes: 12 additions & 0 deletions csharp/src/AdminAPI/Endpoints/SwapEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ public static class SwapEndpoints
{
public static RouteGroupBuilder MapSwapEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/swaps", GetAllAsync)
.Produces<List<SwapDto>>();

group.MapGet("/swaps/{commitId}", GetAsync)
.Produces<SwapDto>();

return group;
}

private static async Task<IResult> GetAllAsync(
ISwapRepository repository,
uint page = 1)
{
var swaps = await repository.GetAllAsync(page);

return Results.Ok(swaps.Select(x => x.ToDto()));
}

private static async Task<IResult> GetAsync(
string commitId,
ISwapRepository repository)
Expand Down
17 changes: 16 additions & 1 deletion csharp/src/AdminAPI/Endpoints/TokenPriceEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Train.Solver.AdminAPI.Models;
using Train.Solver.Common.Enums;
using Train.Solver.Data.Abstractions.Entities;
using Train.Solver.Data.Abstractions.Repositories;
Expand All @@ -14,12 +15,26 @@ public static RouteGroupBuilder MapTokenPriceEndpoints(this RouteGroupBuilder gr
group.MapGet("/token-prices", GetAllTokenPricesAsync)
.Produces<List<TokenPriceDto>>();

group.MapPost("/token-prices", CreateTokenPriceAsync)
.Produces(200);

return group;
}

private static async Task<IResult> GetAllTokenPricesAsync(ITokenPriceRepository repository)
{
var tokenPrices = await repository.GetAllAsync();
return Results.Ok(tokenPrices.Select(x=>x.ToDto()));
return Results.Ok(tokenPrices.Select(x => x.ToDto()));
}

private static async Task<IResult> CreateTokenPriceAsync(
ITokenPriceRepository repository,
CreateTokenPriceRequest request)
{
var tokenPrice = await repository.CreateAsync(request.Symbol, request.ExternalId);

return tokenPrice is null
? Results.BadRequest("Failed to create token price")
: Results.Ok();
}
}
8 changes: 8 additions & 0 deletions csharp/src/AdminAPI/Models/CreateTokenPriceRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Train.Solver.AdminAPI.Models;

public class CreateTokenPriceRequest
{
public string Symbol { get; set; } = null!;

public string ExternalId { get; set; } = null!;
}
14 changes: 14 additions & 0 deletions csharp/src/AdminAPI/Models/RebalanceEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Train.Solver.Infrastructure.Abstractions.Models;

namespace Train.Solver.AdminAPI.Models;

public class RebalanceEntry
{
public required RebalanceSummary Summary { get; set; }

public required string Id { get; set; }

public required string Status { get; set; }

public TransactionDto? Transaction { get; set; }
}
16 changes: 16 additions & 0 deletions csharp/src/AdminAPI/Models/RebalanceSummary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Train.Solver.Infrastructure.Abstractions.Models;

namespace Train.Solver.AdminAPI.Models;

public class RebalanceSummary
{
public required ExtendedNetworkDto Network { get; set; }

public required TokenDto Token { get; set; }

public required string Amount { get; set; }

public required string From { get; set; }

public required string To { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public interface ISwapRepository
{
Task<Swap?> GetAsync(string commitId);

Task<List<Swap>> GetAllAsync(uint page = 1, uint size = 20, string[]? addresses = null);
Task<List<Swap>> GetAllAsync(uint page = 1, uint size = 20);

Task<List<string>> GetNonRefundedSwapIdsAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ public interface ITokenPriceRepository

Task<TokenPrice?> GetAsync(string symbol);

Task<TokenPrice?> CreateAsync(string symbol, string externalId);

Task UpdateAsync(Dictionary<string, decimal> prices);
}
5 changes: 1 addition & 4 deletions csharp/src/Data.Npgsql/EFSwapRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,9 @@ public async Task<Swap> CreateAsync(
return swap;
}

public async Task<List<Swap>> GetAllAsync(uint page = 1, uint size = 20, string[]? addresses = null)
public async Task<List<Swap>> GetAllAsync(uint page = 1, uint size = 15)
{
return await GetBaseQuery()
.Where(x => addresses == null
|| addresses.Contains(x.SourceAddress.ToLower())
|| addresses.Contains(x.DestinationAddress.ToLower()))
.OrderByDescending(x => x.CreatedDate)
.Skip((int)(page * size))
.Take((int)size)
Expand Down
14 changes: 14 additions & 0 deletions csharp/src/Data.Npgsql/EFTokenPriceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ namespace Train.Solver.Data.Npgsql;

public class EFTokenPriceRepository(SolverDbContext dbContext) : ITokenPriceRepository
{
public async Task<TokenPrice?> CreateAsync(string symbol, string externalId)
{
var tokenPrice = new TokenPrice
{
Symbol = symbol,
ExternalId = externalId,
};

dbContext.TokenPrices.Add(tokenPrice);
await dbContext.SaveChangesAsync();

return tokenPrice;
}

public async Task<List<TokenPrice>> GetAllAsync()
{
return await dbContext.TokenPrices.ToListAsync();
Expand Down
4 changes: 2 additions & 2 deletions csharp/src/Infrastructure.Abstractions/Models/SwapDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ public class SwapDto

public string Hashlock { get; set; } = null!;

public TokenNetworkDto Source { get; set; } = null!;
public ExtendedTokenNetworkDto Source { get; set; } = null!;

public BigInteger SourceAmount { get; set; }

public string SourceAddress { get; set; } = null!;

public string SourceContractAddress { get; set; } = null!;

public TokenNetworkDto Destination { get; set; } = null!;
public ExtendedTokenNetworkDto Destination { get; set; } = null!;

public BigInteger DestinationAmount { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions csharp/src/Infrastructure/Extensions/MapperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ public static SwapDto ToDto(this Swap swap)
{
CommitId = swap.CommitId,
Hashlock = swap.Hashlock,
Source = swap.Route.SourceToken.ToWithNetworkDto(),
Source = swap.Route.SourceToken.ToWithExtendedNetworkDto(),
SourceAmount = BigInteger.Parse(swap.SourceAmount),
SourceAddress = swap.SourceAddress,
SourceContractAddress = swap.Route.SourceTokenId == swap.Route.SourceToken.Network.NativeTokenId ?
swap.Route.SourceToken.Network.HTLCNativeContractAddress :
swap.Route.SourceToken.Network.HTLCTokenContractAddress,
Destination = swap.Route.DestinationToken.ToWithNetworkDto(),
Destination = swap.Route.DestinationToken.ToWithExtendedNetworkDto(),
DestinationAddress = swap.DestinationAddress,
DestinationContractAddress = swap.Route.DestinationTokenId == swap.Route.DestinationToken.Network.NativeTokenId ?
swap.Route.DestinationToken.Network.HTLCNativeContractAddress :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { GetTransactionRequest } from "../../Blockchain.Abstraction/Models/Recei
import { TransactionResponse } from "../../Blockchain.Abstraction/Models/ReceiptModels/TransactionResponse";
import { TransactionBuilderRequest } from "../../Blockchain.Abstraction/Models/TransactionBuilderModels/TransactionBuilderRequest";
import { PrepareTransactionResponse } from "../../Blockchain.Abstraction/Models/TransactionBuilderModels/TransferBuilderResponse";
import { BigNumberCoder, Provider, Wallet, Signer, sha256, DateTime, bn, hashMessage, B256Coder, concat, Address, isTransactionTypeScript, transactionRequestify, ScriptTransactionRequest } from "fuels";
import { BigNumberCoder, Provider, Wallet, Signer, sha256, DateTime, bn, hashMessage, B256Coder, concat, Address, isTransactionTypeScript, transactionRequestify, ScriptTransactionRequest} from "fuels";
import { TransactionStatus } from '../../Blockchain.Abstraction/Models/TransacitonModels/TransactionStatus';
import { TransactionType } from "../../Blockchain.Abstraction/Models/TransacitonModels/TransactionType";
import { IFuelBlockchainActivities } from "./IFuelBlockchainActivities";
Expand All @@ -14,7 +14,7 @@ import { BalanceResponse } from "../../Blockchain.Abstraction/Models/BalanceRequ
import { BaseRequest } from "../../Blockchain.Abstraction/Models/BaseRequest";
import { AddLockSignatureRequest } from "../../Blockchain.Abstraction/Models/TransactionBuilderModels/AddLockSignatureRequest";
import TrackBlockEventsAsync from "./Helper/FuelEventTracker";
import { createAddLockSigCallData, createRefundCallData, createLockCallData, createRedeemCallData, createCommitCallData } from "./Helper/FuelTransactionBuilder";
import { createAddLockSigCallData, createRefundCallData, createLockCallData, createRedeemCallData, createCommitCallData, createTransferCallData } from "./Helper/FuelTransactionBuilder";
import { FuelPublishTransactionRequest } from "../Models/FuelPublishTransactionRequest";
import { mapFuelStatusToInternal } from "./Helper/FuelTransactionStatusMapper";
import { FuelComposeTransactionRequest } from "../Models/FuelComposeTransactionRequest";
Expand All @@ -41,27 +41,29 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
) { }


public async BuildTransaction(request: TransactionBuilderRequest): Promise<PrepareTransactionResponse> {
try {
switch (request.type) {
case TransactionType.HTLCLock:
return createLockCallData(request.network, request.prepareArgs);
case TransactionType.HTLCRedeem:
return createRedeemCallData(request.network, request.prepareArgs);
case TransactionType.HTLCRefund:
return createRefundCallData(request.network, request.prepareArgs);
case TransactionType.HTLCAddLockSig:
return createAddLockSigCallData(request.network, request.prepareArgs);
case TransactionType.HTLCCommit:
return createCommitCallData(request.network, request.prepareArgs);
default:
throw new Error(`Unknown function name ${request.type}`);
}
}
catch (error) {
throw error;
public async BuildTransaction(request: TransactionBuilderRequest): Promise<PrepareTransactionResponse> {
try {
switch (request.type) {
case TransactionType.HTLCLock:
return createLockCallData(request.network, request.prepareArgs);
case TransactionType.HTLCRedeem:
return createRedeemCallData(request.network, request.prepareArgs);
case TransactionType.HTLCRefund:
return createRefundCallData(request.network, request.prepareArgs);
case TransactionType.HTLCAddLockSig:
return createAddLockSigCallData(request.network, request.prepareArgs);
case TransactionType.HTLCCommit:
return createCommitCallData(request.network, request.prepareArgs);
case TransactionType.Transfer:
return createTransferCallData(request.network, request.prepareArgs);
default:
throw new Error(`Unknown function name ${request.type}`);
}
}
catch (error) {
throw error;
}
}
}

public async GetBalance(request: BalanceRequest): Promise<BalanceResponse> {

Expand Down Expand Up @@ -195,16 +197,20 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {

const txRequest = ScriptTransactionRequest.from(transactionRequestify(requestData));

const balance = await wallet.getCoins(await provider.getBaseAssetId());
const coinInputs = txRequest.getCoinInputs();

if (!coinInputs.length) {
const balance = await wallet.getCoins(await provider.getBaseAssetId());

for (const coin of balance.coins) {
txRequest.addCoinInput(coin);
for (const coin of balance.coins) {
txRequest.addCoinInput(coin);
}
}

const estimatedDependencies = await wallet.provider.getTransactionCost(txRequest);

txRequest.maxFee = estimatedDependencies.maxFee;

txRequest.gasLimit = estimatedDependencies.gasUsed;

await this.ensureSufficientBalance(
Expand Down
Loading