├── LICENSE.md ├── PiNetworkNet.csproj ├── README.md └── src ├── PiNetworkClient.cs └── PiNetworkData.cs /LICENSE.md: -------------------------------------------------------------------------------- 1 | PiOS License 2 | 3 | Copyright (C) 4 | 5 | Permission is hereby granted by the application software developer (“Software Developer”), free 6 | of charge, to any person obtaining a copy of this application, software and associated 7 | documentation files (the “Software”), which was developed by the Software Developer for use on 8 | Pi Network, whereby the purpose of this license is to permit the development of derivative works 9 | based on the Software, including the right to use, copy, modify, merge, publish, distribute, 10 | sub-license, and/or sell copies of such derivative works and any Software components incorporated 11 | therein, and to permit persons to whom such derivative works are furnished to do so, in each case, 12 | solely to develop, use and market applications for the official Pi Network. For purposes of this 13 | license, Pi Network shall mean any application, software, or other present or future platform 14 | developed, owned or managed by Pi Community Company, and its parents, affiliates or subsidiaries, 15 | for which the Software was developed, or on which the Software continues to operate. However, 16 | you are prohibited from using any portion of the Software or any derivative works thereof in any 17 | manner (a) which infringes on any Pi Network intellectual property rights, (b) to hack any of Pi 18 | Network’s systems or processes or (c) to develop any product or service which is competitive with 19 | the Pi Network. 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or 22 | substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 25 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 26 | AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, PUBLISHERS, OR COPYRIGHT HOLDERS OF THIS 27 | SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO BUSINESS INTERRUPTION, LOSS OF USE, DATA OR PROFITS) 29 | HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 30 | TORT (INCLUDING NEGLIGENCE) ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 31 | OR OTHER DEALINGS IN THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 32 | 33 | Pi, Pi Network and the Pi logo are trademarks of the Pi Community Company. 34 | -------------------------------------------------------------------------------- /PiNetworkNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pi-csharp 2 | 3 | Pi Network C# client library for server side applications. 4 | 5 | - Create new payment 6 | - Get payment 7 | - Approve payment 8 | - Complete payment 9 | - Cancel payment 10 | - Get incomplete server payments 11 | - Make transaction for withdraw request. 12 | 13 | Require 14 | 15 | - API Key from Pi Network 16 | - App Seed Wallet when make A2U transactions. 17 | -------------------------------------------------------------------------------- /src/PiNetworkClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | using Newtonsoft.Json; 6 | using RestSharp; 7 | using stellar_dotnet_sdk.responses; 8 | using stellar_dotnet_sdk; 9 | 10 | namespace PiNetworkNet 11 | { 12 | public class PiNetworkClient 13 | { 14 | private readonly RestClient _restClient = new RestClient("https://api.minepi.com/v2"); 15 | private readonly string _apiKey; 16 | public PiNetworkClient(string apiKey) 17 | { 18 | _restClient.AddDefaultHeader("Content-Type", "application/json"); 19 | //_restClient.AddDefaultHeader("Accept", "application/json"); 20 | _apiKey = apiKey; 21 | } 22 | 23 | public async Task Me(string accessToken) 24 | { 25 | var request = new RestRequest("/me"); 26 | request.AddHeader("Accept", $"application/json"); 27 | request.AddHeader("Authorization", $"Bearer {accessToken}"); 28 | request.Method = Method.Get; 29 | var response = await _restClient.ExecuteAsync(request); 30 | if (response.IsSuccessful) 31 | { 32 | return JsonConvert.DeserializeObject(response.Content); 33 | } 34 | else 35 | { 36 | try 37 | { 38 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 39 | if (error != null) 40 | { 41 | throw new PiNetworkException() 42 | { 43 | PiError = error, 44 | }; 45 | } 46 | else 47 | { 48 | throw new Exception($"{response.Content}"); 49 | } 50 | } 51 | catch 52 | { 53 | throw new Exception($"{response.Content}"); 54 | } 55 | } 56 | } 57 | 58 | public async Task Get(string identifier) 59 | { 60 | try 61 | { 62 | var request = new RestRequest($"/payments/{identifier}"); 63 | request.AddHeader("Authorization", $"Key {_apiKey}"); 64 | request.AddHeader("Accept", $"application/json"); 65 | request.Method = Method.Get; 66 | var response = await _restClient.ExecuteAsync(request); 67 | if (response.IsSuccessful) 68 | { 69 | return JsonConvert.DeserializeObject(response.Content); 70 | } 71 | else 72 | { 73 | try 74 | { 75 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 76 | if (error != null) 77 | { 78 | throw new PiNetworkException() 79 | { 80 | PiError = error, 81 | }; 82 | } 83 | else 84 | { 85 | throw new Exception($"{response.Content}"); 86 | } 87 | } 88 | catch 89 | { 90 | throw new Exception($"{response.Content}"); 91 | } 92 | } 93 | } 94 | catch { throw; } 95 | } 96 | 97 | public async Task> GetIncompleteServerPayments() 98 | { 99 | try 100 | { 101 | var request = new RestRequest("/payments/incomplete_server_payments"); 102 | request.AddHeader("Accept", $"application/json"); 103 | request.AddHeader("Authorization", $"Key {_apiKey}"); 104 | request.Method = Method.Get; 105 | var response = await _restClient.ExecuteAsync(request); 106 | if (response.IsSuccessful) 107 | { 108 | var payments = JsonConvert.DeserializeObject(response.Content); 109 | if (payments != null && payments.IncompletePayments != null && payments.IncompletePayments.Count > 0) 110 | return payments.IncompletePayments; 111 | } 112 | else 113 | { 114 | try 115 | { 116 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 117 | if (error != null) 118 | { 119 | throw new PiNetworkException() 120 | { 121 | PiError = error, 122 | }; 123 | } 124 | else 125 | { 126 | throw new Exception($"{response.Content}"); 127 | } 128 | } 129 | catch 130 | { 131 | throw new Exception($"{response.Content}"); 132 | } 133 | } 134 | } 135 | catch 136 | { 137 | throw; 138 | } 139 | return null; 140 | } 141 | 142 | public async Task Create(CreatePaymentDto dto) 143 | { 144 | try 145 | { 146 | var request = new RestRequest($"/payments"); 147 | request.AddHeader("Authorization", $"Key {_apiKey}"); 148 | request.AddHeader("Accept", $"application/json"); 149 | request.AddJsonBody(JsonConvert.SerializeObject(dto)); 150 | request.Method = Method.Post; 151 | var response = await _restClient.ExecuteAsync(request); 152 | if (response.IsSuccessful) 153 | { 154 | var payment = JsonConvert.DeserializeObject(response.Content); 155 | if (payment != null 156 | && string.IsNullOrEmpty(payment.ToAddress) 157 | && !string.IsNullOrEmpty(payment.Identifier)) 158 | { 159 | return await Get(payment.Identifier); 160 | } 161 | return payment; 162 | } 163 | else 164 | { 165 | try 166 | { 167 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 168 | if (error != null) 169 | { 170 | throw new PiNetworkException() 171 | { 172 | PiError = error, 173 | }; 174 | } 175 | else 176 | { 177 | throw new Exception($"{response.Content}"); 178 | } 179 | } 180 | catch 181 | { 182 | throw new Exception($"{response.Content}"); 183 | } 184 | } 185 | } 186 | catch 187 | { 188 | throw; 189 | } 190 | } 191 | 192 | public async Task Approve(string identifier) 193 | { 194 | try 195 | { 196 | var request = new RestRequest($"/payments/{identifier}/approve"); 197 | request.AddHeader("Accept", $"application/json"); 198 | request.AddHeader("Authorization", $"Key {_apiKey}"); 199 | request.Method = Method.Post; 200 | var response = await _restClient.ExecuteAsync(request); 201 | if (response.IsSuccessful) 202 | { 203 | return JsonConvert.DeserializeObject(response.Content); 204 | } 205 | else 206 | { 207 | try 208 | { 209 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 210 | if (error != null) 211 | { 212 | throw new PiNetworkException() 213 | { 214 | PiError = error, 215 | }; 216 | } 217 | else 218 | { 219 | throw new Exception($"{response.Content}"); 220 | } 221 | } 222 | catch 223 | { 224 | throw new Exception($"{response.Content}"); 225 | } 226 | } 227 | } 228 | catch { throw; } 229 | } 230 | 231 | public async Task Cancel(string identifier) 232 | { 233 | try 234 | { 235 | var request = new RestRequest($"/payments/{identifier}/cancel"); 236 | request.AddHeader("Accept", $"application/json"); 237 | request.AddHeader("Authorization", $"Key {_apiKey}"); 238 | request.Method = Method.Post; 239 | var response = await _restClient.ExecuteAsync(request); 240 | if (response.IsSuccessful) 241 | { 242 | return JsonConvert.DeserializeObject(response.Content); 243 | } 244 | else 245 | { 246 | try 247 | { 248 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 249 | if (error != null) 250 | { 251 | throw new PiNetworkException() 252 | { 253 | PiError = error, 254 | }; 255 | } 256 | else 257 | { 258 | throw new Exception($"{response.Content}"); 259 | } 260 | } 261 | catch 262 | { 263 | throw new Exception($"{response.Content}"); 264 | } 265 | } 266 | } 267 | catch { throw; } 268 | } 269 | 270 | public async Task Complete(string identifier, string tx) 271 | { 272 | var request = new RestRequest($"/payments/{identifier}/complete"); 273 | request.AddHeader("Accept", $"application/json"); 274 | request.AddHeader("Authorization", $"Key {_apiKey}"); 275 | var txid = new Tx() { TxId = tx }; 276 | request.AddJsonBody(JsonConvert.SerializeObject(txid)); 277 | request.Method = Method.Post; 278 | var response = await _restClient.ExecuteAsync(request); 279 | if (response.IsSuccessful) 280 | { 281 | return JsonConvert.DeserializeObject(response.Content); 282 | } 283 | else 284 | { 285 | try 286 | { 287 | PiNetworkError error = JsonConvert.DeserializeObject(response.Content); 288 | if (error != null) 289 | { 290 | throw new PiNetworkException() 291 | { 292 | PiError = error, 293 | }; 294 | } 295 | else 296 | { 297 | throw new Exception($"{response.Content}"); 298 | } 299 | } 300 | catch 301 | { 302 | throw new Exception($"{response.Content}"); 303 | } 304 | } 305 | } 306 | 307 | 308 | protected async Task GetServerAsync(string network) 309 | { 310 | Server server; 311 | if (network == "Pi Network") 312 | { 313 | server = new Server("https://api.mainnet.minepi.com"); 314 | } 315 | else if (network == "Pi Testnet") 316 | { 317 | server = new Server("https://api.testnet.minepi.com"); 318 | } 319 | else 320 | { 321 | server = new Server("https://horizon-testnet.stellar.org"); 322 | } 323 | await Task.CompletedTask; 324 | return server; 325 | } 326 | 327 | public async Task GetAccountBalance(string network, string account) 328 | { 329 | //Set network and server 330 | Server server = await GetServerAsync(network); 331 | KeyPair keypair; 332 | 333 | //Generate a keypair from the account id. 334 | try 335 | { 336 | if (account.StartsWith("S")) 337 | { 338 | keypair = KeyPair.FromSecretSeed(account); 339 | } 340 | else 341 | { 342 | keypair = KeyPair.FromAccountId(account); 343 | } 344 | } 345 | catch 346 | { 347 | return 0.0; 348 | } 349 | 350 | //Load the account 351 | AccountResponse accountResponse = await server.Accounts.Account(keypair.AccountId); 352 | 353 | //Get the balance 354 | Balance[] balances = accountResponse.Balances; 355 | 356 | //Show the balance 357 | for (int i = 0; i < balances.Length; i++) 358 | { 359 | Balance asset = balances[i]; 360 | Console.WriteLine("Asset Code: " + asset.AssetCode); 361 | Console.WriteLine("Asset Amount: " + asset.BalanceString); 362 | if (asset.AssetType == "native") 363 | { 364 | return double.Parse(asset.BalanceString); 365 | } 366 | } 367 | return 0.0; 368 | } 369 | 370 | public async Task SendNativeAssets(string network, string seed, TransactionData data, uint fee = 100000) 371 | { 372 | //Source keypair from the secret seed 373 | KeyPair sourceKeypair = KeyPair.FromSecretSeed(seed); 374 | return await SendNativeAssets(network, sourceKeypair, data, fee); 375 | } 376 | 377 | public async Task SendNativeAssets(string network, KeyPair sourceKeypair, TransactionData data, uint fee = 100000) 378 | { 379 | //Set network and server 380 | Server server = await GetServerAsync(network); 381 | 382 | //Destination keypair from the account id 383 | KeyPair destinationKeyPair = KeyPair.FromAccountId(data.ToAddress); 384 | 385 | //Load source account data 386 | AccountResponse sourceAccountResponse = await server.Accounts.Account(sourceKeypair.AccountId); 387 | 388 | //Create source account object 389 | Account sourceAccount = new Account(sourceKeypair.AccountId, sourceAccountResponse.SequenceNumber); 390 | 391 | //Create asset object with specific amount 392 | //You can use native or non native ones. 393 | Asset asset = new AssetTypeNative(); 394 | double balance = 0.0; 395 | for (int i = 0; i < sourceAccountResponse.Balances.Length; i++) 396 | { 397 | Balance ast = sourceAccountResponse.Balances[i]; 398 | if (ast.AssetType == "native") 399 | { 400 | if (double.TryParse(ast.BalanceString, out balance)) 401 | break; 402 | } 403 | } 404 | if (balance < data.Amount + 0.01) 405 | { 406 | throw new Exception($"Not enough balance ({balance})"); 407 | } 408 | string amount = $"{Math.Floor(data.Amount * 10000000.0)/ 10000000.0:F7}"; 409 | try 410 | { 411 | //Create payment operation 412 | PaymentOperation operation = new PaymentOperation.Builder(destinationKeyPair, asset, amount) 413 | .SetSourceAccount(sourceAccount.KeyPair) 414 | .Build(); 415 | 416 | var Identifier = string.IsNullOrEmpty(data.Identifier) ? $"" : $"{data.Identifier.Trim()}"; 417 | MemoText memo = new MemoText(string.IsNullOrEmpty(Identifier) ? $"" : Identifier.Substring(0, Math.Min(Identifier.Length, 28))); 418 | //Create transaction and add the payment operation we created 419 | Transaction transaction = new TransactionBuilder(sourceAccount) 420 | .AddOperation(operation) 421 | .AddMemo(memo) 422 | .SetFee(fee) 423 | .Build(); 424 | 425 | //Sign Transaction 426 | transaction.Sign(sourceKeypair, new Network(network)); 427 | 428 | //Try to send the transaction 429 | var tx = await server.SubmitTransaction(transaction); 430 | return tx; 431 | } 432 | catch (Exception exception) 433 | { 434 | Console.WriteLine("Send Transaction Failed"); 435 | Console.WriteLine("Exception: " + exception.Message); 436 | throw; 437 | } 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/PiNetworkData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace PiNetworkNet 6 | { 7 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 8 | public sealed class PiAuthDto 9 | { 10 | [JsonProperty("accessToken")] 11 | public string AccessToken { get; set; } 12 | 13 | [JsonProperty("user")] 14 | public PiUser User { get; set; } 15 | } 16 | 17 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 18 | public sealed class PiUser 19 | { 20 | [JsonProperty("uid")] 21 | public string Uid { get; set; } 22 | 23 | [JsonProperty("credentials")] 24 | public Credentials Credentials { get; set; } 25 | 26 | [JsonProperty("username")] 27 | public string Username { get; set; } 28 | } 29 | 30 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 31 | public sealed class PiValidTime 32 | { 33 | [JsonProperty("timestamp")] 34 | public long TimeStamp { get; set; } 35 | [JsonProperty("iso8601")] 36 | public DateTimeOffset Iso8601 { get; set; } 37 | } 38 | 39 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 40 | public sealed class Credentials 41 | { 42 | [JsonProperty("scopes")] 43 | public List Scopes { get; set; } 44 | [JsonProperty("valid_until")] 45 | public PiValidTime ValidTime { get; set; } 46 | }; 47 | 48 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 49 | public sealed class PaymentDto 50 | { 51 | [JsonProperty("identifier")] 52 | public string Identifier { get; set; } 53 | 54 | [JsonProperty("user_uid")] 55 | public string Useruid { get; set; } 56 | 57 | [JsonProperty("amount")] 58 | public double Amount { get; set; } 59 | 60 | [JsonProperty("memo")] 61 | public string Memo { get; set; } 62 | 63 | [JsonProperty("metadata")] 64 | public Metadata Metadata { get; set; } 65 | 66 | [JsonProperty("from_address")] 67 | public string FromAddress { get; set; } 68 | 69 | [JsonProperty("to_address")] 70 | public string ToAddress { get; set; } 71 | 72 | [JsonProperty("created_at")] 73 | //public string created_at { get; set; } 74 | public DateTimeOffset CreatedAt { get; set; } 75 | 76 | [JsonProperty("direction")] 77 | public string Direction { get; set; } 78 | 79 | [JsonProperty("network")] 80 | public string Network { get; set; } 81 | 82 | [JsonProperty("status")] 83 | public Status Status { get; set; } 84 | 85 | [JsonProperty("transaction")] 86 | public TransactionStatus Transaction { get; set; } 87 | } 88 | 89 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 90 | public sealed class Metadata 91 | { 92 | [JsonProperty("id")] 93 | public Guid? Id { get; set; } 94 | [JsonProperty("cat")] 95 | public string Category { get; set; } 96 | [JsonProperty("data")] 97 | public object Data { get; set; } 98 | } 99 | 100 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 101 | public sealed class Status 102 | { 103 | [JsonProperty("developer_approved")] 104 | public bool DeveloperApproved { get; set; } 105 | 106 | [JsonProperty("transaction_verified")] 107 | public bool TransactionVerified { get; set; } 108 | 109 | [JsonProperty("developer_completed")] 110 | public bool DeveloperCompleted { get; set; } 111 | 112 | [JsonProperty("cancelled")] 113 | public bool Cancelled { get; set; } 114 | 115 | [JsonProperty("user_cancelled")] 116 | public bool UserCancelled { get; set; } 117 | } 118 | 119 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 120 | public sealed class TransactionStatus 121 | { 122 | [JsonProperty("txid")] 123 | public string TxId { get; set; } 124 | 125 | [JsonProperty("verified")] 126 | public bool Verified { get; set; } 127 | 128 | [JsonProperty("_link")] 129 | public string Link { get; set; } 130 | } 131 | 132 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 133 | public sealed class Tx 134 | { 135 | [JsonProperty("txid")] 136 | public string TxId { get; set; } 137 | } 138 | 139 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 140 | public sealed class IncompleteServerPayments 141 | { 142 | [JsonProperty("incomplete_server_payments")] 143 | public List IncompletePayments { get; set; } 144 | } 145 | 146 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 147 | public sealed class PaymentArgs 148 | { 149 | [JsonProperty("amount")] 150 | public double Amount; 151 | [JsonProperty("memo")] 152 | public string Memo; 153 | [JsonProperty("metadata")] 154 | public object Metadata; 155 | [JsonProperty("uid")] 156 | public string Uid; 157 | } 158 | 159 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 160 | public sealed class CreatePaymentDto 161 | { 162 | [JsonProperty("payment")] 163 | public PaymentArgs Payment; 164 | } 165 | 166 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 167 | public sealed class TransactionData 168 | { 169 | public double Amount; 170 | public string Identifier; 171 | public string FromAddress; 172 | public string ToAddress; 173 | } 174 | 175 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 176 | public sealed class PiNetworkError 177 | { 178 | [JsonProperty("error")] 179 | public string ErrorName { get; set; } 180 | 181 | [JsonProperty("error_message")] 182 | public string ErrorMessage { get; set; } 183 | 184 | [JsonProperty("payment")] 185 | public PaymentDto Payment { get; set; } 186 | } 187 | 188 | public class PiNetworkException: Exception 189 | { 190 | public PiNetworkError PiError { get; set; } 191 | } 192 | } 193 | --------------------------------------------------------------------------------