diff --git a/.gitmodules b/.gitmodules index 13b69f4e43..3d50c2b6e2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -199,6 +199,13 @@ path = vendor/nim-toml-serialization url = https://github.com/status-im/nim-toml-serialization.git ignore = untracked +[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/status-im/nimqml.git branch = master [submodule "vendor/gnosis-chain-configs"] path = vendor/gnosis-chain-configs diff --git a/Makefile b/Makefile index e0faa59f23..e244fbfad5 100644 --- a/Makefile +++ b/Makefile @@ -485,6 +485,10 @@ force_build_alone_tools: | $(FORCE_BUILD_ALONE_TOOLS_DEPS) # https://www.gnu.org/software/make/manual/html_node/Multiple-Rules.html#Multiple-Rules # Already defined as a reult nimbus_beacon_node: force_build_alone_tools +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" GOERLI_TESTNETS_PARAMS := \ --tcp-port=$$(( $(BASE_PORT) + $(NODE_ID) )) \ diff --git a/beacon_chain/spec/eth2_apis/rest_validator_calls.nim b/beacon_chain/spec/eth2_apis/rest_validator_calls.nim index b5526c7923..251d7250b9 100644 --- a/beacon_chain/spec/eth2_apis/rest_validator_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_validator_calls.nim @@ -13,6 +13,14 @@ import export client, rest_types, eth2_rest_serialization +proc getAttesterDuties*( + epoch: Epoch, + body: seq[ValidatorIndex] + ): RestResponse[GetAttesterDutiesResponse] {. + rest, endpoint: "/eth/v1/validator/duties/attester/{epoch}", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Validator/getAttesterDuties + proc getAttesterDutiesPlain*( epoch: Epoch, body: seq[ValidatorIndex] @@ -21,6 +29,13 @@ proc getAttesterDutiesPlain*( meth: MethodPost.} ## https://ethereum.github.io/beacon-APIs/#/Validator/getAttesterDuties +proc getProposerDuties*( + epoch: Epoch + ): RestResponse[GetProposerDutiesResponse] {. + rest, endpoint: "/eth/v1/validator/duties/proposer/{epoch}", + meth: MethodGet.} + ## https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties + proc getProposerDutiesPlain*( epoch: Epoch ): RestPlainResponse {. @@ -28,6 +43,14 @@ proc getProposerDutiesPlain*( meth: MethodGet.} ## https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties +proc getSyncCommitteeDuties*( + epoch: Epoch, + body: seq[ValidatorIndex] + ): RestResponse[GetSyncCommitteeDutiesResponse] {. + rest, endpoint: "/eth/v1/validator/duties/sync/{epoch}", + meth: MethodPost.} + ## https://ethereum.github.io/beacon-APIs/#/Validator/getSyncCommitteeDuties + proc getSyncCommitteeDutiesPlain*( epoch: Epoch, body: seq[ValidatorIndex] diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index 01f01a0f88..7cfada9b8b 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -180,6 +180,10 @@ type phase0.Attestation | electra.SingleAttestation + ForkyAttesterSlashing* = + phase0.AttesterSlashing | + electra.AttesterSlashing + ForkedAttestation* = object case kind*: ConsensusFork of ConsensusFork.Phase0: phase0Data*: phase0.Attestation @@ -1252,28 +1256,43 @@ template getForkedBlockField*( of ConsensusFork.Electra: unsafeAddr x.electraData.message.y of ConsensusFork.Fulu: unsafeAddr x.fuluData.message.y)[] -template signature*(x: ForkedSignedBeaconBlock | +template getForkedBodyField*( + x: ForkedSignedBeaconBlock | + ForkedMsgTrustedSignedBeaconBlock | + ForkedTrustedSignedBeaconBlock, + y: untyped): untyped = + # unsafeAddr avoids a copy of the field in some cases + (case x.kind + of ConsensusFork.Phase0: unsafeAddr x.phase0Data.message.body.y + of ConsensusFork.Altair: unsafeAddr x.altairData.message.body.y + of ConsensusFork.Bellatrix: unsafeAddr x.bellatrixData.message.body.y + of ConsensusFork.Capella: unsafeAddr x.capellaData.message.body.y + of ConsensusFork.Deneb: unsafeAddr x.denebData.message.body.y + of ConsensusFork.Electra: unsafeAddr x.electraData.message.body.y + of ConsensusFork.Fulu: unsafeAddr x.fuluData.message.body.y)[] + +func signature*(x: ForkedSignedBeaconBlock | ForkedMsgTrustedSignedBeaconBlock | ForkedSignedBlindedBeaconBlock): ValidatorSig = withBlck(x): forkyBlck.signature -template signature*(x: ForkedTrustedSignedBeaconBlock): TrustedSig = +func signature*(x: ForkedTrustedSignedBeaconBlock): TrustedSig = withBlck(x): forkyBlck.signature -template root*(x: ForkedSignedBeaconBlock | +func root*(x: ForkedSignedBeaconBlock | ForkedMsgTrustedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Eth2Digest = withBlck(x): forkyBlck.root -template slot*(x: ForkedSignedBeaconBlock | +func slot*(x: ForkedSignedBeaconBlock | ForkedMsgTrustedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Slot = withBlck(x): forkyBlck.message.slot -template shortLog*(x: ForkedBeaconBlock | ForkedBlindedBeaconBlock): auto = +func shortLog*(x: ForkedBeaconBlock | ForkedBlindedBeaconBlock): auto = withBlck(x): shortLog(forkyBlck) -template shortLog*(x: ForkedSignedBeaconBlock | +func shortLog*(x: ForkedSignedBeaconBlock | ForkedMsgTrustedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock | ForkedSignedBlindedBeaconBlock): auto = diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 7f3280991f..9228b72800 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -11,6 +11,7 @@ import # Status libraries + std/times, stew/[byteutils, endians2, objects], nimcrypto/sha2, chronicles, diff --git a/ngui/.gitignore b/ngui/.gitignore new file mode 100644 index 0000000000..8d657bc2ad --- /dev/null +++ b/ngui/.gitignore @@ -0,0 +1 @@ +resources.cpp diff --git a/ngui/attestationlist.nim b/ngui/attestationlist.nim new file mode 100644 index 0000000000..38624b2b75 --- /dev/null +++ b/ngui/attestationlist.nim @@ -0,0 +1,80 @@ +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: phase0.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, + ) + +proc toAttestationInfo*(v: electra.Attestation): AttestationInfo = + AttestationInfo( + slot: v.data.slot.int, + index: 0.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[AttestationInfo]): AttestationList = + new(result, delete) + result.data = ObjectTableModelImpl[AttestationInfo](items: data) + 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[AttestationInfo]) = + self.data.setNewData(self, v) + + 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..1b8b7e4f8a --- /dev/null +++ b/ngui/attesterslashinglist.nim @@ -0,0 +1,56 @@ +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: ForkyAttesterSlashing): 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[ForkyAttesterSlashing] + ): 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[ForkyAttesterSlashing]) = + 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..451e0c2a70 --- /dev/null +++ b/ngui/blockmodel.nim @@ -0,0 +1,154 @@ +import + std/[sequtils, times], + NimQml, + ../beacon_chain/spec/eth2_apis/rest_beacon_client, + "."/[ + 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( + forkyBlck.message.body.attestations.asSeq().mapIt(it.toAttestationInfo()) + ), + depositsx: + newDepositList(forkyBlck.message.body.deposits.mapIt(it.toDepositInfo())), + attester_slashingsx: + newAttesterSlashingList(forkyBlck.message.body.attester_slashings.asSeq()), + proposer_slashingsx: + newProposerSlashingList(forkyBlck.message.body.proposer_slashings.asSeq()), + voluntary_exitsx: + newVoluntaryExitList(forkyBlck.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( + forkyBlck.message.body.attestations.asSeq().mapIt(it.toAttestationInfo()) + ) + self.depositsx.setNewData( + forkyBlck.message.body.deposits.mapIt(it.toDepositInfo()) + ) + self.attester_slashingsx.setNewData( + forkyBlck.message.body.attester_slashings.asSeq() + ) + self.proposer_slashingsx.setNewData( + forkyBlck.message.body.proposer_slashings.asSeq() + ) + self.voluntary_exitsx.setNewData(forkyBlck.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..883fa69d70 --- /dev/null +++ b/ngui/depositlist.nim @@ -0,0 +1,55 @@ +import + std/[tables], NimQml, ../beacon_chain/spec/datatypes/base, ./objecttablemodel, ./utils + +type DepositInfo* = object + pubkey*: string + withdrawal_credentials*: string + amount*: Gwei + 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..142aab7aac --- /dev/null +++ b/ngui/epochmodel.nim @@ -0,0 +1,54 @@ +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..c8c54d577f --- /dev/null +++ b/ngui/footermodel.nim @@ -0,0 +1,54 @@ +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..016e391417 --- /dev/null +++ b/ngui/mainmodel.nim @@ -0,0 +1,162 @@ +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 + cfg: RuntimeConfig + 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, cfg: RuntimeConfig): MainModel = + let client = RestClientRef.new(url).get() + + var + headBlock = + (waitFor client.getBlockV2(BlockIdent.init(BlockIdentType.Head), cfg)).get() + epoch = getForkedBlockField(headBlock[], slot).epoch + genesis = (waitFor client.getGenesis()).data.data + peerList = newPeerList(@[]) + + let res = MainModel( + app: app, + cfg: cfg, + 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))).valueOr( + default(GetBlockHeaderResponse) + ).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(), self.cfg) + ) + 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..008941f042 --- /dev/null +++ b/ngui/ngui.nim @@ -0,0 +1,66 @@ +import std/os + +import confutils +import ../beacon_chain/networking/network_metadata + +import NimQml +import mainmodel + +const + dothersideDir = currentSourcePath.parentDir & "/../vendor/DOtherSide/" + corePrivate = + gorge("pkg-config --variable=includedir Qt5Core") & "/QtCore/" & + gorge("pkg-config --modversion Qt5Core") + cflags = + gorge( + "pkg-config --cflags --static Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets" + ) & " -I" & dothersideDir & "lib/include -I" & corePrivate & " -I" & corePrivate & + "/QtCore" + ldflags = gorge "pkg-config --libs --static Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets" + +{.compile(dothersideDir & "lib/src/DOtherSide.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosQMetaObject.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosQDeclarative.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosQObject.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DOtherSideTypesCpp.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosQObjectImpl.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosQAbstractItemModel.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosQQuickImageProvider.cpp", cflags).} +{.compile(dothersideDir & "lib/src/DosLambdaInvoker.cpp", cflags).} + +{.passl: ldflags.} + +static: + discard staticExec( + "rcc " & currentSourcePath.parentDir & "/resources.qrc -o " & + currentSourcePath.parentDir & "/resources.cpp" + ) +{.compile(currentSourcePath.parentDir & "/resources.cpp", cflags).} + +proc mainProc(url, network: string) = + let app = newQApplication() + defer: + app.delete + let cfg = getMetadataForNetwork(network).cfg + let main = newMainModel(app, url, cfg) + defer: + main.delete + + let engine = newQQmlApplicationEngine() + defer: + engine.delete + + let mainVariant = newQVariant(main) + defer: + mainVariant.delete + + engine.setRootContextProperty("main", mainVariant) + + engine.addImportPath("qrc:/") + engine.load(newQUrl("qrc:/ui/main.qml")) + app.exec() + +when isMainModule: + cli do(url = "http://localhost:5052", network = "mainnet"): + mainProc(url, network) + 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..ab513bbb32 --- /dev/null +++ b/ngui/nodemodel.nim @@ -0,0 +1,110 @@ +import NimQml + +import + std/[sequtils, json, times], + ../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.getDebugChainHeadsV2()).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..ce7fbc5f29 --- /dev/null +++ b/ngui/objecttablemodel.nim @@ -0,0 +1,86 @@ +{.push raises: [Defect].} + +import NimQml + +import std/[algorithm, tables, typetraits] + +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(distinctBase 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..9a1a756a16 --- /dev/null +++ b/ngui/peerlist.nim @@ -0,0 +1,40 @@ +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..2065f0fc0a --- /dev/null +++ b/ngui/poolmodel.nim @@ -0,0 +1,91 @@ +import NimQml + +import + std/[sequtils, times], + ../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(default(seq[AttestationInfo])), + attesterSlashingsx: newAttesterSlashingList(default(seq[phase0.AttesterSlashing])), + 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))) + .mapIt(it.toAttestationInfo()) + ) + + 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..056c1a9454 --- /dev/null +++ b/ngui/proposerslashinglist.nim @@ -0,0 +1,56 @@ +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/resources.qrc b/ngui/resources.qrc new file mode 100644 index 0000000000..24a946310c --- /dev/null +++ b/ngui/resources.qrc @@ -0,0 +1,12 @@ + + + ui/Blocks.qml + ui/main.qml + ui/Node.qml + ui/ObjectTableView.qml + ui/Peers.qml + ui/Pools.qml + ui/Slots.qml + ui/States.qml + + \ No newline at end of file diff --git a/ngui/slotlist.nim b/ngui/slotlist.nim new file mode 100644 index 0000000000..c92b92a833 --- /dev/null +++ b/ngui/slotlist.nim @@ -0,0 +1,80 @@ +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 ..< SLOTS_PER_EPOCH: + let + slot = epoch.start_slot() + i + block_root = + try: + let h = waitFor client.getBlockHeader(BlockIdent.init(slot)) + if h.isSome() and h[].data.header.message.slot == slot: + toBlockLink(h[].data.root) + else: + "N/A" + except CatchableError as exc: + "N/A" + res.add SlotInfo( + slot: slot.int, + proposer_index: proposers[i].validator_index.int, + block_root: block_root, + ) + res + +QtObject: + type SlotList* = ref object of QAbstractTableModel + # TODO this could be a generic ObjectTableModel, except generics + method don't work.. + data: ObjectTableModelImpl[SlotInfo] + + proc setup(self: SlotList) = + self.QAbstractTableModel.setup + + proc delete(self: SlotList) = + self.QAbstractTableModel.delete + + proc newSlotList*(data: seq[SlotInfo]): SlotList = + new(result, delete) + result.data = ObjectTableModelImpl[SlotInfo](items: data) + result.setup + + method rowCount(self: SlotList, index: QModelIndex = nil): int = + self.data.rowCount(index) + + method columnCount(self: SlotList, index: QModelIndex = nil): int = + self.data.columnCount(index) + + method headerData*( + self: SlotList, section: int, orientation: QtOrientation, role: int + ): QVariant = + self.data.headerData(section, orientation, role) + + method data(self: SlotList, index: QModelIndex, role: int): QVariant = + self.data.data(index, role) + + method roleNames(self: SlotList): Table[int, string] = + self.data.roleNames() + + proc setNewData*(self: SlotList, v: seq[SlotInfo]) = + self.data.setNewData(self, v) + + proc sort*(self: SlotList, section: int) {.slot.} = + self.data.sort(self, section) diff --git a/ngui/ui/Blocks.qml b/ngui/ui/Blocks.qml new file mode 100644 index 0000000000..169b947375 --- /dev/null +++ b/ngui/ui/Blocks.qml @@ -0,0 +1,213 @@ +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: "Block / slot" + } + TextField { + selectByMouse: true + id: urlTextField + width: 640 + text: "head" + } + Button { + text: "Load" + onClicked: main.onLoadBlock(urlTextField.text) + enabled: urlTextField.text !== "" + } + } + + GridLayout { + columns: 2 + + Text { + text: "Slot" + } + TextEdit { + text: viewData.slot + readOnly: true + selectByMouse: true + } + + Text { + text: "Time" + } + TextEdit { + text: viewData.time + readOnly: true + selectByMouse: true + } + + Text { + text: "Block root" + } + TextEdit { + text: viewData.root + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + + Text { + text: "Proposer" + } + TextEdit { + text: viewData.proposer_index + readOnly: true + selectByMouse: true + } + + Text { + text: "Parent root" + } + TextEdit { + text: viewData.parent_root + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + onLinkActivated: main.openUrl(link) + } + + Text { + text: "State root" + } + TextEdit { + text: viewData.state_root + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + + Text { + text: "Randao reveal" + } + TextEdit { + text: viewData.randao_reveal + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + + Text { + text: "Eth1" + } + TextEdit { + text: viewData.eth1_data + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + + Text { + text: "Graffiti" + } + TextEdit { + text: viewData.graffiti + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + + Text { + text: "Signature" + } + TextEdit { + text: viewData.signature + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + } + + TabBar { + id: tabBar + + TabButton { + text: "Attestations: " + viewData.attestations.rowCount() + width: implicitWidth + } + TabButton { + text: "Deposits: " + viewData.deposits.rowCount() + width: implicitWidth + } + TabButton { + text: "Attester slashings: " + viewData.attester_slashings.rowCount() + width: implicitWidth + } + TabButton { + text: "Proposer slashings: " + viewData.proposer_slashings.rowCount() + width: implicitWidth + } + TabButton { + text: "Voluntary exits: " + viewData.voluntary_exits.rowCount() + width: implicitWidth + } + } + + StackLayout { + Layout.fillHeight: true + Layout.fillWidth: true + currentIndex: tabBar.currentIndex + + ObjectTableView { + model: viewData.attestations + columnWidthProvider: function (column) { + if (column == 0) + return 120 + if (column == 1) + return 60 + if (column == 2) + return 250 + if (column == 3) + return 100 + if (column == 4) + return 250 + if (column == 5) + return 100 + if (column == 6) + return 250 + return 350 + } + } + + ObjectTableView { + model: viewData.deposits + columnWidthProvider: function (column) { + if (column == 0) + return 250 + if (column == 1) + return 250 + if (column == 2) + return 100 + return 350 + } + } + + ObjectTableView { + model: viewData.attester_slashings + columnWidthProvider: function (column) { + return 350 + } + } + ObjectTableView { + model: viewData.proposer_slashings + columnWidthProvider: function (column) { + return 350 + } + } + ObjectTableView { + model: viewData.voluntary_exits + columnWidthProvider: function (column) { + return 350 + } + } + } + } +} diff --git a/ngui/ui/Node.qml b/ngui/ui/Node.qml new file mode 100644 index 0000000000..a7a77a0c98 --- /dev/null +++ b/ngui/ui/Node.qml @@ -0,0 +1,75 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +Rectangle { + property var viewData + + ScrollView { + anchors.fill: parent + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + clip: true + contentWidth: parent.width + id: sv + + GridLayout { + columns: 2 + width: sv.availableWidth + Text { + text: "Genesis" + } + TextEdit { + text: viewData.genesis + readOnly: true + selectByMouse: true + Layout.fillWidth: true + wrapMode: TextEdit.Wrap + } + + Text { + text: "Heads" + } + TextEdit { + text: viewData.heads + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + Layout.fillWidth: true + onLinkActivated: main.openUrl(link) + } + + Text { + text: "Identity" + } + TextEdit { + text: viewData.identity + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + Layout.fillWidth: true + wrapMode: TextEdit.Wrap + } + + Text { + text: "Version" + } + TextEdit { + text: viewData.version + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + + Text { + text: "Health" + } + TextEdit { + text: viewData.health + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + } + } + } +} diff --git a/ngui/ui/ObjectTableView.qml b/ngui/ui/ObjectTableView.qml new file mode 100644 index 0000000000..c37fda706d --- /dev/null +++ b/ngui/ui/ObjectTableView.qml @@ -0,0 +1,82 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 + +TableView { + topMargin: columnsHeader.implicitHeight + + id: tableView + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + + columnWidthProvider: function (column) { + return tableView.columns > 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 { + id: stringTxt + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + font.pointSize: 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.pointSize: 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..ba591369bb --- /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 120 + if (column == 1) + return 60 + if (column == 2) + return 250 + if (column == 3) + return 100 + if (column == 4) + return 250 + if (column == 5) + return 100 + 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..e93563c648 --- /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 120 + if (column == 1) + return 120 + return 700 + } + } +} 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..645d4d758b --- /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..9ed105a084 --- /dev/null +++ b/ngui/voluntaryexitlist.nim @@ -0,0 +1,54 @@ +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/tests/test_blockchain_dag.nim b/tests/test_blockchain_dag.nim index 146fd20e24..f7465fb455 100644 --- a/tests/test_blockchain_dag.nim +++ b/tests/test_blockchain_dag.nim @@ -711,7 +711,9 @@ suite "Diverging hardforks": altairRuntimeConfig = defaultRuntimeConfig phase0RuntimeConfig.ALTAIR_FORK_EPOCH = FAR_FUTURE_EPOCH + phase0RuntimeConfig.BELLATRIX_FORK_EPOCH = FAR_FUTURE_EPOCH altairRuntimeConfig.ALTAIR_FORK_EPOCH = 2.Epoch + altairRuntimeConfig.BELLATRIX_FORK_EPOCH = FAR_FUTURE_EPOCH var db = makeTestDB(SLOTS_PER_EPOCH) diff --git a/vendor/DOtherSide b/vendor/DOtherSide new file mode 160000 index 0000000000..7b7c0a91b5 --- /dev/null +++ b/vendor/DOtherSide @@ -0,0 +1 @@ +Subproject commit 7b7c0a91b558b13a968c57b9647bfc15ed962ead 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