mirror of
https://github.com/EvernodeXRPL/hpcore.git
synced 2026-04-29 15:37:59 +00:00
Added .Net Core ToDo contract example.
This commit is contained in:
150
examples/hpclient/text-client.js
Normal file
150
examples/hpclient/text-client.js
Normal 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
2
examples/todo_contract/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin
|
||||
obj
|
||||
19
examples/todo_contract/DbModel.cs
Normal file
19
examples/todo_contract/DbModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
68
examples/todo_contract/HPHelper.cs
Normal file
68
examples/todo_contract/HPHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
examples/todo_contract/HPModel.cs
Normal file
64
examples/todo_contract/HPModel.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
39
examples/todo_contract/Migrations/20200114133142_InitialCreate.Designer.cs
generated
Normal file
39
examples/todo_contract/Migrations/20200114133142_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
136
examples/todo_contract/Program.cs
Normal file
136
examples/todo_contract/Program.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
examples/todo_contract/ToDoContract.csproj
Normal file
19
examples/todo_contract/ToDoContract.csproj
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user