Added .Net Core ToDo contract example.

This commit is contained in:
ravinsp
2020-01-15 11:20:28 +05:30
parent b6497d0f82
commit f8dd2e014b
12 changed files with 567 additions and 2 deletions

View File

@@ -0,0 +1,150 @@
//
// HotPocket client example code adopted from:
// https://github.com/codetsunami/hotpocket/blob/master/hp_client.js
//
const fs = require('fs')
const ws_api = require('ws');
const sodium = require('libsodium-wrappers')
const readline = require('readline')
// sodium has a trigger when it's ready, we will wait and execute from there
sodium.ready.then(main).catch((e) => { console.log(e) })
function main() {
var keys = sodium.crypto_sign_keypair()
// check for client keys
if (!fs.existsSync('.hp_client_keys')) {
keys.privateKey = sodium.to_hex(keys.privateKey)
keys.publicKey = sodium.to_hex(keys.publicKey)
fs.writeFileSync('.hp_client_keys', JSON.stringify(keys))
} else {
keys = JSON.parse(fs.readFileSync('.hp_client_keys'))
keys.privateKey = Uint8Array.from(Buffer.from(keys.privateKey, 'hex'))
keys.publicKey = Uint8Array.from(Buffer.from(keys.publicKey, 'hex'))
}
var server = 'wss://localhost:8080'
if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2]
if (process.argv.length == 4) server = 'wss://' + process.argv[2] + ':' + process.argv[3]
var ws = new ws_api(server, {
rejectUnauthorized: false
})
/* anatomy of a public challenge
{
version: '0.1',
type: 'public_challenge',
challenge: '<hex string>'
}
*/
// if the console ctrl + c's us we should close ws gracefully
process.once('SIGINT', function (code) {
console.log('SIGINT received...');
ws.close()
});
function create_input_container(inp) {
let inp_container = {
nonce: (new Date()).getTime().toString(),
input: Buffer.from(inp).toString('hex'),
max_ledger_seqno: 9999999
}
let inp_container_bytes = JSON.stringify(inp_container);
let sig_bytes = sodium.crypto_sign_detached(inp_container_bytes, keys.privateKey);
let signed_inp_container = {
type: "contract_input",
content: inp_container_bytes.toString('hex'),
sig: Buffer.from(sig_bytes).toString('hex')
}
return JSON.stringify(signed_inp_container);
}
function create_status_request() {
let statreq = { type: 'stat' }
return JSON.stringify(statreq);
}
function handle_public_challange(m) {
let pkhex = 'ed' + Buffer.from(keys.publicKey).toString('hex');
console.log('My public key is: ' + pkhex);
// sign the challenge and send back the response
var sigbytes = sodium.crypto_sign_detached(m.challenge, keys.privateKey);
var response = {
type: 'challenge_resp',
challenge: m.challenge,
sig: Buffer.from(sigbytes).toString('hex'),
pubkey: pkhex
}
ws.send(JSON.stringify(response))
// start listening for stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("Ready to accept inputs.")
// Capture user input from the console.
var input_pump = () => {
rl.question('', (inp) => {
let msgtosend = "";
if (inp == "stat")
msgtosend = create_status_request();
else
msgtosend = create_input_container(inp);
ws.send(msgtosend)
input_pump()
})
}
input_pump();
}
ws.on('message', (data) => {
try {
m = JSON.parse(data)
} catch (e) {
console.log("Exception: " + data);
return
}
if (m.type == 'public_challenge') {
handle_public_challange(m);
}
else if (m.type == 'contract_output') {
console.log(Buffer.from(m.content, 'hex').toString());
}
else if (m.type == 'request_status_result') {
if (m.status != "accepted")
console.log("Input status: " + m.status);
}
else {
console.log(m);
}
});
ws.on('close', () => {
console.log('Server disconnected.');
});
}

2
examples/todo_contract/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin
obj

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
namespace ToDoContract
{
public class DataContext : DbContext
{
public DbSet<ToDoEntry> ToDoEntries { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=state/todo.db");
}
public class ToDoEntry
{
public int Id { get; set; }
public string Content { get; set; }
public string CreatedBy { get; set; }
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Mono.Unix.Native;
using Newtonsoft.Json;
namespace HotPocket
{
public static class HotPocketHelper
{
const int FD_READ_BUFFER_LEN = 1024;
public static async Task<ContractArgs> GetContractArgsAsync()
{
using (var s = new StreamReader(Console.OpenStandardInput()))
{
var input = await s.ReadToEndAsync();
var contractArgs = JsonConvert.DeserializeObject<ContractArgs>(input);
return contractArgs;
}
}
public static string ReadStringFromFD(int fd)
{
return Encoding.UTF8.GetString(ReadBytesFromFD(fd));
}
public static void WriteStringToFD(int fd, string str)
{
WriteBytesToFD(fd, Encoding.UTF8.GetBytes(str));
}
public static unsafe byte[] ReadBytesFromFD(int fd)
{
// Keep reading the fd bytes and fill the memory stream until no more bytes.
using (var ms = new MemoryStream())
{
int readbytes = 0;
do
{
var buffer = new byte[FD_READ_BUFFER_LEN];
fixed (byte* p = buffer)
{
IntPtr ptr = (IntPtr)p;
readbytes = (int)Syscall.read(fd, ptr, FD_READ_BUFFER_LEN);
}
ms.Write(buffer, 0, readbytes);
} while (readbytes == FD_READ_BUFFER_LEN);
return ms.ToArray();
}
}
public static unsafe void WriteBytesToFD(int fd, byte[] buffer)
{
fixed (byte* p = buffer)
{
IntPtr ptr = (IntPtr)p;
Syscall.write(fd, ptr, (ulong)buffer.Length);
}
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace HotPocket
{
public class ContractArgs
{
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("pubkey")]
public string PubKey { get; set; }
[JsonProperty("ts")]
public string Timestamp { get; set; }
[JsonProperty("hpfd")]
public IOPipe HotPocketPipe { get; set; }
[JsonProperty("nplfd")]
public IOPipe NplPipe { get; set; }
[JsonProperty("usrfd")]
public Dictionary<string, IOPipe> UserPipes { get; set; }
[JsonProperty("unl")]
public string[] Unl { get; set; }
}
[JsonConverter(typeof(IOPipeJsonConverter))]
public class IOPipe
{
public int ReadFD { get; set; }
public int WriteFD { get; set; }
}
public class IOPipeJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IOPipe);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var array = JArray.Load(reader);
var pipe = (existingValue as IOPipe ?? new IOPipe());
pipe.ReadFD = (int)array.ElementAtOrDefault(0);
pipe.WriteFD = (int)array.ElementAtOrDefault(1);
return pipe;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var pipe = (IOPipe)value;
serializer.Serialize(writer, new[] { pipe.ReadFD, pipe.WriteFD });
}
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using ToDoContract;
namespace ToDoContract.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20200114133142_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0");
modelBuilder.Entity("ToDoContract.ToDoEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ToDoEntries");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ToDoContract.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ToDoEntries",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Content = table.Column<string>(nullable: true),
CreatedBy = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ToDoEntries", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ToDoEntries");
}
}
}

View File

@@ -0,0 +1,37 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using ToDoContract;
namespace ToDoContract.Migrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0");
modelBuilder.Entity("ToDoContract.ToDoEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ToDoEntries");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using HotPocket;
using System.Linq;
namespace ToDoContract
{
/*
* This is a simple multi-user ToDo list contract which uses sqlite database as storage.
* In order to run this .Net Core should be installed on the system. If using docker,
* mcr.microsoft.com/dotnet/core/sdk:3.1 docker image must be used.
*
* Produce deployable output with: dotnet publish -c Release
*
* User inputs can be submitted in the following format.
* Insert a new ToDo record: add <title>
* Retrieve all records owned by user: get all
* Retrieve record by ID: get <id>
* Delete all records owned by user: delete all
* Delete record by ID: delete <id>
*/
public class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Starting .Net ToDo contract");
using (var dataContext = new DataContext())
{
dataContext.Database.Migrate();
}
ContractArgs contractArgs = await HotPocketHelper.GetContractArgsAsync();
foreach (var user in contractArgs.UserPipes)
{
var pubkey = user.Key;
var pipe = user.Value;
var input = HotPocketHelper.ReadStringFromFD(pipe.ReadFD);
if (string.IsNullOrEmpty(input))
continue;
var output = await HandleUserInputAsync(pubkey, input);
HotPocketHelper.WriteStringToFD(pipe.WriteFD, output);
}
}
static async Task<string> HandleUserInputAsync(string userId, string input)
{
var parts = input.Trim().Split(' ', 2);
if (parts.Length < 2)
return "Invalid input format";
var command = parts[0].ToLower();
var param = parts[1];
using (var dataContext = new DataContext())
{
if (command == "add") // add new record.
{
var entry = new ToDoEntry
{
Content = param,
CreatedBy = userId
};
dataContext.ToDoEntries.Add(entry);
await dataContext.SaveChangesAsync();
return "Added entry with id " + entry.Id;
}
else if (command == "get" || command == "delete")
{
if (param == "all") // get/delete all records.
{
// Get all entries belonging to this user.
var entries = await dataContext.ToDoEntries.Where(e => e.CreatedBy == userId).OrderBy(e => e.Id).ToListAsync();
if (command == "get")
{
return JsonConvert.SerializeObject(entries.Select(e => $"ID-{e.Id}: {e.Content}"));
}
else
{
// Delete all records for this user.
dataContext.RemoveRange(entries);
await dataContext.SaveChangesAsync();
return $"{entries.Count} record(s) deleted";
}
}
else // get/delete by ID.
{
int id = 0;
if (int.TryParse(param, out id))
{
var entry = await dataContext.ToDoEntries.FirstOrDefaultAsync(e => e.Id == id);
if (entry == null)
{
return $"Record id {id} does not exist";
}
else if (entry.CreatedBy != userId)
{
return $"You do not have permission for record id {id}";
}
else
{
if (command == "get")
{
return entry.Content;
}
else
{
dataContext.Remove(entry);
await dataContext.SaveChangesAsync();
return $"Record id {id} deleted";
}
}
}
else
{
return "Invalid record id";
}
}
}
else
{
return "Invalid command";
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
</Project>

View File

@@ -146,9 +146,9 @@ void create_request_status_result(std::string &msg, std::string_view status, std
.append(reason)
.append(SEP_COMMA)
.append(FLD_ORIGIN)
.append(":{")
.append("\":{\"")
.append(FLD_TYPE)
.append(SEP_COMMA)
.append(SEP_COLON)
.append(origin_type)
.append("\"")
.append(origin_extra_data)

View File

@@ -67,6 +67,7 @@ do
mkdir ./node$n/bin
cp ../../../examples/echo_contract/contract.js ./node$n/bin/contract.js
cp ../bin/appbill ./node$n/bin/
# cp -r ../../../examples/todo_contract/bin/Release/netcoreapp3.1/publish/* ./node$n/bin/
done
# Function to generate JSON array string while skiping a given index.