From 906ec7f14e041fe69f66ff6cdb6e3ae57c4c0ac2 Mon Sep 17 00:00:00 2001 From: remoterami <142154971+remoterami@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:58:31 +0200 Subject: [PATCH 01/31] using lateral joins --- backend/pkg/api/data_access/vdb_rewards.go | 20 ++++++++++++-------- backend/pkg/api/data_access/vdb_summary.go | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/backend/pkg/api/data_access/vdb_rewards.go b/backend/pkg/api/data_access/vdb_rewards.go index 2212bee81..06d51bbc2 100644 --- a/backend/pkg/api/data_access/vdb_rewards.go +++ b/backend/pkg/api/data_access/vdb_rewards.go @@ -106,12 +106,13 @@ func (d *DataAccessService) GetValidatorDashboardRewards(ctx context.Context, da LeftJoin(goqu.L("blocks b"), goqu.On(goqu.L("v.validator_index = b.proposer AND b.status = '1'"))). LeftJoin(goqu.L("execution_payloads ep"), goqu.On(goqu.L("ep.block_hash = b.exec_block_hash"))). LeftJoin( - goqu.Dialect("postgres"). + goqu.Lateral(goqu.Dialect("postgres"). From("relays_blocks"). Select( goqu.L("exec_block_hash"), goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"), + Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")). + GroupBy("exec_block_hash")).As("rb"), goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")), ) @@ -561,12 +562,13 @@ func (d *DataAccessService) GetValidatorDashboardGroupRewards(ctx context.Contex LeftJoin(goqu.L("blocks b"), goqu.On(goqu.L("v.validator_index = b.proposer AND b.status = '1'"))). LeftJoin(goqu.L("execution_payloads ep"), goqu.On(goqu.L("ep.block_hash = b.exec_block_hash"))). LeftJoin( - goqu.Dialect("postgres"). + goqu.Lateral(goqu.Dialect("postgres"). From("relays_blocks"). Select( goqu.L("exec_block_hash"), goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"), + Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")). + GroupBy("exec_block_hash")).As("rb"), goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")), ). Where(goqu.L("b.epoch = ?", epoch)) @@ -736,12 +738,13 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex LeftJoin(goqu.L("blocks b"), goqu.On(goqu.L("v.validator_index = b.proposer AND b.status = '1'"))). LeftJoin(goqu.L("execution_payloads ep"), goqu.On(goqu.L("ep.block_hash = b.exec_block_hash"))). LeftJoin( - goqu.Dialect("postgres"). + goqu.Lateral(goqu.Dialect("postgres"). From("relays_blocks"). Select( goqu.L("exec_block_hash"), goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"), + Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")). + GroupBy("exec_block_hash")).As("rb"), goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")), ). Where(goqu.L("b.epoch >= ?", startEpoch)) @@ -987,12 +990,13 @@ func (d *DataAccessService) GetValidatorDashboardDuties(ctx context.Context, das From(goqu.L("blocks b")). LeftJoin(goqu.L("execution_payloads ep"), goqu.On(goqu.L("ep.block_hash = b.exec_block_hash"))). LeftJoin( - goqu.Dialect("postgres"). + goqu.Lateral(goqu.Dialect("postgres"). From("relays_blocks"). Select( goqu.L("exec_block_hash"), goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"), + Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")). + GroupBy("exec_block_hash")).As("rb"), goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")), ). Where(goqu.L("b.epoch = ?", epoch)). diff --git a/backend/pkg/api/data_access/vdb_summary.go b/backend/pkg/api/data_access/vdb_summary.go index 310e9e2b4..a8f54b116 100644 --- a/backend/pkg/api/data_access/vdb_summary.go +++ b/backend/pkg/api/data_access/vdb_summary.go @@ -191,12 +191,13 @@ func (d *DataAccessService) GetValidatorDashboardSummary(ctx context.Context, da From(goqu.L("blocks b")). LeftJoin(goqu.L("execution_payloads ep"), goqu.On(goqu.L("ep.block_hash = b.exec_block_hash"))). LeftJoin( - goqu.Dialect("postgres"). + goqu.Lateral(goqu.Dialect("postgres"). From("relays_blocks"). Select( goqu.L("exec_block_hash"), goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"), + Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")). + GroupBy("exec_block_hash")).As("rb"), goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")), ). Where(goqu.L("b.epoch >= ? AND b.epoch <= ? AND b.status = '1'", epochMin, epochMax)). @@ -473,7 +474,12 @@ func (d *DataAccessService) GetValidatorDashboardSummary(ctx context.Context, da // Efficiency var totalAttestationEfficiency, totalProposerEfficiency, totalSyncEfficiency sql.NullFloat64 - if !total.AttestationIdealReward.IsZero() { + if !total.AttestationIdealRewargoqu.Dialect("postgres"). + From("relays_blocks"). + Select( + goqu.L("exec_block_hash"), + goqu.MAX("value").As("value")). + GroupBy("exec_block_hash").As("rb"),d.IsZero() { totalAttestationEfficiency.Float64 = total.AttestationReward.Div(total.AttestationIdealReward).InexactFloat64() totalAttestationEfficiency.Valid = true } @@ -829,12 +835,13 @@ func (d *DataAccessService) internal_getElClAPR(ctx context.Context, dashboardId From(goqu.L("blocks AS b")). LeftJoin(goqu.L("execution_payloads AS ep"), goqu.On(goqu.L("b.exec_block_hash = ep.block_hash"))). LeftJoin( - goqu.Dialect("postgres"). + goqu.Lateral(goqu.Dialect("postgres"). From("relays_blocks"). Select( goqu.L("exec_block_hash"), goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"), + Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")). + GroupBy("exec_block_hash")).As("rb"), goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")), ). Where(goqu.L("b.status = '1'")) From 515522144834943ab43145ca054d4731406f04c3 Mon Sep 17 00:00:00 2001 From: remoterami <142154971+remoterami@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:29:55 +0200 Subject: [PATCH 02/31] garbage removed --- backend/pkg/api/data_access/vdb_summary.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/pkg/api/data_access/vdb_summary.go b/backend/pkg/api/data_access/vdb_summary.go index a8f54b116..58e29ad2a 100644 --- a/backend/pkg/api/data_access/vdb_summary.go +++ b/backend/pkg/api/data_access/vdb_summary.go @@ -474,12 +474,7 @@ func (d *DataAccessService) GetValidatorDashboardSummary(ctx context.Context, da // Efficiency var totalAttestationEfficiency, totalProposerEfficiency, totalSyncEfficiency sql.NullFloat64 - if !total.AttestationIdealRewargoqu.Dialect("postgres"). - From("relays_blocks"). - Select( - goqu.L("exec_block_hash"), - goqu.MAX("value").As("value")). - GroupBy("exec_block_hash").As("rb"),d.IsZero() { + if !total.AttestationIdealReward.IsZero() { totalAttestationEfficiency.Float64 = total.AttestationReward.Div(total.AttestationIdealReward).InexactFloat64() totalAttestationEfficiency.Valid = true } From e55729f1068a62d34107ab06be624ba749d15875 Mon Sep 17 00:00:00 2001 From: "Peter (bitfly)" <1674920+peterbitfly@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:40:56 +0200 Subject: [PATCH 03/31] feat(exporter): increase validator status update query speed (#899) --- backend/pkg/exporter/db/db.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/pkg/exporter/db/db.go b/backend/pkg/exporter/db/db.go index b58c72c6a..0d092c1d1 100644 --- a/backend/pkg/exporter/db/db.go +++ b/backend/pkg/exporter/db/db.go @@ -523,6 +523,14 @@ func SaveValidators(epoch uint64, validators []*types.Validator, client rpc.Clie return fmt.Errorf("error preparing insert validator statement: %w", err) } + validatorStatusUpdateStmt, err := tx.Prepare(`UPDATE validators SET status = $1 WHERE validatorindex = $2;`) + if err != nil { + return fmt.Errorf("error preparing update validator status statement: %w", err) + } + + log.Info("updating validator status and metadata") + valiudatorUpdateTs := time.Now() + updates := 0 for _, v := range validators { // exchange farFutureEpoch with the corresponding max sql value @@ -610,10 +618,14 @@ func SaveValidators(epoch uint64, validators []*types.Validator, client rpc.Clie } if c.Status != v.Status { - log.Infof("Status changed for validator %v from %v to %v", v.Index, c.Status, v.Status) - log.Infof("v.ActivationEpoch %v, latestEpoch %v, lastAttestationSlots[v.Index] %v, thresholdSlot %v, lastGlobalAttestedEpoch: %v, lastValidatorAttestedEpoch: %v", v.ActivationEpoch, latestEpoch, lastAttestationSlot, thresholdSlot, lastGlobalAttestedEpoch, lastValidatorAttestedEpoch) - queries.WriteString(fmt.Sprintf("UPDATE validators SET status = '%s' WHERE validatorindex = %d;\n", v.Status, c.Index)) - updates++ + log.Debugf("Status changed for validator %v from %v to %v", v.Index, c.Status, v.Status) + log.Debugf("v.ActivationEpoch %v, latestEpoch %v, lastAttestationSlots[v.Index] %v, thresholdSlot %v, lastGlobalAttestedEpoch: %v, lastValidatorAttestedEpoch: %v", v.ActivationEpoch, latestEpoch, lastAttestationSlot, thresholdSlot, lastGlobalAttestedEpoch, lastValidatorAttestedEpoch) + //queries.WriteString(fmt.Sprintf("UPDATE validators SET status = '%s' WHERE validatorindex = %d;\n", v.Status, c.Index)) + _, err := validatorStatusUpdateStmt.Exec(v.Status, c.Index) + if err != nil { + return fmt.Errorf("error updating validator status: %w", err) + } + //updates++ } // if c.Balance != v.Balance { // // log.LogInfo("Balance changed for validator %v from %v to %v", v.Index, c.Balance, v.Balance) @@ -658,6 +670,11 @@ func SaveValidators(epoch uint64, validators []*types.Validator, client rpc.Clie } } + err = validatorStatusUpdateStmt.Close() + if err != nil { + return fmt.Errorf("error closing validator status update statement: %w", err) + } + err = insertStmt.Close() if err != nil { return fmt.Errorf("error closing insert validator statement: %w", err) @@ -673,6 +690,7 @@ func SaveValidators(epoch uint64, validators []*types.Validator, client rpc.Clie } log.Infof("validator table update completed, took %v", time.Since(updateStart)) } + log.Infof("updating validator status and metadata completed, took %v", time.Since(valiudatorUpdateTs)) s := time.Now() newValidators := []struct { From 9da57af5b81416f6c22492fb8d1dc0057641b8c2 Mon Sep 17 00:00:00 2001 From: remoterami <142154971+remoterami@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:26:21 +0200 Subject: [PATCH 04/31] removed query to mat view --- backend/pkg/api/data_access/vdb_blocks.go | 58 ++++++++++------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/backend/pkg/api/data_access/vdb_blocks.go b/backend/pkg/api/data_access/vdb_blocks.go index 2a2a0953a..14cbb3e1e 100644 --- a/backend/pkg/api/data_access/vdb_blocks.go +++ b/backend/pkg/api/data_access/vdb_blocks.go @@ -115,7 +115,7 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das Epoch uint64 `db:"epoch"` Slot uint64 `db:"slot"` Status uint64 `db:"status"` - Block sql.NullInt64 `db:"block"` + Block sql.NullInt64 `db:"exec_block_number"` FeeRecipient []byte `db:"fee_recipient"` ElReward decimal.NullDecimal `db:"el_reward"` ClReward decimal.NullDecimal `db:"cl_reward"` @@ -161,7 +161,7 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das params = append(params, currentCursor.Slot) where += `WHERE (` if onlyPrimarySort { - where += `slot` + sign + fmt.Sprintf(`$%d`, len(params)) + where += `blocks.slot` + sign + fmt.Sprintf(`$%d`, len(params)) } else { params = append(params, val) secSign := ` < ` @@ -172,7 +172,7 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das // explicit cast to int because type of 'status' column is text for some reason sortColName += "::int" } - where += fmt.Sprintf(`(slot`+secSign+`$%d AND `+sortColName+` = $%d) OR `+sortColName+sign+`$%d`, len(params)-1, len(params), len(params)) + where += fmt.Sprintf(`(blocks.slot`+secSign+`$%d AND `+sortColName+` = $%d) OR `+sortColName+sign+`$%d`, len(params)-1, len(params), len(params)) } where += `) ` } @@ -226,25 +226,30 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das groupIdCol := "group_id" // this is actually just used for sorting for "reward".. will not consider EL rewards of unfinalized blocks atm - reward := "reward" + // reward := "reward" if dashboardId.Validators != nil { groupIdCol = fmt.Sprintf("%d AS %s", t.DefaultGroupId, groupIdCol) - reward = "coalesce(rb.value / 1e18, ep.fee_recipient_reward) AS " + reward + // reward = "coalesce(rb.value / 1e18, ep.fee_recipient_reward) AS " + reward } selectFields := fmt.Sprintf(` - r.proposer, + blocks.proposer, %s, - r.epoch, - r.slot, - r.status, - block, + blocks.epoch, + blocks.slot, + blocks.status, + exec_block_number, COALESCE(rb.proposer_fee_recipient, blocks.exec_fee_recipient) AS fee_recipient, COALESCE(rb.value / 1e18, ep.fee_recipient_reward) AS el_reward, cp.cl_attestations_reward / 1e9 + cp.cl_sync_aggregate_reward / 1e9 + cp.cl_slashing_inclusion_reward / 1e9 as cl_reward, - r.graffiti_text`, groupIdCol) - query := fmt.Sprintf(`SELECT distinct on (slot) + blocks.graffiti_text`, groupIdCol) + distinct := "slot" + if !onlyPrimarySort { + distinct = sortColName + ", " + distinct + } + query := fmt.Sprintf(`SELECT distinct on (%s) %s - FROM ( SELECT * FROM (`, selectFields) + FROM blocks + `, distinct, selectFields) // supply scheduled proposals, if any if len(scheduledProposers) > 0 { // distinct to filter out duplicates in an edge case (if dutiesInfo didn't update yet after a block was proposed, but the blocks table was) @@ -277,19 +282,8 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das UNION (`, distinct, selectFields, len(params)-2, len(params)-1, len(params)) } - query += fmt.Sprintf(` - SELECT - proposer, - epoch, - blocks.slot, - status, - exec_block_number AS block, - %s, - graffiti_text - FROM blocks - `, reward) - if dashboardId.Validators == nil { + /*if dashboardId.Validators == nil { query += ` LEFT JOIN cached_proposal_rewards ON cached_proposal_rewards.dashboard_id = $1 AND blocks.slot = cached_proposal_rewards.slot ` @@ -304,32 +298,30 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das if len(scheduledProposers) > 0 { query += `)` } - query += `) as u ` + query += `) as u `*/ if dashboardId.Validators == nil { query += fmt.Sprintf(` INNER JOIN (%s) validators ON validators.validator_index = proposer `, filteredValidatorsQuery) } else { - query += `WHERE proposer = ANY($1) ` + where += `WHERE proposer = ANY($1) ` } params = append(params, limit+1) limitStr := fmt.Sprintf(` LIMIT $%d `, len(params)) - rewardsStr := `) r - LEFT JOIN consensus_payloads cp on r.slot = cp.slot - LEFT JOIN blocks on r.slot = blocks.slot + rewardsStr := ` + LEFT JOIN consensus_payloads cp on blocks.slot = cp.slot LEFT JOIN execution_payloads ep ON ep.block_hash = blocks.exec_block_hash LEFT JOIN relays_blocks rb ON rb.exec_block_hash = blocks.exec_block_hash ` // relay bribe deduplication; select most likely (=max) relay bribe value for each block - relayOrder := `` if colSort.Column != enums.VDBBlockProposerReward { - relayOrder += `, rb.value ` + secSort + orderBy += `, rb.value ` + secSort } startTime := time.Now() - err = d.alloyReader.SelectContext(ctx, &proposals, query+where+orderBy+limitStr+rewardsStr+orderBy+relayOrder, params...) + err = d.alloyReader.SelectContext(ctx, &proposals, query+rewardsStr+where+orderBy+limitStr, params...) log.Debugf("=== getting past blocks took %s", time.Since(startTime)) if err != nil { return nil, nil, err From 5871feb694edd811d9afd34afcf2fb9b0cceea9e Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:09:02 +0200 Subject: [PATCH 05/31] (BEDS-94) better client notification dummy (#901) --- backend/pkg/api/types/notifications.go | 4 ++-- frontend/types/api/notifications.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/pkg/api/types/notifications.go b/backend/pkg/api/types/notifications.go index 183164040..bb5fdb8e0 100644 --- a/backend/pkg/api/types/notifications.go +++ b/backend/pkg/api/types/notifications.go @@ -166,7 +166,7 @@ type InternalPutUserNotificationSettingsPairedDevicesResponse ApiDataResponse[No type NotificationSettingsClient struct { Id uint64 `json:"id"` Name string `json:"name"` - Category string `json:"category"` + Category string `json:"category" tstype:"'execution_layer' | 'consensus_layer' | 'other'" faker:"oneof: execution_layer, consensus_layer, other"` IsSubscribed bool `json:"is_subscribed"` } @@ -196,7 +196,7 @@ type NotificationSettings struct { GeneralSettings NotificationSettingsGeneral `json:"general_settings"` Networks []NotificationNetwork `json:"networks"` PairedDevices []NotificationPairedDevice `json:"paired_devices"` - Clients []NotificationSettingsClient `json:"clients"` + Clients []NotificationSettingsClient `json:"clients" faker:"slice_len=10"` } type InternalGetUserNotificationSettingsResponse ApiDataResponse[NotificationSettings] diff --git a/frontend/types/api/notifications.ts b/frontend/types/api/notifications.ts index 0fe772280..0a3b47725 100644 --- a/frontend/types/api/notifications.ts +++ b/frontend/types/api/notifications.ts @@ -160,7 +160,7 @@ export type InternalPutUserNotificationSettingsPairedDevicesResponse = ApiDataRe export interface NotificationSettingsClient { id: number /* uint64 */; name: string; - category: string; + category: 'execution_layer' | 'consensus_layer' | 'other'; is_subscribed: boolean; } export type InternalPutUserNotificationSettingsClientResponse = ApiDataResponse; From cedde44bf65dde9d05f3c548f7387d3d34fb3195 Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:58:38 +0200 Subject: [PATCH 06/31] (BEDS-539) fix put network notifications (#902) --- backend/pkg/api/data_access/dummy.go | 15 ++++++++++++- backend/pkg/api/handlers/common.go | 30 ++++++++++++++++++++++++++ backend/pkg/api/handlers/public.go | 32 +++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index b3743d335..b82c03e47 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -396,7 +396,20 @@ func (d *DummyService) GetValidatorDashboardRocketPoolMinipools(ctx context.Cont } func (d *DummyService) GetAllNetworks() ([]t.NetworkInfo, error) { - return getDummyData[[]t.NetworkInfo]() + return []types.NetworkInfo{ + { + ChainId: 1, + Name: "ethereum", + }, + { + ChainId: 100, + Name: "gnosis", + }, + { + ChainId: 17000, + Name: "holesky", + }, + }, nil } func (d *DummyService) GetSearchValidatorByIndex(ctx context.Context, chainId, index uint64) (*t.SearchValidator, error) { diff --git a/backend/pkg/api/handlers/common.go b/backend/pkg/api/handlers/common.go index 1af4a9234..6b0d9e3c5 100644 --- a/backend/pkg/api/handlers/common.go +++ b/backend/pkg/api/handlers/common.go @@ -19,6 +19,7 @@ import ( "github.com/gobitfly/beaconchain/pkg/commons/log" "github.com/gorilla/mux" "github.com/invopop/jsonschema" + "github.com/shopspring/decimal" "github.com/xeipuuv/gojsonschema" "github.com/alexedwards/scs/v2" @@ -252,6 +253,35 @@ func (v *validationError) checkUint(param, paramName string) uint64 { return num } +func (v *validationError) checkWeiDecimal(param, paramName string) decimal.Decimal { + dec := decimal.Zero + // check if only numbers are contained in the string with regex + if !reInteger.MatchString(param) { + v.add(paramName, fmt.Sprintf("given value '%s' is not a wei string (must be positive integer)", param)) + return dec + } + dec, err := decimal.NewFromString(param) + if err != nil { + v.add(paramName, fmt.Sprintf("given value '%s' is not a wei string (must be positive integer)", param)) + return dec + } + return dec +} + +func (v *validationError) checkWeiMinMax(param, paramName string, min, max decimal.Decimal) decimal.Decimal { + dec := v.checkWeiDecimal(param, paramName) + if v.hasErrors() { + return dec + } + if dec.LessThan(min) { + v.add(paramName, fmt.Sprintf("given value '%s' is too small, minimum value is %s", dec, min)) + } + if dec.GreaterThan(max) { + v.add(paramName, fmt.Sprintf("given value '%s' is too large, maximum value is %s", dec, max)) + } + return dec +} + func (v *validationError) checkBool(param, paramName string) bool { if param == "" { return false diff --git a/backend/pkg/api/handlers/public.go b/backend/pkg/api/handlers/public.go index 53e20447f..30fb5a8ca 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -11,6 +11,7 @@ import ( "github.com/gobitfly/beaconchain/pkg/api/enums" "github.com/gobitfly/beaconchain/pkg/api/types" "github.com/gorilla/mux" + "github.com/shopspring/decimal" ) // All handler function names must include the HTTP method and the path they handle @@ -2243,7 +2244,7 @@ func (h *HandlerService) PublicPutUserNotificationSettingsGeneral(w http.Respons // @Accept json // @Produce json // @Param network path string true "The networks name or chain ID." -// @Param request body types.NotificationSettingsNetwork true "Description Todo" +// @Param request body handlers.PublicPutUserNotificationSettingsNetworks.request true "Description Todo" // @Success 200 {object} types.InternalPutUserNotificationSettingsNetworksResponse // @Failure 400 {object} types.ApiErrorResponse // @Router /users/me/notifications/settings/networks/{network} [put] @@ -2254,19 +2255,40 @@ func (h *HandlerService) PublicPutUserNotificationSettingsNetworks(w http.Respon handleErr(w, r, err) return } - var req types.NotificationSettingsNetwork + type request struct { + IsGasAboveSubscribed bool `json:"is_gas_above_subscribed"` + GasAboveThreshold string `json:"gas_above_threshold"` + IsGasBelowSubscribed bool `json:"is_gas_below_subscribed"` + GasBelowThreshold string `json:"gas_below_threshold" ` + IsParticipationRateSubscribed bool `json:"is_participation_rate_subscribed"` + ParticipationRateThreshold float64 `json:"participation_rate_threshold" faker:"boundary_start=0, boundary_end=1"` + } + var req request if err := v.checkBody(&req, r); err != nil { handleErr(w, r, err) return } checkMinMax(&v, req.ParticipationRateThreshold, 0, 1, "participation_rate_threshold") - chainId := v.checkNetworkParameter(mux.Vars(r)["network"]) + + minWei := decimal.New(1000000, 1) // 0.001 Gwei + maxWei := decimal.New(1000000000000, 1) // 1000 Gwei + gasAboveThreshold := v.checkWeiMinMax(req.GasAboveThreshold, "gas_above_threshold", minWei, maxWei) + gasBelowThreshold := v.checkWeiMinMax(req.GasBelowThreshold, "gas_below_threshold", minWei, maxWei) if v.hasErrors() { handleErr(w, r, v) return } - err = h.dai.UpdateNotificationSettingsNetworks(r.Context(), userId, chainId, req) + settings := types.NotificationSettingsNetwork{ + IsGasAboveSubscribed: req.IsGasAboveSubscribed, + GasAboveThreshold: gasAboveThreshold, + IsGasBelowSubscribed: req.IsGasBelowSubscribed, + GasBelowThreshold: gasBelowThreshold, + IsParticipationRateSubscribed: req.IsParticipationRateSubscribed, + ParticipationRateThreshold: req.ParticipationRateThreshold, + } + + err = h.dai.UpdateNotificationSettingsNetworks(r.Context(), userId, chainId, settings) if err != nil { handleErr(w, r, err) return @@ -2274,7 +2296,7 @@ func (h *HandlerService) PublicPutUserNotificationSettingsNetworks(w http.Respon response := types.InternalPutUserNotificationSettingsNetworksResponse{ Data: types.NotificationNetwork{ ChainId: chainId, - Settings: req, + Settings: settings, }, } returnOk(w, r, response) From a17d2a80ad183ef021c16562c852dd8994e27d5a Mon Sep 17 00:00:00 2001 From: marcel-bitfly <174338434+marcel-bitfly@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:54:47 +0200 Subject: [PATCH 07/31] style: fix `typo` in `comment` --- frontend/utils/validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/utils/validation.ts b/frontend/utils/validation.ts index 4d2f0082f..32c85c59f 100644 --- a/frontend/utils/validation.ts +++ b/frontend/utils/validation.ts @@ -7,7 +7,7 @@ export const createSchemaObject = (schema: Record) => { } export const validation = { - // exopose thirdparty validation here, when needed + // expose thirdparty validation here, when needed boolean, url: (message: string) => string().url(message), } From 9963c832f30cb1de79e591fd94b6fbdb41904e32 Mon Sep 17 00:00:00 2001 From: marcel-bitfly <174338434+marcel-bitfly@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:02:16 +0200 Subject: [PATCH 08/31] feat(notifications): add `networks tab` See: BEDS-203 --- frontend/components/bc/LoadingSpinner.vue | 14 +- frontend/components/bc/input/BcInputText.vue | 20 ++- frontend/components/bc/input/BcInputUnit.vue | 36 ++++ .../NotificationsManagementModal.vue | 10 +- .../NotificationsManagementNetwork.vue | 157 ++++++++++++++++++ frontend/locales/en.json | 7 + .../useNotificationsManagementStore.ts | 50 +++--- frontend/types/customFetch.ts | 19 ++- frontend/utils/format.ts | 49 +++++- 9 files changed, 322 insertions(+), 40 deletions(-) create mode 100644 frontend/components/bc/input/BcInputUnit.vue create mode 100644 frontend/components/notifications/management/NotificationsManagementNetwork.vue diff --git a/frontend/components/bc/LoadingSpinner.vue b/frontend/components/bc/LoadingSpinner.vue index e15d69520..34e56972c 100644 --- a/frontend/components/bc/LoadingSpinner.vue +++ b/frontend/components/bc/LoadingSpinner.vue @@ -1,6 +1,7 @@ + + + + diff --git a/frontend/components/notifications/management/NotificationsManagementModal.vue b/frontend/components/notifications/management/NotificationsManagementModal.vue index 544794fe0..35589e0f2 100644 --- a/frontend/components/notifications/management/NotificationsManagementModal.vue +++ b/frontend/components/notifications/management/NotificationsManagementModal.vue @@ -26,7 +26,6 @@ const tabs: HashTabs = [ { icon: faMonitorWaveform, key: 'machines', - placeholder: 'Machines coming soon!', title: $t('notifications.tabs.machines'), }, { @@ -35,15 +34,9 @@ const tabs: HashTabs = [ placeholder: 'Clients coming soon!', title: $t('notifications.tabs.clients'), }, - { - icon: faCog, - key: 'rocketpool', - title: $t('notifications.tabs.rocketpool'), - }, { icon: faNetworkWired, key: 'network', - placeholder: 'Network coming soon!', title: $t('notifications.tabs.network'), }, ] @@ -71,6 +64,9 @@ const tabs: HashTabs = [ + diff --git a/frontend/components/notifications/management/NotificationsManagementNetwork.vue b/frontend/components/notifications/management/NotificationsManagementNetwork.vue new file mode 100644 index 000000000..99a11365f --- /dev/null +++ b/frontend/components/notifications/management/NotificationsManagementNetwork.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/frontend/locales/en.json b/frontend/locales/en.json index c39a43c33..b022f5d80 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -74,6 +74,7 @@ "tx_batch": "Tx Batch | Tx Batches", "unavailable": "Unavailable", "units": { + "GWEI": "GWei", "per_day": "per day" }, "upcoming": "Upcoming", @@ -717,6 +718,12 @@ "footer": { "subscriptions": "Network ({count} Subscriptions)" }, + "settings": { + "alert_if_gas_above": "Alert if gas is above", + "alert_if_gas_below": "Alert if gas is below", + "alert_if_participation_rate_below": "Alert if participation rate is below", + "new_reward_round": "New reward round" + }, "title": "Network" }, "overview": { diff --git a/frontend/stores/notifications/useNotificationsManagementStore.ts b/frontend/stores/notifications/useNotificationsManagementStore.ts index 2b7c060b7..f6c9a908c 100644 --- a/frontend/stores/notifications/useNotificationsManagementStore.ts +++ b/frontend/stores/notifications/useNotificationsManagementStore.ts @@ -1,8 +1,10 @@ import type { InternalGetUserNotificationSettingsResponse, InternalPutUserNotificationSettingsGeneralResponse, + InternalPutUserNotificationSettingsNetworksResponse, InternalPutUserNotificationSettingsPairedDevicesResponse, NotificationSettings, + NotificationSettingsNetwork, } from '~/types/api/notifications' import { API_PATH } from '~/types/customFetch' @@ -28,23 +30,8 @@ export const useNotificationsManagementStore = defineStore('notifications-manage rocket_pool_max_collateral_threshold: 0, rocket_pool_min_collateral_threshold: 0, }, - networks: [ { - chain_id: 0, - settings: { - gas_above_threshold: '0.0', - gas_below_threshold: '0.0', - is_gas_above_subscribed: false, - is_gas_below_subscribed: false, - is_participation_rate_subscribed: false, - participation_rate_threshold: 0, - }, - } ], - paired_devices: [ { - id: '', - is_notifications_enabled: false, - name: '', - paired_timestamp: 0, - } ], + networks: [], + paired_devices: [], }, ) @@ -55,11 +42,14 @@ export const useNotificationsManagementStore = defineStore('notifications-manage method: 'PUT', }) } + const isLoadingGetSettings = ref(false) const getSettings = async () => { + isLoadingGetSettings.value = true const { data } = await fetch( API_PATH.NOTIFICATIONS_MANAGEMENT_GENERAL, ) settings.value = data + isLoadingGetSettings.value = false } const removeDevice = async (id: string) => { @@ -93,16 +83,34 @@ export const useNotificationsManagementStore = defineStore('notifications-manage { paired_device_id: id, }, - ).then(() => { - // using optimistic ui here to avoid calling the api after put - settings.value.paired_devices - }) + ) + } + const setNotificationForNetwork = async ({ + chain_id, + settings, + }: { + chain_id: string, + settings: NotificationSettingsNetwork, + }) => { + await fetch( + API_PATH.NOTIFICATIONS_MANAGEMENT_NETWORK_SET_NOTIFICATION, + { + body: { + ...settings, + }, + }, + { + network: chain_id, + }, + ) } return { getSettings, + isLoadingGetSettings, removeDevice, saveSettings, + setNotificationForNetwork, setNotificationForPairedDevice, settings, } diff --git a/frontend/types/customFetch.ts b/frontend/types/customFetch.ts index de028c8af..522702911 100644 --- a/frontend/types/customFetch.ts +++ b/frontend/types/customFetch.ts @@ -42,16 +42,17 @@ export enum API_PATH { NOTIFICATIONS_CLIENTS = '/notifications/clients', NOTIFICATIONS_DASHBOARDS = '/notifications/dashboards', NOTIFICATIONS_MACHINE = '/notifications/machines', - NOTIFICATIONS_MANAGEMENT_GENERAL = '/notifications/managementGeneral', - NOTIFICATIONS_MANAGEMENT_PAIRED_DEVICES_DELETE = '/notifications/managementPairedDevicesDelete', - NOTIFICATIONS_MANAGEMENT_PAIRED_DEVICES_SET_NOTIFICATION = '/notifications/managementPairedDevicesSetNotification', - NOTIFICATIONS_MANAGEMENT_SAVE = '/notifications/managementSave', + NOTIFICATIONS_MANAGEMENT_GENERAL = '/notifications/management/general', + NOTIFICATIONS_MANAGEMENT_NETWORK_SET_NOTIFICATION = '/notifications/management/network/set_notification', + NOTIFICATIONS_MANAGEMENT_PAIRED_DEVICES_DELETE = '/notifications/management/paired_devices/delete', + NOTIFICATIONS_MANAGEMENT_PAIRED_DEVICES_SET_NOTIFICATION = '/notifications/management/paired_devices/set_notifications', + NOTIFICATIONS_MANAGEMENT_SAVE = '/notifications/management/save', NOTIFICATIONS_NETWORK = '/notifications/networks', NOTIFICATIONS_OVERVIEW = '/notifications', - NOTIFICATIONS_ROCKETPOOL = '/notifications/rocket-pool', + NOTIFICATIONS_ROCKETPOOL = '/notifications/rocket_pool', NOTIFICATIONS_TEST_EMAIL = '/notifications/test_email', NOTIFICATIONS_TEST_PUSH = '/notifications/test_push', - NOTIFICATIONS_TEST_WEBHOOK = '/users/me/notifications/test-webhook', + NOTIFICATIONS_TEST_WEBHOOK = '/users/me/notifications/test_webhook', PRODUCT_SUMMARY = '/productSummary', REGISTER = '/register', SAVE_DASHBOARDS_SETTINGS = '/settings-dashboards', @@ -297,6 +298,12 @@ export const mapping: Record = { [API_PATH.NOTIFICATIONS_MANAGEMENT_GENERAL]: { path: '/users/me/notifications/settings', }, + [API_PATH.NOTIFICATIONS_MANAGEMENT_NETWORK_SET_NOTIFICATION]: { + getPath: pathValues => + `/users/me/notifications/settings/networks/${pathValues?.network}`, + method: 'PUT', + path: '/users/me/notifications/settings/networks/{network}', + }, [API_PATH.NOTIFICATIONS_MANAGEMENT_PAIRED_DEVICES_DELETE]: { getPath: pathValues => `/users/me/notifications/settings/paired-devices/${pathValues?.paired_device_id}`, diff --git a/frontend/utils/format.ts b/frontend/utils/format.ts index 9d54cb665..ca5b1db48 100644 --- a/frontend/utils/format.ts +++ b/frontend/utils/format.ts @@ -1,8 +1,11 @@ -import { commify } from '@ethersproject/units' +import { + commify, + formatUnits, +} from '@ethersproject/units' import { DateTime, type StringUnitLength, } from 'luxon' -import { type ComposerTranslation } from 'vue-i18n' +import type { ComposerTranslation } from 'vue-i18n' import type { AgeFormat } from '~/types/settings' import { type ChainIDs, epochToTs, slotToTs, @@ -412,3 +415,45 @@ export function formatFraction(value: NumberOrString, option?: { locale?: string minimumFractionDigits: 0, }).format(number * 100) } +/** + * This should convert 20 to 0.2 + */ +export function formatToFraction(value: NumberOrString, option?: { locale?: string }) { + const { + locale = 'en-US', + } = option ?? {} + const number = Number(value ?? 0) + return new Intl.NumberFormat(locale, { + // maximumFractionDigits: 0, + // minimumFractionDigits: 0, + }).format(number / 100) +} + +export function formatWeiTo(wei: string, { + maximumFractionDigits = 0, + minimumFractionDigits = 0, + unit, +}: { + maximumFractionDigits?: number, + minimumFractionDigits?: number, + unit: 'gwei', +}) { + return new Intl.NumberFormat('en-US', { + maximumFractionDigits, + minimumFractionDigits, + }).format(Number(formatUnits(wei, unit))) +} + +export function formatToWei(value: string, { + from, +}: { + from: 'gwei', +}, +) { + const bigValue = BigInt(Math.round(Number(value))) + let result = '' + if (from === 'gwei') { + result = `${bigValue * 1_000_000_000n}` + } + return result +} From 0e55b322af5d2d5fcbca9cfeb65ddd3ca0c465d4 Mon Sep 17 00:00:00 2001 From: marcel-bitfly <174338434+marcel-bitfly@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:10:51 +0200 Subject: [PATCH 09/31] refactor(BcLoadingSpinner): rename `component` It is easier to work with components if their name reflect the file path in nuxt. --- .../components/bc/{LoadingSpinner.vue => BcLoadingSpinner.vue} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/components/bc/{LoadingSpinner.vue => BcLoadingSpinner.vue} (100%) diff --git a/frontend/components/bc/LoadingSpinner.vue b/frontend/components/bc/BcLoadingSpinner.vue similarity index 100% rename from frontend/components/bc/LoadingSpinner.vue rename to frontend/components/bc/BcLoadingSpinner.vue From 203901b517d721f4946f72e2e48d635af026d9a2 Mon Sep 17 00:00:00 2001 From: marcel-bitfly <174338434+marcel-bitfly@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:30:38 +0200 Subject: [PATCH 10/31] refactor(notifications): add `possibility to show loading state` Change `getSettings` function and use `nuxt status` from `useAsyncData` --- .../NotificationsManagementGeneralTab.vue | 31 ++++++++++++++-- .../NotificationsManagementMachines.vue | 36 +++++++++++++++++-- .../useNotificationsManagementStore.ts | 9 ++--- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/frontend/components/notifications/management/NotificationsManagementGeneralTab.vue b/frontend/components/notifications/management/NotificationsManagementGeneralTab.vue index 2a8d21f43..d351b9c2d 100644 --- a/frontend/components/notifications/management/NotificationsManagementGeneralTab.vue +++ b/frontend/components/notifications/management/NotificationsManagementGeneralTab.vue @@ -12,6 +12,13 @@ const { fetch } = useCustomFetch() const toast = useBcToast() const notificationsManagementStore = useNotificationsManagementStore() +const { + status, +} = useAsyncData( + () => notificationsManagementStore + .getSettings() + .then(({ data }) => notificationsManagementStore.settings = data), +) const isVisible = ref(false) @@ -100,8 +107,8 @@ const textMutedUntil = computed(() => { ), }) }) -await notificationsManagementStore.getSettings() -watchDebounced(notificationsManagementStore.settings.general_settings, async () => { + +watchDebounced(() => notificationsManagementStore.settings.general_settings, async () => { await notificationsManagementStore.saveSettings() }, { deep: true, @@ -114,6 +121,18 @@ watchDebounced(notificationsManagementStore.settings.general_settings, async () v-model="isVisible" />
+
+ +
{{ $t("notifications.general.do_not_disturb") }} @@ -224,12 +243,20 @@ watchDebounced(notificationsManagementStore.settings.general_settings, async () diff --git a/frontend/components/notifications/management/NotificationsManagementClients.vue b/frontend/components/notifications/management/NotificationsManagementClients.vue new file mode 100644 index 000000000..87072bf7b --- /dev/null +++ b/frontend/components/notifications/management/NotificationsManagementClients.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/frontend/components/notifications/management/NotificationsManagementModal.vue b/frontend/components/notifications/management/NotificationsManagementModal.vue index 35589e0f2..aaad72ecb 100644 --- a/frontend/components/notifications/management/NotificationsManagementModal.vue +++ b/frontend/components/notifications/management/NotificationsManagementModal.vue @@ -31,7 +31,6 @@ const tabs: HashTabs = [ { icon: faBolt, key: 'clients', - placeholder: 'Clients coming soon!', title: $t('notifications.tabs.clients'), }, { @@ -64,12 +63,12 @@ const tabs: HashTabs = [ + -