diff --git a/.claude/rules/key-files.md b/.claude/rules/key-files.md index 28acaaa1..6920a321 100644 --- a/.claude/rules/key-files.md +++ b/.claude/rules/key-files.md @@ -32,7 +32,22 @@ | IAddressActivator | `src/Workflow.Abstractions/Workflows/IAddressActivator.cs` | | AddressGenerator (EVM) | `src/Workflow.EVM/Workflows/AddressGenerator.cs` | | AddressGenerator (Starknet) | `js/src/Blockchain/Blockchain.Starknet/Workflows/StarknetAddressGenerator.ts` | +| AddressGenerator (Solana) | `src/Workflow.Solana/Workflows/SolanaAddressGenerator.cs` | | AddressActivator (Starknet) | `js/src/Blockchain/Blockchain.Starknet/Workflows/StarknetAddressActivator.ts` | +| Solana network runtime | `src/Workflow.Solana/Workflows/SolanaNetworkRuntime.cs` | +| Solana transaction processor | `src/Workflow.Solana/Workflows/SolanaTransactionProcessor.cs` | +| Solana blockchain activities interface | `src/Workflow.Solana/Activities/ISolanaBlockchainActivities.cs` | +| Solana blockchain activities impl | `src/Workflow.Solana/Activities/SolanaBlockchainActivities.cs` | +| Solana workflow activities interface | `src/Workflow.Solana/Activities/ISolanaWorkflowActivities.cs` | +| Solana workflow activities impl | `src/Workflow.Solana/Activities/SolanaWorkflowActivities.cs` | +| Solana RPC event listener | `src/Workflow.Solana/Workflows/SolanaRPCEventListener.cs` | +| Solana event listener base | `src/Workflow.Solana/Workflows/SolanaEventListenerBase.cs` | +| Solana listener capabilities | `src/Workflow.Solana/SolanaListenerCapabilities.cs` | +| Solana anchor helper | `src/Workflow.Solana/Helpers/SolanaAnchorHelper.cs` | +| Solana HTLC models | `src/Workflow.Solana/Models/SolanaHTLCModels.cs` | +| Solana models | `src/Workflow.Solana/Models/SolanaModels.cs` | +| Solana helper | `src/Workflow.Solana/Helpers/SolanaHelper.cs` | +| Solana DI extensions | `src/Workflow.Solana/Extensions/TrainSolverBuilderExtensions.cs` | | WalletGenerator | `src/Workflow.Swap/Workflows/WalletGenerator.cs` | | IWalletGenerator | `src/Workflow.Abstractions/Workflows/IWalletGenerator.cs` | | WalletActivation | `src/Workflow.Swap/Workflows/WalletActivation.cs` | diff --git a/CLAUDE.md b/CLAUDE.md index 2969cfa8..54d2240c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ csharp/ # .NET 9 backend (Aspire, Temporal, APIs, workflows) ├── src/ # Source projects ├── tests/ # xUnit tests └── tools/ # DataSeeder, ProtoGenerator -js/ # TypeScript workers (Aztec, Starknet network support) +js/ # TypeScript workers (Aztec, Starknet, Solana network support) treasury/ # Infisical-backed key management service (JS) protos/blockchain.proto # Language-agnostic network worker contract schema ``` @@ -87,7 +87,7 @@ dotnet test tests/Workflow.Tests/Workflow.Tests.csproj ## JS Project (`js/`) -TypeScript Temporal workers for non-EVM networks (Aztec, Starknet). +TypeScript Temporal workers for non-EVM networks (Aztec, Starknet, Solana). ```bash # Install (from js/) @@ -123,6 +123,107 @@ Full HTLC integration — supports all 6 contract functions (lock/redeem/refund - **Same Dockerfile as Aztec**: Differentiated by `TrainSolver__NetworkType` env var - **AppHost**: `workflow-runner-starknet` container with `TrainSolver__NetworkSlug=starknet-sepolia` +### Solana (`js/src/Blockchain/Blockchain.Solana/`) — SUPERSEDED by C# Workflow.Solana + +**Note:** The JS Solana worker has been replaced by the C# `Workflow.Solana` project (see below). The JS code remains in the repo for reference but is no longer used. AppHost now runs a C# project instead of a JS container. + +### Workflow.Solana (`csharp/src/Workflow.Solana/`) + +C# Solana integration using SolNet SDK with `recentBlockhash` + ComputeBudget priority fees. HTLC event listener polls Anchor program transactions (no Anchor events — instruction-based detection). + +- **NetworkType**: `solana`, task queue: `solana` +- **SDK**: SolNet v6.1.0 (`Solnet.Rpc`, `Solnet.Programs`, `Solnet.Wallet`) +- **DI registration**: `.WithSolanaWorkflows()` in `TrainSolverBuilderExtensions` +- **AppHost**: `workflow-runner-solana` C# project with `TrainSolver__NetworkType=solana` + +**Transaction Pipeline (recentBlockhash + priority fees):** +Uses `getLatestBlockhash` for transaction lifetime instead of durable nonce accounts. Each transaction includes ComputeBudget instructions for priority fee bidding. Expired transactions are retried with escalated fees. +- Pipeline: Queued → GetLatestBlockhash → GetPriorityFee → Build(blockhash, ComputeBudget) → Sign → Pending → Publish → InFlight → Confirmed/Expired +- Priority fee: 75th percentile of `getRecentPrioritizationFees` (floor: 1000 microLamports) +- Expiry detection: `currentBlockHeight > lastValidBlockHeight` OR `elapsed > stuckTimeoutSeconds` +- Retry on expiry: up to 3 retries with 1.5x fee escalation (1x → 1.5x → 2.25x → fail) +- Redis used only for TP state persistence (`solana:tp-state:{slug}:{wallet}`, 7-day TTL) + +**ComputeBudget helpers** (`SolanaHelper`): +- `SetComputeUnitLimit(uint units)` — discriminant `0x02` + 4-byte LE u32 +- `SetComputeUnitPrice(ulong microLamports)` — discriminant `0x03` + 8-byte LE u64 +- Both return `TransactionInstruction` with `ComputeBudget111111111111111111111111111111` program ID + +**Transaction Processor** (`SolanaTransactionProcessor`): +- 3-queue pipeline: Queued → Pending (blockhash + signed) → InFlight (published, polling) +- Supports all HTLC signals: SubmitLock, SubmitRedeem, SubmitRefund, SubmitUserRefund, SubmitTransfer +- HTLC tx building: TP reads HTLC PDA account on-chain (for redeem/refund account metas), builds Anchor instructions via `SolanaAnchorHelper`, signs via treasury +- `SolanaQueuedRequest` carries typed request (`LockRequest`, `RedeemRequest`, `RefundRequest`, `TransferRequest`) — routed by `Type` in `BuildUnsignedTransactionForQueuedAsync` +- **No gas bumping** — Solana has no mempool replacement. Uses priority fee escalation on retry instead. +- **No cancellation tx** — no self-transfer pattern +- **No orphaned monitoring** — expired transactions are retried or failed +- `maxInFlightTransactions = 10` (configurable via DB) +- Continue-as-new at 50 iterations +- State persisted to Redis (`solana:tp-state:{slug}:{wallet}`, 7-day TTL) + +**Network Runtime** (`SolanaNetworkRuntime`): +- Bootstrap: fetch network + wallets, start TransactionProcessors + event listeners +- Listener support: bootstraps `rpc-log-event-listener` from `EventListenerConfigs`, composes subscriptions via `SolanaListenerCapabilities` +- No GasStation (Solana uses priority fees, not EVM gas estimation) +- Health check every ~1 min: re-fetch wallets, reconcile TPs/listeners, update subscriptions if filter addresses change +- Update handlers: GetBalance, GetBatchBalances, ValidateAddress, ValidateNode, and all 6 HTLC Build* methods (BuildSolverLock, BuildUserLock, BuildSolverRedeem, BuildUserRedeem, BuildSolverRefund, BuildUserRefund) via local activities +- Build* methods read HTLC PDA account on-chain for redeem/refund (to get sender/src_receiver for account metas) +- Continue-as-new at 200 operations + +**Event Listener** (`SolanaRPCEventListener`): +- Polls `getSignaturesForAddress(HTLCProgramId)` for HTLC program transactions +- Parses Anchor instruction discriminators (`sha256("global:")[..8]`) to identify function calls +- Function name determines event type directly: `user_lock_sol`→UserTokenLocked, `solver_lock_sol`→SolverTokenLocked, `redeem_user_sol`→SolverTokenRedeemed, `redeem_solver_sol`→UserTokenRedeemed, `refund_user_sol`→UserTokenRefunded, `refund_solver_sol`→SolverTokenRefunded +- Parses lock instruction data for event fields including reward, solverData, userData (full Borsh Vec parsing) +- **Timelock conversion**: Solana stores `timelockDelta` (relative seconds) in instruction data. Activity converts to absolute Unix timestamp via `blockTime + timelockDelta` (from `getTransaction` response). OrderWorkflow expects absolute `TimeLock` for validation. +- Checkpoint: slot + signature stored in Redis (`solana:checkpoint:{slug}:{type}`) +- Continue-as-new at 50 iterations +- Self-contained `SolanaEventListenerBase` (no dependency on Workflow.EVM) + +**HTLC Contract** (`6zasug6x5AY93zNVjPZPGoqQfdTBd3C1w6CU9NDKtNH8`): +- Anchor program `native_htlc` on Solana devnet +- 6 functions with `_sol` suffix: `user_lock_sol`, `solver_lock_sol`, `redeem_user_sol`, `redeem_solver_sol`, `refund_user_sol`, `refund_solver_sol` +- No Anchor events in instruction data — detection via instruction discriminator parsing + PDA account reads +- **PDA seeds**: UserLock `["user_lock", hashlock]`, SolverLock `["solver_lock", hashlock, index_u64_le]`, SolverCounter `["solver_count", hashlock]` +- **Solver lock counter**: SolverCounter PDA tracks how many solver locks exist per hashlock. Next index = counter.count + 1 (or 1 if counter doesn't exist). Layout: `disc(8) + count(u64)`. +- **PDA account layout** (simplified): `disc(8) + secret(32) + amount(u64) + sender(32) + timelock(u64) + status(u8) + src_receiver(32)`. Status: 0=committed, 1=locked, 2=refunded, 3=redeemed. +- **user_lock_sol instruction data**: `disc(8) + hashlock(32) + amount(u64) + timelock_delta(u64) + quote_expiry(u64) + sender(borsh_string) + recipient(borsh_string) + src_chain(borsh_string) + dst_chain(borsh_string) + dst_address(borsh_string) + dst_amount(u64) + dst_token(borsh_string) + reward_amount(u64) + reward_token(borsh_string) + reward_recipient(borsh_string) + reward_timelock_delta(u64) + user_data(borsh_vec) + solver_data(borsh_vec)`. RewardRecipient is String type (can be any format). +- **solver_lock_sol instruction data**: `disc(8) + hashlock(32) + index(u64) + amount(u64) + reward(u64) + timelock_delta(u64) + reward_timelock_delta(u64) + sender(borsh_string) + recipient(borsh_string) + reward_recipient(borsh_pubkey32) + src_chain(borsh_string) + dst_chain(borsh_string) + dst_address(borsh_string) + dst_amount(u64) + dst_token(borsh_string) + data(borsh_vec)`. RewardRecipient is Pubkey type (must be valid Solana address). +- **user_lock_sol account metas** (3): sender(signer,mut), userLock(init,mut,PDA["user_lock",hashlock]), system_program +- **solver_lock_sol account metas** (4): sender(signer,mut), solverCounter(init_if_needed,mut,PDA["solver_count",hashlock]), solverLock(init,mut,PDA["solver_lock",hashlock,index_le]), system_program +- **redeem_user_sol account metas** (4): caller(signer,mut), userLock(mut,PDA["user_lock",hashlock]), recipient(unchecked,mut), system_program +- **redeem_solver_sol account metas** (5): caller(signer,mut), solverLock(mut,PDA["solver_lock",hashlock,index_le]), recipient(unchecked,mut), rewardRecipient(unchecked,mut), system_program +- **refund_user_sol account metas** (4): caller(signer,mut), userLock(mut,PDA["user_lock",hashlock]), sender(unchecked,mut), system_program +- **refund_solver_sol account metas** (4): caller(signer,mut), solverLock(mut,PDA["solver_lock",hashlock,index_le]), sender(unchecked,mut), system_program +- **Borsh encoding**: `SolanaAnchorHelper` provides `WriteBorshU64`, `WriteBorshString`, `WriteBorshPubkey`, `WriteBorshBytes32`, `WriteBorshVec` for instruction data serialization +- **Instruction builders**: `BuildUserLockSolInstructionData`, `BuildSolverLockSolInstructionData`, `BuildRedeemUserSolInstructionData`, `BuildRedeemSolverSolInstructionData`, `BuildRefundUserSolInstructionData`, `BuildRefundSolverSolInstructionData` in `SolanaAnchorHelper` +- **Instruction parsers**: `ParseUserLockInstructionData`, `ParseSolverLockInstructionData` in `SolanaAnchorHelper` — used by event listener for lock events. Redeem/refund don't need parsing (hashlock from PDA). + +**Address Generator** (`SolanaAddressGenerator`): +- Single step: call treasury `/api/treasury/solana/generate` → return address +- No funding/deployment needed (Solana addresses are just keypairs) + +**Activities** (`SolanaBlockchainActivities`): +- `GetBalanceAsync`: Native SOL via `rpcClient.GetBalanceAsync`, SPL via `GetTokenAccountsByOwnerAsync` +- `GetLatestBlockhashAsync`: Returns `SolanaBlockhashInfo { Blockhash, LastValidBlockHeight }` +- `GetRecentPriorityFeeAsync`: Raw HTTP JSON-RPC `getRecentPrioritizationFees`, returns 75th percentile (floor: 1000 µL) +- `BuildHTLCLockTransactionAsync`: Builds Anchor `user_lock_sol` or `solver_lock_sol` instruction. Solver lock: reads SolverCounter PDA to get next index, derives SolverLock PDA, 4 accounts. User lock: derives UserLock PDA, 3 accounts. Uses `IsSolverLock` flag. +- `BuildHTLCRedeemTransactionAsync`: Builds Anchor `redeem_user_sol` (4 accounts) or `redeem_solver_sol` (5 accounts, includes rewardRecipient). Uses `IsRedeemingUserLock` flag, `Index` for solver lock. +- `BuildHTLCRefundTransactionAsync`: Builds Anchor `refund_user_sol` or `refund_solver_sol` — both 4 accounts, different PDA seeds. Uses `IsRefundingSolverLock` flag, `Index` for solver lock. +- `GetHTLCAccountDataAsync`: Reads HTLC PDA via `getAccountInfo` → Borsh-decodes → returns `SolanaHTLCAccount`. Uses `IsUserLock` flag and `Index` for correct PDA derivation. +- `GetBlockHeightAsync`: Current block height for expiry checks +- `BuildTransferTransactionAsync`: Uses `SetRecentBlockHash` + `SetComputeUnitLimit` + `SetComputeUnitPrice` instructions before `SystemProgram.Transfer`. Builds unsigned tx with zero-byte signature placeholders. +- `SignTransactionAsync`: POST `/api/treasury/solana/sign` with `{ address, unsignedTxn }` → returns `{ signedTxn }` +- `PublishTransactionAsync`: `rpcClient.SendTransactionAsync` with error classification (Published, DuplicateTransaction, InsufficientFunds, BlockhashNotFound, Failed) +- `GetHTLCEventsAsync`: Polls `getSignaturesForAddress` → `getTransaction` → parse Anchor discriminators (`user_lock_sol`, `solver_lock_sol`, etc.) → parse instruction data for lock events, read UserLock PDA for redeem/refund → returns categorized events + +**Unsigned Transaction Wire Format:** +`[compact-u16 sig_count][64-zero-bytes per sig][message bytes]` — compatible with treasury's `Transaction.from(buffer)` in `@solana/web3.js` v1. + +**Native token address**: `11111111111111111111111111111111` (System Program). Also accepts EVM-style null address `0x0000000000000000000000000000000000000000`. + +**SPL token transfers**: Not yet implemented (only native SOL). Will need ATA (Associated Token Account) creation when added. + ## Treasury (`treasury/`) Infisical-backed key management and signing service. diff --git a/csharp/TrainSolver.slnx b/csharp/TrainSolver.slnx index 124778ef..174f1d5c 100644 --- a/csharp/TrainSolver.slnx +++ b/csharp/TrainSolver.slnx @@ -33,6 +33,7 @@ + diff --git a/csharp/src/AdminAPI/Endpoints/TransactionBuilderEndpoints.cs b/csharp/src/AdminAPI/Endpoints/TransactionBuilderEndpoints.cs index 7b2629b3..c79fcb8f 100644 --- a/csharp/src/AdminAPI/Endpoints/TransactionBuilderEndpoints.cs +++ b/csharp/src/AdminAPI/Endpoints/TransactionBuilderEndpoints.cs @@ -74,35 +74,31 @@ private static async Task GetQuoteAsync( try { - var quoteRequest = new QuoteRequest - { - SourceNetwork = request.SourceNetwork, - SourceTokenContract = request.SourceTokenContract, - DestinationNetwork = request.DestinationNetwork, - DestinationTokenContract = request.DestinationTokenContract, - Amount = amount, - IncludeReward = request.IncludeReward, - }; - - var quote = await quoteService.GetQuoteAsync(quoteRequest); - var sourceNetwork = await networkRepository.GetAsync(request.SourceNetwork); var destinationNetwork = await networkRepository.GetAsync(request.DestinationNetwork); if (sourceNetwork == null) - { return Results.NotFound($"Source network '{request.SourceNetwork}' not found."); - } if (destinationNetwork == null) - { return Results.NotFound($"Destination network '{request.DestinationNetwork}' not found."); - } var sourceTokenContract = request.SourceTokenContract ?? sourceNetwork.Type.NativeTokenAddress; - var sourceHtlcContract = ResolveTrainContractAddress(sourceNetwork); - var destTokenContract = request.DestinationTokenContract ?? destinationNetwork.Type.NativeTokenAddress; + + var quoteRequest = new QuoteRequest + { + SourceNetwork = request.SourceNetwork, + SourceTokenContract = sourceTokenContract, + DestinationNetwork = request.DestinationNetwork, + DestinationTokenContract = destTokenContract, + Amount = amount, + IncludeReward = request.IncludeReward, + }; + + var quote = await quoteService.GetQuoteAsync(quoteRequest); + + var sourceHtlcContract = ResolveTrainContractAddress(sourceNetwork); var destHtlcContract = ResolveTrainContractAddress(destinationNetwork); var response = new GetQuoteResponse diff --git a/csharp/src/AdminPanel/wwwroot/index.html b/csharp/src/AdminPanel/wwwroot/index.html index fa28554d..419dd8fe 100644 --- a/csharp/src/AdminPanel/wwwroot/index.html +++ b/csharp/src/AdminPanel/wwwroot/index.html @@ -28,7 +28,7 @@ Reload 🗙 - +