diff --git a/.gitmodules b/.gitmodules index 507435e0ca..facf0ad24d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -224,3 +224,11 @@ url = https://github.com/status-im/nim-zlib.git ignore = untracked branch = master +[submodule "vendor/DOtherSide"] + path = vendor/DOtherSide + url = https://github.com/filcuc/DOtherSide.git + branch = master +[submodule "vendor/nimqml"] + path = vendor/nimqml + url = https://github.com/filcuc/nimqml.git + branch = master diff --git a/Makefile b/Makefile index 8e79951425..75c1482ce2 100644 --- a/Makefile +++ b/Makefile @@ -257,6 +257,11 @@ $(TOOLS): | build deps MAKE="$(MAKE)" V="$(V)" $(ENV_SCRIPT) scripts/compile_nim_program.sh $@ "$${TOOL_DIR}/$@.nim" $(NIM_PARAMS) && \ echo -e $(BUILD_END_MSG) "build/$@" +ngui/ngui: | build deps + + echo -e $(BUILD_MSG) "build/$@" && \ + MAKE="$(MAKE)" V="$(V)" $(ENV_SCRIPT) scripts/compile_nim_program.sh $@ "ngui.ngui.nim" $(NIM_PARAMS) && \ + echo -e $(BUILD_END_MSG) "ngui/ngui" + clean_eth2_network_simulation_data: rm -rf tests/simulation/data diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index 46d6fd1fe8..8c235c3669 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -365,22 +365,29 @@ template getForkedBlockField*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBe of BeaconBlockFork.Altair: unsafeAddr x.altairData.message.y of BeaconBlockFork.Bellatrix: unsafeAddr x.bellatrixData.message.y)[] -template signature*(x: ForkedSignedBeaconBlock): ValidatorSig = +template getForkedBodyField*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock, y: untyped): untyped = + # unsafeAddr avoids a copy of the field in some cases + (case x.kind + of BeaconBlockFork.Phase0: unsafeAddr x.phase0Data.message.body.y + of BeaconBlockFork.Altair: unsafeAddr x.altairData.message.body.y + of BeaconBlockFork.Bellatrix: unsafeAddr x.bellatrixData.message.body.y)[] + +func signature*(x: ForkedSignedBeaconBlock): ValidatorSig = withBlck(x): blck.signature -template signature*(x: ForkedTrustedSignedBeaconBlock): TrustedSig = +func signature*(x: ForkedTrustedSignedBeaconBlock): TrustedSig = withBlck(x): blck.signature -template root*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Eth2Digest = +func root*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Eth2Digest = withBlck(x): blck.root -template slot*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Slot = +func slot*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Slot = withBlck(x): blck.message.slot -template shortLog*(x: ForkedBeaconBlock): auto = +func shortLog*(x: ForkedBeaconBlock): auto = withBlck(x): shortLog(blck) -template shortLog*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): auto = +func shortLog*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): auto = withBlck(x): shortLog(blck) chronicles.formatIt ForkedBeaconBlock: it.shortLog diff --git a/ngui/attestationlist.nim b/ngui/attestationlist.nim new file mode 100644 index 0000000000..b7db634563 --- /dev/null +++ b/ngui/attestationlist.nim @@ -0,0 +1,65 @@ +import + std/[sequtils, tables], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/[eth2_merkleization, helpers], + ./objecttablemodel, ./utils + +type + AttestationInfo* = object + slot*: int + index*: int + beacon_block_root*: string + source_epoch*: int + source_root*: string + target_epoch*: int + target_root*: string + aggregation_bits*: string + +proc toAttestationInfo*(v: Attestation): AttestationInfo = + AttestationInfo( + slot: v.data.slot.int, + index: v.data.index.int, + beacon_block_root: toBlockLink(v.data.beacon_block_root), + source_epoch: v.data.source.epoch.int, + source_root: toBlockLink(v.data.source.root), + target_epoch: v.data.target.epoch.int, + target_root: toBlockLink(v.data.target.root), + aggregation_bits: $v.aggregation_bits, + ) + +QtObject: + type AttestationList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[AttestationInfo] + + proc setup(self: AttestationList) = self.QAbstractTableModel.setup + + proc delete(self: AttestationList) = + self.QAbstractTableModel.delete + + proc newAttestationList*(data: seq[Attestation]): AttestationList = + new(result, delete) + result.data = ObjectTableModelImpl[AttestationInfo](items: data.mapIt(it.toAttestationInfo())) + result.setup + + method rowCount(self: AttestationList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: AttestationList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*(self: AttestationList, section: int, orientation: QtOrientation, role: int): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: AttestationList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: AttestationList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: AttestationList, v: seq[Attestation]) = + self.data.setNewData(self, v.mapIt(it.toAttestationInfo())) + + proc sort*(self: AttestationList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/ngui/attesterslashinglist.nim b/ngui/attesterslashinglist.nim new file mode 100644 index 0000000000..4dc1ee25fa --- /dev/null +++ b/ngui/attesterslashinglist.nim @@ -0,0 +1,51 @@ +import + std/[sequtils, tables], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/[helpers], + ./objecttablemodel, ./utils + +type + AttesterSlashingInfo* = object + info*: string + +proc toAttesterSlashingInfo*(v: AttesterSlashing): AttesterSlashingInfo = + AttesterSlashingInfo( + info: $v + ) + +QtObject: + type AttesterSlashingList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[AttesterSlashingInfo] + + proc setup(self: AttesterSlashingList) = self.QAbstractTableModel.setup + + proc delete(self: AttesterSlashingList) = + self.QAbstractTableModel.delete + + proc newAttesterSlashingList*(data: openArray[AttesterSlashing]): AttesterSlashingList = + new(result, delete) + result.data = ObjectTableModelImpl[AttesterSlashingInfo](items: data.mapIt(it.toAttesterSlashingInfo())) + result.setup + + method rowCount(self: AttesterSlashingList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: AttesterSlashingList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*(self: AttesterSlashingList, section: int, orientation: QtOrientation, role: int): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: AttesterSlashingList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: AttesterSlashingList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: AttesterSlashingList, v: openArray[AttesterSlashing]) = + self.data.setNewData(self, v.mapIt(it.toAttesterSlashingInfo())) + + proc sort*(self: AttesterSlashingList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/ngui/blockmodel.nim b/ngui/blockmodel.nim new file mode 100644 index 0000000000..c7fe0d57b8 --- /dev/null +++ b/ngui/blockmodel.nim @@ -0,0 +1,97 @@ +import NimQml + +import + std/[sequtils, times], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/datatypes/[phase0, altair], + "."/[ + attestationlist, depositlist, attesterslashinglist, proposerslashinglist, + voluntaryexitlist, utils] + +QtObject: + type + BlockModel* = ref object of QObject + blck: ForkedSignedBeaconBlock + attestationsx: AttestationList + depositsx: DepositList + attester_slashingsx: AttesterSlashingList + proposer_slashingsx: ProposerSlashingList + voluntary_exitsx: VoluntaryExitList + genesis_time*: uint64 + + proc delete*(self: BlockModel) = + self.QObject.delete + + proc setup*(self: BlockModel) = + self.QObject.setup + + proc newBlockModel*(forked: ForkedSignedBeaconBlock, genesis_time: uint64): BlockModel = + + let res = withBlck(forked): BlockModel( + blck: forked, + attestationsx: newAttestationList(blck.message.body.attestations.asSeq()), + depositsx: newDepositList(blck.message.body.deposits.mapIt(it.toDepositInfo())), + attester_slashingsx: newAttesterSlashingList(blck.message.body.attester_slashings.asSeq()), + proposer_slashingsx: newProposerSlashingList(blck.message.body.proposer_slashings.asSeq()), + voluntary_exitsx: newVoluntaryExitList(blck.message.body.voluntary_exits.asSeq()), + genesis_time: genesis_time, + ) + res.setup() + res + + proc `blck=`*(self: BlockModel, forked: ForkedSignedBeaconBlock) = + self.blck = forked + withBlck(forked): + self.attestationsx.setNewData(blck.message.body.attestations.asSeq()) + self.depositsx.setNewData(blck.message.body.deposits.mapIt(it.toDepositInfo())) + self.attester_slashingsx.setNewData(blck.message.body.attester_slashings.asSeq()) + self.proposer_slashingsx.setNewData(blck.message.body.proposer_slashings.asSeq()) + self.voluntary_exitsx.setNewData(blck.message.body.voluntary_exits.asSeq()) + + proc slot*(self: BlockModel): int {.slot.} = getForkedBlockField(self.blck, slot).int + QtProperty[int] slot: read = slot + + proc time*(self: BlockModel): string {.slot.} = + let t = self.genesis_time + getForkedBlockField(self.blck, slot) * SECONDS_PER_SLOT + $fromUnix(t.int64).utc + QtProperty[string] time: read = time + + proc root*(self: BlockModel): string {.slot.} = toDisplayHex(self.blck.root.data) + QtProperty[string] root: read = root + + proc proposer_index*(self: BlockModel): int {.slot.} = getForkedBlockField(self.blck, proposer_index).int + QtProperty[int] proposer_index: read = proposer_index + + proc parent_root*(self: BlockModel): string {.slot.} = toBlockLink(getForkedBlockField(self.blck, parent_root)) + QtProperty[string] parent_root: read = parent_root + + proc state_root*(self: BlockModel): string {.slot.} = toDisplayHex(getForkedBlockField(self.blck, state_root).data) + QtProperty[string] state_root: read = state_root + + proc randao_reveal*(self: BlockModel): string {.slot.} = toDisplayHex(getForkedBodyField(self.blck, randao_reveal)) + QtProperty[string] randao_reveal: read = randao_reveal + + proc eth1_data*(self: BlockModel): string {.slot.} = RestJson.encode(getForkedBodyField(self.blck, eth1_data), pretty=true) + QtProperty[string] eth1_data: read = eth1_data + + proc graffiti*(self: BlockModel): string {.slot.} = $getForkedBodyField(self.blck, graffiti) + QtProperty[string] graffiti: read = graffiti + + proc proposer_slashings*(self: BlockModel): QVariant {.slot.} = newQVariant(self.proposer_slashingsx) + QtProperty[QVariant] proposer_slashings: read = proposer_slashings + + proc attester_slashings*(self: BlockModel): QVariant {.slot.} = newQVariant(self.attester_slashingsx) + QtProperty[QVariant] attester_slashings: read = attester_slashings + + proc attestations*(self: BlockModel): QVariant {.slot.} = newQVariant(self.attestationsx) + QtProperty[QVariant] attestations: read = attestations + + proc deposits*(self: BlockModel): QVariant {.slot.} = newQVariant(self.depositsx) + QtProperty[QVariant] deposits: read = deposits + + proc voluntary_exits*(self: BlockModel): QVariant {.slot.} = newQVariant(self.voluntary_exitsx) + QtProperty[QVariant] voluntary_exits: read = voluntary_exits + + proc signature*(self: BlockModel): string {.slot.} = toDisplayHex(self.blck.signature) + QtProperty[string] signature: read = signature diff --git a/ngui/depositlist.nim b/ngui/depositlist.nim new file mode 100644 index 0000000000..cc7a49abc6 --- /dev/null +++ b/ngui/depositlist.nim @@ -0,0 +1,56 @@ +import + std/[tables], + NimQml, + ../beacon_chain/spec/datatypes/base, + ./objecttablemodel, ./utils + +type + DepositInfo* = object + pubkey*: string + withdrawal_credentials*: string + amount*: uint64 + signature*: string + +proc toDepositInfo*(v: Deposit): DepositInfo = + DepositInfo( + pubkey: toDisplayHex(v.data.pubkey.toRaw()), + withdrawal_credentials: toDisplayHex(v.data.withdrawal_credentials), + amount: v.data.amount, + signature: toDisplayHex(v.data.signature), + ) + +QtObject: + type DepositList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[DepositInfo] + + proc setup(self: DepositList) = self.QAbstractTableModel.setup + + proc delete(self: DepositList) = + self.QAbstractTableModel.delete + + proc newDepositList*(data: seq[DepositInfo]): DepositList = + new(result, delete) + result.data = ObjectTableModelImpl[DepositInfo](items: data) + result.setup + + method rowCount(self: DepositList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: DepositList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*(self: DepositList, section: int, orientation: QtOrientation, role: int): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: DepositList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: DepositList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: DepositList, v: seq[DepositInfo]) = + self.data.setNewData(self, v) + + proc sort*(self: DepositList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/ngui/epochmodel.nim b/ngui/epochmodel.nim new file mode 100644 index 0000000000..6c201c25db --- /dev/null +++ b/ngui/epochmodel.nim @@ -0,0 +1,52 @@ +import NimQml + +import + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ./slotlist + +QtObject: + type + EpochModel* = ref object of QObject + client: RestClientRef + epoch: int + slotList: SlotList + + proc delete*(self: EpochModel) = + self.QObject.delete + + proc setup*(self: EpochModel) = + self.QObject.setup + + proc newEpochModel*(client: RestClientRef, epoch: int): EpochModel = + let data = client.loadSlots(epoch.Epoch) + let res = EpochModel(client: client, epoch: epoch, slotList: newSlotList(data)) + res.setup() + res + + proc epoch*(self: EpochModel): int {.slot.} = self.epoch + proc epochChanged*(self: EpochModel, v: int) {.signal.} + QtProperty[int] epoch: + read = epoch + notify = epochChanged + + proc getSlotList*(self: EpochModel): QVariant {.slot.} = newQVariant(self.slotList) + QtProperty[QVariant] slotList: read = getSlotList + + proc setNewData*(self: EpochModel, epoch: int, data: seq[SlotInfo]) = + self.epoch = epoch + self.epochChanged(epoch) + + self.slotList.setNewData(data) + + proc reload(self: EpochModel) {.slot.} = + self.slotList.setNewData(self.client.loadSlots(self.epoch.Epoch)) + + proc next(self: EpochModel) {.slot.} = + self.epoch = self.epoch + 1 + self.epochChanged(self.epoch) + self.reload() # TODO listen to epochchanged + + proc prev(self: EpochModel) {.slot.} = + self.epoch = self.epoch - 1 + self.epochChanged(self.epoch) + self.reload() # TODO listen to epochchanged diff --git a/ngui/footermodel.nim b/ngui/footermodel.nim new file mode 100644 index 0000000000..f9c3352b32 --- /dev/null +++ b/ngui/footermodel.nim @@ -0,0 +1,42 @@ +import NimQml + +QtObject: + type + FooterModel* = ref object of QObject + finalized: string + head: string + syncing: string + + proc delete*(self: FooterModel) = + self.QObject.delete + + proc setup*(self: FooterModel) = + self.QObject.setup + + proc newFooterModel*(): FooterModel = + let res = FooterModel() + res.setup() + res + + proc finalized*(self: FooterModel): string {.slot.} = self.finalized + proc finalizedChanged*(self: FooterModel, v: string) {.signal.} + proc `finalized=`*(self: FooterModel, v: string) = + self.finalized = v + self.finalizedChanged(v) + QtProperty[string] finalized: + read = finalized + notify = finalizedChanged + + proc head*(self: FooterModel): string {.slot.} = self.head + proc headChanged*(self: FooterModel, v: string) {.signal.} + proc `head=`*(self: FooterModel, v: string) = + self.head = v + self.headChanged(v) + QtProperty[string] head: read = head; notify = headChanged + + proc syncing*(self: FooterModel): string {.slot.} = self.syncing + proc syncingChanged*(self: FooterModel, v: string) {.signal.} + proc `syncing=`*(self: FooterModel, v: string) = + self.syncing = v + self.syncingChanged(v) + QtProperty[string] syncing: read = syncing; notify = syncingChanged diff --git a/ngui/mainmodel.nim b/ngui/mainmodel.nim new file mode 100644 index 0000000000..0a939fc745 --- /dev/null +++ b/ngui/mainmodel.nim @@ -0,0 +1,145 @@ +import + NimQml, + "."/[ + blockmodel, footermodel, epochmodel, peerlist, slotlist, nodemodel, + poolmodel] + +import + std/[os, strutils], + chronos, metrics, + + # Local modules + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/datatypes/[phase0, altair], + ../beacon_chain/spec/[eth2_merkleization, helpers] + +QtObject: + type MainModel* = ref object of QObject + app: QApplication + blck: BlockModel + footer: FooterModel + client: RestClientRef + peerList: PeerList + epochModel: EpochModel + nodeModel: NodeModel + poolModel: PoolModel + + genesis: RestGenesis + currentIndex: int + + proc delete*(self: MainModel) = + self.QObject.delete + self.blck.delete + + proc setup(self: MainModel) = + self.QObject.setup + self.blck.setup + + proc newMainModel*(app: QApplication, url: string): MainModel = + let + client = RestClientRef.new(url).get() + + var + headBlock = (waitFor client.getBlockV2(BlockIdent.init(BlockIdentType.Head), defaultRuntimeConfig)).get() + epoch = getForkedBlockField(headBlock[], slot).epoch + genesis = (waitFor client.getGenesis()).data.data + peerList = newPeerList(@[]) + + let res = MainModel( + app: app, + blck: newBlockModel(headBlock[], genesis.genesis_time), + client: client, + footer: newFooterModel(), + peerList: peerList, + epochModel: newEpochModel(client, epoch.int), + nodeModel: newNodeModel(client), + poolModel: newPoolModel(client), + genesis: genesis, + ) + res.setup() + res + + proc onExitTriggered(self: MainModel) {.slot.} = + self.app.quit + + proc updateFooter(self: MainModel) {.slot.} = + let + checkpoints = (waitFor self.client.getStateFinalityCheckpoints(StateIdent.init(StateIdentType.Head))).data.data + head = (waitFor self.client.getBlockHeader(BlockIdent.init(BlockIdentType.Head))).data.data + syncing = (waitFor self.client.getSyncingStatus()).data.data + + self.footer.finalized = $shortLog(checkpoints.finalized) + self.footer.head = $shortLog(head.header.message.slot) + self.footer.syncing = $syncing + + proc updateSlots(self: MainModel) {.slot.} = + let + slots = self.client.loadSlots(self.epochModel.epoch.Epoch) + self.epochModel.setNewData(self.epochModel.epoch.int, slots) + + proc updatePeers(self: MainModel) {.slot.} = + try: + self.peerList.setNewData(waitFor(self.client.getPeers(@[], @[])).data.data) + except CatchableError as exc: + echo exc.msg + + proc getPeerList*(self: MainModel): QVariant {.slot.} = + newQVariant(self.peerList) + QtProperty[QVariant] peerList: + read = getPeerList + + proc getFooter*(self: MainModel): QVariant {.slot.} = + newQVariant(self.footer) + QtProperty[QVariant] footer: + read = getFooter + + proc getEpochModel*(self: MainModel): QVariant {.slot.} = + newQVariant(self.epochModel) + QtProperty[QVariant] epochModel: + read = getEpochModel + + proc getBlck(self: MainModel): QVariant {.slot.} = newQVariant(self.blck) + proc blckChanged*(self: MainModel, blck: QVariant) {.signal.} + proc setBlck(self: MainModel, blck: ForkedSignedBeaconBlock) = + self.blck.blck = blck + self.blckChanged(newQVariant(self.blck)) + + QtProperty[QVariant] blck: + read = getBlck + write = setBlck + notify = blckChanged + + proc getCurrentIndex(self: MainModel): int {.slot.} = self.currentIndex + proc currentIndexChanged*(self: MainModel, v: int) {.signal.} + proc setCurrentIndex(self: MainModel, v: int) = + self.currentIndex = v + self.currentIndexChanged(v) + + QtProperty[int] currentIndex: + read = getCurrentIndex + write = setCurrentIndex + notify = currentIndexChanged + + proc getNodeModel(self: MainModel): QVariant {.slot.} = newQVariant(self.nodeModel) + QtProperty[QVariant] nodeModel: + read = getNodeModel + + proc getPoolModel(self: MainModel): QVariant {.slot.} = newQVariant(self.poolModel) + QtProperty[QVariant] poolModel: + read = getPoolModel + + proc onLoadBlock(self: MainModel, root: string) {.slot.} = + try: + var blck = waitFor(self.client.getBlockV2( + BlockIdent.decodeString(root).tryGet(), defaultRuntimeConfig)) + if blck.isSome(): + self.setBlck(blck.get()[]) + + except CatchableError as exc: + echo exc.msg + discard + + proc openUrl(self: MainModel, url: string) {.slot.} = + if url.startsWith("block://"): + self.onLoadBlock(url[8..^1]) + self.setCurrentIndex(1) diff --git a/ngui/ngui.nim b/ngui/ngui.nim new file mode 100644 index 0000000000..fe9a9f6567 --- /dev/null +++ b/ngui/ngui.nim @@ -0,0 +1,35 @@ +import std/os + +import confutils + +import NimQml +import mainmodel + +# Build DOtherSide first! `cd vendor/DOtherSide; mkdir build; cd build; cmake ..; make` +{.passL: "-L " & currentSourcePath.parentDir & "/../vendor/DOtherSide/build/lib/".} +{.passL: "-lDOtherSideStatic".} +{.passl: gorge("pkg-config --libs --static Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets").} +{.passl: "-Wl,-as-needed".} + +proc mainProc(url: string) = + let app = newQApplication() + defer: app.delete + + let main = newMainModel(app, url) + defer: main.delete + + let engine = newQQmlApplicationEngine() + defer: engine.delete + + let mainVariant = newQVariant(main) + defer: mainVariant.delete + + engine.setRootContextProperty("main", mainVariant) + + engine.load("ui/main.qml") + app.exec() + +when isMainModule: + cli do(url = "http://localhost:5052"): + mainProc(url) + GC_fullcollect() diff --git a/ngui/nim.cfg b/ngui/nim.cfg new file mode 100644 index 0000000000..0709b5f2ce --- /dev/null +++ b/ngui/nim.cfg @@ -0,0 +1,4 @@ +--path:"../vendor/nimqml/src" +-d:"libp2p_pki_schemes=secp256k1" +--dynliboverrideall +gcc.linkerexe="g++" diff --git a/ngui/nodemodel.nim b/ngui/nodemodel.nim new file mode 100644 index 0000000000..5263af2056 --- /dev/null +++ b/ngui/nodemodel.nim @@ -0,0 +1,92 @@ +import NimQml + +import + std/[sequtils, json, times], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ./attestationlist, ./utils +template xxx(body): string = + try: + $(body) + except CatchableError as exc: + exc.msg + +QtObject: + type + NodeModel* = ref object of QObject + client: RestClientRef + genesis: string + heads: string + identity: string + version: string + health: string + + proc delete*(self: NodeModel) = + self.QObject.delete + + proc setup*(self: NodeModel) = + self.QObject.setup + + proc newNodeModel*(client: RestClientRef): NodeModel = + let res = NodeModel(client: client) + res.setup() + res + + proc getgenesis*(self: NodeModel): string {.slot.} = self.genesis + proc genesisChanged*(self: NodeModel, v: string) {.signal.} + proc setgenesis*(self: NodeModel, v: string) = + self.genesis = v + self.genesisChanged(v) + QtProperty[string] genesis: + read = getgenesis + notify = genesisChanged + write = setgenesis + + proc getheads*(self: NodeModel): string {.slot.} = self.heads + proc headsChanged*(self: NodeModel, v: string) {.signal.} + proc setheads*(self: NodeModel, v: string) = + self.heads = v + self.headsChanged(v) + QtProperty[string] heads: + read = getheads + notify = headsChanged + write = setheads + + proc getidentity*(self: NodeModel): string {.slot.} = self.identity + proc identityChanged*(self: NodeModel, v: string) {.signal.} + proc setidentity*(self: NodeModel, v: string) = + self.identity = v + self.identityChanged(v) + QtProperty[string] identity: + read = getidentity + notify = identityChanged + write = setidentity + + proc getversion*(self: NodeModel): string {.slot.} = self.version + proc versionChanged*(self: NodeModel, v: string) {.signal.} + proc setversion*(self: NodeModel, v: string) = + self.version = v + self.versionChanged(v) + QtProperty[string] version: + read = getversion + notify = versionChanged + write = setversion + + proc gethealth*(self: NodeModel): string {.slot.} = self.health + proc healthChanged*(self: NodeModel, v: string) {.signal.} + proc sethealth*(self: NodeModel, v: string) = + self.health = v + self.healthChanged(v) + QtProperty[string] health: + read = gethealth + notify = healthChanged + write = sethealth + + proc update*(self: NodeModel) {.slot.} = + self.setgenesis(xxx(waitFor(self.client.getGenesis()).data.data)) + self.setheads(xxx(waitFor(self.client.getDebugChainHeads()).data.data.mapIt( + toBlockLink(it.root) & " @ " & $it.slot + ).join("\n"))) + self.setidentity(xxx(waitFor(self.client.getNetworkIdentity()).data.data)) + self.setversion(xxx(waitFor(self.client.getNodeVersion()).data.data.version)) + self.sethealth(xxx(waitFor(self.client.getHealth()))) diff --git a/ngui/objecttablemodel.nim b/ngui/objecttablemodel.nim new file mode 100644 index 0000000000..22e9f535ef --- /dev/null +++ b/ngui/objecttablemodel.nim @@ -0,0 +1,79 @@ +{.push raises: [Defect].} + +import NimQml + +import + std/[algorithm, tables] + +type ObjectTableModelImpl*[T] = object + items*: seq[T] + sortColumn*: int + direction*: bool + +func rowCount*(self: ObjectTableModelImpl, index: QModelIndex = nil): int = + self.items.len + +func columnCount*(self: ObjectTableModelImpl, index: QModelIndex = nil): int = + for j in default(type(self.items[0])).fields(): # TODO avoid default + result += 1 + +func headerData*(self: ObjectTableModelImpl, section: int, orientation: QtOrientation, role: int): QVariant = + ## Returns the data for the given role and section in the header with the specified orientation + var i = 0 + for n, v in default(self.T).fieldPairs(): # TODO avoid default + if i == section: + return newQVariant(n) + i += 1 + +func data*(self: ObjectTableModelImpl, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.items.len: + return + let peer = self.items[index.row] + var i = 0 + for j in peer.fields(): + if i == index.column: + return newQVariant(j) + i += 1 + +func roleNames*(self: ObjectTableModelImpl): Table[int, string] = + {0: "display",}.toTable + +func doSort(self: var ObjectTableModelImpl) = + let + column = self.sortColumn + dir = self.direction + func myCmp(x, y: self.T): int = + var i = 0 + for xv, yv in fields(x, y): + if i == column: + let c = cmp(xv, yv) + return if not dir: c else: -c + i += 1 + 0 + + sort(self.items, myCmp) + +func setNewData*(self: var ObjectTableModelImpl, model: QAbstractTableModel, items: seq[self.T]) = + model.beginResetModel() + self.items = items + self.doSort() + model.endResetModel() + +func sort*(self: var ObjectTableModelImpl, model: QAbstractTableModel, section: int) = + model.beginResetModel() + if self.sortColumn == section: + self.direction = not self.direction + else: + self.direction = false + self.sortColumn = section + + self.doSort() + + model.endResetModel() + +func init*[E](T: type ObjectTableModelImpl[E], items: seq[E]): T = + var res = T(items: items) + res.doSort() + res diff --git a/ngui/peerlist.nim b/ngui/peerlist.nim new file mode 100644 index 0000000000..7588dc540f --- /dev/null +++ b/ngui/peerlist.nim @@ -0,0 +1,39 @@ +import + std/tables, + NimQml, + ../beacon_chain/spec/eth2_apis/rest_types, + ./objecttablemodel + +QtObject: + type PeerList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[RestNodePeer] + + proc setup(self: PeerList) = self.QAbstractTableModel.setup + proc delete(self: PeerList) = self.QAbstractTableModel.delete + + proc newPeerList*(items: seq[RestNodePeer]): PeerList = + new(result, delete) + result.data = ObjectTableModelImpl[RestNodePeer].init(items) + result.setup + + method rowCount(self: PeerList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: PeerList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*(self: PeerList, section: int, orientation: QtOrientation, role: int): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: PeerList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: PeerList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: PeerList, v: seq[RestNodePeer]) = + self.data.setNewData(self, v) + + proc sort*(self: PeerList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/ngui/poolmodel.nim b/ngui/poolmodel.nim new file mode 100644 index 0000000000..760e8c7d36 --- /dev/null +++ b/ngui/poolmodel.nim @@ -0,0 +1,70 @@ +import NimQml + +import + std/[sequtils, times], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ./attestationlist, ./attesterslashinglist, proposerslashinglist, voluntaryexitlist, ./utils + +template xxx(body): untyped = + try: + body.data.data + except CatchableError as exc: + debugEcho exc.msg + @[] + +QtObject: + type + PoolModel* = ref object of QObject + client: RestClientRef + attestationsx: AttestationList + attesterSlashingsx: AttesterSlashingList + proposerSlashingsx: ProposerSlashingList + voluntaryExitsx: VoluntaryExitList + + proc delete*(self: PoolModel) = + self.QObject.delete + + proc setup*(self: PoolModel) = + self.QObject.setup + + proc newPoolModel*(client: RestClientRef): PoolModel = + let res = PoolModel( + client: client, + attestationsx: newAttestationList(@[]), + attesterSlashingsx: newAttesterSlashingList(@[]), + proposerSlashingsx: newProposerSlashingList(@[]), + voluntaryExitsx: newVoluntaryExitList(@[]), + ) + res.setup() + res + + proc attestations*(self: PoolModel): QVariant {.slot.} = newQVariant(self.attestationsx) + QtProperty[QVariant] attestations: read = attestations + + proc attesterSlashings*(self: PoolModel): QVariant {.slot.} = newQVariant(self.attesterSlashingsx) + QtProperty[QVariant] attesterSlashings: read = attesterSlashings + + proc proposerSlashings*(self: PoolModel): QVariant {.slot.} = newQVariant(self.proposerSlashingsx) + QtProperty[QVariant] proposerSlashings: read = proposerSlashings + + proc voluntaryExits*(self: PoolModel): QVariant {.slot.} = newQVariant(self.voluntaryExitsx) + QtProperty[QVariant] voluntaryExits: read = voluntaryExits + + proc updateAttestations*(self: PoolModel) {.slot.} = + self.attestationsx.setNewData(xxx(waitFor self.client.getPoolAttestations(none(Slot), none(CommitteeIndex)))) + + proc updateAttesterSlashings*(self: PoolModel) {.slot.} = + self.attesterSlashingsx.setNewData(xxx(waitFor self.client.getPoolAttesterSlashings())) + + proc updateProposerSlashings*(self: PoolModel) {.slot.} = + self.proposerSlashingsx.setNewData(xxx(waitFor self.client.getPoolProposerSlashings())) + + proc updateVoluntaryExits*(self: PoolModel) {.slot.} = + self.voluntaryExitsx.setNewData(xxx(waitFor self.client.getPoolVoluntaryExits())) + + proc update*(self: PoolModel) {.slot.} = + self.updateAttestations() + self.updateAttesterSlashings() + self.updateProposerSlashings() + self.updateVoluntaryExits() diff --git a/ngui/proposerslashinglist.nim b/ngui/proposerslashinglist.nim new file mode 100644 index 0000000000..dd5033f79f --- /dev/null +++ b/ngui/proposerslashinglist.nim @@ -0,0 +1,51 @@ +import + std/[sequtils, tables], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/helpers, + ./objecttablemodel, ./utils + +type + ProposerSlashingInfo* = object + info*: string + +proc toProposerSlashingInfo*(v: ProposerSlashing): ProposerSlashingInfo = + ProposerSlashingInfo( + info: $v + ) + +QtObject: + type ProposerSlashingList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[ProposerSlashingInfo] + + proc setup(self: ProposerSlashingList) = self.QAbstractTableModel.setup + + proc delete(self: ProposerSlashingList) = + self.QAbstractTableModel.delete + + proc newProposerSlashingList*(data: openArray[ProposerSlashing]): ProposerSlashingList = + new(result, delete) + result.data = ObjectTableModelImpl[ProposerSlashingInfo](items: data.mapIt(it.toProposerSlashingInfo())) + result.setup + + method rowCount(self: ProposerSlashingList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: ProposerSlashingList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*(self: ProposerSlashingList, section: int, orientation: QtOrientation, role: int): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: ProposerSlashingList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: ProposerSlashingList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: ProposerSlashingList, v: seq[ProposerSlashing]) = + self.data.setNewData(self, v.mapIt(it.toProposerSlashingInfo())) + + proc sort*(self: ProposerSlashingList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/ngui/slotlist.nim b/ngui/slotlist.nim new file mode 100644 index 0000000000..fdebb11bdd --- /dev/null +++ b/ngui/slotlist.nim @@ -0,0 +1,74 @@ +import + std/[tables], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/[eth2_merkleization, helpers], + ./objecttablemodel, ./utils + +type + SlotInfo* = object + slot*: int + proposer_index*: int + block_root*: string + +proc loadSlots*(client: RestClientRef, epoch: Epoch): seq[SlotInfo] {.raises: [Defect].} = + var res: seq[SlotInfo] + let proposers = try: + (waitFor client.getProposerDuties(epoch)).data.data + except CatchableError: + newSeq[RestProposerDuty](SLOTS_PER_EPOCH) + + for i in 0.. 0 + && tableView.width > 10 ? tableView.width / tableView.columns : 100 + } + rowHeightProvider: function (column) { + return 35 + } + onWidthChanged: forceLayout() + + ScrollBar.horizontal: ScrollBar {} + ScrollBar.vertical: ScrollBar {} + + delegate: Rectangle { + clip: true + TextEdit { + anchors.fill: parent + anchors.margins: 10 + + text: display + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + + onLinkActivated: main.openUrl(link) + } + } + + Rectangle { + // mask the headers + z: 3 + color: "#222222" + y: tableView.contentY + x: tableView.contentX + width: tableView.leftMargin + height: tableView.topMargin + } + + Row { + id: columnsHeader + y: tableView.contentY + z: 2 + Repeater { + model: tableView.columns > 0 ? tableView.columns : 1 + Label { + property bool sortDirection + width: tableView.columnWidthProvider(modelData) + height: 35 + text: tableView.model.headerData(modelData, Qt.Horizontal) + color: '#aaaaaa' + font.pixelSize: 15 + padding: 10 + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + clip: true + background: Rectangle { + color: "#333333" + } + + MouseArea { + anchors.fill: parent + onClicked: { + tableView.model.sort(modelData, sortDirection) + sortDirection = !sortDirection + } + } + } + } + } +} diff --git a/ngui/ui/Peers.qml b/ngui/ui/Peers.qml new file mode 100644 index 0000000000..7b7fa7bc2e --- /dev/null +++ b/ngui/ui/Peers.qml @@ -0,0 +1,17 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +RowLayout { + property var viewData + + id: layout + + ObjectTableView { + Layout.fillHeight: true + Layout.fillWidth: true + + id: tableView + model: viewData + } +} diff --git a/ngui/ui/Pools.qml b/ngui/ui/Pools.qml new file mode 100644 index 0000000000..bc4b142a9b --- /dev/null +++ b/ngui/ui/Pools.qml @@ -0,0 +1,82 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +Rectangle { + property var viewData + + ColumnLayout { + anchors.fill: parent + + TabBar { + id: tabBar + + TabButton { + text: "Attestations" + width: implicitWidth + onClicked: viewData.updateAttestations() + } + TabButton { + text: "Attester slashings" + width: implicitWidth + onClicked: viewData.updateAttesterSlashings() + } + TabButton { + text: "Proposer slashings" + width: implicitWidth + onClicked: viewData.updatProposerSlashings() + } + TabButton { + text: "Voluntary exits" + width: implicitWidth + onClicked: viewData.updateVoluntaryExits() + } + } + + StackLayout { + Layout.fillHeight: true + Layout.fillWidth: true + currentIndex: tabBar.currentIndex + + ObjectTableView { + model: viewData.attestations + columnWidthProvider: function (column) { + if (column == 0) + return 70 + if (column == 1) + return 50 + if (column == 2) + return 250 + if (column == 3) + return 70 + if (column == 4) + return 250 + if (column == 5) + return 70 + if (column == 6) + return 250 + return 350 + } + } + + ObjectTableView { + model: viewData.attesterSlashings + columnWidthProvider: function (column) { + return 350 + } + } + ObjectTableView { + model: viewData.proposerSlashings + columnWidthProvider: function (column) { + return 350 + } + } + ObjectTableView { + model: viewData.voluntaryExits + columnWidthProvider: function (column) { + return 350 + } + } + } + } +} diff --git a/ngui/ui/Slots.qml b/ngui/ui/Slots.qml new file mode 100644 index 0000000000..6b1a3dbcc5 --- /dev/null +++ b/ngui/ui/Slots.qml @@ -0,0 +1,42 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +ColumnLayout { + property var viewData + + id: layout + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 5 + Button { + text: "Prev" + onClicked: viewData.prev() + } + Text { + text: "Epoch" + } + Text { + text: viewData.epoch + } + + Button { + text: "Next" + onClicked: viewData.next() + } + } + + ObjectTableView { + model: viewData.slotList + Layout.alignment: Qt.AlignHCenter + + columnWidthProvider: function (column) { + if (column == 0) + return 70 + if (column == 1) + return 70 + return 450 + } + } +} diff --git a/ngui/ui/States.qml b/ngui/ui/States.qml new file mode 100644 index 0000000000..2d7939c319 --- /dev/null +++ b/ngui/ui/States.qml @@ -0,0 +1,40 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +Rectangle { + property var viewData + + ColumnLayout { + anchors.fill: parent + RowLayout { + Label { + text: "Root / slot" + } + TextField { + selectByMouse: true + id: urlTextField + Layout.fillWidth: true + text: "head" + } + Button { + text: "Load" + onClicked: main.onLoadState(urlTextField.text) + enabled: urlTextField.text !== "" + } + } + + GridLayout { + columns: 2 + + Text { + text: "Data" + } + TextEdit { + text: viewData.state + readOnly: true + selectByMouse: true + } + } + } +} diff --git a/ngui/ui/main.qml b/ngui/ui/main.qml new file mode 100644 index 0000000000..152a6096ac --- /dev/null +++ b/ngui/ui/main.qml @@ -0,0 +1,87 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +ApplicationWindow { + width: 1400 + height: 900 + title: "ngui" + visible: true + + header: TabBar { + id: tabBar + + currentIndex: main.currentIndex + + TabButton { + text: "Slots" + onClicked: main.updateSlots() + } + TabButton { + text: "Blocks" + } + TabButton { + text: "Peers" + onClicked: main.updatePeers() + } + TabButton { + text: "Node" + onClicked: main.nodeModel.update() + } + TabButton { + text: "Pools" + onClicked: main.poolModel.update() + } + } + + footer: RowLayout { + Text { + text: "Finalized" + } + Text { + text: main.footer.finalized + } + + Text { + text: "Head" + } + Text { + text: main.footer.head + } + + Text { + text: "Sync state" + } + Text { + text: main.footer.syncing + } + + Timer { + interval: 12000 + running: true + repeat: true + onTriggered: main.updateFooter() + } + } + + StackLayout { + anchors.fill: parent + currentIndex: tabBar.currentIndex + + Slots { + viewData: main.epochModel + } + Blocks { + viewData: main.blck + } + Peers { + viewData: main.peerList + } + Node { + viewData: main.nodeModel + } + Pools { + viewData: main.poolModel + } + } +} diff --git a/ngui/utils.nim b/ngui/utils.nim new file mode 100644 index 0000000000..d3fa14e89e --- /dev/null +++ b/ngui/utils.nim @@ -0,0 +1,21 @@ +{.push raises: [Defect].} + +import + stew/byteutils, + ../beacon_chain/spec/datatypes/base + +func toDisplayHex*(v: openArray[byte]): string = + "
0x" & toHex(v) & "
" + +func toDisplayHex*(v: Eth2Digest): string = toDisplayHex(v.data) +func toDisplayHex*(v: ValidatorSig | TrustedSig): string = toDisplayHex(toRaw(v)) + +func toBlockLink*(v: Eth2Digest): string = + let + display = toDisplayHex(v) + target = "0x" & toHex(v.data) + + "" & display & "" + +func toValidatorLink*(v: ValidatorIndex): string = + "" & $v & "" diff --git a/ngui/voluntaryexitlist.nim b/ngui/voluntaryexitlist.nim new file mode 100644 index 0000000000..b39a43c15f --- /dev/null +++ b/ngui/voluntaryexitlist.nim @@ -0,0 +1,51 @@ +import + std/[sequtils, tables], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + ../beacon_chain/spec/helpers, + ./objecttablemodel, ./utils + +type + VoluntaryExitInfo* = object + info*: string + +proc toVoluntaryExitInfo*(v: SignedVoluntaryExit): VoluntaryExitInfo = + VoluntaryExitInfo( + info: $v + ) + +QtObject: + type VoluntaryExitList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[VoluntaryExitInfo] + + proc setup(self: VoluntaryExitList) = self.QAbstractTableModel.setup + + proc delete(self: VoluntaryExitList) = + self.QAbstractTableModel.delete + + proc newVoluntaryExitList*(data: openArray[SignedVoluntaryExit]): VoluntaryExitList = + new(result, delete) + result.data = ObjectTableModelImpl[VoluntaryExitInfo](items: data.mapIt(it.toVoluntaryExitInfo())) + result.setup + + method rowCount(self: VoluntaryExitList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: VoluntaryExitList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*(self: VoluntaryExitList, section: int, orientation: QtOrientation, role: int): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: VoluntaryExitList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: VoluntaryExitList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: VoluntaryExitList, v: openArray[SignedVoluntaryExit]) = + self.data.setNewData(self, v.mapIt(it.toVoluntaryExitInfo())) + + proc sort*(self: VoluntaryExitList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/vendor/DOtherSide b/vendor/DOtherSide new file mode 160000 index 0000000000..81ea295cc2 --- /dev/null +++ b/vendor/DOtherSide @@ -0,0 +1 @@ +Subproject commit 81ea295cc27fb6e81f435cc1ae0fea70bc99780c diff --git a/vendor/nimqml b/vendor/nimqml new file mode 160000 index 0000000000..9a65a1847e --- /dev/null +++ b/vendor/nimqml @@ -0,0 +1 @@ +Subproject commit 9a65a1847e31f97a6f42dc9624e6d86795fcf491