diff --git a/examples/ChaincallinkExamples.cs b/examples/ChaincallinkExamples.cs new file mode 100644 index 0000000..6372012 --- /dev/null +++ b/examples/ChaincallinkExamples.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PiApps.Chainlink.Examples +{ + /// + /// Chainlink Integration Examples for Pi SDK .NET + /// Complete working examples demonstrating all Chainlink features + /// + public static class ChaincallinkExamples + { + /// + /// Example 1: Portfolio Price Monitoring + /// Track real-time prices for multiple assets + /// + public static async Task PortfolioMonitoringAsync() + { + using (var client = ChaincallinkClientFactory.CreateFromEnvironment()) + { + var pairs = new List { "PI/USD", "BTC/USD", "ETH/USD", "XLM/USD", "USDC/USD" }; + var prices = await client.GetPricesAsync(pairs); + + var holdings = new Dictionary + { + { "PI", 1000m }, + { "BTC", 0.5m }, + { "ETH", 5m }, + { "XLM", 10000m }, + { "USDC", 50000m } + }; + + decimal totalValue = 0; + + foreach (var (pair, data) in prices) + { + var asset = pair.Split('/')[0]; + var amount = holdings.ContainsKey(asset) ? holdings[asset] : 0; + var value = amount * data.Rate; + totalValue += value; + + Console.WriteLine($"{pair}: ${data.Rate} (Confidence: {data.Confidence * 100:F2}%)"); + } + + Console.WriteLine($"Portfolio Total: ${totalValue:F2}"); + } + } + + /// + /// Example 2: Automated Trading with VRF + /// Execute trades based on oracle prices with randomized execution + /// + public static async Task AutomatedTradingWithVRFAsync() + { + using (var client = ChaincallinkClientFactory.CreateFromEnvironment()) + { + var btcPrice = await client.GetPriceAsync("BTC/USD"); + + decimal buyThreshold = 40000m; + decimal sellThreshold = 50000m; + + Console.WriteLine($"Current BTC Price: ${btcPrice.Rate}"); + + if (btcPrice.Rate < buyThreshold) + { + var vrf = await client.RequestVRFAsync("trading-job-1", "btc-buy", (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + var tradeSize = (vrf.Nonce % 5) + 1; + Console.WriteLine($"Buy Signal: {tradeSize} BTC at ${btcPrice.Rate}"); + } + else if (btcPrice.Rate > sellThreshold) + { + var vrf = await client.RequestVRFAsync("trading-job-1", "btc-sell", (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + var tradeSize = (vrf.Nonce % 5) + 1; + Console.WriteLine($"Sell Signal: {tradeSize} BTC at ${btcPrice.Rate}"); + } + } + } + + /// + /// Example 3: Staking with Keeper Automation + /// Automate staking rewards collection using Keepers + /// + public static async Task StakingAutomationAsync() + { + using (var client = ChaincallinkClientFactory.CreateFromEnvironment()) + { + var jobs = await client.ListKeeperJobsAsync("active"); + var stakingJobs = jobs.FindAll(j => j.Name.Contains("staking")); + + Console.WriteLine($"Found {stakingJobs.Count} active staking jobs:"); + + foreach (var job in stakingJobs) + { + Console.WriteLine($"\nJob: {job.Name}"); + Console.WriteLine($"Status: {job.Status}"); + Console.WriteLine($"Executions: {job.ExecutionCount} ({job.SuccessCount} successful)"); + Console.WriteLine($"Success Rate: {job.GetSuccessRate():F2}%"); + Console.WriteLine($"Next Execution: {job.NextExecution:O}"); + + // Execute if due + if (DateTime.UtcNow >= job.NextExecution) + { + var (success, txHash) = await client.ExecuteKeeperJobAsync(job.Id); + if (success) + Console.WriteLine($"✓ Execution Triggered: {txHash}"); + } + } + } + } + + /// + /// Example 4: Cross-Chain Payments with CCIP + /// Send stablecoins across blockchains + /// + public static async Task CrossChainPaymentsAsync() + { + using (var client = ChaincallinkClientFactory.CreateFromEnvironment()) + { + var message = new CCIPMessage + { + SourceChain = "ethereum", + DestinationChain = "polygon", + Receiver = "0x1234567890123456789012345678901234567890", + Data = System.Text.Json.JsonSerializer.SerializeToElement(new + { + type = "payment", + amount = 1000, + currency = "USDC", + reference = "PAY-2025-001" + }), + Tokens = new List + { + new TokenTransfer { Token = "USDC", Amount = "1000000000" } + } + }; + + Console.WriteLine("Initiating cross-chain payment..."); + Console.WriteLine($"From: {message.SourceChain} → To: {message.DestinationChain}"); + Console.WriteLine($"Receiver: {message.Receiver}"); + Console.WriteLine("Amount: 1000 USDC"); + + var messageId = await client.SendCCIPMessageAsync(message); + Console.WriteLine($"Message ID: {messageId}"); + + // Poll message status + var status = await client.GetCCIPMessageStatusAsync(messageId); + Console.WriteLine($"Initial Status: {status.Status}"); + + for (int i = 0; i < 5; i++) + { + await Task.Delay(2000); + status = await client.GetCCIPMessageStatusAsync(messageId); + Console.WriteLine($"Status Update: {status.Status} (Confirmations: {status.Confirmations})"); + + if (status.Status == MessageState.Executed) + { + Console.WriteLine("✓ Payment delivered!"); + break; + } + } + } + } + + /// + /// Example 5: Oracle Health Monitoring + /// Monitor Chainlink network and adjust strategies + /// + public static async Task HealthMonitoringAsync() + { + using (var client = ChaincallinkClientFactory.CreateFromEnvironment()) + { + var health = await client.GetHealthStatusAsync(); + + Console.WriteLine("Chainlink Network Health:"); + Console.WriteLine($"Status: {health.Status}"); + Console.WriteLine($"Active Nodes: {health.ActiveNodes}/{health.TotalNodes}"); + Console.WriteLine($"Uptime: {health.Uptime * 100:F2}%"); + Console.WriteLine($"Price Feeds: {health.FeedsCount}"); + Console.WriteLine($"Avg Latency: {health.AverageLatencyMs}ms"); + + // Implement fallback strategy + if (health.Status == NetworkStatus.Degraded) + { + Console.WriteLine("⚠ Network degraded - increasing cache duration"); + client.SetCacheDuration(600); + } + else if (health.Status == NetworkStatus.Offline) + { + Console.WriteLine("✗ Network offline - using fallback data"); + } + else + { + Console.WriteLine("✓ Network healthy"); + client.SetCacheDuration(300); + } + } + } + + /// + /// Run all examples + /// + public static async Task RunAllExamplesAsync() + { + Console.WriteLine("=== Chainlink Pi SDK Integration Examples ===\n"); + + try + { + Console.WriteLine("📊 Example 1: Portfolio Monitoring"); + Console.WriteLine("----------------------------------------"); + await PortfolioMonitoringAsync(); + + Console.WriteLine("\n\n📈 Example 2: Automated Trading with VRF"); + Console.WriteLine("----------------------------------------"); + await AutomatedTradingWithVRFAsync(); + + Console.WriteLine("\n\n🔄 Example 3: Staking Automation"); + Console.WriteLine("----------------------------------------"); + await StakingAutomationAsync(); + + Console.WriteLine("\n\n💳 Example 4: Cross-Chain Payments"); + Console.WriteLine("----------------------------------------"); + await CrossChainPaymentsAsync(); + + Console.WriteLine("\n\n⚡ Example 5: Health Monitoring"); + Console.WriteLine("----------------------------------------"); + await HealthMonitoringAsync(); + + Console.WriteLine("\n\n✓ All examples completed successfully!"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"✗ Error running examples: {ex.Message}"); + } + } + } +} diff --git a/src/Chainlink/ChaincallinkClient.cs b/src/Chainlink/ChaincallinkClient.cs new file mode 100644 index 0000000..d592848 --- /dev/null +++ b/src/Chainlink/ChaincallinkClient.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PiApps.Chainlink +{ + /// + /// Price feed data structure from Chainlink oracle + /// + public class PriceFeedData + { + public string Pair { get; set; } + public decimal Rate { get; set; } + public DateTime Timestamp { get; set; } + public string Source { get; set; } + public decimal Confidence { get; set; } // 0-1 + public int Nodes { get; set; } + public int Decimals { get; set; } + } + + /// + /// Keeper job information for automation tracking + /// + public class KeeperJob + { + public string Id { get; set; } + public string Name { get; set; } + public string ContractAddress { get; set; } + public string FunctionSelector { get; set; } + public JobStatus Status { get; set; } + public int RepeatIntervalSeconds { get; set; } + public DateTime NextExecution { get; set; } + public DateTime? LastExecution { get; set; } + public int ExecutionCount { get; set; } + public int SuccessCount { get; set; } + public string LastError { get; set; } + + public decimal GetSuccessRate() => ExecutionCount > 0 + ? (decimal)SuccessCount / ExecutionCount * 100 + : 0; + } + + /// + /// VRF request structure + /// + public class VRFRequest + { + public string JobId { get; set; } + public string Seed { get; set; } + public uint Nonce { get; set; } + public string CallbackAddress { get; set; } + } + + /// + /// CCIP cross-chain message + /// + public class CCIPMessage + { + public string SourceChain { get; set; } + public string DestinationChain { get; set; } + public string Receiver { get; set; } + public JsonElement Data { get; set; } + public List Tokens { get; set; } + } + + /// + /// Token transfer for CCIP + /// + public class TokenTransfer + { + public string Token { get; set; } + public string Amount { get; set; } // BigInteger as string + } + + /// + /// CCIP message status + /// + public class CCIPMessageStatus + { + public string MessageId { get; set; } + public MessageState Status { get; set; } + public string SourceChain { get; set; } + public string DestinationChain { get; set; } + public int? Confirmations { get; set; } + } + + /// + /// Chainlink network health status + /// + public class HealthStatus + { + public NetworkStatus Status { get; set; } + public int ActiveNodes { get; set; } + public int TotalNodes { get; set; } + public decimal Uptime { get; set; } // 0-1 + public int FeedsCount { get; set; } + public double AverageLatencyMs { get; set; } + } + + public enum JobStatus + { + Pending, + Active, + Paused, + Completed, + Failed + } + + public enum MessageState + { + Pending, + Confirmed, + Executed, + Failed + } + + public enum NetworkStatus + { + Healthy, + Degraded, + Offline + } + + /// + /// Chainlink client for Pi SDK .NET + /// + public class ChaincallinkClient : IDisposable + { + private readonly HttpClient _httpClient; + private readonly string _apiKey; + private readonly string _baseUrl; + private readonly Dictionary _cache; + private int _cacheDurationSeconds; + + public ChaincallinkClient(string apiKey, string baseUrl = null, int cacheDurationSeconds = 300) + { + _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); + _baseUrl = baseUrl ?? "https://api.chain.link"; + _cacheDurationSeconds = cacheDurationSeconds; + _cache = new Dictionary(); + + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "PiApps-Chainlink-Client/1.0"); + } + + /// + /// Get single price from Chainlink Price Feed + /// + public async Task GetPriceAsync(string pair) + { + var cacheKey = $"price:{pair}"; + if (TryGetCached(cacheKey, out var cached)) + return cached; + + var response = await _httpClient.GetAsync($"{_baseUrl}/price-feeds/{pair}"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize(json); + + SetCached(cacheKey, data); + return data; + } + + /// + /// Get multiple prices in batch + /// + public async Task> GetPricesAsync(List pairs) + { + var cacheKey = $"prices:{string.Join(",", pairs)}"; + if (TryGetCached>(cacheKey, out var cached)) + return cached; + + var content = new StringContent( + JsonSerializer.Serialize(new { pairs }), + System.Text.Encoding.UTF8, + "application/json" + ); + + var response = await _httpClient.PostAsync($"{_baseUrl}/price-feeds/batch", content); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize>(json); + + SetCached(cacheKey, data); + return data; + } + + /// + /// Request VRF (Verifiable Random Function) + /// + public async Task RequestVRFAsync(string jobId, string seed, uint nonce) + { + var content = new StringContent( + JsonSerializer.Serialize(new { jobId, seed, nonce }), + System.Text.Encoding.UTF8, + "application/json" + ); + + var response = await _httpClient.PostAsync($"{_baseUrl}/vrf/request", content); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json); + } + + /// + /// Get keeper job status + /// + public async Task GetKeeperJobAsync(string jobId) + { + var cacheKey = $"keeper:{jobId}"; + if (TryGetCached(cacheKey, out var cached)) + return cached; + + var response = await _httpClient.GetAsync($"{_baseUrl}/keepers/{jobId}"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize(json); + + SetCached(cacheKey, data); + return data; + } + + /// + /// List all keeper jobs + /// + public async Task> ListKeeperJobsAsync(string status = null) + { + var url = status != null + ? $"{_baseUrl}/keepers?status={status}" + : $"{_baseUrl}/keepers"; + + var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize>(json); + } + + /// + /// Execute keeper job immediately + /// + public async Task<(bool Success, string TxHash)> ExecuteKeeperJobAsync(string jobId) + { + InvalidateCache($"keeper:{jobId}"); + + var response = await _httpClient.PostAsync( + $"{_baseUrl}/keepers/{jobId}/execute", + new StringContent("", System.Text.Encoding.UTF8, "application/json") + ); + + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + + return ( + result.TryGetProperty("success", out var success) && success.GetBoolean(), + result.TryGetProperty("txHash", out var txHash) ? txHash.GetString() : null + ); + } + + /// + /// Send CCIP cross-chain message + /// + public async Task SendCCIPMessageAsync(CCIPMessage message) + { + var content = new StringContent( + JsonSerializer.Serialize(message), + System.Text.Encoding.UTF8, + "application/json" + ); + + var response = await _httpClient.PostAsync($"{_baseUrl}/ccip/send", content); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json); + + return result.TryGetProperty("messageId", out var messageId) + ? messageId.GetString() + : null; + } + + /// + /// Get CCIP message status + /// + public async Task GetCCIPMessageStatusAsync(string messageId) + { + var cacheKey = $"ccip:{messageId}"; + if (TryGetCached(cacheKey, out var cached)) + return cached; + + var response = await _httpClient.GetAsync($"{_baseUrl}/ccip/{messageId}"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize(json); + + SetCached(cacheKey, data); + return data; + } + + /// + /// Get Chainlink network health + /// + public async Task GetHealthStatusAsync() + { + var cacheKey = "health"; + if (TryGetCached(cacheKey, out var cached)) + return cached; + + var response = await _httpClient.GetAsync($"{_baseUrl}/health"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize(json); + + SetCached(cacheKey, data); + return data; + } + + /// + /// Clear all cached data + /// + public void ClearCache() + { + _cache.Clear(); + } + + /// + /// Set cache duration + /// + public void SetCacheDuration(int seconds) + { + _cacheDurationSeconds = seconds; + } + + /// + /// Check if cache entry exists and is valid + /// + private bool TryGetCached(string key, out T value) + { + value = default; + + if (!_cache.TryGetValue(key, out var entry)) + return false; + + if (DateTime.UtcNow > entry.ExpiresAt) + { + _cache.Remove(key); + return false; + } + + value = (T)entry.Data; + return true; + } + + /// + /// Set cache entry + /// + private void SetCached(string key, object data) + { + var expiresAt = DateTime.UtcNow.AddSeconds(_cacheDurationSeconds); + _cache[key] = (expiresAt, data); + } + + /// + /// Invalidate cache entry + /// + private void InvalidateCache(string key) + { + if (_cache.ContainsKey(key)) + _cache.Remove(key); + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + } + + /// + /// Factory for creating client instances + /// + public static class ChaincallinkClientFactory + { + /// + /// Create client from environment variables + /// + public static ChaincallinkClient CreateFromEnvironment(int cacheDurationSeconds = 300) + { + var apiKey = Environment.GetEnvironmentVariable("CHAINLINK_API_KEY"); + if (string.IsNullOrEmpty(apiKey)) + throw new InvalidOperationException("CHAINLINK_API_KEY environment variable not set"); + + var baseUrl = Environment.GetEnvironmentVariable("CHAINLINK_BASE_URL") + ?? "https://api.chain.link"; + + return new ChaincallinkClient(apiKey, baseUrl, cacheDurationSeconds); + } + } +}