diff --git a/.github/workflows/solana.yml b/.github/workflows/solana.yml index 68950697..bb05b389 100644 --- a/.github/workflows/solana.yml +++ b/.github/workflows/solana.yml @@ -1,6 +1,7 @@ name: Solana Workflow runner on: + workflow_dispatch: push: branches: [ disable ] paths: @@ -37,4 +38,4 @@ jobs: dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} build-args: | - DOTNET_VERSION=${{ env.DOTNET_VERSION }} \ No newline at end of file + DOTNET_VERSION=${{ env.DOTNET_VERSION }} diff --git a/csharp/TrainSolver.slnx b/csharp/TrainSolver.slnx index 96ce8ca9..e73875de 100644 --- a/csharp/TrainSolver.slnx +++ b/csharp/TrainSolver.slnx @@ -33,6 +33,7 @@ + diff --git a/csharp/src/Infrastructure.Abstractions/Models/DetailedNetworkDto.cs b/csharp/src/Infrastructure.Abstractions/Models/DetailedNetworkDto.cs index 8b325e87..0f578e82 100644 --- a/csharp/src/Infrastructure.Abstractions/Models/DetailedNetworkDto.cs +++ b/csharp/src/Infrastructure.Abstractions/Models/DetailedNetworkDto.cs @@ -1,6 +1,4 @@ -using Train.Solver.Common.Enums; - -namespace Train.Solver.Infrastructure.Abstractions.Models; +namespace Train.Solver.Infrastructure.Abstractions.Models; public class DetailedNetworkDto : ExtendedNetworkDto { diff --git a/csharp/src/Infrastructure.Abstractions/Models/ExtendedNetworkDto.cs b/csharp/src/Infrastructure.Abstractions/Models/ExtendedNetworkDto.cs index 32ea271f..f1b3ac67 100644 --- a/csharp/src/Infrastructure.Abstractions/Models/ExtendedNetworkDto.cs +++ b/csharp/src/Infrastructure.Abstractions/Models/ExtendedNetworkDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Train.Solver.Common.Enums; +using Train.Solver.Common.Enums; namespace Train.Solver.Infrastructure.Abstractions.Models; diff --git a/csharp/src/SmartNodeInvoker/ISmartNodeInvoker.cs b/csharp/src/SmartNodeInvoker/ISmartNodeInvoker.cs index e2d0b62d..dfcad8e2 100644 --- a/csharp/src/SmartNodeInvoker/ISmartNodeInvoker.cs +++ b/csharp/src/SmartNodeInvoker/ISmartNodeInvoker.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Train.Solver.Common.Enums; - -namespace Train.Solver.SmartNodeInvoker; +namespace Train.Solver.SmartNodeInvoker; public interface ISmartNodeInvoker { diff --git a/csharp/src/SmartNodeInvoker/SmartNodeInvoker.cs b/csharp/src/SmartNodeInvoker/SmartNodeInvoker.cs index d24ded24..a4e59bf9 100644 --- a/csharp/src/SmartNodeInvoker/SmartNodeInvoker.cs +++ b/csharp/src/SmartNodeInvoker/SmartNodeInvoker.cs @@ -1,13 +1,5 @@ -using Microsoft.Extensions.Logging; -using StackExchange.Redis; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using StackExchange.Redis; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Train.Solver.Common.Enums; using Train.Solver.Common.Helpers; namespace Train.Solver.SmartNodeInvoker; @@ -78,7 +70,6 @@ private async Task> OrderNodesByScoreAsync(string redisKey, IEnumer .ToList(); } - private async Task IncrementScoreAsync(string redisKey, string node, int delta) { var newScore = await cache.SortedSetIncrementAsync(redisKey, node, delta); diff --git a/csharp/src/Workflow.EVM/Models/EstimateFeeRequest.cs b/csharp/src/Workflow.Abstractions/Models/EstimateFeeRequest.cs similarity index 79% rename from csharp/src/Workflow.EVM/Models/EstimateFeeRequest.cs rename to csharp/src/Workflow.Abstractions/Models/EstimateFeeRequest.cs index e4ab8194..fb313bc1 100644 --- a/csharp/src/Workflow.EVM/Models/EstimateFeeRequest.cs +++ b/csharp/src/Workflow.Abstractions/Models/EstimateFeeRequest.cs @@ -1,7 +1,6 @@ using System.Numerics; -using Train.Solver.Workflow.Abstractions.Models; -namespace Train.Solver.Workflow.EVM.Models; +namespace Train.Solver.Workflow.Abstractions.Models; public class EstimateFeeRequest : BaseRequest { diff --git a/csharp/src/Workflow.EVM/Models/NextNonceRequest.cs b/csharp/src/Workflow.Abstractions/Models/NextNonceRequest.cs similarity index 52% rename from csharp/src/Workflow.EVM/Models/NextNonceRequest.cs rename to csharp/src/Workflow.Abstractions/Models/NextNonceRequest.cs index d912e6cd..c06e67f7 100644 --- a/csharp/src/Workflow.EVM/Models/NextNonceRequest.cs +++ b/csharp/src/Workflow.Abstractions/Models/NextNonceRequest.cs @@ -1,6 +1,4 @@ -using Train.Solver.Workflow.Abstractions.Models; - -namespace Train.Solver.Workflow.EVM.Models; +namespace Train.Solver.Workflow.Abstractions.Models; public class NextNonceRequest : BaseRequest { diff --git a/csharp/src/Workflow.Abstractions/Models/TransactionBuilderRequest.cs b/csharp/src/Workflow.Abstractions/Models/TransactionBuilderRequest.cs index 3467f538..9e0c3459 100644 --- a/csharp/src/Workflow.Abstractions/Models/TransactionBuilderRequest.cs +++ b/csharp/src/Workflow.Abstractions/Models/TransactionBuilderRequest.cs @@ -7,4 +7,6 @@ public class TransactionBuilderRequest : BaseRequest public TransactionType Type { get; set; } public string PrepareArgs { get; set; } = null!; + + public string? FromAddress { get; set; } } diff --git a/csharp/src/Workflow.EVM/Activities/EVMBlockchainActivities.cs b/csharp/src/Workflow.EVM/Activities/EVMBlockchainActivities.cs index 24a3f30e..d535c0a4 100644 --- a/csharp/src/Workflow.EVM/Activities/EVMBlockchainActivities.cs +++ b/csharp/src/Workflow.EVM/Activities/EVMBlockchainActivities.cs @@ -109,7 +109,7 @@ public virtual async Task GetBatchTransactionAsync(GetBatch [Activity] public virtual Task BuildTransactionAsync(TransactionBuilderRequest request) { - PrepareTransactionDto result = request.Type switch + var result = request.Type switch { TransactionType.Transfer => EVMTransactionBuilder.BuildTransferTransaction(request.Network, request.PrepareArgs), TransactionType.Approve => EVMTransactionBuilder.BuildApproveTransaction(request.Network, request.PrepareArgs), diff --git a/csharp/src/Workflow.EVM/Program.cs b/csharp/src/Workflow.EVM/Program.cs index 15253408..f9d77f22 100644 --- a/csharp/src/Workflow.EVM/Program.cs +++ b/csharp/src/Workflow.EVM/Program.cs @@ -1,7 +1,6 @@ using Train.Solver.Infrastructure.DependencyInjection; using Train.Solver.Infrastructure.Logging.OpenTelemetry; using Train.Solver.Infrastrucutre.Secret.Treasury.Extensions; -using Train.Solver.Workflow.EVM.Activities; using Train.Solver.Workflow.EVM.Extensions; IHost host = Host.CreateDefaultBuilder(args) diff --git a/csharp/src/Workflow.Solana/Activities/ISolanaBlockchainActivities.cs b/csharp/src/Workflow.Solana/Activities/ISolanaBlockchainActivities.cs index a7806973..fff7a701 100644 --- a/csharp/src/Workflow.Solana/Activities/ISolanaBlockchainActivities.cs +++ b/csharp/src/Workflow.Solana/Activities/ISolanaBlockchainActivities.cs @@ -1,29 +1,28 @@ using Temporalio.Activities; -using Train.Solver.Blockchain.Abstractions.Models; using Train.Solver.Blockchain.Solana.Models; +using Train.Solver.Infrastructure.Abstractions.Models; +using Train.Solver.Workflow.Abstractions.Models; +using Train.Solver.Workflow.Solana.Models; -namespace Train.Solver.Blockchain.Solana.Activities; +namespace Train.Solver.Workflow.Solana.Activities; public interface ISolanaBlockchainActivities { [Activity] - Task EstimateFeeAsync(EstimateFeeRequest request); + Task BuildTransactionAsync(TransactionBuilderRequest request); [Activity] - Task GetNextNonceAsync(NextNonceRequest request); - - [Activity] - Task BuildTransactionAsync(TransactionBuilderRequest request); - - [Activity] - Task GetTransactionAsync(GetTransactionRequest request); + Task GetTransactionAsync(SolanaGetReceiptRequest request); [Activity] Task SimulateTransactionAsync(SolanaPublishTransactionRequest request); [Activity] - Task ComposeSolanaTranscationAsync(SolanaComposeTransactionRequest request); + Task ComposeSolanaTranscationAsync(SolanaComposeTransactionRequest request); [Activity] Task PublishTransactionAsync(SolanaPublishTransactionRequest request); + + [Activity] + Task SignTransactionAsync(SolanaSignTransactionRequest request); } diff --git a/csharp/src/Workflow.Solana/Activities/SolanaBlockchainActivities.cs b/csharp/src/Workflow.Solana/Activities/SolanaBlockchainActivities.cs index b09398df..a252cfb2 100644 --- a/csharp/src/Workflow.Solana/Activities/SolanaBlockchainActivities.cs +++ b/csharp/src/Workflow.Solana/Activities/SolanaBlockchainActivities.cs @@ -1,232 +1,83 @@ -using System.Numerics; +using Nethereum.Hex.HexConvertors.Extensions; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; using Solnet.Rpc; using Solnet.Rpc.Builders; using Solnet.Rpc.Models; using Solnet.Wallet; using Solnet.Wallet.Utilities; -using StackExchange.Redis; +using System.Buffers; +using System.Numerics; using Temporalio.Activities; -using static Train.Solver.Blockchain.Common.Helpers.ResilientNodeHelper; -using Nethereum.Web3; -using Train.Solver.Infrastructure.Abstractions.Exceptions; -using Train.Solver.Data.Abstractions.Repositories; -using Train.Solver.Data.Abstractions.Entities; -using Train.Solver.Blockchain.Abstractions.Activities; -using Train.Solver.Blockchain.Abstractions.Models; -using Train.Solver.Util.Extensions; -using Train.Solver.Infrastructure.Abstractions; using Train.Solver.Blockchain.Solana.Extensions; using Train.Solver.Blockchain.Solana.Helpers; using Train.Solver.Blockchain.Solana.Models; using Train.Solver.Blockchain.Solana.Programs; -using Train.Solver.Blockchain.Common.Helpers; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; -using System.Buffers; -using Nethereum.Hex.HexConvertors.Extensions; -using System.Text; +using Train.Solver.Common.Enums; +using Train.Solver.Data.Abstractions.Repositories; +using Train.Solver.Infrastructure.Abstractions; +using Train.Solver.Infrastructure.Abstractions.Exceptions; +using Train.Solver.Infrastructure.Abstractions.Models; +using Train.Solver.Workflow.Abstractions.Activities; +using Train.Solver.Workflow.Abstractions.Models; +using Train.Solver.Workflow.Solana.Helpers; +using Train.Solver.Workflow.Solana.Models; +using Transaction = Solnet.Rpc.Models.Transaction; -namespace Train.Solver.Blockchain.Solana.Activities; +namespace Train.Solver.Workflow.Solana.Activities; public class SolanaBlockchainActivities( INetworkRepository networkRepository, - IDatabase cache, IPrivateKeyProvider privateKeyProvider) : ISolanaBlockchainActivities, IBlockchainActivities { private const int MaxConcurrentTaskCount = 4; - private const int LamportsPerSignature = 5000; private const int LamportsPerRent = 3000000; private const int BlockhashNotFoundErrorCode = -32002; [Activity] - public virtual async Task BuildTransactionAsync(TransactionBuilderRequest request) + public virtual async Task BuildTransactionAsync(TransactionBuilderRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network is null) - { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); - } - - var solverAccount = await networkRepository.GetSolverAccountAsync(network.Name); - - if (solverAccount is null) - { - throw new ArgumentNullException(nameof(network), $"Solver account for {request.NetworkName} not found"); - } - - PrepareTransactionResponse result; + PrepareTransactionDto result; switch (request.Type) { case TransactionType.Transfer: - result = await SolanaTransactionBuilder.BuildTransferTransactionAsync(network, request.Args); + result = await SolanaTransactionBuilder.BuildTransferTransactionAsync(request.Network, request.PrepareArgs); break; case TransactionType.HTLCLock: - result = await SolanaTransactionBuilder.BuildHTLCLockTransactionAsync(network, solverAccount, request.Args); + result = await SolanaTransactionBuilder.BuildHTLCLockTransactionAsync(request.Network, request.FromAddress!, request.PrepareArgs); break; case TransactionType.HTLCRedeem: - result = await SolanaTransactionBuilder.BuildHTLCRedeemTransactionAsync(network, solverAccount, request.Args); + result = await SolanaTransactionBuilder.BuildHTLCRedeemTransactionAsync(request.Network, request.FromAddress!, request.PrepareArgs); break; case TransactionType.HTLCRefund: - result = await SolanaTransactionBuilder.BuildHTLCRefundTransactionAsync(network, solverAccount, request.Args); + result = await SolanaTransactionBuilder.BuildHTLCRefundTransactionAsync(request.Network, request.FromAddress!, request.PrepareArgs); break; case TransactionType.HTLCAddLockSig: - result = await SolanaTransactionBuilder.BuildHTLCAddlockSigTransactionAsync(network, solverAccount, request.Args); + result = await SolanaTransactionBuilder.BuildHTLCAddlockSigTransactionAsync(request.Network, request.FromAddress!, request.PrepareArgs); break; default: - throw new ArgumentOutOfRangeException(nameof(request.Type), $"Not supported transaction type {request.Type} for network {request.NetworkName}"); + throw new ArgumentOutOfRangeException(nameof(request.Type), $"Not supported transaction type {request.Type} for network {request.Network.Name}"); } return result; } - [Activity] - public virtual async Task EstimateFeeAsync(EstimateFeeRequest request) - { - var result = new Dictionary(); - - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network is null) - { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); - } - - var currency = network.Tokens.SingleOrDefault(x => x.Asset == request.Asset); - - if (currency is null) - { - throw new ArgumentNullException(nameof(currency), $"Currency {request.Asset} for {request.NetworkName} is missing"); - } - - var privateKeyResult = await privateKeyProvider.GetAsync(request.FromAddress); - - var node = network.Nodes.FirstOrDefault(); - - if (node is null) - { - throw new ArgumentNullException(nameof(node), $"Node for network: {network.Id} is not configured"); - } - - var rpcClient = ClientFactory.GetClient(node.Url); - - var solanaAccount = new Account(privateKeyResult, request.FromAddress); - - var balanceForRentExemptionResult = await rpcClient.GetMinimumBalanceForRentExemptionAsync(165); - - if (!balanceForRentExemptionResult.WasSuccessful) - { - throw new Exception($"Failed to get minimum balance for rent exemption in network: {request.NetworkName}"); - } - - var builder = new TransactionBuilder() - .SetFeePayer(solanaAccount); - - var signers = new List { solanaAccount }; - - var transaction = Convert.FromBase64String(request.CallData!); - var tx = Solnet.Rpc.Models.Transaction.Deserialize(transaction); - - foreach (var instruction in tx.Instructions) - { - builder.AddInstruction(instruction); - } - - if (tx.FeePayer != solanaAccount) - { - var managedAddressPrivateKey = await privateKeyProvider.GetAsync(tx.FeePayer); - signers.Add(new Account(managedAddressPrivateKey, tx.FeePayer)); - } - - var latestBlockHashResponse = await rpcClient.GetLatestBlockHashAsync(); - - if (!latestBlockHashResponse.WasSuccessful) - { - throw new Exception($"Failed to get latest block hash, error: {latestBlockHashResponse.RawRpcResponse}"); - } - - builder.SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); - - var rawTx = builder.Build(signers); - - var simulatedTransaction = await rpcClient.SimulateTransactionAsync(rawTx); - - if (!simulatedTransaction.WasSuccessful || simulatedTransaction.Result.Value.Error != null) - { - if (!simulatedTransaction.WasSuccessful) - { - throw new Exception($"Failed to simulate transaction in network{request.NetworkName}: Reason {simulatedTransaction.Reason}"); - } - - throw new Exception($"Failed to simulate transaction in network{request.NetworkName}: Error Type {simulatedTransaction.Result.Value.Error.Type}"); - } - - var computeUnitsUsed = SolanaConstants.BaseLimit + TransactionLogExtension.ExtractTotalComputeUnitsUsed(simulatedTransaction.Result.Value.Logs.ToList()); - - var baseFeeInLamports = balanceForRentExemptionResult.Result + ComputeRentFee(request.NetworkName, tx.Instructions) + signers.Count * LamportsPerSignature; - - var nativeCurrency = network.Tokens.SingleOrDefault(x => x.TokenContract is null); - - if (nativeCurrency is null) - { - throw new ArgumentNullException(nameof(nativeCurrency), $"Native currency is not configured on {request.NetworkName} network"); - } - - decimal computeUnitPrice = 0; - - if (!SolanaConstants.HighComputeUnitPrice.TryGetValue(request.NetworkName, out computeUnitPrice)) - { - throw new($"High compute unit price is not configured on {request.NetworkName} network"); - } - - computeUnitsUsed = computeUnitsUsed.PercentageIncrease(200); - - var fee = new Fee( - nativeCurrency.Asset, - nativeCurrency.Decimals, - new SolanaFeeData( - computeUnitPrice.ToString(), - computeUnitsUsed.ToString(), - baseFeeInLamports.ToString())); - - var balance = await GetBalanceAsync(new BalanceRequest - { - NetworkName = request.NetworkName, - Address = request.FromAddress, - Asset = fee.Asset - }); - - var amount = BigInteger.Parse(fee.AmountInWei) + BigInteger.Parse(request.Amount); - - if (BigInteger.Parse(balance.AmountInWei) < amount) - { - throw new Exception($"Insufficient funds in {request.NetworkName}. {request.FromAddress}. Required {amount} {fee.Asset}"); - } - - return fee; - } - [Activity] public virtual async Task GetBalanceAsync(BalanceRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network is null) - { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); - } - - var node = network.Nodes.FirstOrDefault(); + var node = request.Network.Nodes.FirstOrDefault(); if (node is null) { - throw new ArgumentNullException(nameof(node), $"Primary node is not configured on {request.NetworkName} network"); + throw new ArgumentNullException(nameof(node), $"Primary node is not configured on {request.Network.Name} network"); } var rpcClient = ClientFactory.GetClient(node.Url); - var currency = network.Tokens.SingleOrDefault(x => x.Asset.ToUpper() == request.Asset.ToUpper()); + var currency = request.Network.Tokens.SingleOrDefault(x => x.Symbol.ToUpper() == request.Asset.ToUpper()); if (currency is null) { throw new ArgumentNullException(nameof(currency), $"Invalid currency"); @@ -236,9 +87,8 @@ public virtual async Task GetBalanceAsync(BalanceRequest reques try { - if (currency.TokenContract is null) + if (currency.Contract is null) { - var response = await rpcClient.GetBalanceAsync(request.Address); if (!response.WasSuccessful) @@ -250,7 +100,7 @@ public virtual async Task GetBalanceAsync(BalanceRequest reques } else { - var response = await rpcClient.GetTokenAccountsByOwnerAsync(request.Address, currency.TokenContract); + var response = await rpcClient.GetTokenAccountsByOwnerAsync(request.Address, currency.Contract); if (!response.WasSuccessful) { @@ -265,45 +115,40 @@ public virtual async Task GetBalanceAsync(BalanceRequest reques } catch (Exception ex) { - throw new Exception($"Failed to get balance of {currency.Asset} on {request.Address} address in {request.NetworkName} network , message {ex.Message}"); + throw new Exception($"Failed to get balance of {currency.Symbol} on {request.Address} address in {request.Network.Name} network , message {ex.Message}"); } var balanceResponse = new BalanceResponse { - AmountInWei = balance.ToString(), - Decimals = currency.Decimals, + Amount = balance, }; return balanceResponse; } [Activity] - public virtual async Task GetTransactionAsync(GetTransactionRequest request) + public virtual async Task GetTransactionAsync(SolanaGetReceiptRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); + var node = request.Network.Nodes.FirstOrDefault(); - if (network is null) + if (node is null) { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); + throw new($"Primary node is not configured on {request.Network.Name} network"); } - var nodes = network.Nodes; - - if (!nodes.Any()) - { - throw new ArgumentException($"Primary node is not configured on {request.NetworkName} network", nameof(nodes)); - } + var rpcClient = ClientFactory.GetClient(node.Url); - var epochInfoResponse = await GetDataFromNodesAsync(nodes, - async url => await ClientFactory.GetClient(url).GetEpochInfoAsync()); + var epochInfoResponse = await rpcClient.GetEpochInfoAsync(); TransactionResponse transaction; + try { - transaction = await GetDataFromNodesAsync(nodes, - async url => await GetTransactionAsync(request.TransactionHash, network, epochInfoResponse.Result, - ClientFactory.GetClient(url))); - + transaction = await GetTransactionReceiptAsync( + rpcClient, + request.Network, + epochInfoResponse.Result, + request.TxHash); } catch (AggregateException ae) { @@ -314,12 +159,11 @@ public virtual async Task GetTransactionAsync(GetTransactio throw transactionNotConfirmedException; } - var status = await GetDataFromNodesAsync(nodes, - async url => await ClientFactory.GetClient(url).GetSignatureStatusAsync(request.TransactionHash)); + var status = await rpcClient.GetSignatureStatusAsync(request.TxHash); if (status.Result.Value.FirstOrDefault() != null) { - throw new TransactionNotComfirmedException($"Transaction is not confirmed yet, TxHash: {request.TransactionHash}."); + throw new TransactionNotComfirmedException($"Transaction is not confirmed yet, TxHash: {request.TxHash}."); } throw; @@ -330,31 +174,19 @@ public virtual async Task GetTransactionAsync(GetTransactio throw new TransactionFailedException("Transaction failed"); } + await CheckBlockHeightAsync(rpcClient, request.TransactionBlockHeight); + return transaction; } [Activity] public virtual async Task GetEventsAsync(EventRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network is null) - { - throw new ArgumentNullException(nameof(network), $"Chain for network: {request.NetworkName} is not configured"); - } - - var node = network!.Nodes.FirstOrDefault(); + var node = request.Network.Nodes.FirstOrDefault(); if (node is null) { - throw new ArgumentNullException(nameof(node), $"Node for network: {request.NetworkName} is not configured"); - } - - var solverAccount = await networkRepository.GetSolverAccountAsync(network.Name); - - if (solverAccount is null) - { - throw new ArgumentNullException(nameof(network), $"Solver account for {request.NetworkName} not found"); + throw new ArgumentNullException(nameof(node), $"Node for network: {request.Network.Name} is not configured"); } var rpcClient = ClientFactory.GetClient(node.Url); @@ -363,18 +195,15 @@ public virtual async Task GetEventsAsync(EventRequest re var blocksForProcessing = Enumerable.Range((int)request.FromBlock, (int)(request.ToBlock - request.FromBlock) + 1).ToArray(); var events = new HTLCBlockEventResponse(); - var currencies = await networkRepository.GetTokensAsync(); - foreach (var blockChunk in blocksForProcessing.Chunk(MaxConcurrentTaskCount)) { foreach (var currentBlock in blockChunk) { blockProcessingTasks[currentBlock] = EventDecoder.GetBlockEventsAsync( rpcClient, - currentBlock, - network, - currencies, - solverAccount); + request.Network, + request.WalletAddresses, + currentBlock); } await Task.WhenAll(blockProcessingTasks.Values); @@ -394,18 +223,11 @@ public virtual async Task GetEventsAsync(EventRequest re [Activity] public virtual async Task GetLastConfirmedBlockNumberAsync(BaseRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network is null) - { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); - } - - var node = network.Nodes.FirstOrDefault(); + var node = request.Network.Nodes.FirstOrDefault(); if (node is null) { - throw new ArgumentNullException(nameof(node), $"Node for network: {request.NetworkName} is not configured"); + throw new ArgumentNullException(nameof(node), $"Node for network: {request.Network.Name} is not configured"); } var rpcClient = ClientFactory.GetClient(node.Url); @@ -433,56 +255,21 @@ public virtual async Task GetLastConfirmedBlockNumberAsync( }; } - [Activity] - public virtual async Task GetNextNonceAsync(NextNonceRequest request) - { - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network == null) - { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); - } - - var node = network.Nodes.FirstOrDefault(); - - if (node is null) - { - throw new ArgumentNullException(nameof(node), $"Node for network: {network.Name} is not configured"); - } - - var rpcClient = ClientFactory.GetClient(node.Url); - - var latestBlockHashResponse = await ClientFactory - .GetClient(node.Url) - .GetLatestBlockHashAsync(); - - if (!latestBlockHashResponse.WasSuccessful) - { - throw new Exception($"Failed to get latest block hash, error: {latestBlockHashResponse.RawRpcResponse}"); - } - - await cache.StringSetAsync(RedisHelper.BuildNonceKey(request.NetworkName, request.Address), - latestBlockHashResponse.Result.Value.LastValidBlockHeight, - expiry: TimeSpan.FromDays(7)); - - return latestBlockHashResponse.Result.Value.Blockhash; - } - [Activity] public async Task ValidateAddLockSignatureAsync(AddLockSignatureRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); + var network = await networkRepository.GetAsync(request.Network.Name); if (network is null) { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); + throw new ArgumentNullException(nameof(network), $"Network {request.Network.Name} not found"); } var currency = network.Tokens.Single(x => x.Asset.ToUpper() == request.Asset.ToUpper()); if (currency is null) { - throw new ArgumentNullException(nameof(currency), $"Currency {request.Asset} for {request.NetworkName} is missing"); + throw new ArgumentNullException(nameof(currency), $"Currency {request.Asset} for {request.Network.Name} is missing"); } if (request.Signature is null) @@ -493,7 +280,7 @@ public async Task ValidateAddLockSignatureAsync(AddLockSignatureRequest re var message = Ed25519Program.CreateAddLockSigMessage(new() { Hashlock = request.Hashlock.HexToByteArray(), - Id = request.Id.HexToByteArray(), + Id = request.CommitId.HexToByteArray(), Timelock = request.Timelock, SignerPublicKey = new PublicKey(request.SignerAddress), }); @@ -512,18 +299,11 @@ public async Task ValidateAddLockSignatureAsync(AddLockSignatureRequest re [Activity] public async Task SimulateTransactionAsync(SolanaPublishTransactionRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); - - if (network == null) - { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); - } - - var node = network.Nodes.FirstOrDefault(); + var node = request.Network.Nodes.FirstOrDefault(); if (node is null) { - throw new ArgumentNullException(nameof(node), $"Node for network: {network.Name} is not configured"); + throw new ArgumentNullException(nameof(node), $"Node for network: {request.Network.Name} is not configured"); } var rpcClient = ClientFactory.GetClient(node.Url); @@ -534,21 +314,21 @@ public async Task SimulateTransactionAsync(SolanaPublishTransactionRequest reque { if (!simulatedTransaction.WasSuccessful) { - throw new Exception($"Failed to simulate transaction in network {network.Name}: Reason {simulatedTransaction.Reason}"); + throw new Exception($"Failed to simulate transaction in network {request.Network.Name}: Reason {simulatedTransaction.Reason}"); } if (simulatedTransaction.Result.Value.Error.Type == TransactionErrorType.BlockhashNotFound) { throw new NonceMissMatchException( - $"Nonce mismatch error Failed to simulate transaction in network {network.Name}: Error Type {simulatedTransaction.Result.Value.Error.Type}"); + $"Nonce mismatch error Failed to simulate transaction in network {request.Network.Name}: Error Type {simulatedTransaction.Result.Value.Error.Type}"); } - throw new Exception($"Failed to simulate transaction in network {network.Name}: Error Type {simulatedTransaction.Result.Value.Error.Type}"); + throw new Exception($"Failed to simulate transaction in network {request.Network.Name}: Error Type {simulatedTransaction.Result.Value.Error.Type}"); } } [Activity] - public async Task ComposeSolanaTranscationAsync(SolanaComposeTransactionRequest request) + public async Task ComposeSolanaTranscationAsync(SolanaComposeTransactionRequest request) { var solanaAddress = new PublicKey(request.FromAddress); @@ -563,7 +343,7 @@ public async Task ComposeSolanaTranscationAsync(SolanaComposeTransaction } var transactionBytes = Convert.FromBase64String(request.CallData); - var tx = Solnet.Rpc.Models.Transaction.Deserialize(transactionBytes); + var tx = Transaction.Deserialize(transactionBytes); foreach (var instruction in tx.Instructions) { builder.AddInstruction(instruction); @@ -574,29 +354,53 @@ public async Task ComposeSolanaTranscationAsync(SolanaComposeTransaction signers.Add(tx.FeePayer); } + if (!SolanaConstants.MediumComputeUnitPrice.TryGetValue(request.Network.Name, out var computeUnitPrice)) + { + throw new($"Compute unit is not configured for netwokr {request.Network.Name}"); + } + + var node = request.Network.Nodes.FirstOrDefault(); + + if (node is null) + { + throw new ArgumentNullException(nameof(node), $"Node for network: {request.Network.Name} is not configured"); + } + + var rpcClient = ClientFactory.GetClient(node.Url); + + var latestBlockHashResponse = await rpcClient.GetLatestBlockHashAsync(); + + if (!latestBlockHashResponse.WasSuccessful) + { + throw new Exception($"Failed to get latest block hash, error: {latestBlockHashResponse.RawRpcResponse}"); + } + builder - .AddInstruction(ComputeBudgetProgram.SetComputeUnitLimit(uint.Parse(request.Fee.SolanaFeeData!.ComputeUnitLimit))) - .AddInstruction(ComputeBudgetProgram.SetComputeUnitPrice((ulong)Web3.Convert.ToWei(decimal.Parse(request.Fee.SolanaFeeData.ComputeUnitPrice), SolanaConstants.MicroLamportsDecimal))) - .SetRecentBlockHash(request.LastValidBlockHash); + .AddInstruction(ComputeBudgetProgram.SetComputeUnitPrice((ulong)Web3.Convert.ToWei(computeUnitPrice, SolanaConstants.MicroLamportsDecimal))) + .SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); - var rawTxResult = await SignSolanaTransactionAsync(builder, signers); + var rawTxResult = Convert.ToBase64String(builder.Serialize()); - return rawTxResult; + return new() + { + LastValidBlockHeight = latestBlockHashResponse.Result.Value.LastValidBlockHeight.ToString(), + RawTx = rawTxResult + }; } [Activity] public async Task PublishTransactionAsync(SolanaPublishTransactionRequest request) { - var network = await networkRepository.GetAsync(request.NetworkName); + var network = await networkRepository.GetAsync(request.Network.Name); if (network is null) { - throw new ArgumentNullException(nameof(network), $"Network {request.NetworkName} not found"); + throw new ArgumentNullException(nameof(network), $"Network {request.Network.Name} not found"); } if (network == null) { - throw new($"Network {request.NetworkName} not found"); + throw new($"Network {request.Network.Name} not found"); } var node = network.Nodes.FirstOrDefault(); @@ -608,9 +412,11 @@ public async Task PublishTransactionAsync(SolanaPublishTransactionReques var rpcClient = ClientFactory.GetClient(node.Url); + var signedRawData = Convert.FromBase64String(request.RawTx); + try { - var transactionResult = await rpcClient.SendSolanaTransactionAsync(request.RawTx); + var transactionResult = await rpcClient.SendSolanaTransactionAsync(signedRawData); if (!transactionResult.WasSuccessful) { @@ -631,47 +437,30 @@ public async Task PublishTransactionAsync(SolanaPublishTransactionReques { } - return CalculateTransactionHash(request.RawTx); + return CalculateTransactionHash(signedRawData); } private static bool ValidateAddress(string address) => PublicKey.IsValid(address); - private async Task SignSolanaTransactionAsync( - TransactionBuilder builder, - List managedAddresses) - { - var signers = new List(); + //private static BigInteger ComputeRentFee( + // string networkName, + // List instructions) + //{ + // int accountCreationCount = 0; - foreach (var address in managedAddresses) - { - var privateKeyResult = await privateKeyProvider.GetAsync(address); + // foreach (var instruction in instructions) + // { + // var lockDescriminator = FieldEncoder.Sighash(SolanaConstants.LockSighash); - var solanaAccount = new Account(privateKeyResult, address); - signers.Add(solanaAccount); - } + // if (instruction.Data.Take(8).SequenceEqual(lockDescriminator)) + // { + // accountCreationCount++; + // } + // } - return builder.Build(signers); - } - - private static BigInteger ComputeRentFee( - string networkName, - List instructions) - { - int accountCreationCount = 0; - - foreach (var instruction in instructions) - { - var lockDescriminator = FieldEncoder.Sighash(SolanaConstants.LockSighash); - - if (instruction.Data.Take(8).SequenceEqual(lockDescriminator)) - { - accountCreationCount++; - } - } - - return accountCreationCount * LamportsPerRent; - } + // return accountCreationCount * LamportsPerRent; + //} private static string CalculateTransactionHash(byte[] rawTransactionBytes) { @@ -685,45 +474,34 @@ private static string CalculateTransactionHash(byte[] rawTransactionBytes) } private async Task CheckBlockHeightAsync( - Network network, - string fromAddress) + IRpcClient rpcClient, + string lastValidBlockHeight) { - var primaryNode = network.Nodes.FirstOrDefault(); - - if (primaryNode is null) - { - throw new ArgumentNullException(nameof(primaryNode), $"Primary node is not configured on {network.Name} network"); - } - - var primaryRpcClient = ClientFactory.GetClient(primaryNode.Url); - - var epochInfoResponseResult = await primaryRpcClient.GetEpochInfoAsync(); + var epochInfoResponseResult = await rpcClient.GetEpochInfoAsync(); if (!epochInfoResponseResult.WasSuccessful) { - throw new Exception($"Failed to get latestBlock for {network.Name} network"); + throw new Exception($"Failed to get latestBlock"); } - if (!string.IsNullOrEmpty(fromAddress)) + if (ulong.Parse(lastValidBlockHeight) <= epochInfoResponseResult.Result.BlockHeight) { - var lastValidBlockHeight = await cache.StringGetAsync( - RedisHelper.BuildNonceKey(network.Name, fromAddress)); - - if (lastValidBlockHeight.HasValue && ulong.Parse(lastValidBlockHeight.ToString()) <= epochInfoResponseResult.Result.BlockHeight) - { - throw new TransactionFailedRetriableException("Transaction not found"); - } + throw new TransactionFailedRetriableException("Transaction not found"); } } - private static async Task GetTransactionAsync( - string transactionId, - Network network, + private static async Task GetTransactionReceiptAsync( + IRpcClient rpcClient, + DetailedNetworkDto network, EpochInfo epochInfo, - IRpcClient rpcClient) + string transactionId) { - var feeCurrency = network.Tokens - .Single(x => string.IsNullOrEmpty(x.TokenContract)); + var feeCurrency = network.NativeToken; + + if (feeCurrency == null) + { + throw new($"Fee currency not cinfigured for network {network.Name}"); + } var transactionReceiptResult = await rpcClient.GetParsedTransactionAsync(transactionId); @@ -737,18 +515,31 @@ private static async Task GetTransactionAsync( var result = new TransactionResponse { TransactionHash = transactionId, - FeeAmount = transactionReceiptResult.Result.Meta.Fee.ToString(), - FeeAsset = feeCurrency.Asset, + FeeAmount = transactionReceiptResult.Result.Meta.Fee, + FeeAsset = feeCurrency.Symbol, FeeDecimals = feeCurrency.Decimals, Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(transactionReceiptResult.Result.BlockTime * 1000), Status = transactionReceiptResult.Result.Meta.Err is null ? TransactionStatus.Completed : TransactionStatus.Failed, Confirmations = confirmations, NetworkName = network.Name, - Decimals = feeCurrency.Decimals, }; return result; } - private static string FormatAddress(string address) => address; + public async Task SignTransactionAsync(SolanaSignTransactionRequest request) + { + var signedTransaction = await privateKeyProvider.SignAsync( + request.SignerAgentUrl, + request.Network.Type, + request.FromAddress, + request.UnsignRawTransaction); + + if (string.IsNullOrEmpty(signedTransaction)) + { + throw new Exception($"Failed to sign transaction for {request.FromAddress} on network {request.Network.Name}. RawTx {request.UnsignRawTransaction}"); + } + + return signedTransaction; + } } diff --git a/csharp/src/Workflow.Solana/Extensions/TrainSolverBuilderExtensions.cs b/csharp/src/Workflow.Solana/Extensions/TrainSolverBuilderExtensions.cs index da873058..24c35ecf 100644 --- a/csharp/src/Workflow.Solana/Extensions/TrainSolverBuilderExtensions.cs +++ b/csharp/src/Workflow.Solana/Extensions/TrainSolverBuilderExtensions.cs @@ -1,24 +1,19 @@ using Temporalio.Extensions.Hosting; -using Train.Solver.Blockchain.Common.Activities; -using Train.Solver.Blockchain.Common.Worklows; -using Train.Solver.Blockchain.Solana.Activities; -using Train.Solver.Blockchain.Solana.Workflows; -using Train.Solver.Data.Abstractions.Entities; using Train.Solver.Infrastructure.DependencyInjection; +using Train.Solver.Workflow.Solana.Activities; +using Train.Solver.Workflow.Solana.Workflows; -namespace Train.Solver.Blockchain.Solana.Extensions; +namespace Train.Solver.Workflow.Solana.Extensions; public static class TrainSolverBuilderExtensions { public static TrainSolverBuilder WithSolanaWorkflows( this TrainSolverBuilder builder) { - var temporalBuilder = builder.Services - .AddHostedTemporalWorker(nameof(NetworkType.Solana)) + builder.Services + .AddHostedTemporalWorker(builder.Options.NetworkType) .AddWorkflow() - .AddWorkflow() - .AddTransientActivities() - .AddTransientActivities(); + .AddTransientActivities(); return builder; } diff --git a/csharp/src/Workflow.Solana/Helpers/EventDecoder.cs b/csharp/src/Workflow.Solana/Helpers/EventDecoder.cs index 4df25475..ef91c6db 100644 --- a/csharp/src/Workflow.Solana/Helpers/EventDecoder.cs +++ b/csharp/src/Workflow.Solana/Helpers/EventDecoder.cs @@ -1,15 +1,13 @@ -using System.Numerics; -using Nethereum.Hex.HexConvertors.Extensions; -using Nethereum.Web3; +using Nethereum.Hex.HexConvertors.Extensions; using Solnet.Rpc; using Solnet.Rpc.Builders; -using Solnet.Rpc.Models; using Solnet.Wallet; -using Train.Solver.Blockchain.Abstractions.Models; +using System.Numerics; using Train.Solver.Blockchain.Solana.Extensions; using Train.Solver.Blockchain.Solana.Models; using Train.Solver.Blockchain.Solana.Programs.HTLCProgram; -using Train.Solver.Data.Abstractions.Entities; +using Train.Solver.Infrastructure.Abstractions.Models; +using Train.Solver.Workflow.Abstractions.Models; namespace Train.Solver.Blockchain.Solana.Helpers; @@ -17,10 +15,9 @@ public static class EventDecoder { public static async Task GetBlockEventsAsync( IRpcClient rpcClient, - int block, - Network network, - List currencies, - string solverAccount) + DetailedNetworkDto network, + string[] solverAccounts, + int block) { var blockResponseResult = await rpcClient.GetParsedEventBlockAsync(block); @@ -31,6 +28,8 @@ public static async Task GetBlockEventsAsync( var result = new HTLCBlockEventResponse(); + var currencies = network.Tokens; + if (blockResponseResult.Result != null && blockResponseResult.Result.Transactions.Any()) { var htlcTokenContractAddress = network.HTLCTokenContractAddress; @@ -48,6 +47,8 @@ public static async Task GetBlockEventsAsync( var isLockEvent = transaction.Meta.LogMessages .Any(x => x.Contains(SolanaConstants.HtlcConstants.addLockEventPrefixPattern)); + var accountForSimulation = solverAccounts.First(); + if (isCommitEvent) { var prefixPattern = "Program return: " + htlcTokenContractAddress + " "; @@ -61,23 +62,25 @@ public static async Task GetBlockEventsAsync( var commitEvent = await DeserializeCommitEventDataAsync( rpcClient, - id, network, - solverAccount); + id, + accountForSimulation); if (commitEvent == null) { continue; } - if (commitEvent.ReceiverAddress != solverAccount) + var receiverAddress = commitEvent.ReceiverAddress; + + if (!solverAccounts.Contains(receiverAddress)) { continue; } var destinationCurrency = currencies - .FirstOrDefault(x => x.Asset == commitEvent.DestinationAsset - && x.Network.Name == commitEvent.DestinationNetwork); + .FirstOrDefault(x => x.Symbol == commitEvent.DestinationAsset + && network.Name == commitEvent.DestinationNetwork); if (destinationCurrency is null) { @@ -85,7 +88,7 @@ public static async Task GetBlockEventsAsync( } var sourceCurrency = network.Tokens - .FirstOrDefault(x => x.Asset == commitEvent.SourceAsset); + .FirstOrDefault(x => x.Symbol == commitEvent.SourceAsset); if (sourceCurrency is null) { @@ -95,9 +98,9 @@ public static async Task GetBlockEventsAsync( var commitEventMessage = new HTLCCommitEventMessage { TxId = transaction.Transaction.Signatures.First(), - Id = commitEvent.Id, - AmountInWei = commitEvent.AmountInWei, - ReceiverAddress = solverAccount, + CommitId = commitEvent.Id, + Amount = BigInteger.Parse(commitEvent.AmountInWei), + ReceiverAddress = receiverAddress, SourceNetwork = network.Name, SourceAsset = commitEvent.SourceAsset, DestinationAddress = commitEvent.DestinationAddress, @@ -105,8 +108,6 @@ public static async Task GetBlockEventsAsync( DestinationAsset = commitEvent.DestinationAsset, SenderAddress = commitEvent.SenderAddress, TimeLock = commitEvent.TimeLock, - DestinationNetworkType = destinationCurrency.Network.Type, - SourceNetworkType = sourceCurrency.Network.Type }; result.HTLCCommitEventMessages.Add(commitEventMessage); @@ -125,9 +126,9 @@ public static async Task GetBlockEventsAsync( var addLockMessageResult = await DeserializeAddLockEventDataAsync( rpcClient, - id, network, - solverAccount); + id, + accountForSimulation); if (addLockMessageResult == null) { @@ -146,8 +147,8 @@ public static async Task GetBlockEventsAsync( private static async Task DeserializeCommitEventDataAsync( IRpcClient rpcClient, + DetailedNetworkDto network, string commitId, - Network network, string solverAccount) { if (solverAccount is null) @@ -194,13 +195,12 @@ private static async Task DeserializeCommitEventData response.Id = commitId; return response; - } private static async Task DeserializeAddLockEventDataAsync( IRpcClient rpcClient, - string id, - Network network, + DetailedNetworkDto network, + string commitId, string solverAccount) { try @@ -218,7 +218,7 @@ private static async Task DeserializeCommitEventData builder.SetGetDetailsInstruction( new PublicKey(htlcContractAddress), - id.HexToByteArray()); + commitId.HexToByteArray()); var latestBlockHashResponse = await rpcClient.GetLatestBlockHashAsync(); @@ -248,7 +248,7 @@ private static async Task DeserializeCommitEventData var response = new HTLCLockEventMessage(); - response.Id = id; + response.CommitId = commitId; response.HashLock = ExtractString( simulatedTransaction.Result.Value.Logs.FirstOrDefault(x => diff --git a/csharp/src/Workflow.Solana/Helpers/SolanaTransactionBuilder.cs b/csharp/src/Workflow.Solana/Helpers/SolanaTransactionBuilder.cs index bf6628e6..85b4c84f 100644 --- a/csharp/src/Workflow.Solana/Helpers/SolanaTransactionBuilder.cs +++ b/csharp/src/Workflow.Solana/Helpers/SolanaTransactionBuilder.cs @@ -1,6 +1,4 @@ -using System.Numerics; -using System.Text.Json; -using Nethereum.Hex.HexConvertors.Extensions; +using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Hex.HexTypes; using Nethereum.Web3; using Solnet.Extensions; @@ -9,17 +7,20 @@ using Solnet.Rpc; using Solnet.Rpc.Builders; using Solnet.Wallet; -using Train.Solver.Blockchain.Abstractions.Models; -using Train.Solver.Data.Abstractions.Entities; +using System.Numerics; +using System.Text.Json; using Train.Solver.Blockchain.Solana.Programs.HTLCProgram; using Train.Solver.Blockchain.Solana.Programs.HTLCProgram.Models; +using Train.Solver.Data.Abstractions.Entities; +using Train.Solver.Infrastructure.Abstractions.Models; +using Train.Solver.Workflow.Abstractions.Models; -namespace Train.Solver.Blockchain.Solana.Helpers; +namespace Train.Solver.Workflow.Solana.Helpers; public static class SolanaTransactionBuilder { - public static async Task BuildHTLCLockTransactionAsync( - Network network, + public static async Task BuildHTLCLockTransactionAsync( + DetailedNetworkDto network, string solverAccount, string args) { @@ -30,7 +31,7 @@ public static async Task BuildHTLCLockTransactionAsy throw new Exception($"Occured exception during deserializing {args}"); } - var currency = network.Tokens.SingleOrDefault(x => x.Asset.ToUpper() == request.SourceAsset.ToUpper()); + var currency = network.Tokens.SingleOrDefault(x => x.Symbol.ToUpper() == request.SourceAsset.ToUpper()); if (currency is null) { @@ -38,20 +39,23 @@ public static async Task BuildHTLCLockTransactionAsy $"Currency {request.SourceAsset} for {network.Name} is missing"); } - var isNative = currency.Id == network.NativeTokenId; + var account = new Account(); + + var isNative = currency.Symbol.ToUpper() == network.NativeToken!.Symbol.ToUpper(); + var node = network.Nodes.FirstOrDefault(); if (node is null) { - throw new ArgumentNullException(nameof(node), $"Node is not configured on {network.Name} network"); + throw new($"Node is not configured on {network.Name} network"); } + var rpcClient = ClientFactory.GetClient(node.Url); + var htlcContractAddress = isNative ? network.HTLCNativeContractAddress : network.HTLCTokenContractAddress; - var rpcClient = ClientFactory.GetClient(node.Url); - var builder = new TransactionBuilder() .SetFeePayer(new PublicKey(solverAccount)); @@ -67,50 +71,49 @@ await GetOrCreateAssociatedTokenAccount( new HTLCLockRequest { Hashlock = request.Hashlock.HexToByteArray(), - Id = request.Id.HexToByteArray(), + Id = request.CommitId.HexToByteArray(), SignerPublicKey = new PublicKey(solverAccount), ReceiverPublicKey = new PublicKey(request.Receiver), - Amount = BigInteger.Parse(request.Amount), + Amount = request.Amount, Timelock = new BigInteger(request.Timelock), - SourceAsset = currency.Asset, + SourceAsset = currency.Symbol, DestinationNetwork = request.DestinationNetwork, SourceAddress = request.DestinationAddress, DestinationAsset = request.DestinationAsset, - SourceTokenPublicKey = new PublicKey(currency.TokenContract), - Reward = BigInteger.Parse(request.Reward), + SourceTokenPublicKey = new PublicKey(currency.Contract), + Reward = request.Reward, RewardTimelock = new BigInteger(request.RewardTimelock), }); - var latestBlockHashResponse = await rpcClient.GetLatestBlockHashAsync(); + var latestBlockResult = await rpcClient.GetLatestBlockHashAsync(); - if (!latestBlockHashResponse.WasSuccessful) + if (!latestBlockResult.WasSuccessful) { - throw new Exception($"Failed to get latest block hash, error: {latestBlockHashResponse.RawRpcResponse}"); + throw new ($"Failed to get last valid block"); } - builder.SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); + builder.SetRecentBlockHash(latestBlockResult.Result.Value.Blockhash); var serializedTx = Convert.ToBase64String(builder.Serialize()); - var response = new PrepareTransactionResponse + + var response = new PrepareTransactionDto { Data = serializedTx, ToAddress = htlcContractAddress, - Asset = network.NativeToken.Asset, - AmountInWei = "0", - CallDataAmountInWei = request.Amount, - CallDataAsset = currency.Asset, + Asset = network.NativeToken.Symbol, + Amount = 0 }; if (isNative) { - response.AmountInWei = request.Amount; + response.Amount = request.Amount; } return response; } - public static async Task BuildHTLCRedeemTransactionAsync( - Network network, + public static async Task BuildHTLCRedeemTransactionAsync( + DetailedNetworkDto network, string solverAccount, string args) { @@ -131,7 +134,7 @@ public static async Task BuildHTLCRedeemTransactionA throw new ArgumentNullException(nameof(request.SenderAddress), "Sender address is required"); } - var currency = network.Tokens.SingleOrDefault(x => x.Asset.ToUpper() == request.Asset.ToUpper()); + var currency = network.Tokens.SingleOrDefault(x => x.Symbol.ToUpper() == request.Asset.ToUpper()); if (currency is null) { @@ -139,37 +142,38 @@ public static async Task BuildHTLCRedeemTransactionA $"Currency {request.Asset} for {network.Name} is missing"); } - var isNative = currency.Id == network.NativeTokenId; + var isNative = currency.Symbol.ToUpper() == network.NativeToken!.Symbol.ToUpper(); + var node = network.Nodes.FirstOrDefault(); if (node is null) { - throw new ArgumentNullException(nameof(node), $"Node is not configured on {network.Name} network"); + throw new($"Node is not configured on {network.Name} network"); } + var rpcClient = ClientFactory.GetClient(node.Url); + var htlcContractAddress = isNative ? network.HTLCNativeContractAddress : network.HTLCTokenContractAddress; - var rpcClient = ClientFactory.GetClient(node.Url); - var builder = new TransactionBuilder() .SetFeePayer(new PublicKey(solverAccount)); - await GetOrCreateAssociatedTokenAccount( + await GetOrCreateAssociatedTokenAccount( rpcClient, builder, currency, - new PublicKey(request.DestinationAddress), + new PublicKey(solverAccount), new PublicKey(solverAccount)); builder.SetRedeemTransactionInstruction( new PublicKey(htlcContractAddress), new HTLCRedeemRequest { - Id = request.Id.HexToByteArray(), + Id = request.CommitId.HexToByteArray(), Secret = BigInteger.Parse(request.Secret).ToHexBigInteger().HexValue.HexToByteArray(), - SourceTokenPublicKey = new PublicKey(currency.TokenContract), + SourceTokenPublicKey = new PublicKey(currency.Contract), SignerPublicKey = new PublicKey(solverAccount), ReceiverPublicKey = new PublicKey(request.DestinationAddress), SenderPublicKey = new PublicKey(request.SenderAddress), @@ -178,31 +182,29 @@ await GetOrCreateAssociatedTokenAccount( new PublicKey(request.SenderAddress), }); - var latestBlockHashResponse = await rpcClient.GetLatestBlockHashAsync(); + var latestBlockResult = await rpcClient.GetLatestBlockHashAsync(); - if (!latestBlockHashResponse.WasSuccessful) + if (!latestBlockResult.WasSuccessful) { - throw new Exception($"Failed to get latest block hash, error: {latestBlockHashResponse.RawRpcResponse}"); + throw new($"Failed to get last valid block"); } - builder.SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); + builder.SetRecentBlockHash(latestBlockResult.Result.Value.Blockhash); var serializedTx = Convert.ToBase64String(builder.Serialize()); - var response = new PrepareTransactionResponse + var response = new PrepareTransactionDto { Data = serializedTx, ToAddress = htlcContractAddress, - Asset = network.NativeToken.Asset, - AmountInWei = "0", - CallDataAsset = currency.Asset, - CallDataAmountInWei = "0", + Asset = network.NativeToken.Symbol, + Amount = 0, }; return response; } - public static async Task BuildHTLCRefundTransactionAsync( - Network network, + public static async Task BuildHTLCRefundTransactionAsync( + DetailedNetworkDto network, string solverAccount, string args) { @@ -218,14 +220,15 @@ public static async Task BuildHTLCRefundTransactionA throw new ArgumentNullException(nameof(request.DestinationAddress), "Receiver address is required"); } - var currency = network.Tokens.SingleOrDefault(x => x.Asset.ToUpper() == request.Asset.ToUpper()); + var currency = network.Tokens.SingleOrDefault(x => x.Symbol.ToUpper() == request.Asset.ToUpper()); if (currency is null) { throw new ArgumentNullException(nameof(currency), "Currency {request.Asset} for {network.Name} is missing"); } - var isNative = currency.Id == network.NativeTokenId; + var isNative = currency.Symbol.ToUpper() == network.NativeToken!.Symbol.ToUpper(); + var node = network.Nodes.FirstOrDefault(); if (node is null) @@ -253,8 +256,8 @@ await GetOrCreateAssociatedTokenAccount( new PublicKey(htlcContractAddress), new HTLCRefundRequest { - Id = request.Id.HexToByteArray(), - SourceTokenPublicKey = new PublicKey(currency.TokenContract), + Id = request.CommitId.HexToByteArray(), + SourceTokenPublicKey = new PublicKey(currency.Contract), SignerPublicKey = new PublicKey(solverAccount), ReceiverPublicKey = new PublicKey(request.DestinationAddress) }); @@ -269,22 +272,21 @@ await GetOrCreateAssociatedTokenAccount( builder.SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); var serializedTx = Convert.ToBase64String(builder.Serialize()); - var response = new PrepareTransactionResponse + var response = new PrepareTransactionDto { Data = serializedTx, ToAddress = htlcContractAddress, - Asset = network.NativeToken.Asset, - AmountInWei = "0", - CallDataAsset = currency.Asset, - CallDataAmountInWei = "0", + Asset = network.NativeToken.Symbol, + Amount = 0, }; return response; } - public static async Task BuildTransferTransactionAsync(Network network, string args) + public static async Task BuildTransferTransactionAsync( + DetailedNetworkDto network, + string args) { - var request = JsonSerializer.Deserialize(args); if (request is null) @@ -299,7 +301,7 @@ public static async Task BuildTransferTransactionAsy throw new ArgumentNullException(nameof(node), $"Node is not configured on {network.Name} network"); } - var currency = network.Tokens.SingleOrDefault(x => x.Asset == request.Asset); + var currency = network.Tokens.SingleOrDefault(x => x.Symbol == request.Asset); if (currency is null) { @@ -335,28 +337,21 @@ public static async Task BuildTransferTransactionAsy builder.SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); - if (request.Memo != null) - { - builder.AddInstruction(MemoProgram.NewMemo(publicKeyFromAddress, request.Memo)); - } - var serializedTx = Convert.ToBase64String(builder.Serialize()); - var response = new PrepareTransactionResponse + var response = new PrepareTransactionDto { Data = serializedTx, ToAddress = request.ToAddress, Asset = request.Asset, - AmountInWei = amountInBaseUnits.ToString(), - CallDataAmountInWei = amountInBaseUnits.ToString(), - CallDataAsset = currency.Asset, + Amount = request.Amount, }; return response; } - public static async Task BuildHTLCAddlockSigTransactionAsync( - Network network, + public static async Task BuildHTLCAddlockSigTransactionAsync( + DetailedNetworkDto network, string solverAccount, string args) { @@ -377,7 +372,7 @@ public static async Task BuildHTLCAddlockSigTransact throw new ArgumentNullException(nameof(request.SignerAddress), "Sender address is required"); } - var currency = network.Tokens.SingleOrDefault(x => x.Asset.ToUpper() == request.Asset.ToUpper()); + var currency = network.Tokens.SingleOrDefault(x => x.Symbol.ToUpper() == request.Asset.ToUpper()); if (currency is null) { @@ -385,7 +380,7 @@ public static async Task BuildHTLCAddlockSigTransact $"Currency {request.Asset} for {network.Name} is missing"); } - var isNative = currency.Id == network.NativeTokenId; + var isNative = currency.Symbol.ToUpper() == network.NativeToken!.Symbol.ToUpper(); var node = network.Nodes.FirstOrDefault(); if (node is null) @@ -408,7 +403,7 @@ public static async Task BuildHTLCAddlockSigTransact { AddLockSigMessageRequest = new() { - Id = request.Id.HexToByteArray(), + Id = request.CommitId.HexToByteArray(), Hashlock = request.Hashlock.HexToByteArray(), Timelock = request.Timelock, SignerPublicKey = new PublicKey(request.SignerAddress), @@ -427,14 +422,12 @@ public static async Task BuildHTLCAddlockSigTransact builder.SetRecentBlockHash(latestBlockHashResponse.Result.Value.Blockhash); var serializedTx = Convert.ToBase64String(builder.Serialize()); - var response = new PrepareTransactionResponse + var response = new PrepareTransactionDto { Data = serializedTx, ToAddress = htlcContractAddress, - Asset = network.NativeToken.Asset, - AmountInWei = "0", - CallDataAsset = currency.Asset, - CallDataAmountInWei = "0", + Asset = network.NativeToken.Symbol, + Amount = 0, }; return response; @@ -442,7 +435,7 @@ public static async Task BuildHTLCAddlockSigTransact public static async Task CreateTransactionInstructionAsync( this TransactionBuilder builder, - Token currency, + TokenDto currency, IRpcClient rpcClient, PublicKey publicKeyFromAddress, string toAddress, @@ -451,7 +444,7 @@ public static async Task CreateTransactionInstructionAsync( { var publicKeyToAddress = new PublicKey(toAddress); - if (string.IsNullOrEmpty(currency.TokenContract)) + if (string.IsNullOrEmpty(currency.Contract)) { //SolanaTransactionProcessorWorkflow transfer builder.AddInstruction( @@ -466,9 +459,9 @@ public static async Task CreateTransactionInstructionAsync( { //SPL token transfer var token = new TokenDef( - currency.TokenContract, - currency.Asset, - currency.Asset, + currency.Contract, + currency.Symbol, + currency.Symbol, currency.Decimals); var tokenDefs = new TokenMintResolver(); @@ -483,18 +476,18 @@ public static async Task CreateTransactionInstructionAsync( { destination = destinationWallet.JitCreateAssociatedTokenAccount( builder, - currency.TokenContract, + currency.Contract, publicKeyFromAddress); } else { destination = AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(publicKeyToAddress, - new PublicKey(currency.TokenContract)); + new PublicKey(currency.Contract)); } var source = sourceWallet.JitCreateAssociatedTokenAccount( builder, - currency.TokenContract, + currency.Contract, publicKeyFromAddress); builder.AddInstruction( @@ -513,19 +506,19 @@ public static async Task CreateTransactionInstructionAsync( return builder; } - public async static Task GetOrCreateAssociatedTokenAccount( + private static async Task GetOrCreateAssociatedTokenAccount( IRpcClient rpcClient, TransactionBuilder builder, - Token currency, + TokenDto currency, PublicKey ownerPublicKey, PublicKey feePayerPublicKey) { try { var token = new TokenDef( - currency.TokenContract, - currency.Asset, - currency.Asset, + currency.Contract, + currency.Symbol, + currency.Symbol, currency.Decimals); var tokenDefs = new TokenMintResolver(); @@ -535,7 +528,7 @@ public async static Task GetOrCreateAssociatedTokenAccount( wallet.JitCreateAssociatedTokenAccount( builder, - currency.TokenContract, + currency.Contract, feePayerPublicKey); } catch (TokenWalletException ex) diff --git a/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionRequest.cs b/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionRequest.cs index cbd80ac0..c1eb1dd3 100644 --- a/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionRequest.cs +++ b/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionRequest.cs @@ -1,14 +1,12 @@ -using Train.Solver.Blockchain.Abstractions.Models; +using Train.Solver.Infrastructure.Abstractions.Models; namespace Train.Solver.Blockchain.Solana.Models; public class SolanaComposeTransactionRequest { - public required Fee Fee { get; set; } + public required DetailedNetworkDto Network { get; set; } = null!; public required string FromAddress { get; set; } = null!; public required string CallData { get; set; } = null!; - - public required string LastValidBlockHash { get; set; } = null!; } diff --git a/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionResponse.cs b/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionResponse.cs new file mode 100644 index 00000000..5d9f6990 --- /dev/null +++ b/csharp/src/Workflow.Solana/Models/SolanaComposeTransactionResponse.cs @@ -0,0 +1,8 @@ +namespace Train.Solver.Workflow.Solana.Models; + +public class SolanaComposeTransactionResponse +{ + public required string LastValidBlockHeight { get; set; } = null!; + + public required string RawTx { get; set; } = null!; +} diff --git a/csharp/src/Workflow.Solana/Models/SolanaGetReceiptRequest.cs b/csharp/src/Workflow.Solana/Models/SolanaGetReceiptRequest.cs new file mode 100644 index 00000000..3b873893 --- /dev/null +++ b/csharp/src/Workflow.Solana/Models/SolanaGetReceiptRequest.cs @@ -0,0 +1,12 @@ +using Train.Solver.Infrastructure.Abstractions.Models; + +namespace Train.Solver.Workflow.Solana.Models; + +public class SolanaGetReceiptRequest +{ + public required DetailedNetworkDto Network { get; set; } = null!; + + public required string TxHash { get; set; } = null!; + + public required string TransactionBlockHeight { get; set; } = null!; +} diff --git a/csharp/src/Workflow.Solana/Models/SolanaGetTransactionRequest.cs b/csharp/src/Workflow.Solana/Models/SolanaGetTransactionRequest.cs deleted file mode 100644 index 67e00f98..00000000 --- a/csharp/src/Workflow.Solana/Models/SolanaGetTransactionRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Train.Solver.Blockchain.Abstractions.Models; - -namespace Train.Solver.Blockchain.Solana.Models; - -public class SolanaGetTransactionRequest : GetTransactionRequest -{ - public required string FromAddress { get; set; } = null!; -} diff --git a/csharp/src/Workflow.Solana/Models/SolanaPublishTransactionRequest.cs b/csharp/src/Workflow.Solana/Models/SolanaPublishTransactionRequest.cs index 0699597c..e98fc721 100644 --- a/csharp/src/Workflow.Solana/Models/SolanaPublishTransactionRequest.cs +++ b/csharp/src/Workflow.Solana/Models/SolanaPublishTransactionRequest.cs @@ -1,8 +1,8 @@ -using Train.Solver.Blockchain.Abstractions.Models; +using Train.Solver.Workflow.Abstractions.Models; namespace Train.Solver.Blockchain.Solana.Models; public class SolanaPublishTransactionRequest : BaseRequest { - public byte[] RawTx { get; set; } = null!; + public string RawTx { get; set; } = null!; } diff --git a/csharp/src/Workflow.Solana/Models/SolanaSignTransactionRequest.cs b/csharp/src/Workflow.Solana/Models/SolanaSignTransactionRequest.cs new file mode 100644 index 00000000..84842be8 --- /dev/null +++ b/csharp/src/Workflow.Solana/Models/SolanaSignTransactionRequest.cs @@ -0,0 +1,12 @@ +using Train.Solver.Workflow.Abstractions.Models; + +namespace Train.Solver.Workflow.Solana.Models; + +public class SolanaSignTransactionRequest : BaseRequest +{ + public required string SignerAgentUrl { get; set; } + + public required string UnsignRawTransaction { get; set; } = null!; + + public required string FromAddress { get; set; } = null!; +} diff --git a/csharp/src/Workflow.Solana/Program.cs b/csharp/src/Workflow.Solana/Program.cs index 76aa76bc..f595ae5c 100644 --- a/csharp/src/Workflow.Solana/Program.cs +++ b/csharp/src/Workflow.Solana/Program.cs @@ -1,8 +1,7 @@ -using Train.Solver.Blockchain.Solana.Extensions; -using Train.Solver.Infrastructure.Extensions; -using Train.Solver.Data.Npgsql.Extensions; +using Train.Solver.Data.Npgsql.Extensions; using Train.Solver.Infrastructure.Logging.OpenTelemetry; -using Train.Solver.Infrastructure.Secret.HashicorpKeyVault; +using Train.Solver.Infrastructure.DependencyInjection; +using Train.Solver.Workflow.Solana.Extensions; IHost host = Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration(builder => @@ -17,7 +16,6 @@ .AddTrainSolver(hostContext.Configuration) .WithOpenTelemetryLogging("Solana Runner") .WithNpgsqlRepositories() - .WithHashicorpKeyVault() .WithSolanaWorkflows(); }) .Build(); diff --git a/csharp/src/Workflow.Solana/Workflows.Solana.csproj b/csharp/src/Workflow.Solana/Workflows.Solana.csproj index 4cb028e4..46a42618 100644 --- a/csharp/src/Workflow.Solana/Workflows.Solana.csproj +++ b/csharp/src/Workflow.Solana/Workflows.Solana.csproj @@ -12,10 +12,11 @@ - + + + - diff --git a/csharp/src/Workflow.Solana/Workflows/SolanaTransactionProcessor.cs b/csharp/src/Workflow.Solana/Workflows/SolanaTransactionProcessor.cs index 467abe4c..7b447220 100644 --- a/csharp/src/Workflow.Solana/Workflows/SolanaTransactionProcessor.cs +++ b/csharp/src/Workflow.Solana/Workflows/SolanaTransactionProcessor.cs @@ -1,14 +1,15 @@ using Temporalio.Exceptions; using Temporalio.Workflows; -using Train.Solver.Blockchain.Abstractions.Models; -using Train.Solver.Blockchain.Common.Extensions; -using Train.Solver.Blockchain.Common.Helpers; -using Train.Solver.Blockchain.Solana.Activities; using Train.Solver.Infrastructure.Abstractions.Exceptions; using Train.Solver.Blockchain.Solana.Models; using static Temporalio.Workflows.Workflow; +using Train.Solver.Workflow.Solana.Activities; +using Train.Solver.Workflow.Abstractions.Models; +using Train.Solver.Workflow.Common.Helpers; +using Train.Solver.Workflow.Common.Extensions; +using Train.Solver.Workflow.Solana.Models; -namespace Train.Solver.Blockchain.Solana.Workflows; +namespace Train.Solver.Workflow.Solana.Workflows; [Workflow] public class SolanaTransactionProcessor @@ -18,55 +19,31 @@ public async Task RunAsync(TransactionRequest request, Tran { var preparedTransaction = await ExecuteActivityAsync( (ISolanaBlockchainActivities x) => x.BuildTransactionAsync(new TransactionBuilderRequest - { - NetworkName = request.NetworkName, - Args = request.PrepareArgs, - Type = request.Type - } - ), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); - - if (context.Fee == null) - { - var fee = await ExecuteActivityAsync( - (ISolanaBlockchainActivities x) => x.EstimateFeeAsync(new EstimateFeeRequest - { - NetworkName = request.NetworkName, - FromAddress = request.FromAddress!, - ToAddress = preparedTransaction.ToAddress, - Asset = preparedTransaction.Asset, - Amount = preparedTransaction.AmountInWei, - CallData = preparedTransaction.Data, - } - ), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); - - if (fee is null) { - throw new("Unable to pay fees"); - } - - context.Fee = fee; - } - - var lastValidBLockHash = await ExecuteActivityAsync( - (ISolanaBlockchainActivities x) => x.GetNextNonceAsync( - new NextNonceRequest() - { - Address = request.NetworkName, - NetworkName = request.NetworkName, - }), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); + Network = request.Network, + PrepareArgs = request.PrepareArgs, + Type = request.Type + }), + TemporalHelper.DefaultActivityOptions(request.Network.Type)); - var rawTx = await ExecuteActivityAsync( + var composedTransaction = await ExecuteActivityAsync( (ISolanaBlockchainActivities x) => x.ComposeSolanaTranscationAsync(new SolanaComposeTransactionRequest() - { - Fee = context.Fee, - FromAddress = request.FromAddress, - CallData = preparedTransaction.Data, - LastValidBlockHash = lastValidBLockHash, - }), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); + { + Network = request.Network, + FromAddress = request.FromAddress, + CallData = preparedTransaction.Data! + }), + TemporalHelper.DefaultActivityOptions(request.Network.Type)); + + var signedTx = await ExecuteActivityAsync( + (ISolanaBlockchainActivities x) => x.SignTransactionAsync(new SolanaSignTransactionRequest() + { + Network = request.Network, + UnsignRawTransaction = composedTransaction.RawTx, + FromAddress = request.FromAddress, + SignerAgentUrl = request.SignerAgentUrl + }), + TemporalHelper.DefaultActivityOptions(request.Network.Type)); TransactionResponse confirmedTransaction; @@ -76,11 +53,11 @@ public async Task RunAsync(TransactionRequest request, Tran await ExecuteActivityAsync( (ISolanaBlockchainActivities x) => x.SimulateTransactionAsync( new SolanaPublishTransactionRequest() - { - RawTx = rawTx, - NetworkName = request.NetworkName - }), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); + { + RawTx = composedTransaction.RawTx, + Network= request.Network + }), + TemporalHelper.DefaultActivityOptions(request.Network.Type)); //Send transaction @@ -88,24 +65,21 @@ await ExecuteActivityAsync( (ISolanaBlockchainActivities x) => x.PublishTransactionAsync( new SolanaPublishTransactionRequest() { - RawTx = rawTx, - NetworkName = request.NetworkName + RawTx = composedTransaction.RawTx, + Network = request.Network }), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); + TemporalHelper.DefaultActivityOptions(request.Network.Type)); //Wait for transaction receipt confirmedTransaction = await ExecuteActivityAsync( - (ISolanaBlockchainActivities x) => x.GetTransactionAsync( - new GetTransactionRequest() - { - NetworkName = request.NetworkName, - TransactionHash = transactionId - }), - TemporalHelper.DefaultActivityOptions(request.NetworkType)); - - confirmedTransaction.Asset = preparedTransaction.CallDataAsset; - confirmedTransaction.Amount = preparedTransaction.CallDataAmountInWei; + (ISolanaBlockchainActivities x) => x.GetTransactionAsync(new SolanaGetReceiptRequest + { + TxHash = transactionId, + Network = request.Network, + TransactionBlockHeight = composedTransaction.LastValidBlockHeight + }), + TemporalHelper.DefaultActivityOptions(request.Network.Type)); } catch (ActivityFailureException ex) { diff --git a/treasury/package-lock.json b/treasury/package-lock.json index 46b71242..43188f7c 100644 --- a/treasury/package-lock.json +++ b/treasury/package-lock.json @@ -17,6 +17,8 @@ "@nestjs/config": "^4.0.2", "@nestjs/core": "11.1.2", "@nestjs/platform-express": "^11.1.6", + "@solana/web3.js": "^1.98.4", + "bs58": "^6.0.0", "class-transformer": "0.5.1", "class-validator": "0.14.2", "ethers": "^6.14.3", @@ -1136,6 +1138,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -5389,6 +5400,139 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/errors/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@solana/web3.js/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", @@ -5525,7 +5669,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5800,12 +5943,27 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -6550,6 +6708,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6997,6 +7167,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7153,6 +7329,35 @@ "node": ">=0.10.0" } }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/borsh/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/borsh/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -7223,6 +7428,15 @@ "node": ">= 6" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -7269,6 +7483,20 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -8355,6 +8583,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8664,6 +8904,21 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/esbuild": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", @@ -9190,6 +9445,14 @@ ], "license": "MIT" }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -9260,6 +9523,12 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -10498,6 +10767,15 @@ "node": ">=14.18.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", @@ -10855,6 +11133,15 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isows": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", @@ -11006,6 +11293,65 @@ "dev": true, "license": "MIT" }, + "node_modules/jayson": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.2.0.tgz", + "integrity": "sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -11977,9 +12323,9 @@ } }, "node_modules/koa": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz", - "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "license": "MIT", "dependencies": { "accepts": "^1.3.5", @@ -14423,6 +14769,44 @@ "node": ">= 18" } }, + "node_modules/rpc-websockets": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.1.tgz", + "integrity": "sha512-bY6a+i/lEtBJ/mUxwsCTgevoV1P0foXTVA7UoThzaIWbM+3NDqorf8NBWs5DmqKTFeA1IoNzgvkWjFCPgnzUiQ==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^8.3.2", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14966,6 +15350,12 @@ "license": "MIT", "peer": true }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, "node_modules/stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", @@ -14975,6 +15365,15 @@ "stubs": "^3.0.0" } }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/stream-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", @@ -15261,6 +15660,15 @@ "node": ">=4.0.0" } }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/supertest": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", @@ -15667,6 +16075,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -16155,7 +16568,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -16320,6 +16732,20 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -16358,9 +16784,9 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -16474,9 +16900,9 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", "peer": true, "dependencies": { diff --git a/treasury/package.json b/treasury/package.json index e287548e..310e286e 100644 --- a/treasury/package.json +++ b/treasury/package.json @@ -27,6 +27,8 @@ "@nestjs/config": "^4.0.2", "@nestjs/core": "11.1.2", "@nestjs/platform-express": "^11.1.6", + "@solana/web3.js": "^1.98.4", + "bs58": "^6.0.0", "class-transformer": "0.5.1", "class-validator": "0.14.2", "ethers": "^6.14.3", diff --git a/treasury/src/treasury/shared/networks.types.ts b/treasury/src/treasury/shared/networks.types.ts index 796f5379..0c885369 100644 --- a/treasury/src/treasury/shared/networks.types.ts +++ b/treasury/src/treasury/shared/networks.types.ts @@ -1 +1 @@ -export type Network = 'evm' | 'starknet' | 'solana' | 'fuel' | 'aztec'; \ No newline at end of file +export type Network = 'evm' | 'starknet' | 'solana' | 'fuel' | 'aztec' | 'solana'; diff --git a/treasury/src/treasury/solana/solana.service.ts b/treasury/src/treasury/solana/solana.service.ts new file mode 100644 index 00000000..0b925ef1 --- /dev/null +++ b/treasury/src/treasury/solana/solana.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { TreasuryService } from '../../app/interfaces/treasury.interface'; +import { Network } from '../shared/networks.types'; +import { BaseSignRequest, BaseSignResponse, GenerateResponse } from '../../app/dto/base.dto'; +import { PrivateKeyService } from '../../kv/vault.service'; +import { Keypair, Transaction } from "@solana/web3.js"; +import bs58 from "bs58"; + +@Injectable() +export class SolanaTreasuryService extends TreasuryService { + + readonly network: Network = 'solana'; + + constructor(privateKeyService: PrivateKeyService) { + super(privateKeyService); + } + + async sign(request: BaseSignRequest): Promise { + + const privateKey = await this.privateKeyService.getAsync(request.address); + + const account = Keypair.fromSecretKey(bs58.decode(privateKey)); + + let transaction: Transaction = Transaction.from(Buffer.from(request.unsignedTxn, 'base64')); + + transaction.sign(account); + + return { signedTxn: transaction.serialize().toString('base64') }; + } + + async generate(): Promise { + const keypair = Keypair.generate(); + + await this.privateKeyService.setAsync(keypair.publicKey.toBase58(), keypair.secretKey.toString()); + + return { address: keypair.publicKey.toBase58() }; + } +} \ No newline at end of file