Skip to content

feat(webgl): native controller support & interop #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ crashlytics-build.properties
.DS_Store

# builds
dojo-webgl
dojo-ios
dojo-macos
dojo-windows
webgl
/dojo-webgl
/dojo-ios
/dojo-macos
/dojo-windows
/webgl
95 changes: 95 additions & 0 deletions Assets/Dojo/Plugins/WebGL/controller.jslib
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
mergeInto(LibraryManager.library, {
ControllerProbe: function (cb) {
window.starknet_controller.probe().then((account) => {
dynCall_vi(cb, account ? true : false);
});
},
ControllerConnect: async function (rpcUrl, policies, chainId, cb) {
rpcUrl = UTF8ToString(rpcUrl);
chainId = UTF8ToString(chainId);
// chain id is optional
if (chainId == "") {
const provider = new wasm_bindgen.Provider(rpcUrl);
chainId = await provider.chainId();
}
policies = JSON.parse(UTF8ToString(policies));

const opts = {
chains: [{ rpcUrl }],
defaultChainId: chainId,
policies: {
contracts: policies.reduce((acc, policy) => {
if (!acc[policy.target]) {
// If target doesn't exist, initialize it
acc[policy.target] = {
methods: [],
name: 'Unity Game',
description: 'Unity game actions',
};
}
// Append methods from the current policy to the target's methods array
acc[policy.target].methods.push({
entrypoint: policy.method,
description: policy.description,
});
return acc;
}, {}),
},
};

console.log(opts);

const controller = new ControllerProvider(opts);
controller
.waitForKeychain()
.then(() => controller.connect())
.then((account) => {
dynCall_vi(cb, account ? true : false);
})
.catch((error) => {
dynCall_vi(cb, false);
});
},
ControllerDisconnect: function (cb) {
window.starknet_controller.disconnect().then(() => {
dynCall_vi(cb);
});
},
ControllerExecute: function (cb, calls) {
calls = JSON.parse(UTF8ToString(calls));
window.starknet_controller.account.execute(calls).then((result) => {
console.log(result);
const bufferSize = lengthBytesUTF8(result.transaction_hash) + 1;
const buffer = _malloc(bufferSize);
stringToUTF8(result.transaction_hash, buffer, bufferSize);
dynCall_vi(cb, buffer);
}).catch((error) => {
console.error(error);
const bufferSize = lengthBytesUTF8(error.message) + 1;
const buffer = _malloc(bufferSize);
stringToUTF8(error.message, buffer, bufferSize);
dynCall_vi(cb, buffer);
});
},
ControllerAddress: function () {
const address = window.starknet_controller.account.address;
const bufferSize = lengthBytesUTF8(address) + 1;
const buffer = _malloc(bufferSize);
stringToUTF8(address, buffer, bufferSize);
return buffer;
},
ControllerUsername: async function (cb) {
const username = await window.starknet_controller.username();
const bufferSize = lengthBytesUTF8(username) + 1;
const buffer = _malloc(bufferSize);
stringToUTF8(username, buffer, bufferSize);
dynCall_vi(cb, buffer);
},
ControllerChainId: async function (cb) {
const chainId = await window.starknet_controller.account.chainId();
const bufferSize = lengthBytesUTF8(chainId) + 1;
const buffer = _malloc(bufferSize);
stringToUTF8(chainId, buffer, bufferSize);
dynCall_vi(cb, buffer);
},
});
32 changes: 32 additions & 0 deletions Assets/Dojo/Plugins/WebGL/controller.jslib.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Assets/Dojo/Runtime/Controller.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
using Dojo.Starknet;
using System.Linq;

namespace Dojo
namespace Dojo.Controller
{
[Serializable]
public struct Policy
{
public FieldElement target;
Expand All @@ -27,22 +28,38 @@ public dojo.Policy ToNative()
}
}

public unsafe class Controller
public class Controller
{
private dojo.ControllerAccount* controller;
public FieldElement Address => new FieldElement(dojo.controller_address(controller));
public FieldElement ChainId => new FieldElement(dojo.controller_chain_id(controller));
public string Username => CString.ToString(dojo.controller_username(controller));
unsafe private dojo.ControllerAccount* controller;
#if UNITY_WEBGL && !UNITY_EDITOR
public FieldElement Address => new FieldElement(ControllerInterop.Address());
#else
unsafe public FieldElement Address => new FieldElement(dojo.controller_address(controller));
#endif

private static dojo.FnPtr_ControllerAccountPtr_Void onConnectCallback;
private static TaskCompletionSource<Controller> connectionTask;

private Controller(dojo.ControllerAccount* controller)
unsafe private Controller(dojo.ControllerAccount* controller)
{
this.controller = controller;
}

public static Controller GetAccount(Policy[] policies, FieldElement chainId)
private Controller() {}

#if UNITY_WEBGL && !UNITY_EDITOR
public static async Task<Controller> Connect(string rpcUrl, Policy[] policies, FieldElement chainId = null)
{
if (await ControllerInterop.Connect(rpcUrl, policies, chainId))
{
return new Controller();
}

Debug.LogWarning("Failed to connect to controller");
return null;
}
#else
unsafe private static Controller GetAccount(Policy[] policies, FieldElement chainId)
{
var nativePolicies = policies.Select(p => p.ToNative()).ToArray();
dojo.Policy* policiesPtr = null;
Expand All @@ -64,8 +81,15 @@ public static Controller GetAccount(Policy[] policies, FieldElement chainId)
return new Controller(result._ok);
}

public static Task<Controller> Connect(string rpcUrl, Policy[] policies)
unsafe public static Task<Controller> Connect(string rpcUrl, Policy[] policies, FieldElement chainId = null)
{
if (chainId != null) {
var account = GetAccount(policies, chainId);
if (account != null) {
return Task.FromResult(account);
}
}

connectionTask = new TaskCompletionSource<Controller>();
var nativePolicies = policies.Select(p => p.ToNative()).ToArray();
CString crpcUrl = CString.FromString(rpcUrl);
Expand All @@ -89,8 +113,11 @@ public static Task<Controller> Connect(string rpcUrl, Policy[] policies)

return connectionTask.Task;
}
#endif

public static bool Clear(Policy[] policies, FieldElement chainId)
#if UNITY_WEBGL && !UNITY_EDITOR
#else
unsafe public static bool Clear(Policy[] policies, FieldElement chainId)
{
var nativePolicies = policies.Select(p => p.ToNative()).ToArray();
dojo.Policy* policiesPtr = null;
Expand All @@ -110,28 +137,24 @@ public static bool Clear(Policy[] policies, FieldElement chainId)

return result.ok;
}
#endif

public FieldElement ExecuteRaw(dojo.Call[] calls)
#if UNITY_WEBGL && !UNITY_EDITOR
public async Task<FieldElement> Execute(Call[] calls)
{
dojo.Call* callsPtr;
fixed (dojo.Call* ptr = &calls[0])
{
callsPtr = ptr;
}

var result = dojo.controller_execute_raw(controller, callsPtr, (UIntPtr)calls.Length);
if (result.tag == dojo.ResultFieldElement_Tag.ErrFieldElement)
{
throw new Exception(result.err.message);
var hashOrError = await ControllerInterop.Execute(calls);
if (hashOrError.StartsWith("0x")) {
return new FieldElement(hashOrError);
}

return new FieldElement(result.ok);
throw new Exception(hashOrError);
}

public FieldElement ExecuteFromOutside(dojo.Call[] calls)
#else
unsafe private FieldElement ExecuteSync(Call[] calls)
{
var nativeCalls = calls.Select(c => c.ToNative()).ToArray();
dojo.Call* callsPtr;
fixed (dojo.Call* ptr = &calls[0])
fixed (dojo.Call* ptr = &nativeCalls[0])
{
callsPtr = ptr;
}
Expand All @@ -145,20 +168,34 @@ public FieldElement ExecuteFromOutside(dojo.Call[] calls)
return new FieldElement(result.ok);
}

public FieldElement Nonce()
public async Task<FieldElement> Execute(Call[] calls)
{
if (controller == null)
{
throw new InvalidOperationException("Controller is not initialized");
}
return await Task.Run(() => ExecuteSync(calls));
}
#endif

var result = dojo.controller_nonce(controller);
if (result.tag == dojo.ResultFieldElement_Tag.ErrFieldElement)
{
throw new Exception(result.err.message);
}
#if UNITY_WEBGL && !UNITY_EDITOR
public Task<string> Username()
{
return ControllerInterop.Username();
}
#else
unsafe public Task<string> Username()
{
return Task.Run(() => CString.ToString(dojo.controller_username(controller)));
}
#endif

return new FieldElement(result.ok);
#if UNITY_WEBGL && !UNITY_EDITOR
public Task<FieldElement> ChainId()
{
return ControllerInterop.ChainId();
}
#else
unsafe public Task<FieldElement> ChainId()
{
return Task.Run(() => new FieldElement(dojo.controller_chain_id(controller)));
}
#endif
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading