Skip to content

Instantly share code, notes, and snippets.

@jakintosh
Last active July 7, 2022 14:33
Show Gist options
  • Save jakintosh/8578f39a66e50c4beb24bc34275e6ce5 to your computer and use it in GitHub Desktop.
Save jakintosh/8578f39a66e50c4beb24bc34275e6ce5 to your computer and use it in GitHub Desktop.
A test of calling the holochain conductor from C# (Unity) app
using NativeWebSocket;
using SouthPointe.Serialization.MessagePack;
using System;
using UnityEngine;
namespace Holochain {
public class Holochain : MonoBehaviour {
// types
[Serializable]
private class MyInput {
public string first_name;
public string last_name;
public MyInput ( string firstName, string lastName ) {
first_name = firstName;
last_name = lastName;
}
}
[Serializable]
private class MyOutput {
public string output;
public override string ToString () => $"MyOutput({output})";
}
// data
private bool _holochainIsRunning = false;
private AppConductor conductor;
// lifecycle
private void Awake () {
_holochainIsRunning = true;
conductor = new AppConductor(
dnaHash: DNAHash.Get(),
agentPubKey: AgentPubKey.Get()
);
conductor.OnResponse.AddListener( bytes => {
try {
var formatter = new MessagePackFormatter();
var content = formatter.Deserialize<MyOutput>( bytes );
Debug.Log( $"Holochain: AppConductor received content:\n{content.output}" );
} catch { Debug.Log( "Holochain: Couldn't deserialize zome_call bytes into MyOutput" ); }
} );
}
private void Update () {
if ( _holochainIsRunning ) {
conductor.DispatchMessageQueue();
if ( Input.GetKeyDown( KeyCode.Space ) ) { SendRequest(); }
}
}
protected void OnApplicationQuit () {
if ( _holochainIsRunning ) {
conductor.CloseConnection();
}
}
// functions
private void SendRequest () {
var formatter = new MessagePackFormatter();
var payload = formatter.Serialize(
new MyInput(
firstName: "Mochi",
lastName: "May"
)
);
conductor.CallFunction(
zome: "simple",
function: "say_my_name",
payload: payload
);
}
}
static public class AgentPubKey {
private static string _agentPubKey = "uhCAkt_cNGyYJZIp08b2ZzxoE6EqPndRPb_WwjVkM_mOBcFyq7zCw";
public static string Get () => _agentPubKey;
}
static public class DNAHash {
private static string _dnaHash = "uhC0kTMixTG0lNZCF4SZfQMGozf2WfjQht7E06_wy3h29-zPpWxPQ";
public static string Get () => _dnaHash;
}
static public class CellID {
public static string[] Get ()
=> Create( DNAHash.Get(), AgentPubKey.Get() );
public static string[] Create ( string dnaHash, string agentPubKey )
=> new string[] { dnaHash, agentPubKey };
}
// holochain conductor data types
[Serializable]
public struct InstalledCell {
public byte[][] cell_id;
public string cell_nick;
}
[Serializable]
public struct InstalledAppInfo {
public string installed_app_id;
public InstalledCell[] cell_data;
}
[Serializable]
public class RustEnum<T> {
public string type;
public T data;
public RustEnum ( string type, T data ) {
this.type = type;
this.data = data;
}
}
[Serializable]
public class WireMessage : RustEnum<byte[]> {
public long id;
public WireMessage (
string type,
byte[] data,
long id
) : base( type, data ) {
this.id = id;
}
public override string ToString () => $"{{\n type: \"{type}\",\n id: {id},\n data: {System.Text.Encoding.UTF8.GetString( data )}\n}}";
}
[Serializable]
public class ConductorRequest : WireMessage {
public ConductorRequest ( byte[] data, long id )
: base( type: "Request", data: data, id: id ) { }
}
[Serializable]
public class ZomeCallRequest : RustEnum<ZomeCallRequest.Data> {
[Serializable]
public struct Data {
public string[] cell_id;
public string zome_name;
public string fn_name;
public byte[] payload;
public byte[] cap;
public string provenance;
}
public ZomeCallRequest (
string dnaHash,
string agentPubKey,
string zomeName,
string functionName,
byte[] payload,
string provenance
) : base(
type: "zome_call",
data: new Data {
cell_id = CellID.Create( dnaHash, agentPubKey ),
zome_name = zomeName,
fn_name = functionName,
payload = payload,
cap = null,
provenance = provenance
}
) { }
}
[Serializable]
public class ConductorResponse {
public string type;
public long id;
public byte[] data;
public override string ToString () => $"{{\n type: \"{type}\",\n id: {id},\n data: {System.Text.Encoding.UTF8.GetString( data )}\n}}";
}
[Serializable]
public class ZomeCallResponse {
public string type;
public byte[] data;
public override string ToString () => $"{{\n type: \"{type}\",\n data: {System.Text.Encoding.UTF8.GetString( data )}\n}}";
}
public abstract class Conductor {
// public methods
public async void OpenConnection ( int port ) {
var url = $"ws://localhost:{port}";
websocket = new WebSocket( url );
websocket.OnOpen += OnOpen;
websocket.OnMessage += OnMessage;
websocket.OnError += OnError;
websocket.OnClose += OnClose;
Debug.Log( $"Holochain: Opening connection to conductor at `{url}`" );
await websocket.Connect();
await websocket.Receive();
}
public void DispatchMessageQueue () {
#if !UNITY_WEBGL || UNITY_EDITOR
websocket.DispatchMessageQueue();
#endif
}
public async void CloseConnection () {
await websocket.Close();
}
// internal requests
private long _requestId = 1;
protected WebSocket websocket;
protected async void SendRequest ( byte[] payload ) {
var formatter = new MessagePackFormatter();
var request = new ConductorRequest(
id: _requestId++,
data: payload
);
var packedRequest = formatter.Serialize( request );
Debug.Log( $"Holochain: Sending request\n{request.ToString()}" );
await websocket.Send( packedRequest );
Debug.Log( $"Holochain: Conductor sent request\n{System.Text.Encoding.UTF8.GetString( packedRequest )}" );
}
// protected handlers
protected abstract void OnOpen ();
protected abstract void OnMessage ( byte[] bytes );
protected abstract void OnError ( string error );
protected abstract void OnClose ( WebSocketCloseCode code );
}
public class AdminConductor : Conductor {
public AdminConductor () => OpenConnection( port: 63125 );
protected override void OnOpen () => Debug.Log( "Holochain: AdminConductor websocket connection opened" );
protected override void OnMessage ( byte[] bytes ) => Debug.Log( $"Holochain: AdminConductor received bytes:\n{bytes}" );
protected override void OnError ( string error ) => Debug.Log( $"Holochain: AdminConductor received error:\n{error}" );
protected override void OnClose ( WebSocketCloseCode code ) => Debug.Log( $"Holochain: AdminConductor closed with code {code}" );
}
public class AppConductor : Conductor {
public UnityEngine.Events.UnityEvent<byte[]> OnResponse = new UnityEngine.Events.UnityEvent<byte[]>();
// constructor
public AppConductor ( string dnaHash, string agentPubKey ) {
this.dnaHash = dnaHash;
this.agentPubKey = agentPubKey;
OpenConnection( port: 8888 );
}
// public methods
public void CallFunction ( string zome, string function, byte[] payload ) {
if ( websocket.State != WebSocketState.Open ) {
Debug.Log( $"Holochain: AppConductor couldn't call `{zome}/{function},` websocket connection not open" );
return;
}
var formatter = new MessagePackFormatter();
var zomeCallPayload = formatter.Serialize<ZomeCallRequest>(
new ZomeCallRequest(
dnaHash: dnaHash,
agentPubKey: agentPubKey,
zomeName: zome,
functionName: function,
payload: payload,
provenance: agentPubKey
)
);
SendRequest( zomeCallPayload );
}
// event handlers
protected override void OnOpen ()
=> Debug.Log( "Holochain: AppConductor websocket connection opened" );
protected override void OnError ( string error )
=> Debug.Log( $"Holochain: AppConductor received error: {error}" );
protected override void OnClose ( WebSocketCloseCode code )
=> Debug.Log( $"Holochain: AppConductor closed with code {code}" );
protected override void OnMessage ( byte[] bytes ) {
try {
var formatter = new MessagePackFormatter();
Debug.Log( $"Holochain: AppConductor received msgpack bytes: {System.Text.Encoding.UTF8.GetString( bytes )}" );
var response = formatter.Deserialize<ConductorResponse>( bytes );
Debug.Log( $"Holochain: AppConductor received response:\n{response}" );
try {
var zomeResponse = formatter.Deserialize<ZomeCallResponse>( response.data );
Debug.Log( $"Holochain: AppConductor received content:\n{zomeResponse}" );
OnResponse?.Invoke( zomeResponse.data );
} catch { Debug.Log( "Holochain: Couldn't deserialize ConductorResponse bytes into ZomeCallResponse" ); }
} catch { Debug.Log( "Holochain: Couldn't deserialize received message bytes into ConductorResponse" ); }
}
// private data
private string dnaHash;
private string agentPubKey;
}
}
use hdk::prelude::*;
#[derive(Serialize, Deserialize, Debug)]
pub struct SomeExternalInput {
first_name: String,
last_name: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SomeExternalOutput {
output: String,
}
#[hdk_extern]
pub fn say_my_name(external_input: SomeExternalInput) -> ExternResult<SomeExternalOutput> {
let message: String = format!(
"Your name is {} {}",
external_input.first_name, external_input.last_name
);
let output: SomeExternalOutput = SomeExternalOutput { output: message };
Ok(output)
}
@jakintosh
Copy link
Author

Wow, yeah, this was not the final permutation of what I have locally–the real downside of a gist, I guess.

It's now updated to reflect what was actually the final version of this that was working (as of almost a year ago).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment