Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/rules/key-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
105 changes: 103 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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/)
Expand Down Expand Up @@ -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:<fn>")[..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<u8> 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.
Expand Down
1 change: 1 addition & 0 deletions csharp/TrainSolver.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<Project Path="src/Workflow.Common/Workflow.Common.csproj" />
<Project Path="src/Workflow.EVM/Workflow.EVM.csproj" />
<Project Path="src/Workflow.EVM.Common/Workflow.EVM.Common.csproj" />
<Project Path="src/Workflow.Solana/Workflow.Solana.csproj" />
<Project Path="src/Workflow.Swap/Workflow.Swap.csproj" />
<Project Path="src/Workflow.Tron/Workflow.Tron.csproj" />
</Folder>
Expand Down
32 changes: 14 additions & 18 deletions csharp/src/AdminAPI/Endpoints/TransactionBuilderEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,35 +74,31 @@ private static async Task<IResult> 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
Expand Down
2 changes: 1 addition & 1 deletion csharp/src/AdminPanel/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.9.0/ethers.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
Expand Down
8 changes: 8 additions & 0 deletions csharp/src/AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ and assign it to the project with Admin role
.WaitFor(temporal)
.WaitFor(treasuryApi);

builder.AddProject<Projects.Workflow_Solana>("workflow-runner-solana")
.AddTrainSolverDefaults(solverDb, redis, temporal, alloy)
.WithReplicas(1)
.WaitFor(treasuryApi)
.WithEnvironment("TrainSolver__NetworkType", "solana")
.WithEnvironment("TrainSolver__NetworkSlug", "solana-devnet")
.WithEnvironment("QuoteSigning__HmacSecretKey", quoteSigningKey);

builder.AddProject<Projects.StationAPI>("station-api")
.WithExternalHttpEndpoints();

Expand Down
1 change: 1 addition & 0 deletions csharp/src/AppHost/AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ProjectReference Include="..\API\API.csproj" />
<ProjectReference Include="..\StationAPI\StationAPI.csproj" />
<ProjectReference Include="..\Workflow.EVM\Workflow.EVM.csproj" />
<ProjectReference Include="..\Workflow.Solana\Workflow.Solana.csproj" />
<ProjectReference Include="..\Workflow.Swap\Workflow.Swap.csproj" />
<ProjectReference Include="..\Workflow.Tron\Workflow.Tron.csproj" />
</ItemGroup>
Expand Down
5 changes: 5 additions & 0 deletions csharp/src/AppHost/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Parameters": {
"infisical-client-id": "fe6f6b34-16d3-44da-8d78-6fad178fceff",
"infisical-client-secret": "eca125c7ddec754459c9e40d1cad24c8a80b9942a22e7e516e738ce351315b47",
"infisical-project-id": "b2856518-0246-4d80-a19e-64afb46882d3"
}
}
Loading