diff --git a/client/cmd/electron-demo/.gitignore b/client/cmd/electron-demo/.gitignore new file mode 100644 index 0000000000..342a40ac58 --- /dev/null +++ b/client/cmd/electron-demo/.gitignore @@ -0,0 +1,8 @@ +package-lock.json +site/ +node_modules/ +*.h +*.so +*.dll +build/ +bin/ \ No newline at end of file diff --git a/client/cmd/electron-demo/Makefile b/client/cmd/electron-demo/Makefile new file mode 100644 index 0000000000..10b85c1e7a --- /dev/null +++ b/client/cmd/electron-demo/Makefile @@ -0,0 +1,31 @@ +default: build + +compile-go: + go build -buildmode=c-shared -o ./libdexc/libdexc.so ./libdexc/... + +npm-install: + npm install + +cleanup: + rm -r build 2> /dev/null || true + rm -r site 2> /dev/null || true + rm -r /tmp/dexc 2> /dev/null || true + mkdir -p site + +move-site: + cp -r ../../webserver/site/src/ ./site/ + cp -r ../../webserver/site/dist/ ./site/ + +build-c: + ./node_modules/.bin/node-gyp configure + ./node_modules/.bin/node-gyp build + ./node_modules/.bin/electron-rebuild + +build: compile-go npm-install cleanup move-site build-c + + +run: + ./node_modules/.bin/electron main.js + +run-simnet: build + ./node_modules/.bin/electron main.js --simnet \ No newline at end of file diff --git a/client/cmd/electron-demo/binding.gyp b/client/cmd/electron-demo/binding.gyp new file mode 100644 index 0000000000..bfcfb1e442 --- /dev/null +++ b/client/cmd/electron-demo/binding.gyp @@ -0,0 +1,9 @@ +{ + "targets": [ + { + "target_name": "dexc", + "sources": [ "cnode/cnode.cc" ], + "libraries": [" +#include "../libdexc/libdexc.h" + +namespace cnode { + +using v8::FunctionCallbackInfo; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +const char* ToCString(const String::Utf8Value& value) { + return *value ? *value : ""; +} + +void Method(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + String::Utf8Value str(isolate, args[0]); + const char* cstr = ToCString(str); + char * charstr = const_cast(cstr); + char* result = Call(charstr); + args.GetReturnValue().Set(String::NewFromUtf8(isolate, result).ToLocalChecked()); +} + +void Initialize(Local exports) { + NODE_SET_METHOD(exports, "call", Method); +} + +NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) + +} // namespace cnode \ No newline at end of file diff --git a/client/cmd/electron-demo/index.html b/client/cmd/electron-demo/index.html new file mode 100644 index 0000000000..b39b86e09b --- /dev/null +++ b/client/cmd/electron-demo/index.html @@ -0,0 +1,12 @@ + + + + + Decred DEX Electron Integration Example + + + +
+ + + \ No newline at end of file diff --git a/client/cmd/electron-demo/index.js b/client/cmd/electron-demo/index.js new file mode 100644 index 0000000000..50cda40f2a --- /dev/null +++ b/client/cmd/electron-demo/index.js @@ -0,0 +1,162 @@ +const { ipcRenderer } = require('electron') +const path = require('path') + +Mainnet = 0 +Testnet = 1 +Simnet = 2 + +NetName = { + [Mainnet]: Mainnet, + [Testnet]: Testnet, + [Simnet]: Simnet +} + +LogLevelTrace = 0 +LogLevelDebug = 1 +LogLevelInfo = 2 +LogLevelWarn = 3 +LogLevelError = 4 +LogLevelCritical = 5 +LogLevelOff = 6 + +DCR_ID = 42 +BTC_ID = 0 + +function appGetPath (name) { + return fetchIPC('getPath', name) +} + +function fetchIPC (func, ...args) { + let res = ipcRenderer.sendSync(func, ...args) + res = res ? JSON.parse(res) : null + if (res && res.error) throw Error(String(res.error)) + return res +} + +class DEX { + static startCore () { + const dbPath = path.join(appGetPath('temp'), (new Date().getTime()).toString(), 'dexc', 'db.db') + return fetchIPC('callDEX', 'startCore', { + dbPath: dbPath, + net: Simnet, + logLevel: LogLevelDebug, + }) + } + + static isInitialized () { + const res = fetchIPC('callDEX', 'IsInitialized', '') + console.log("isInitialized res", res, typeof res) + return res + } + + static init (pw) { + return fetchIPC('callDEX', 'Init', { pass: pw }) + } + + static startServer (addr) { + return fetchIPC('callDEX', 'startServer', addr) + } + + static createWallet (assetID, config, pass, appPass) { + return fetchIPC('callDEX', 'CreateWallet', { + assetID: assetID, + config: config, + pass: pass, + appPass: appPass, + }) + } + + static register (addr, appPass, fee, cert) { + return fetchIPC('callDEX', 'Register', { + url: addr, + appPass: appPass, + fee: fee, + cert: cert, + }) + } + + static user () { + return fetchIPC('callDEX', 'User', '') + } +} + +/* sleep can be used by async functions to pause for a specified period. */ +function sleep (ms) { +return new Promise(resolve => setTimeout(resolve, ms)) +} + +const mainDiv = document.getElementById('main') + +async function writeMain (s) { + const div = document.createElement('div') + div.textContent = s + mainDiv.appendChild(div) +} + +function stringToUTF8Hex (s) { + return s.split("").map(c => c.charCodeAt(0).toString(16).padStart(2, "0")).join("") +} + +async function prepSimnet () { + + const pw = "abc" + if (!DEX.isInitialized()) { + await writeMain('Initializing DEX') + await writeMain(`result: ${DEX.init(pw)}`) + } + + const homeDir = appGetPath('home') + const dextestDir = path.join(homeDir, 'dextest') + + const user = DEX.user() + if (!user.assets[DCR_ID].wallet) { + const walletCertPath = path.join(dextestDir, 'dcr', 'alpha', 'rpc.cert') + await writeMain('Loading simnet Decred wallet') + DEX.createWallet(DCR_ID, { + account: 'default', + username: 'user', + password: 'pass', + rpccert: walletCertPath, + rpclisten: '127.0.0.1:19567' + }, pw, pw) + + await writeMain('Loading simnet Bitcoin wallet') + DEX.createWallet(BTC_ID, { + walletname: '', // alpha wallet + rpcuser: 'user', + rpcpassword: 'pass', + rpcport: '20556', + }, pw, pw) + } + + const simnetDexAddr = '127.0.0.1:17273' + if (!user.exchanges[simnetDexAddr]) { + await writeMain('Registering at simnet DEX server') + const defaultRegFee = 1e8 + const serverCertPath = path.join(dextestDir, 'dcrdex', 'rpc.cert') + DEX.register(simnetDexAddr, pw, defaultRegFee, serverCertPath) + } +} + +async function run () { + let net = Mainnet + const args = fetchIPC('cmdArgs', '') + if (args.indexOf('--simnet') > -1) net = Simnet + else if (args.indexOf('--testnet') > -1) net = Simnet + + console.log("using network", NetName[net]) + + await writeMain('Starting DEX Core') + DEX.startCore() + + if (net == Simnet) prepSimnet() + + // Start server + const addr = `localhost:54321` + await writeMain('Starting Web Server') + await writeMain(`result: ${DEX.startServer(addr)}`) + + window.location.href = `http://${addr}` +} + +run() \ No newline at end of file diff --git a/client/cmd/electron-demo/libdexc/adapter.go b/client/cmd/electron-demo/libdexc/adapter.go new file mode 100644 index 0000000000..941cad73c5 --- /dev/null +++ b/client/cmd/electron-demo/libdexc/adapter.go @@ -0,0 +1,221 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync/atomic" + + "decred.org/dcrdex/client/core" + "decred.org/dcrdex/client/webserver" + "decred.org/dcrdex/dex" + "github.com/decred/slog" + + _ "decred.org/dcrdex/client/asset/btc" // register btc asset + _ "decred.org/dcrdex/client/asset/dcr" // register dcr asset + _ "decred.org/dcrdex/client/asset/ltc" // register ltc asset +) + +type callHandler func(json.RawMessage) (string, error) + +func reply(thing interface{}) (string, error) { + b, err := json.Marshal(thing) + if err != nil { + return "", err + } + return string(b), nil +} + +func replyWithErrorCheck(thing interface{}, err error) (string, error) { + if err != nil { + return "", err + } + return reply(thing) +} + +// CoreAdapter manages a Core instance and possibly a Server instance for +// a library user. +type CoreAdapter struct { + ctx context.Context + kill context.CancelFunc + inited uint32 + logLevel slog.Level + serverRunning uint32 + core *core.Core + webServer *dex.ConnectionMaster + + preInitMethods map[string]callHandler + directMethods map[string]callHandler +} + +func NewCoreAdapter() *CoreAdapter { + c := &CoreAdapter{} + + c.preInitMethods = map[string]callHandler{ + "IsInitialized": c.isInitialized, + } + + c.directMethods = map[string]callHandler{ + // Some control functions. + "startServer": c.startServer, + "shutdown": c.shutdown, + // Pass-throughs to Core + "Init": c.init, + "CreateWallet": c.createWallet, + "User": c.user, + "Register": c.register, + } + + return c +} + +func (c *CoreAdapter) startCore(raw json.RawMessage) error { + if !atomic.CompareAndSwapUint32(&c.inited, 0, 1) { + return fmt.Errorf("already initialized") + } + + form := new(struct { + DBPath string `json:"dbPath"` + Net dex.Network `json:"net"` + LogLevel slog.Level `json:"logLevel"` + }) + if err := json.Unmarshal(raw, form); err != nil { + return err + } + + err := os.MkdirAll(filepath.Dir(form.DBPath), 0777) + if err != nil { + return err + } + + c.ctx, c.kill = context.WithCancel(context.Background()) + ccore, err := core.New(&core.Config{ + DBPath: form.DBPath, + Net: form.Net, + Logger: dex.StdOutLogger("CORE", form.LogLevel), + }) + if err != nil { + return fmt.Errorf("error creating client core: %v", err) + } + c.core = ccore + c.logLevel = form.LogLevel + + go ccore.Run(c.ctx) + <-ccore.Ready() + + return nil +} + +func (c *CoreAdapter) startServer(raw json.RawMessage) (string, error) { + if !atomic.CompareAndSwapUint32(&c.serverRunning, 0, 1) { + return "", fmt.Errorf("already initialized") + } + var webAddr string + if err := json.Unmarshal(raw, &webAddr); err != nil { + return "", err + } + + webSrv, err := webserver.New(c.core, webAddr, dex.StdOutLogger("SRVR", c.logLevel), false) + if err != nil { + return "", fmt.Errorf("Error creating web server: %v", err) + } + cm := dex.NewConnectionMaster(webSrv) + err = cm.Connect(c.ctx) + if err != nil { + return "", fmt.Errorf("Error starting web server: %v", err) + } + c.webServer = cm + go func() { + defer atomic.StoreUint32(&c.serverRunning, 0) + cm.Wait() + }() + return "", nil +} + +func (c *CoreAdapter) shutdown(json.RawMessage) (string, error) { + if atomic.LoadUint32(&c.inited) == 0 || c.kill == nil { + return "", fmt.Errorf("already shut down") + } + c.kill() + return "", nil +} + +func (c *CoreAdapter) handlers(funcName string) (callHandler, callHandler) { + return c.preInitMethods[funcName], c.directMethods[funcName] +} + +func (c *CoreAdapter) run(callData *CallData) (string, error) { + switch preInitHandler, coreHandler := c.handlers(callData.Function); { + case callData.Function == "startCore": + return "", c.startCore(callData.Params) + case preInitHandler != nil: + return preInitHandler(callData.Params) + case atomic.LoadUint32(&c.inited) == 0: + return "", fmt.Errorf("not initialized") + case c.core == nil: + return "", fmt.Errorf("core not constructed. probably an initialization error") + case coreHandler != nil: + return coreHandler(callData.Params) + } + return "", fmt.Errorf("no method %q", callData.Function) +} + +func (c *CoreAdapter) init(raw json.RawMessage) (string, error) { + form := new(struct { + Pass string `json:"pass"` + }) + if err := json.Unmarshal(raw, form); err != nil { + return "", err + } + + return "", c.core.InitializeClient([]byte(form.Pass)) +} + +func (c *CoreAdapter) isInitialized(json.RawMessage) (string, error) { + return replyWithErrorCheck(c.core.IsInitialized()) +} + +func (c *CoreAdapter) createWallet(raw json.RawMessage) (string, error) { + form := new(struct { + AssetID uint32 `json:"assetID"` + Config map[string]string `json:"config"` + Pass string `json:"pass"` + AppPW string `json:"appPass"` + }) + if err := json.Unmarshal(raw, form); err != nil { + return "", err + } + return "", c.core.CreateWallet([]byte(form.AppPW), []byte(form.Pass), &core.WalletForm{ + AssetID: form.AssetID, + Config: form.Config, + }) +} + +func (c *CoreAdapter) user(raw json.RawMessage) (string, error) { + return reply(c.core.User()) +} + +func (c *CoreAdapter) getFee(raw json.RawMessage) (string, error) { + form := new(struct { + Addr string `json:"addr"` + Cert string `json:"cert"` // Not required if there is an entry for the server in the dcrdex/client/core/certs.go + }) + if err := json.Unmarshal(raw, form); err != nil { + return "", err + } + return replyWithErrorCheck(c.core.GetFee(form.Addr, []byte(form.Cert))) +} + +func (c *CoreAdapter) register(raw json.RawMessage) (string, error) { + form := new(core.RegisterForm) + if err := json.Unmarshal(raw, form); err != nil { + return "", err + } + + return replyWithErrorCheck(c.core.Register(form)) +} diff --git a/client/cmd/electron-demo/libdexc/libdexc.go b/client/cmd/electron-demo/libdexc/libdexc.go new file mode 100644 index 0000000000..68ebd37cc1 --- /dev/null +++ b/client/cmd/electron-demo/libdexc/libdexc.go @@ -0,0 +1,43 @@ +package main + +import "C" +import ( + "encoding/json" + "fmt" +) + +// CallData is the type sent for all golink calls. +type CallData struct { + Function string `json:"function"` + Params json.RawMessage `json:"params"` +} + +func callError(s string, a ...interface{}) *C.char { + b, _ := json.Marshal(&struct { + Error string `json:"error"` + }{ + Error: fmt.Sprintf(s, a...), + }) + return C.CString(string(b)) +} + +var adapter = NewCoreAdapter() + +// Call is used to invoke a registered function. +//export Call +func Call(msg *C.char) *C.char { + jsonStr := C.GoString(msg) + cd := new(CallData) + err := json.Unmarshal([]byte(jsonStr), cd) + if err != nil { + return callError("json Unmarshal error: %v", err) + } + + res, err := adapter.run(cd) + if err != nil { + return callError("%s error: %v", cd.Function, err) + } + return C.CString(res) +} + +func main() {} diff --git a/client/cmd/electron-demo/main.js b/client/cmd/electron-demo/main.js new file mode 100644 index 0000000000..0398330288 --- /dev/null +++ b/client/cmd/electron-demo/main.js @@ -0,0 +1,72 @@ +const { app, ipcMain, BrowserWindow } = require('electron') +const dexc = require('./build/Release/dexc') +const process = require('process') + +Mainnet = 0 +Testnet = 1 +Simnet = 2 + +LogLevelTrace = 0 +LogLevelDebug = 1 +LogLevelInfo = 2 +LogLevelWarn = 3 +LogLevelError = 4 +LogLevelCritical = 5 +LogLevelOff = 6 + +function callDEX (func, params) { + return dexc.call(JSON.stringify({ + function: func, + params: params, + })) +} + +function handleIPC (event, f) { + try { + const res = f() + event.returnValue = res + } catch (error) { + event.returnValue = { error: error } + } +} + +ipcMain.on('callDEX', (event, func, params) => { + handleIPC(event, () => callDEX(func, params)) +}) + +ipcMain.on('getPath', (event, name) => { + handleIPC(event, () => JSON.stringify(app.getPath(name))) +}) + +ipcMain.on('cmdArgs', (event) => { + handleIPC(event, () => JSON.stringify(process.argv)) +}) + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }) + + if (process.argv.indexOf('--simnet') > -1) win.webContents.openDevTools() + win.loadFile('index.html') +} + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + callDEX('shutdown', '') + } +}) + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } +}) \ No newline at end of file diff --git a/client/cmd/electron-demo/package.json b/client/cmd/electron-demo/package.json new file mode 100644 index 0000000000..5d4b0bb35a --- /dev/null +++ b/client/cmd/electron-demo/package.json @@ -0,0 +1,18 @@ +{ + "name": "electron-demo", + "version": "1.0.0", + "description": "Demo of Electron App using with DEX Core Library", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "electron ." + }, + "keywords": [], + "author": "Brian Stafford", + "license": "ISC", + "devDependencies": { + "electron": "^12.0.0", + "electron-rebuild": "^2.3.5", + "node-gyp": "^7.1.2" + } +} diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index cf78425882..ab80b62f9d 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -63,7 +63,7 @@ var ( type clientCore interface { websocket.Core Network() dex.Network - Exchanges() map[string]*core.Exchange + // Exchanges() map[string]*core.Exchange Register(*core.RegisterForm) (*core.RegisterResult, error) Login(pw []byte) (*core.LoginResult, error) InitializeClient(pw []byte) error