Commit a938fa3c authored by Jeremy Bokobza's avatar Jeremy Bokobza Committed by GitHub

Merge pull request #15 from stratisproject/feature/endpoints

Added some API endpoints
parents 76cff8d4 79afd44e
{
"projects": [ "src" ]
"projects": [ "src" ],
"sdk": {
"version": "1.0.0-preview2-003131"
}
}
......@@ -21,7 +21,7 @@ namespace Breeze.Api.Tests
var controller = new WalletController(mockWalletCreate.Object);
// Act
var result = controller.Create(new WalletCreationModel
var result = controller.Create(new WalletCreationRequest
{
Name = "myName",
FolderPath = "",
......@@ -52,7 +52,7 @@ namespace Breeze.Api.Tests
var controller = new WalletController(mockWalletWrapper.Object);
// Act
var result = controller.Recover(new WalletRecoveryModel
var result = controller.Recover(new WalletRecoveryRequest
{
Name = "myName",
FolderPath = "",
......@@ -87,7 +87,7 @@ namespace Breeze.Api.Tests
var controller = new WalletController(mockWalletWrapper.Object);
// Act
var result = controller.Load(new WalletLoadModel
var result = controller.Load(new WalletLoadRequest
{
Name = "myName",
FolderPath = "",
......@@ -113,7 +113,7 @@ namespace Breeze.Api.Tests
var controller = new WalletController(mockWalletWrapper.Object);
// Act
var result = controller.Load(new WalletLoadModel
var result = controller.Load(new WalletLoadRequest
{
Name = "myName",
FolderPath = "",
......
......@@ -2,7 +2,7 @@
"variables": [],
"info": {
"name": "Wallet",
"_postman_id": "11f915f1-7aac-bfeb-000c-b66348f4636f",
"_postman_id": "c9001498-0a8d-e095-7b3e-ba9f2b0df7e7",
"description": "Requests relating to operations on the wallet",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
},
......@@ -50,8 +50,8 @@
{
"name": "Load wallet",
"request": {
"url": "http://localhost:5000/api/v1/wallet/?password=123456&folderPath=MyWallets&name=myFirstWallet",
"method": "GET",
"url": "http://localhost:5000/api/v1/wallet/load",
"method": "POST",
"header": [
{
"key": "Content-Type",
......@@ -61,7 +61,7 @@
],
"body": {
"mode": "raw",
"raw": "{ \n\t\"password\": \"123456\",\n\t\"network\": \"Main\",\n\t\"folderPath\": \"Wallets\",\n\t\"name\": \"myFirstWadllet\"\n}"
"raw": "{ \n\t\"password\": \"123456\",\n\t\"folderPath\": \"Wallets\",\n\t\"name\": \"myFirstWadllet\"\n}"
},
"description": ""
},
......@@ -86,6 +86,97 @@
"description": ""
},
"response": []
},
{
"name": "Get wallet Info",
"request": {
"url": "http://localhost:5000/api/v1/wallet/info?name=mywallet",
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {},
"description": ""
},
"response": []
},
{
"name": "Get Wallet History",
"request": {
"url": "http://localhost:5000/api/v1/wallet/history?name=mywallet",
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {},
"description": ""
},
"response": []
},
{
"name": "Get wallet balance",
"request": {
"url": "http://localhost:5000/api/v1/wallet/balance?name=mywallet",
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {},
"description": ""
},
"response": []
},
{
"name": "Build transaction",
"request": {
"url": "http://localhost:5000/api/v1/wallet/build-transaction",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"password\": \"password\",\r\n \"address\": \"1FYp9uguYCz7DgSF9jTWDeZF8kdRKQTXPg\",\r\n \"amount\": \"0.12\",\r\n \"feeType\": \"low\",\r\n \"allowUnconfirmed\": \"true\"\r\n}"
},
"description": ""
},
"response": []
},
{
"name": "Send transaction",
"request": {
"url": "http://localhost:5000/api/v1/wallet/send-transaction",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"hex\": \"01000000061b1ca819e76f9131b23335ec905ffc5fc27e36a7843a5b7c6d1b455b904359f7000000006b483045022100c11f78ce7f02b2312b6675d3ad99cec6ede879d446c2b14628ef4f8ce9b3fdc5022073649a14971568a1cd2aa84b5dd404645f29e49882f60a9642850539443872fe012102a41e4348bb233e40cf3a3402e2dc92a31b69ef56090fff242aa7e4bff828929fffffffff1d3c389af5fdd047e307e5d5f87656bb2ef0c40b6ee879d342a59192090d3fbc000000006b483045022100dc7e0445fe98f3e76d68906c640ecca597598a03b48e6b85d72918347b9da7330220340ce9e9533ea84375a1f2122b7868b8ab556da53f1e1af14d1a71b0b123aade012102a41e4348bb233e40cf3a3402e2dc92a31b69ef56090fff242aa7e4bff828929fffffffff1ed624bad3df9d3a7be56dcd5d97c996fccc78164f16f59658b33f8da8859deb000000006b483045022100ba2a55f55a37b6712dd25dbef411aed869190ef60a208b39d4bd8e0ce8635b4d02201976d63489e23205aab651a9def43d6b3a740ba06de2ecabc43504241a71f229012102a41e4348bb233e40cf3a3402e2dc92a31b69ef56090fff242aa7e4bff828929fffffffff24e5cb4893beb0bb60193dbe11a9778d07e127c1cbc939c0a0388b1013ef75d9000000006a47304402203c27eea34db0ba070bee38d625d2cfcec1a0f5d8a9124023c84e9963d37f6145022015f7657cc57be515e6aa43c93c73457f5583b7c90c0af4e8a2f913257df27b0b012102a41e4348bb233e40cf3a3402e2dc92a31b69ef56090fff242aa7e4bff828929fffffffff997e6738c45eaf7af8bed7dc09e258139bffb0d2be8b4167473b6943adc0b28b000000006a47304402202d4c6df39b725d571d67bef14f0c6baa0cf4b93aa54aac2d2a15d3d940510d0602203643162545d5b63c007986627a317ed962f4d5023e4c15e9636a4eede86930c7012102a41e4348bb233e40cf3a3402e2dc92a31b69ef56090fff242aa7e4bff828929fffffffffcff2021b6b0bcd2a8b38583539dc140b98da4f41ae1e4adb089dc2cf3b66d6c6000000006a473044022019d5264c99145c7203e690fb2f57b0e218af2761e024f9ec1b774c703939b96e02204c2430fc4ae0fa43afb19a722f7b5d706bf5f2d5ee85229cbdc7a7b26433f5fd012102a41e4348bb233e40cf3a3402e2dc92a31b69ef56090fff242aa7e4bff828929fffffffff018f73e606000000001976a914ec093b0943ec524769553e1b7261b67ecab47e8688ac00000000\"\n}"
},
"description": ""
},
"response": []
}
]
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security;
using Breeze.Wallet.Errors;
using Microsoft.AspNetCore.Mvc;
using Breeze.Wallet.Models;
using Breeze.Wallet.Wrappers;
......@@ -24,14 +26,15 @@ namespace Breeze.Wallet.Controllers
/// </summary>
/// <param name="walletCreation">The object containing the parameters used to create the wallet.</param>
/// <returns>A JSON object containing the mnemonic created for the new wallet.</returns>
[Route("create")]
[HttpPost]
public IActionResult Create([FromBody]WalletCreationModel walletCreation)
public IActionResult Create([FromBody]WalletCreationRequest walletCreation)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return this.BadRequest(string.Join(Environment.NewLine, errors));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
......@@ -41,21 +44,20 @@ namespace Breeze.Wallet.Controllers
}
catch (NotSupportedException e)
{
Console.WriteLine(e);
// indicates that this wallet already exists
return this.StatusCode((int) HttpStatusCode.Conflict, "This wallet already exists.");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Conflict, "This wallet already exists.", e.ToString());
}
}
[HttpGet]
public IActionResult Load([FromQuery]WalletLoadModel walletLoad)
[Route("load")]
[HttpPost]
public IActionResult Load([FromBody]WalletLoadRequest walletLoad)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return this.BadRequest(string.Join(Environment.NewLine, errors));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
......@@ -66,34 +68,28 @@ namespace Breeze.Wallet.Controllers
}
catch (FileNotFoundException e)
{
Console.WriteLine(e);
// indicates that this wallet does not exist
return this.StatusCode((int)HttpStatusCode.NotFound, "Wallet not found.");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Conflict, "This wallet already exists.", e.ToString());
}
catch (SecurityException e)
{
Console.WriteLine(e);
// indicates that the password is wrong
return this.StatusCode((int)HttpStatusCode.Forbidden, "Wrong password, please try again.");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Forbidden, "Wrong password, please try again.", e.ToString());
}
catch (Exception e)
{
Console.WriteLine(e);
return this.StatusCode((int)HttpStatusCode.BadRequest, e.Message);
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
[Route("recover")]
[HttpPost]
public IActionResult Recover([FromBody]WalletRecoveryModel walletRecovery)
public IActionResult Recover([FromBody]WalletRecoveryRequest walletRecovery)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return this.BadRequest(string.Join(Environment.NewLine, errors));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
......@@ -104,22 +100,132 @@ namespace Breeze.Wallet.Controllers
}
catch (FileNotFoundException e)
{
Console.WriteLine(e);
// indicates that this wallet does not exist
return this.StatusCode((int)HttpStatusCode.NotFound, "Wallet not found.");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.NotFound, "Wallet not found.", e.ToString());
}
catch (SecurityException e)
{
Console.WriteLine(e);
// indicates that the password is wrong
return this.StatusCode((int)HttpStatusCode.Forbidden, "Wrong password, please try again.");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.Forbidden, "Wrong password, please try again.", e.ToString());
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
[Route("info")]
[HttpGet]
public IActionResult GetInfo([FromQuery] WalletName model)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
{
return this.Json(this.walletWrapper.GetInfo(model.Name));
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
[Route("history")]
[HttpGet]
public IActionResult GetHistory([FromQuery] WalletName model)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
{
return this.Json(this.walletWrapper.GetHistory(model.Name));
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
[Route("balance")]
[HttpGet]
public IActionResult GetBalance([FromQuery] WalletName model)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
{
return this.Json(this.walletWrapper.GetBalance(model.Name));
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
[Route("build-transaction")]
[HttpPost]
public IActionResult BuildTransaction([FromBody] BuildTransactionRequest request)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
{
return this.Json(this.walletWrapper.BuildTransaction(request.Password, request.Address, request.Amount, request.FeeType, request.AllowUnconfirmed));
}
catch (Exception e)
{
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
[Route("send-transaction")]
[HttpPost]
public IActionResult SendTransaction([FromBody] SendTransactionRequest request)
{
// checks the request is valid
if (!this.ModelState.IsValid)
{
var errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
}
try
{
var result = this.walletWrapper.SendTransaction(request.Hex);
if (result)
{
return this.Ok();
}
return this.StatusCode((int)HttpStatusCode.BadRequest);
}
catch (Exception e)
{
Console.WriteLine(e);
return this.StatusCode((int)HttpStatusCode.BadRequest, e.Message);
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
}
......
using System.Collections.Generic;
using System.Net;
namespace Breeze.Wallet.Errors
{
public static class ErrorHelpers
{
public static ErrorResult BuildErrorResponse(HttpStatusCode statusCode, string message, string description)
{
ErrorResponse errorResponse = new ErrorResponse
{
Errors = new List<ErrorModel>
{
new ErrorModel
{
Status = (int) statusCode,
Message = message,
Description = description
}
}
};
return new ErrorResult((int)statusCode, errorResponse);
}
}
}
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Breeze.Wallet.Errors
{
public class ErrorResponse
{
[JsonProperty(PropertyName = "errors")]
public List<ErrorModel> Errors { get; set; }
}
public class ErrorModel
{
[JsonProperty(PropertyName = "status")]
public int Status { get; set; }
[JsonProperty(PropertyName = "message")]
public string Message { get; set; }
[JsonProperty(PropertyName = "description")]
public string Description { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Breeze.Wallet.Errors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Breeze.Wallet.Errors
{
public class ErrorResult : ObjectResult
{
public ErrorResult(int statusCode, ErrorResponse value) : base(value)
{
StatusCode = statusCode;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletBalanceModel
{
[JsonProperty(PropertyName = "isSynced")]
public bool IsSynced { get; set; }
[JsonProperty(PropertyName = "confirmed")]
public Money Confirmed { get; set; }
[JsonProperty(PropertyName = "unconfirmed")]
public Money Unconfirmed { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletBuildTransactionModel
{
[JsonProperty(PropertyName = "spendsUnconfirmed")]
public bool SpendsUnconfirmed { get; set; }
[JsonProperty(PropertyName = "fee")]
public Money Fee { get; set; }
[JsonProperty(PropertyName = "feePercentOfSent")]
public double FeePercentOfSent { get; set; }
[JsonProperty(PropertyName = "hex")]
public string Hex { get; set; }
[JsonProperty(PropertyName = "transaction")]
public Transaction Transaction { get; set; }
}
public class Transaction
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "isCoinbase")]
public bool IsCoinbase { get; set; }
[JsonProperty(PropertyName = "block")]
public string Block { get; set; }
[JsonProperty(PropertyName = "spentCoins")]
public IEnumerable<TransactionDetails> SpentCoins { get; set; }
[JsonProperty(PropertyName = "receivedCoins")]
public IEnumerable<TransactionDetails> ReceivedCoins { get; set; }
[JsonProperty(PropertyName = "firstSendDate")]
public DateTime FirstSeenDate { get; set; }
[JsonProperty(PropertyName = "fees")]
public Money Fees { get; set; }
}
public class TransactionDetails
{
[JsonProperty(PropertyName = "transactionId")]
public string TransactionId { get; set; }
[JsonProperty(PropertyName = "index")]
public int Index { get; set; }
[JsonProperty(PropertyName = "value")]
public int Value { get; set; }
[JsonProperty(PropertyName = "scriptPubKey")]
public string ScriptPubKey { get; set; }
[JsonProperty(PropertyName = "redeemScript")]
public string RedeemScript { get; set; }
}
}
......@@ -7,7 +7,7 @@ namespace Breeze.Wallet.Models
/// <summary>
/// Object used to create a new wallet
/// </summary>
public class WalletCreationModel
public class WalletCreationRequest
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
......@@ -21,7 +21,7 @@ namespace Breeze.Wallet.Models
public string Name { get; set; }
}
public class WalletLoadModel
public class WalletLoadRequest
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
......@@ -33,7 +33,7 @@ namespace Breeze.Wallet.Models
public string Name { get; set; }
}
public class WalletRecoveryModel
public class WalletRecoveryRequest
{
[Required(ErrorMessage = "A mnemonic is required.")]
public string Mnemonic { get; set; }
......@@ -49,4 +49,34 @@ namespace Breeze.Wallet.Models
public string Network { get; set; }
}
public class WalletName
{
[Required(ErrorMessage = "The name of the wallet is missing.")]
public string Name { get; set; }
}
public class BuildTransactionRequest
{
[Required(ErrorMessage = "A password is required.")]
public string Password { get; set; }
[Required(ErrorMessage = "A destination address is required.")]
public string Address { get; set; }
[Required(ErrorMessage = "An amount is required.")]
public string Amount { get; set; }
[Required(ErrorMessage = "A fee type is required. It can be 'low', 'medium' or 'high'.")]
public string FeeType { get; set; }
public bool AllowUnconfirmed { get; set; }
}
public class SendTransactionRequest
{
[Required(ErrorMessage = "A transaction in hexadecimal format is required.")]
public string Hex { get; set; }
}
}
using System.Collections.Generic;
using NBitcoin;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletHistoryModel
{
[JsonProperty(PropertyName = "transactions")]
public List<TransactionItem> Transactions { get; set; }
}
public class TransactionItem
{
[JsonProperty(PropertyName = "txId")]
public string TransactionId { get; set; }
[JsonProperty(PropertyName = "amount")]
public Money Amount { get; set; }
[JsonProperty(PropertyName = "confirmed")]
public Money Confirmed { get; set; }
[JsonProperty(PropertyName = "timestamp")]
public string Timestamp { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Breeze.Wallet.Models
{
public class WalletInfoModel
{
[JsonProperty(PropertyName = "filePath")]
public string FilePath { get; set; }
[JsonProperty(PropertyName = "encryptedSeed")]
public string EncryptedSeed { get; set; }
[JsonProperty(PropertyName = "chainCode")]
public string ChainCode { get; set; }
[JsonProperty(PropertyName = "network")]
public string Network { get; set; }
[JsonProperty(PropertyName = "creationTime")]
public string CreationTime { get; set; }
[JsonProperty(PropertyName = "isDecrypted")]
public bool IsDecrypted { get; set; }
[JsonProperty(PropertyName = "uniqueId")]
public string UniqueId { get; set; }
}
}
......@@ -2,15 +2,19 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Breeze.Wallet
namespace Breeze.Wallet.Models
{
public class WalletModel
{
[JsonProperty(PropertyName = "network")]
public string Network { get; set; }
[JsonProperty(PropertyName = "fileName")]
public string FileName { get; set; }
[JsonProperty(PropertyName = "addresses")]
public IEnumerable<string> Addresses { get; set; }
}
}

using Breeze.Wallet.Models;
using HBitcoin.Models;
using NBitcoin;
namespace Breeze.Wallet.Wrappers
{
/// <summary>
......@@ -6,10 +10,21 @@ namespace Breeze.Wallet.Wrappers
/// </summary>
public interface IWalletWrapper
{
string Create(string password, string folderPath, string name, string network);
WalletModel Load(string password, string folderPath, string name);
WalletModel Recover(string password, string folderPath, string name, string network, string mnemonic);
WalletInfoModel GetInfo(string walletName);
WalletBalanceModel GetBalance(string walletName);
WalletHistoryModel GetHistory(string walletName);
WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType, bool allowUnconfirmed);
bool SendTransaction(string transactionHex);
}
}
using System.IO;
using System.Linq;
using Breeze.Wallet.Models;
using HBitcoin.KeyManagement;
using NBitcoin;
......@@ -79,5 +80,31 @@ namespace Breeze.Wallet.Wrappers
return Network.TestNet;
}
}
public WalletInfoModel GetInfo(string name)
{
throw new System.NotImplementedException();
}
public WalletBalanceModel GetBalance(string walletName)
{
throw new System.NotImplementedException();
}
public WalletHistoryModel GetHistory(string walletName)
{
throw new System.NotImplementedException();
}
public WalletBuildTransactionModel BuildTransaction(string password, string address, Money amount, string feeType,
bool allowUnconfirmed)
{
throw new System.NotImplementedException();
}
public bool SendTransaction(string transactionHex)
{
throw new System.NotImplementedException();
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment