Skip to content

Commit

Permalink
popm/wasm: add error codes (#154)
Browse files Browse the repository at this point in the history
Add error codes to the WebAssembly PoP Miner.

These error codes exposed in the NPM package for easy
programatically handling different error types.

Reviewed-on: #154
Reviewed-by: ClaytonNorthey92 <clayton.northey@gmail.com>
Reviewed-by: Marco Peereboom <marco@peereboom.us>
  • Loading branch information
joshuasing authored Jun 27, 2024
1 parent b5b5647 commit be5de3b
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 68 deletions.
40 changes: 40 additions & 0 deletions web/popminer/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

package main

import "syscall/js"

// Method represents a method that can be dispatched.
type Method string

Expand All @@ -24,8 +26,46 @@ const (
MethodBitcoinUTXOs Method = "bitcoinUTXOs" // Retrieve bitcoin UTXOs
)

// ErrorCode is used to differentiate between error types.
type ErrorCode uint32

const (
// errorCodeInvalid is the zero value of ErrorCode.
// This should not be used for anything.
errorCodeInvalid ErrorCode = 0

// ErrorCodeInternal is used when the error is internal, either due to an
// invalid dispatch or a panic.
ErrorCodeInternal ErrorCode = 1000

// ErrorCodeInvalidValue is used when an invalid value was provided for
// a dispatch argument.
ErrorCodeInvalidValue ErrorCode = 2000
)

// String returns a string value representing the error code.
func (e ErrorCode) String() string {
switch e {
case errorCodeInvalid:
return "invalid error code"
case ErrorCodeInternal:
return "internal error"
case ErrorCodeInvalidValue:
return "invalid value"
default:
return "unknown"
}
}

func (e ErrorCode) JSValue() js.Value {
return jsValueSafe(uint32(e))
}

// Error represents an error that has occurred within the WASM PoP Miner.
type Error struct {
// Code is a unique identifier used to differentiate between error types.
Code ErrorCode `json:"code"`

// Message is the error message.
Message string `json:"message"`

Expand Down
26 changes: 14 additions & 12 deletions web/popminer/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ func dispatch(this js.Value, args []js.Value) any {
if r := recover(); r != nil {
log.Criticalf("recovered panic: %v", r)
log.Criticalf(string(debug.Stack()))
reject.Invoke(jsError(fmt.Errorf("recovered panic: %v", r)))
reject.Invoke(jsErrorWithCode(ErrorCodeInternal,
fmt.Errorf("recovered panic: %v", r)))
}
}()

Expand Down Expand Up @@ -194,22 +195,23 @@ func generateKey(_ js.Value, args []js.Value) (any, error) {
btcChainParams = &btcchaincfg.MainNetParams
netNormalized = "mainnet"
default:
return js.Null(), fmt.Errorf("invalid network: %v", net)
return nil, errorWithCode(ErrorCodeInvalidValue,
fmt.Errorf("invalid network: %s", net))
}

// TODO(joshuasing): consider alternative as dcrsecpk256k1 package is large.
privKey, err := dcrsecpk256k1.GeneratePrivateKey()
if err != nil {
log.Errorf("failed to generate private key: %v", err)
return js.Null(), fmt.Errorf("generate secp256k1 private key: %w", err)
return nil, fmt.Errorf("generate secp256k1 private key: %w", err)
}
btcAddress, err := btcutil.NewAddressPubKey(
privKey.PubKey().SerializeCompressed(),
btcChainParams,
)
if err != nil {
log.Errorf("failed to generate btc address: %v", err)
return js.Null(), fmt.Errorf("create BTC address from public key: %w", err)
return nil, fmt.Errorf("create BTC address from public key: %w", err)
}

compressedPubKey := privKey.PubKey().SerializeCompressed()
Expand Down Expand Up @@ -310,11 +312,11 @@ func ping(_ js.Value, _ []js.Value) (any, error) {

activePM, err := activeMiner()
if err != nil {
return js.Null(), err
return nil, err
}
pr, err := activePM.miner.Ping(activePM.ctx, time.Now().Unix())
if err != nil {
return js.Null(), err
return nil, err
}

// TODO(joshuasing): protocol.PingResponse should really use a more accurate
Expand All @@ -337,11 +339,11 @@ func l2Keystones(_ js.Value, args []js.Value) (any, error) {

activePM, err := activeMiner()
if err != nil {
return js.Null(), err
return nil, err
}
pr, err := activePM.miner.L2Keystones(activePM.ctx, count)
if err != nil {
return js.Null(), err
return nil, err
}

keystones := make([]L2Keystone, len(pr.L2Keystones))
Expand Down Expand Up @@ -370,11 +372,11 @@ func bitcoinBalance(_ js.Value, args []js.Value) (any, error) {

activePM, err := activeMiner()
if err != nil {
return js.Null(), err
return nil, err
}
pr, err := activePM.miner.BitcoinBalance(activePM.ctx, scriptHash)
if err != nil {
return js.Null(), err
return nil, err
}

return BitcoinBalanceResult{
Expand All @@ -389,11 +391,11 @@ func bitcoinInfo(_ js.Value, _ []js.Value) (any, error) {

activePM, err := activeMiner()
if err != nil {
return js.Null(), err
return nil, err
}
pr, err := activePM.miner.BitcoinInfo(activePM.ctx)
if err != nil {
return js.Null(), err
return nil, err
}

return BitcoinInfoResult{
Expand Down
72 changes: 66 additions & 6 deletions web/popminer/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package main

import (
"errors"
"reflect"
"runtime/debug"
"strings"
Expand All @@ -20,6 +21,10 @@ var (
arrayConstructor = js.Global().Get("Array")
)

type JSValuer interface {
JSValue() js.Value
}

// jsValueOf returns x as a JavaScript value.
//
// | Go | JavaScript |
Expand All @@ -42,6 +47,8 @@ func jsValueOf(x any) js.Value {
return t
case js.Func:
return t.Value
case JSValuer:
return t.JSValue()
case bool,
int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
Expand Down Expand Up @@ -183,18 +190,71 @@ func jsValueSafe(v any) (jsv js.Value) {
jsv = js.Undefined()
}
}()

// Special handling
switch x := v.(type) {
case JSValuer:
return x.JSValue()
}

return js.ValueOf(v)
}

// jsError returns a [js.Value] representing the given error.
// codedError represents an error that has a related [ErrorCode].
type codedError struct {
code ErrorCode
err error
}

// errorWithCode returns an error containing the given error code.
func errorWithCode(code ErrorCode, err error) error {
return codedError{
code: code,
err: err,
}
}

// codeFromError returns the error code from the error, if possible, otherwise
// ErrorCodeInternal will be returned.
func codeFromError(err error) ErrorCode {
var ce codedError
if errors.As(err, &ce) {
return ce.code
}
return ErrorCodeInternal
}

// Error returns the error string.
func (c codedError) Error() string {
return c.err.Error()
}

// Unwrap returns the wrapped error.
func (c codedError) Unwrap() error {
return c.err
}

// jsError returns a [js.Value] representing the given error. The error code
// will be extracted from the given error using codeFromError, if available,
// otherwise the error code will be ErrorCodeInternal.
func jsError(err error) js.Value {
log.Tracef("jsError: %v", err)
defer log.Tracef("jsError exit")
return newJSError(codeFromError(err), err.Error())
}

// jsErrorWithCode returns a [js.Value] representing the given error with
// an error code.
func jsErrorWithCode(code ErrorCode, err error) js.Value {
return newJSError(code, err.Error())
}

stack := string(debug.Stack())
// newJSError returns a new [js.Value] for an [Error] with the given code and
// message. The stack will be generated, skipping stackSkip callers. The
// timestamp will be set to time.Now() in Unix seconds.
func newJSError(code ErrorCode, message string) js.Value {
return jsValueOf(Error{
Message: err.Error(),
Stack: stack,
Code: code,
Message: message,
Stack: string(debug.Stack()),
Timestamp: time.Now().Unix(),
})
}
62 changes: 30 additions & 32 deletions web/www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,25 @@
<head>
<meta charset="UTF-8" />
<title>PoP miner integration tests</title>
</head>

<body>
<script src="wasm_exec.js"></script>
<script src="popminer.js"></script>

<p>
</head>
<body>
<div>
<button id="VersionButton">version</button>
<span class="VersionShow"></span>
</p>
<pre class="VersionShow"></pre>
</div>

<p>
<div>
<input id="GenerateKeyNetworkInput" value="testnet3">
<label for="GenerateKeyNetworkInput">bitcoin network</label>
<br>

<button id="GenerateKeyButton">generate key</button>
<span class="GenerateKeyShow"></span>
</p>
<pre class="GenerateKeyShow"></pre>
</div>

<p>
<div>
<input id="StartPopMinerLogLevelInput" value="hemiwasm=INFO:popm=INFO:protocol=INFO">
<label for="StartPopMinerLogLevelInput">log level</label>
<br>
Expand All @@ -45,55 +43,55 @@
<br>

<button id="StartPopMinerButton">Run PoP Miner</button>
<span class="StartPopMinerShow"></span>
</p>
<pre class="StartPopMinerShow"></pre>
</div>

<p>
<div>
<button id="StopPopMinerButton">Stop PoP Miner</button>
<span class="StopPopMinerShow"></span>
</p>
<pre class="StopPopMinerShow"></pre>
</div>

<!-- connected commands go here, run after StartPopMinerButton -->
<hr>
<div>The following commands require a live connection (after StartPopMinerButton)</div>
<p>The following commands require a live connection (after StartPopMinerButton)</p>
<br>

<p>
<div>
<button id="PingButton">ping</button>
<span class="PingShow"></span>
</p>
<pre class="PingShow"></pre>
</div>

<p>
<div>
<input id="L2KeystonesNumL2KeystonesInput" value="2" type="number">
<label for="L2KeystonesNumL2KeystonesInput">num l2 keystones</label>
<br>

<button id="L2KeystonesButton">l2 keystones</button>
<span class="L2KeystonesShow"></span>
</p>
<pre class="L2KeystonesShow"></pre>
</div>

<p>
<div>
<input id="BitcoinBalanceScriptHashInput" value="xxx">
<label for="BitcoinBalanceScriptHashInput">script hash</label>
<br>

<button id="BitcoinBalanceButton">bitcoin balance</button>
<span class="BitcoinBalanceShow"></span>
</p>
<pre class="BitcoinBalanceShow"></pre>
</div>

<p>
<div>
<button id="BitcoinInfoButton">bitcoin info</button>
<span class="BitcoinInfoShow"></span>
</p>
<pre class="BitcoinInfoShow"></pre>
</div>

<p>
<div>
<input id="BitcoinUTXOsScriptHashInput" value="xxx">
<label for="BitcoinUTXOsScriptHashInput">script hash</label>
<br>

<button id="BitcoinUTXOsButton">bitcoin utxos</button>
<span class="BitcoinUTXOsShow"></span>
</p>
<pre class="BitcoinUTXOsShow"></pre>
</div>

<script src="index.js" defer></script>
</body>
Expand Down
Loading

0 comments on commit be5de3b

Please sign in to comment.