Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
548113c
Update refund workflow to accept network
PurpleTheBest Aug 18, 2025
d3aacf8
Add support for some erc tokens. (#126)
Mandzikyan Aug 20, 2025
c0a230f
Include native token
PurpleTheBest Aug 21, 2025
64f3644
Merge branch 'dev' of https://github.com/TrainProtocol/solver into dev
PurpleTheBest Aug 21, 2025
e61961b
Fix DefaultActivity overload
PurpleTheBest Aug 21, 2025
9e63782
Fix prepare call data assets.
Aug 21, 2025
6fda041
Check if the sended token contract is corect. (#128)
Mandzikyan Aug 21, 2025
042a83e
Fix build error.
Aug 21, 2025
3b1139d
Fix fuel commit token contract reading.
Aug 21, 2025
2f6a9aa
Fix fuel error handling.
Aug 25, 2025
95dda92
Fix coin selecting and balance checking. (#129)
Mandzikyan Aug 25, 2025
15f1b5a
Add balance fetcher workflow
PurpleTheBest Aug 25, 2025
205e3b8
Merge branch 'dev' of https://github.com/TrainProtocol/solver into dev
PurpleTheBest Aug 25, 2025
add90e7
Fix rebalance for fuel. (#130)
Mandzikyan Aug 26, 2025
879d35d
Add balance Cache service
PurpleTheBest Aug 26, 2025
e5edb2f
Merge branch 'dev' of https://github.com/TrainProtocol/solver into dev
PurpleTheBest Aug 26, 2025
ea75d2e
Fix build error
PurpleTheBest Aug 26, 2025
de9e535
Add balance updater workflow
PurpleTheBest Aug 26, 2025
a9dec04
Read balances from cache
PurpleTheBest Aug 26, 2025
5fa4554
Add swap timestamp and add extra validation for addlock sig remaining…
PurpleTheBest Sep 1, 2025
cacec73
Fix get all network filter
PurpleTheBest Sep 2, 2025
4f2787d
Fix lp tiumelock check.
Sep 2, 2025
44dbaab
Revert "Read balances from cache"
PurpleTheBest Sep 5, 2025
f1a5230
Revert "Add balance updater workflow"
PurpleTheBest Sep 5, 2025
45074e7
Revert balance related workflows
PurpleTheBest Sep 5, 2025
afbc84b
Remove workspace
PurpleTheBest Sep 5, 2025
bec2fbb
Revert balance cache
PurpleTheBest Sep 5, 2025
c72fab6
Fix build errors
PurpleTheBest Sep 5, 2025
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
8 changes: 5 additions & 3 deletions csharp/src/AdminAPI/Endpoints/NetworkEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Newtonsoft.Json.Linq;
using System.Xml.Linq;
using Train.Solver.AdminAPI.Models;
using Train.Solver.Common.Enums;
using Train.Solver.Common.Extensions;
using Train.Solver.Data.Abstractions.Entities;
using Train.Solver.Data.Abstractions.Repositories;
using Train.Solver.Infrastructure.Abstractions.Models;
Expand Down Expand Up @@ -44,10 +46,10 @@ public static RouteGroupBuilder MapNetworkEndpoints(this RouteGroupBuilder group
return group;
}

private static async Task<IResult> GetAllAsync(INetworkRepository repository)
private static async Task<IResult> GetAllAsync(INetworkRepository repository, NetworkType[]? types)
{
var networks = await repository.GetAllAsync();
return Results.Ok(networks.Select(x=>x.ToDetailedDto()));
var networks = await repository.GetAllAsync(types.IsNullOrEmpty() ? null : types);
return Results.Ok(networks.Select(x => x.ToDetailedDto()));
}

private static async Task<IResult> GetAsync(
Expand Down
10 changes: 8 additions & 2 deletions csharp/src/AdminAPI/Endpoints/SwapEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ private static async Task<IResult> RefundAsync(
return Results.BadRequest("Swap already refunded");
}

if(swap.Route.SourceToken.Network.Name != request.NetworkName &&
swap.Route.DestinationToken.Network.Name != request.NetworkName)
{
return Results.BadRequest("Network name does not match swap source or destination network");
}

var wallet = await walletRepository.GetAsync(request.Type, request.Address);

if (wallet == null)
Expand All @@ -96,9 +102,9 @@ private static async Task<IResult> RefundAsync(
}

var workflowId = await temporalClient.StartWorkflowAsync(
(IRefundWorkflow w) => w.RunAsync(commitId, request.Address, wallet.SignerAgent.Name),
(IRefundWorkflow w) => w.RunAsync(commitId, request.NetworkName, request.Address, wallet.SignerAgent.Name),
new(id: TemporalHelper.BuildRefundId(commitId), taskQueue: Constants.CoreTaskQueue));

return Results.Ok();
}
}
}
2 changes: 2 additions & 0 deletions csharp/src/AdminAPI/Models/RefundRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace Train.Solver.AdminAPI.Models;

public class RefundRequest
{
public string NetworkName { get; set; }

public NetworkType Type { get; set; }

public string Address { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public interface INetworkRepository

Task<Token?> GetTokenAsync(string networkName, string symbol);

Task<IEnumerable<Network>> GetAllAsync();
Task<IEnumerable<Network>> GetAllAsync(NetworkType[]? types);

Task<Network?> CreateAsync(
string networkName,
Expand Down
3 changes: 2 additions & 1 deletion csharp/src/Data.Npgsql/EFNetworkRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ public class EFNetworkRepository(
.FirstOrDefaultAsync(x => x.Name == networkName);
}

public async Task<IEnumerable<Network>> GetAllAsync()
public async Task<IEnumerable<Network>> GetAllAsync(NetworkType[]? types)
{
return await dbContext.Networks
.Where(x => types == null || types.Contains(x.Type))
.Include(x => x.Tokens)
.ThenInclude(x => x.TokenPrice)
.Include(x => x.Nodes)
Expand Down
4 changes: 2 additions & 2 deletions csharp/src/Data.Npgsql/EFSwapRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ private IQueryable<Swap> GetBaseQuery()
=> dbContext.Swaps
.Include(x => x.Transactions).ThenInclude(x => x.Network.NativeToken!.TokenPrice)
.Include(x => x.Route.SourceWallet.SignerAgent)
.Include(x => x.Route.SourceToken.Network)
.Include(x => x.Route.SourceToken.Network.NativeToken)
.Include(x => x.Route.SourceToken.TokenPrice)
.Include(x => x.Route.DestinationWallet.SignerAgent)
.Include(x => x.Route.DestinationToken.Network)
.Include(x => x.Route.DestinationToken.Network.NativeToken)
.Include(x => x.Route.DestinationToken.TokenPrice);
}
2 changes: 2 additions & 0 deletions csharp/src/Infrastructure.Abstractions/Models/SwapDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public class SwapDto
{
public int Id { get; set; }

public DateTimeOffset Timestamp { get; set; }

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

public string Hashlock { get; set; } = null!;
Expand Down
2 changes: 2 additions & 0 deletions csharp/src/Infrastructure/Extensions/MapperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static SwapDto ToDto(this Swap swap)
{
Id = swap.Id,
CommitId = swap.CommitId,
Timestamp = swap.CreatedDate,
Hashlock = swap.Hashlock,
Source = swap.Route.SourceToken.ToWithExtendedNetworkDto(),
SourceAmount = BigInteger.Parse(swap.SourceAmount),
Expand All @@ -36,6 +37,7 @@ public static DetailedSwapDto ToDetailedDto(this Swap swap)
{
Id = swap.Id,
CommitId = swap.CommitId,
Timestamp = swap.CreatedDate,
Hashlock = swap.Hashlock,
Source = swap.Route.SourceToken.ToWithExtendedNetworkDto(),
SourceAmount = BigInteger.Parse(swap.SourceAmount),
Expand Down
5 changes: 2 additions & 3 deletions csharp/src/Workflow.Abstractions/Workflows/IRefundWorkflow.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

using Temporalio.Workflows;
using Temporalio.Workflows;

namespace Train.Solver.Workflow.Abstractions.Workflows;

[Workflow]
public interface IRefundWorkflow
{
[WorkflowRun]
Task RunAsync(string commitId, string fromAddress, string signerAgentName);
Task RunAsync(string commitId, string networkName, string fromAddress, string signerAgentName);
}
3 changes: 0 additions & 3 deletions csharp/src/Workflow.Common/Helpers/TemporalHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ public static async Task<TResult> ExecuteChildTransactionProcessorWorkflowAsync<
return await handle.GetResultAsync<TResult>().ConfigureAwait(true);
}

public static ActivityOptions DefaultActivityOptions(string? summary = null) =>
DefaultActivityOptions(null, summary);

public static ActivityOptions DefaultActivityOptions(NetworkType networkType, string? summary = null) =>
DefaultActivityOptions(networkType.ToString(), summary);

Expand Down
2 changes: 1 addition & 1 deletion csharp/src/Workflow.EVM/Helpers/EVMTransactionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static PrepareTransactionDto BuildApproveTransaction(DetailedNetworkDto n
? network.HTLCNativeContractAddress
: network.HTLCTokenContractAddress;

if (currency.Symbol != nativeCurrency.Symbol)
if (!isNative)
{
var response = new PrepareTransactionDto
{
Expand Down
10 changes: 7 additions & 3 deletions csharp/src/Workflow.Swap/Workflows/RefundWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@ namespace Train.Solver.Workflow.Swap.Workflows;
public class RefundWorkflow : IRefundWorkflow
{
[WorkflowRun]
public async Task RunAsync(string commitId, string fromAddress, string signerAgentName)
public async Task RunAsync(string commitId, string networkName, string fromAddress, string signerAgentName)
{
var swap = await ExecuteActivityAsync(
(ISwapActivities x) => x.GetSwapAsync(commitId),
DefaultActivityOptions(Constants.CoreTaskQueue));

var network = await ExecuteActivityAsync(
(INetworkActivities x) => x.GetNetworkAsync(swap.Destination.Network.Name),
(INetworkActivities x) => x.GetNetworkAsync(networkName),
DefaultActivityOptions(Constants.CoreTaskQueue));

var asset = swap.Destination.Network.Name == network.Name
? swap.Destination.Token.Symbol
: swap.Source.Token.Symbol;

var signerAgent = await ExecuteActivityAsync(
(IWalletActivities x) => x.GetSignerAgentAsync(signerAgentName),
DefaultActivityOptions(Constants.CoreTaskQueue));
Expand All @@ -35,7 +39,7 @@ public async Task RunAsync(string commitId, string fromAddress, string signerAge
PrepareArgs = new HTLCRefundTransactionPrepareRequest
{
CommitId = swap.CommitId,
Asset = swap.Destination.Token.Symbol,
Asset = asset,
}.ToJson(),
Type = TransactionType.HTLCRefund,
Network = network,
Expand Down
7 changes: 7 additions & 0 deletions csharp/src/Workflow.Swap/Workflows/SwapWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ await ExecuteTransactionAsync(new TransactionRequest()
throw new ApplicationFailureException("Timelock remaining time is less than min acceptable value");
}

var remainingTimePeriodBetweenAddLockSigAndSolverLockInSeconds = _lpTimeLock.ToUnixTimeSeconds() - _htlcAddLockSigMessage.Timelock;

if (remainingTimePeriodBetweenAddLockSigAndSolverLockInSeconds < _minAcceptableTimelockPeriod.TotalSeconds)
{
throw new ApplicationFailureException("Timelock remaining time is less than solver lock");
}

await ExecuteTransactionAsync(new TransactionRequest()
{
PrepareArgs = new AddLockSigTransactionPrepareRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Train.Solver.Infrastructure.Abstractions.Models;
using Train.Solver.Workflow.Abstractions.Activities;
using Train.Solver.Workflow.Abstractions.Models;
using Train.Solver.Workflow.Common;
using Train.Solver.Workflow.Common.Helpers;
using static Temporalio.Workflows.Workflow;

Expand All @@ -16,7 +17,7 @@ public async Task<PrepareTransactionDto> RunAsync(PrepareTransactionRequest requ
{
var network = await ExecuteActivityAsync(
(INetworkActivities x) => x.GetNetworkAsync(request.NetworkName),
TemporalHelper.DefaultActivityOptions());
TemporalHelper.DefaultActivityOptions(Constants.CoreTaskQueue));

var buildTransaction = await ExecuteActivityAsync(
(IBlockchainActivities x) => x.BuildTransactionAsync(new()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface HTLCCommitEventMessage {
destinationNetwork: string;
destinationAsset: string;
timeLock: number;
tokenContract?: string;
}

export interface HTLCLockEventMessage {
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, AssetId } from "fuels";
import { TransactionStatus } from '../../Blockchain.Abstraction/Models/TransacitonModels/TransactionStatus';
import { TransactionType } from "../../Blockchain.Abstraction/Models/TransacitonModels/TransactionType";
import { IFuelBlockchainActivities } from "./IFuelBlockchainActivities";
Expand Down Expand Up @@ -118,7 +118,7 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
return result;
}

public async getTransaction(request: GetTransactionRequest): Promise<TransactionResponse> {
public async GetTransaction(request: GetTransactionRequest): Promise<TransactionResponse> {

const provider = new Provider(request.network.nodes[0].url);
const transaction = await provider.getTransactionResponse(request.transactionHash);
Expand Down Expand Up @@ -149,7 +149,7 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
return transactionResponse;
}

public async publishTransaction(request: FuelPublishTransactionRequest): Promise<string> {
public async PublishTransaction(request: FuelPublishTransactionRequest): Promise<string> {
let result: string;

try {
Expand Down Expand Up @@ -183,11 +183,13 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
}
}

public async composeRawTransaction(request: FuelComposeTransactionRequest): Promise<string> {
public async ComposeRawTransaction(request: FuelComposeTransactionRequest): Promise<string> {
try {
const provider = new Provider(request.network.nodes[0].url);
const wallet = Wallet.fromAddress(request.fromAddress, provider);
const requestData = JSON.parse(request.callData);
const token = request.network.tokens.find(t => t.symbol === request.callDataAsset);
const nativeToken = request.network.nativeToken;

const isTxnTypeScript = isTransactionTypeScript(JSON.parse(request.callData));

Expand All @@ -196,24 +198,41 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
}

const txRequest = ScriptTransactionRequest.from(transactionRequestify(requestData));
const isNative = token.symbol === nativeToken.symbol;

const coinInputs = txRequest.getCoinInputs();

if (!coinInputs.length) {
if (isNative && coinInputs.length === 0) {

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

for (const coin of balance.coins) {
txRequest.addCoinInput(coin);
}
}
else if (!isNative && coinInputs.length === 0) {
const nativeBalance = await wallet.getCoins(await provider.getBaseAssetId());

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

const assetId: AssetId = new Address(token.contract).toAssetId();

const tokenBalance = await wallet.getCoins(assetId.bits);

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

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

txRequest.maxFee = estimatedDependencies.maxFee;

txRequest.gasLimit = estimatedDependencies.gasUsed;

await this.ensureSufficientBalance(
await this.EnsureSufficientBalance(
{
network: request.network,
rawData: txRequest,
Expand All @@ -230,17 +249,19 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
if (error.metadata.logs.includes("Not Future Timelock")) {
throw new InvalidTimelockException(`Transaction has an invalid timelock`);
}

throw error;
}
}

private async ensureSufficientBalance(request: FuelSufficientBalanceRequest): Promise<void> {
private async EnsureSufficientBalance(request: FuelSufficientBalanceRequest): Promise<void> {

const nativeAssetId = await request.wallet.provider.getBaseAssetId();
const coinInputs = request.rawData.getCoinInputs();

const nativeBalance = Number(
coinInputs.find(coin => coin.assetId === nativeAssetId).amount
);
const nativeBalance = coinInputs
.filter(coin => coin.assetId === nativeAssetId)
.reduce((sum, coin) => sum + Number(coin.amount), 0)

const maxFee = Number(request.rawData.maxFee);

Expand All @@ -261,9 +282,9 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {

const topkenAssetId = new Address(token.contract).toAssetId().bits;

const tokenAssetBalance = Number(
coinInputs.find(coin => coin.assetId === topkenAssetId).amount
);
const tokenAssetBalance = coinInputs
.filter(coin => coin.assetId === topkenAssetId)
.reduce((sum, coin) => sum + Number(coin.amount), 0);

if (tokenAssetBalance < request.callDataAmount) {
throw new Error(`Insufficient balance for ${request.callDataAsset}`);
Expand All @@ -275,7 +296,7 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
}
}

public async signTransaction(request: FuelSignTransactionRequestModel): Promise<string> {
public async SignTransaction(request: FuelSignTransactionRequestModel): Promise<string> {

const treasuryClient = new TreasuryClient(request.signerAgentUrl);

Expand All @@ -284,7 +305,7 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
return response.signedTxn;
}

public async getNextNonce(request: NextNonceRequest): Promise<number> {
public async GetNextNonce(request: NextNonceRequest): Promise<number> {
const lockKey = buildLockKey(request.network.name, request.address);

const nextNonceKey = buildNextNonceKey(request.network.name, request.address);
Expand Down Expand Up @@ -315,7 +336,7 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
}
}

public async checkCurrentNonce(request: CurrentNonceRequest): Promise<void> {
public async CheckCurrentNonce(request: CurrentNonceRequest): Promise<void> {
const currentNonceKey = buildCurrentNonceKey(request.network.name, request.address);

const cached = await this.redis.get(currentNonceKey);
Expand All @@ -332,7 +353,7 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
}


public async updateCurrentNonce(request: CurrentNonceRequest): Promise<void> {
public async UpdateCurrentNonce(request: CurrentNonceRequest): Promise<void> {
const lockKey = buildLockKey(request.network.name, request.address);

const currentNonceKey = buildCurrentNonceKey(request.network.name, request.address);
Expand All @@ -354,6 +375,6 @@ export class FuelBlockchainActivities implements IFuelBlockchainActivities {
}
}

export function formatAddress(address: string): string {
export function FormatAddress(address: string): string {
return address.toLowerCase();
}
Loading