From 9c4cea583cce1e78b9de4bb616c63477071c050a Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Fri, 12 Jul 2024 05:46:59 +1000 Subject: [PATCH] popm/wasm: add events and clean up globals (#175) - Add events to the `popm` package. - Add events to the WebAssembly PoP Miner and the `@hemilabs/pop-miner` package. - Rename `activeMiner()` to `runningMiner()`. - Add a `Service` type and use `svc` global variable (replaces `pm` and `pmMtx`). This allows us to have a global struct to store data in, without the need to use a lock to access the data. - Add 4 initial events: `minerStart`, `minerStop`, `mineKeystone`, and `transactionBroadcast`. Closes #150 --- service/popm/event.go | 57 +++++ service/popm/popm.go | 8 + web/packages/pop-miner/src/browser/index.ts | 22 ++ web/packages/pop-miner/src/browser/wasm.ts | 8 +- web/packages/pop-miner/src/types.ts | 94 +++++++++ web/popminer/api.go | 72 ++++++- web/popminer/dispatch.go | 219 ++++++++++++++------ web/popminer/popminer.go | 98 +++++++-- web/popminer/util.go | 38 ++++ web/www/index.html | 6 + web/www/index.js | 21 ++ 11 files changed, 560 insertions(+), 83 deletions(-) create mode 100644 service/popm/event.go diff --git a/service/popm/event.go b/service/popm/event.go new file mode 100644 index 00000000..0f7f1a25 --- /dev/null +++ b/service/popm/event.go @@ -0,0 +1,57 @@ +// Copyright (c) 2024 Hemi Labs, Inc. +// Use of this source code is governed by the MIT License, +// which can be found in the LICENSE file. + +package popm + +import ( + "github.com/hemilabs/heminetwork/hemi" +) + +// EventHandler is a function that can handle an event. +type EventHandler = func(event EventType, data any) + +// EventType represents a type of event. +type EventType int + +const ( + // EventTypeMineKeystone is an event dispatched when a L2 keystone is being + // mined. + EventTypeMineKeystone EventType = iota + 1 + + // EventTypeTransactionBroadcast is an event dispatched when a Bitcoin + // transaction has been broadcast to the network. + EventTypeTransactionBroadcast +) + +// EventMineKeystone is the data for EventTypeMineKeystone. +type EventMineKeystone struct { + Keystone *hemi.L2Keystone +} + +// EventTransactionBroadcast is the data for EventTypeTransactionBroadcast. +type EventTransactionBroadcast struct { + Keystone *hemi.L2Keystone + TxHash string +} + +// RegisterEventHandler registers an event handler to receive all events +// dispatched by the miner. The dispatched events can be filtered by EventType +// when received. +func (m *Miner) RegisterEventHandler(handler EventHandler) { + m.eventHandlersMtx.Lock() + defer m.eventHandlersMtx.Unlock() + m.eventHandlers = append(m.eventHandlers, handler) +} + +// dispatchEvent calls all registered event handlers with the given eventType +// and data. It is recommended to call this function in a go routine to avoid +// blocking operation while the event is being dispatched, as all event handlers +// will be executed synchronously. +func (m *Miner) dispatchEvent(eventType EventType, data any) { + m.eventHandlersMtx.RLock() + defer m.eventHandlersMtx.RUnlock() + for _, handler := range m.eventHandlers { + handler(eventType, data) + } +} diff --git a/service/popm/popm.go b/service/popm/popm.go index 14e795b8..efa991d2 100644 --- a/service/popm/popm.go +++ b/service/popm/popm.go @@ -123,6 +123,9 @@ type Miner struct { mineNowCh chan struct{} l2Keystones map[string]L2KeystoneProcessingContainer + + eventHandlersMtx sync.RWMutex + eventHandlers []EventHandler } func NewMiner(cfg *Config) (*Miner, error) { @@ -318,6 +321,8 @@ func createTx(l2Keystone *hemi.L2Keystone, btcHeight uint64, utxo *bfgapi.Bitcoi func (m *Miner) mineKeystone(ctx context.Context, ks *hemi.L2Keystone) error { log.Infof("Broadcasting PoP transaction to Bitcoin...") + go m.dispatchEvent(EventTypeMineKeystone, EventMineKeystone{Keystone: ks}) + btcHeight, err := m.bitcoinHeight(ctx) if err != nil { return fmt.Errorf("get Bitcoin height: %w", err) @@ -394,6 +399,9 @@ func (m *Miner) mineKeystone(ctx context.Context, ks *hemi.L2Keystone) error { log.Infof("Successfully broadcast PoP transaction to Bitcoin with TX hash %v", txHash) + go m.dispatchEvent(EventTypeTransactionBroadcast, + EventTransactionBroadcast{Keystone: ks, TxHash: txHash.String()}) + return nil } diff --git a/web/packages/pop-miner/src/browser/index.ts b/web/packages/pop-miner/src/browser/index.ts index f03eec2d..acf8afc2 100644 --- a/web/packages/pop-miner/src/browser/index.ts +++ b/web/packages/pop-miner/src/browser/index.ts @@ -100,3 +100,25 @@ export const bitcoinUTXOs: typeof types.bitcoinUTXOs = ({ scriptHash }) => { scriptHash: scriptHash, }) as Promise; }; + +export const addEventListener: typeof types.addEventListener = ( + eventType, + listener, +) => { + return dispatchVoid({ + method: 'addEventListener', + eventType: eventType, + listener: listener, + }); +}; + +export const removeEventListener: typeof types.addEventListener = ( + eventType, + listener, +) => { + return dispatchVoid({ + method: 'removeEventListener', + eventType: eventType, + listener: listener, + }); +}; diff --git a/web/packages/pop-miner/src/browser/wasm.ts b/web/packages/pop-miner/src/browser/wasm.ts index ecb2eade..a992d4bb 100644 --- a/web/packages/pop-miner/src/browser/wasm.ts +++ b/web/packages/pop-miner/src/browser/wasm.ts @@ -23,14 +23,16 @@ export type Method = | 'l2Keystones' | 'bitcoinBalance' | 'bitcoinInfo' - | 'bitcoinUTXOs'; + | 'bitcoinUTXOs' + | 'addEventListener' + | 'removeEventListener'; /** * Dispatch args. * * @see dispatch */ -export type DispatchArgs = Record & { +export type DispatchArgs = Record & { /** * The method to be dispatched. * @@ -66,7 +68,7 @@ export const init: typeof types.init = async ({ wasmURL }) => { loadPromise = loadWASM({ wasmURL }).catch((err) => { loadPromise = undefined; throw err; - }); + }) as Promise; } globalWASM = globalWASM || (await loadPromise); diff --git a/web/packages/pop-miner/src/types.ts b/web/packages/pop-miner/src/types.ts index acb5c1af..84851fb4 100644 --- a/web/packages/pop-miner/src/types.ts +++ b/web/packages/pop-miner/src/types.ts @@ -232,6 +232,100 @@ export declare function bitcoinAddressToScriptHash( args: BitcoinAddressToScriptHashArgs, ): Promise; +/** + * Represents a type of event. + */ +export type EventType = + | 'minerStart' + | 'minerStop' + | 'mineKeystone' + | 'transactionBroadcast'; + +/** + * An event that has been dispatched. + */ +export type Event = { + type: EventType; +}; + +/** + * Dispatched when the PoP miner has stopped. + */ +export type EventMinerStart = Event & { + type: 'minerStart'; +}; + +/** + * Dispatched when the PoP miner has exited. + */ +export type EventMinerStop = Event & { + type: 'minerStop'; + + /** + * The error that caused the PoP miner to exit, or null. + */ + error?: Error; +}; + +/** + * Dispatched when the PoP miner begins mining a keystone. + */ +export type EventMineKeystone = Event & { + type: 'mineKeystone'; + + /** + * The keystone to be mined. + */ + keystone: L2Keystone; +}; + +/** + * Dispatched when the PoP miner broadcasts a PoP transaction to the Bitcoin + * network. + */ +export type EventTransactionBroadcast = Event & { + type: 'transactionBroadcast'; + + /** + * The keystone that was mined. + */ + keystone: L2Keystone; + + /** + * The hash of the Bitcoin transaction. + */ + txHash: string; +}; + +/** + * An event listener that can receive events. + */ +export interface EventListener { + (event: Event): void; +} + +/** + * Registers an event listener. + * + * @param eventType The event type to listen for. If '*' then listen for all events. + * @param listener The event listener that will be called when the event is dispatched. + */ +export declare function addEventListener( + eventType: EventType | '*', + listener: EventListener, +): Promise; + +/** + * Unregisters an event listener. + * + * @param eventType The event type to stop listening for. + * @param listener The event listener to unregister. + */ +export declare function removeEventListener( + eventType: EventType | '*', + listener: EventListener, +): Promise; + /** * @see startPoPMiner */ diff --git a/web/popminer/api.go b/web/popminer/api.go index d9cf591d..3f75d0bb 100644 --- a/web/popminer/api.go +++ b/web/popminer/api.go @@ -6,7 +6,11 @@ package main -import "syscall/js" +import ( + "syscall/js" + + "github.com/hemilabs/heminetwork/service/popm" +) // Method represents a method that can be dispatched. type Method string @@ -26,6 +30,10 @@ const ( MethodBitcoinBalance Method = "bitcoinBalance" // Retrieve bitcoin balance MethodBitcoinInfo Method = "bitcoinInfo" // Retrieve bitcoin information MethodBitcoinUTXOs Method = "bitcoinUTXOs" // Retrieve bitcoin UTXOs + + // Events + MethodEventListenerAdd Method = "addEventListener" // Register event listener + MethodEventListenerRemove Method = "removeEventListener" // Unregister event listener ) // ErrorCode is used to differentiate between error types. @@ -206,3 +214,65 @@ type BitcoinUTXO struct { // Value is the value of the output in satoshis. Value int64 `json:"value"` } + +// EventType represents a type of event. +type EventType string + +const ( + // EventTypeMinerStart is dispatched when the PoP miner has started. + EventTypeMinerStart EventType = "minerStart" + + // EventTypeMinerStop is dispatched when the PoP miner has exited. + EventTypeMinerStop EventType = "minerStop" + + // EventTypeMineKeystone is dispatched when the PoP miner is mining an L2 + // keystone. + EventTypeMineKeystone EventType = "mineKeystone" + + // EventTypeTransactionBroadcast is dispatched when the PoP miner has + // broadcast a Bitcoin transaction to the network. + EventTypeTransactionBroadcast EventType = "transactionBroadcast" +) + +// popmEvents contains events dispatched by the native PoP Miner. +// These events will be forwarded to JavaScript, however we also dispatch events +// that are specific to the WebAssembly PoP Miner. +var popmEvents = map[popm.EventType]EventType{ + popm.EventTypeMineKeystone: EventTypeMineKeystone, + popm.EventTypeTransactionBroadcast: EventTypeTransactionBroadcast, +} + +// eventTypes is a map used to parse string event types. +var eventTypes = map[string]EventType{ + "*": "*", // Listen for all events. + EventTypeMinerStart.String(): EventTypeMinerStart, + EventTypeMinerStop.String(): EventTypeMinerStop, + EventTypeMineKeystone.String(): EventTypeMineKeystone, + EventTypeTransactionBroadcast.String(): EventTypeTransactionBroadcast, +} + +// String returns the string representation of the event type. +func (e EventType) String() string { + return string(e) +} + +// MarshalJS returns the JavaScript representation of the event type. +func (e EventType) MarshalJS() (js.Value, error) { + return jsValueOf(e.String()), nil +} + +// EventMinerStop is the data for EventTypeMinerStop. +type EventMinerStop struct { + Error *Error `json:"error"` +} + +// EventMineKeystone is the data for EventTypeMineKeystone. +type EventMineKeystone struct { + Keystone L2Keystone `json:"keystone"` +} + +// EventTransactionBroadcast is the data for EventTypeTransactionBroadcast. +type EventTransactionBroadcast struct { + Keystone L2Keystone `json:"keystone"` + TxHash string `json:"txHash"` +} diff --git a/web/popminer/dispatch.go b/web/popminer/dispatch.go index 6b21c25a..e8aa1303 100644 --- a/web/popminer/dispatch.go +++ b/web/popminer/dispatch.go @@ -88,6 +88,22 @@ var handlers = map[Method]*Dispatch{ {Name: "scriptHash", Type: js.TypeString}, }, }, + + // Events + MethodEventListenerAdd: { + Handler: addEventListener, + Required: []DispatchArgs{ + {Name: "eventType", Type: js.TypeString}, + {Name: "handler", Type: js.TypeFunction}, + }, + }, + MethodEventListenerRemove: { + Handler: removeEventListener, + Required: []DispatchArgs{ + {Name: "eventType", Type: js.TypeString}, + {Name: "handler", Type: js.TypeFunction}, + }, + }, } type DispatchArgs struct { @@ -309,77 +325,109 @@ func startPoPMiner(_ js.Value, args []js.Value) (any, error) { log.Tracef("startPoPMiner") defer log.Tracef("startPoPMiner exit") - pmMtx.Lock() - defer pmMtx.Unlock() - if pm != nil { - return nil, errors.New("pop miner already started") + svc.minerMtx.Lock() + defer svc.minerMtx.Unlock() + if svc.miner != nil { + return nil, errors.New("miner already started") + } + + cfg, err := createMinerConfig(args[0]) + if err != nil { + return nil, err + } + + miner, err := popm.NewMiner(cfg) + if err != nil { + return nil, fmt.Errorf("create miner: %w", err) + } + + // Add WebAssembly miner event handler + miner.RegisterEventHandler(svc.handleMinerEvent) + + ctx, cancel := context.WithCancel(context.Background()) + m := &Miner{ + ctx: ctx, + cancel: cancel, + Miner: miner, + errCh: make(chan error, 1), } + svc.miner = m + + // run in background + m.wg.Add(1) + go func() { + defer m.wg.Done() + svc.dispatchEvent(EventTypeMinerStart, nil) + if err := m.Run(m.ctx); !errors.Is(err, context.Canceled) { + svc.dispatchEvent(EventTypeMinerStop, EventMinerStop{ + Error: &Error{ + Code: ErrorCodeInternal, + Message: err.Error(), + Stack: string(debug.Stack()), + Timestamp: time.Now().Unix(), + }, + }) + + // TODO: Fix, this doesn't work very well. + // We should remove the miner from svc here, and make stopPoPMiner + // no longer return the start error. + m.errCh <- err + return + } + + // Exited without error. + svc.dispatchEvent(EventTypeMinerStop, EventMinerStop{}) + }() - pm = new(Miner) - pm.ctx, pm.cancel = context.WithCancel(context.Background()) + return js.Null(), nil +} +// createMinerConfig creates a [popm.Config] from the given JavaScript object. +func createMinerConfig(config js.Value) (*popm.Config, error) { cfg := popm.NewDefaultConfig() - cfg.BTCPrivateKey = args[0].Get("privateKey").String() - cfg.StaticFee = uint(args[0].Get("staticFee").Int()) + cfg.BTCPrivateKey = config.Get("privateKey").String() + cfg.StaticFee = uint(config.Get("staticFee").Int()) - cfg.LogLevel = args[0].Get("logLevel").String() + // Log level + cfg.LogLevel = config.Get("logLevel").String() if cfg.LogLevel == "" { cfg.LogLevel = "popm=ERROR" } if err := loggo.ConfigureLoggers(cfg.LogLevel); err != nil { - pm = nil - return nil, fmt.Errorf("configure logger: %w", err) + return nil, errorWithCode(ErrorCodeInvalidValue, + fmt.Errorf("configure logger: %w", err)) } - network := args[0].Get("network").String() + // Network + network := config.Get("network").String() netOpts, ok := networks[network] if !ok { - pm = nil - return nil, fmt.Errorf("unknown network: %s", network) + return nil, errorWithCode(ErrorCodeInvalidValue, + fmt.Errorf("unknown network: %s", network)) } cfg.BFGWSURL = netOpts.bfgURL cfg.BTCChainName = netOpts.btcChainName - var err error - pm.miner, err = popm.NewMiner(cfg) - if err != nil { - pm = nil - return nil, fmt.Errorf("create PoP miner: %w", err) - } - - // launch in background - pm.wg.Add(1) - go func() { - defer pm.wg.Done() - if err := pm.miner.Run(pm.ctx); !errors.Is(err, context.Canceled) { - // TODO(joshuasing): dispatch event on failure - pmMtx.Lock() - defer pmMtx.Unlock() - pm.err = err // Theoretically this can logic race unless we unset pm - } - }() - - return js.Null(), nil + return cfg, nil } func stopPopMiner(_ js.Value, _ []js.Value) (any, error) { log.Tracef("stopPopMiner") defer log.Tracef("stopPopMiner exit") - pmMtx.Lock() - if pm == nil { - pmMtx.Unlock() - return nil, errors.New("pop miner not running") + svc.minerMtx.Lock() + if svc.miner == nil { + svc.minerMtx.Unlock() + return nil, errors.New("miner not running") } - oldPM := pm - pm = nil - pmMtx.Unlock() - oldPM.cancel() - oldPM.wg.Wait() + // Copy the m and release the lock + m := svc.miner + svc.miner = nil + svc.minerMtx.Unlock() - if oldPM.err != nil { - return nil, oldPM.err + if err := m.shutdown(); err != nil { + return nil, err } return js.Null(), nil @@ -389,11 +437,11 @@ func ping(_ js.Value, _ []js.Value) (any, error) { log.Tracef("ping") defer log.Tracef("ping exit") - activePM, err := activeMiner() + miner, err := runningMiner() if err != nil { return nil, err } - pr, err := activePM.miner.Ping(activePM.ctx, time.Now().Unix()) + pr, err := miner.Ping(miner.ctx, time.Now().Unix()) if err != nil { return nil, err } @@ -416,26 +464,18 @@ func l2Keystones(_ js.Value, args []js.Value) (any, error) { } count := uint64(c) - activePM, err := activeMiner() + miner, err := runningMiner() if err != nil { return nil, err } - pr, err := activePM.miner.L2Keystones(activePM.ctx, count) + pr, err := miner.L2Keystones(miner.ctx, count) if err != nil { return nil, err } keystones := make([]L2Keystone, len(pr.L2Keystones)) for i, ks := range pr.L2Keystones { - keystones[i] = L2Keystone{ - Version: ks.Version, - L1BlockNumber: ks.L1BlockNumber, - L2BlockNumber: ks.L2BlockNumber, - ParentEPHash: ks.ParentEPHash.String(), - PrevKeystoneEPHash: ks.PrevKeystoneEPHash.String(), - StateRoot: ks.StateRoot.String(), - EPHash: ks.EPHash.String(), - } + keystones[i] = convertL2Keystone(&ks) } return L2KeystoneResult{ @@ -449,11 +489,11 @@ func bitcoinBalance(_ js.Value, args []js.Value) (any, error) { scriptHash := args[0].Get("scriptHash").String() - activePM, err := activeMiner() + miner, err := runningMiner() if err != nil { return nil, err } - pr, err := activePM.miner.BitcoinBalance(activePM.ctx, scriptHash) + pr, err := miner.BitcoinBalance(miner.ctx, scriptHash) if err != nil { return nil, err } @@ -468,11 +508,11 @@ func bitcoinInfo(_ js.Value, _ []js.Value) (any, error) { log.Tracef("bitcoinInfo") defer log.Tracef("bitcoinInfo exit") - activePM, err := activeMiner() + miner, err := runningMiner() if err != nil { return nil, err } - pr, err := activePM.miner.BitcoinInfo(activePM.ctx) + pr, err := miner.BitcoinInfo(miner.ctx) if err != nil { return nil, err } @@ -488,11 +528,11 @@ func bitcoinUTXOs(_ js.Value, args []js.Value) (any, error) { scriptHash := args[0].Get("scriptHash").String() - activePM, err := activeMiner() + miner, err := runningMiner() if err != nil { return nil, err } - pr, err := activePM.miner.BitcoinUTXOs(activePM.ctx, scriptHash) + pr, err := miner.BitcoinUTXOs(miner.ctx, scriptHash) if err != nil { return nil, err } @@ -510,3 +550,54 @@ func bitcoinUTXOs(_ js.Value, args []js.Value) (any, error) { UTXOs: utxos, }, nil } + +func addEventListener(_ js.Value, args []js.Value) (any, error) { + log.Tracef("addEventListener") + defer log.Tracef("addEventListener exit") + + eventType := args[0].Get("eventType").String() + handler := args[0].Get("handler") + + event, ok := eventTypes[eventType] + if !ok { + return nil, errorWithCode(ErrorCodeInvalidValue, + fmt.Errorf("invalid event type: %s", eventType)) + } + + svc.listenersMtx.Lock() + svc.listeners[event] = append(svc.listeners[event], handler) + svc.listenersMtx.Unlock() + + return js.Null(), nil +} + +func removeEventListener(_ js.Value, args []js.Value) (any, error) { + log.Tracef("removeEventListener") + defer log.Tracef("removeEventListener exit") + + eventType := args[0].Get("eventType").String() + handler := args[0].Get("handler") + + event, ok := eventTypes[eventType] + if !ok { + return nil, errorWithCode(ErrorCodeInvalidValue, + fmt.Errorf("invalid event type: %s", eventType)) + } + + svc.listenersMtx.Lock() + eventHandlers := svc.listeners[event] + for i, h := range eventHandlers { + if handler.Equal(h) { + // Remove handler from the slice. + // We don't care about the order, so set the current index to the + // value of the last index, then delete the last index. + handlersLen := len(eventHandlers) + eventHandlers[i] = eventHandlers[handlersLen-1] + eventHandlers = eventHandlers[:handlersLen-1] + } + } + svc.listeners[event] = eventHandlers + svc.listenersMtx.Unlock() + + return js.Null(), nil +} diff --git a/web/popminer/popminer.go b/web/popminer/popminer.go index c62b2d36..3089ad38 100644 --- a/web/popminer/popminer.go +++ b/web/popminer/popminer.go @@ -29,20 +29,85 @@ var ( log = loggo.GetLogger("@hemilabs/pop-miner") ) -var ( - pmMtx sync.Mutex - pm *Miner // Global Miner instance. -) +// svc is a global object storing data for the WebAssembly service. +// This can be accessed concurrently, however certain mutexes must be used to +// read/write certain fields. +var svc Service + +// Service is a global struct used to store data for the WebAssembly service. +type Service struct { + minerMtx sync.RWMutex + miner *Miner + + listenersMtx sync.RWMutex + listeners map[EventType][]js.Value +} + +// handleMinerEvent handles an event dispatched by the PoP miner. +func (s *Service) handleMinerEvent(popmEventType popm.EventType, data any) { + eventType, ok := popmEvents[popmEventType] + if !ok { + log.Errorf("unknown popm event type: %v", popmEventType) + return + } + s.dispatchEvent(eventType, convertEvent(data)) +} -// Miner is a global instance of [popm.Miner]. +func (s *Service) dispatchEvent(eventType EventType, data any) { + s.listenersMtx.RLock() + defer s.listenersMtx.RUnlock() + allHs, aok := s.listeners["*"] // Special handlers that receive all events. + hs, ok := s.listeners[eventType] + if !ok && !aok { + // There are no listeners for this event type. + return + } + + jsEvent := jsValueOf(data) + if jsEvent.IsNull() { + jsEvent = objectConstructor.New() + } else if jsEvent.Type() != js.TypeObject { + log.Errorf("Invalid event data: %s, must be %s", + jsEvent.Type(), js.TypeObject) + return + } + jsEvent.Set("type", jsValueOf(eventType)) + + // Dispatch to all handlers for this event type. + for _, h := range hs { + h.Invoke(jsEvent) + } + + // Dispatch to handlers that receive all events, after dispatching to + // handlers specific to this event type. + for _, h := range allHs { + h.Invoke(jsEvent) + } +} + +// Miner represents a running PoP Miner along with its context. +// Errors encountered while starting the miner should be sent to the errCh channel. type Miner struct { - // Don't like adding these into the object but c'est la wasm ctx context.Context cancel context.CancelFunc - miner *popm.Miner + *popm.Miner + + errCh chan error + wg sync.WaitGroup +} + +func (m *Miner) shutdown() error { + m.cancel() + m.wg.Wait() - wg sync.WaitGroup - err error + select { + case err := <-m.errCh: + return err + default: + } + close(m.errCh) + + return nil } func init() { @@ -53,6 +118,9 @@ func main() { log.Tracef("main") defer log.Tracef("main exit") + // Create event listeners map + svc.listeners = make(map[EventType][]js.Value) + // Enable function dispatcher log.Infof("=== Start of Day ===") log.Infof("%v version %v compiled with go version %v %v/%v revision %v", @@ -68,11 +136,11 @@ func main() { <-make(chan struct{}) // prevents the program from exiting } -func activeMiner() (*Miner, error) { - pmMtx.Lock() - defer pmMtx.Unlock() - if pm == nil { - return nil, errors.New("pop miner not running") +func runningMiner() (*Miner, error) { + svc.minerMtx.RLock() + defer svc.minerMtx.RUnlock() + if m := svc.miner; m != nil { + return m, nil } - return pm, nil + return nil, errors.New("miner not running") } diff --git a/web/popminer/util.go b/web/popminer/util.go index f8ee4909..4eea52ff 100644 --- a/web/popminer/util.go +++ b/web/popminer/util.go @@ -15,6 +15,9 @@ import ( "syscall/js" "time" "unsafe" + + "github.com/hemilabs/heminetwork/hemi" + "github.com/hemilabs/heminetwork/service/popm" ) var ( @@ -283,3 +286,38 @@ func newJSError(code ErrorCode, message string) js.Value { Timestamp: time.Now().Unix(), }) } + +// convertL2Keystone converts a [hemi.L2Keystone] to an L2Keystone. +func convertL2Keystone(ks *hemi.L2Keystone) L2Keystone { + if ks == nil { + panic("convertL2Keystone: cannot handle nil *hemi.L2Keystone") + } + + return L2Keystone{ + Version: ks.Version, + L1BlockNumber: ks.L1BlockNumber, + L2BlockNumber: ks.L2BlockNumber, + ParentEPHash: ks.ParentEPHash.String(), + PrevKeystoneEPHash: ks.PrevKeystoneEPHash.String(), + StateRoot: ks.StateRoot.String(), + EPHash: ks.EPHash.String(), + } +} + +// convertEvent converts a popm event struct to a WASM popm event struct. +func convertEvent(data any) any { + switch d := data.(type) { + case popm.EventMineKeystone: + return EventMineKeystone{ + Keystone: convertL2Keystone(d.Keystone), + } + case popm.EventTransactionBroadcast: + return EventTransactionBroadcast{ + Keystone: convertL2Keystone(d.Keystone), + TxHash: d.TxHash, + } + default: + log.Errorf("unknown popm event: %T", data) + return nil + } +} diff --git a/web/www/index.html b/web/www/index.html index 93464d61..96dfe19a 100644 --- a/web/www/index.html +++ b/web/www/index.html @@ -117,6 +117,12 @@

     
 
+    
+


+

Events

+

+    
+ diff --git a/web/www/index.js b/web/www/index.js index 326c2216..5820576a 100644 --- a/web/www/index.js +++ b/web/www/index.js @@ -9,6 +9,7 @@ let wasm; // This stores the global object created by the WASM binary. // Called after the WASM binary has been loaded. async function init() { wasm = globalThis['@hemilabs/pop-miner']; + void registerEventListener(); } async function dispatch(args) { @@ -236,3 +237,23 @@ async function BitcoinUTXOs() { BitcoinUTXOsButton.addEventListener('click', () => { BitcoinUTXOs(); }); + +// Events +const eventsDisplay = document.querySelector('.eventsDisplay'); + +async function registerEventListener() { + try { + const result = await dispatch({ + method: 'addEventListener', + eventType: '*', + handler: handleEvent, + }); + console.debug('addEventListener: ', JSON.stringify(result, null, 2)); + } catch (err) { + console.error('Caught exception', err); + } +} + +function handleEvent(event) { + eventsDisplay.innerText += `\n${JSON.stringify(event, null, 2)}\n`; +}