From 07ff2639ec23900fae48cbaec6c6226a7dbc4cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 30 Oct 2024 16:05:16 +0000 Subject: [PATCH 01/13] bugfix: use corrext axum extractors for ecash route arguments (#5065) --- nym-api/src/ecash/api_routes/partial_signing.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index 1a6b95dd83..70749b1454 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -7,7 +7,7 @@ use crate::ecash::helpers::blind_sign; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use axum::extract::Path; +use axum::extract::Query; use axum::{Json, Router}; use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, @@ -134,7 +134,7 @@ struct ExpirationDateParam { ) )] async fn partial_expiration_date_signatures( - Path(ExpirationDateParam { expiration_date }): Path, + Query(ExpirationDateParam { expiration_date }): Query, state: Arc, ) -> AxumResult> { state.ensure_signer().await?; @@ -172,7 +172,7 @@ async fn partial_expiration_date_signatures( ) )] async fn partial_coin_indices_signatures( - Path(EpochIdParam { epoch_id }): Path, + Query(EpochIdParam { epoch_id }): Query, state: Arc, ) -> AxumResult> { state.ensure_signer().await?; From c90ebf0a6ae2728c863362eb692fdf0213da4fca Mon Sep 17 00:00:00 2001 From: Fouad Date: Wed, 30 Oct 2024 17:15:38 +0000 Subject: [PATCH 02/13] Feature/wallet bonding fixes (#5064) * bonding and unbonding for nym nodes --- nym-wallet/Cargo.lock | 68 +++++++++---------- .../signatures/ed25519_signing_payload.rs | 4 +- .../Bonding/forms/nym-node/FormContext.tsx | 21 +++--- .../Bonding/forms/nym-node/NymNodeAmount.tsx | 36 +++++----- .../Bonding/forms/nym-node/NymNodeData.tsx | 29 ++++---- .../forms/nym-node/NymNodeSignature.tsx | 39 +++++++---- .../forms/nym-node/amountValidationSchema.ts | 4 +- .../Bonding/modals/BondNymNodeModal.tsx | 41 ++++++++--- .../components/Bonding/modals/UnbondModal.tsx | 5 +- nym-wallet/src/context/bonding.tsx | 40 ++++++++++- nym-wallet/src/context/mocks/bonding.tsx | 9 +++ nym-wallet/src/hooks/useGetNodeDetails.ts | 1 + nym-wallet/src/pages/bonding/Bonding.tsx | 19 +++++- .../settings-pages/NodeUnbondPage.tsx | 3 +- nym-wallet/src/types/global.ts | 2 +- 15 files changed, 203 insertions(+), 118 deletions(-) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index bd31b34bc3..fe3e2b65d8 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -173,7 +173,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -658,7 +658,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1022,7 +1022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1083,7 +1083,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1751,7 +1751,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1931,7 +1931,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -3667,7 +3667,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -3854,7 +3854,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -3983,7 +3983,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4140,7 +4140,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4151,9 +4151,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -4178,7 +4178,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4704,7 +4704,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4798,9 +4798,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -4825,13 +4825,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4842,14 +4842,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa 1.0.9", "memchr", @@ -4865,7 +4865,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -5219,7 +5219,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -5262,9 +5262,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -5769,7 +5769,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -5856,7 +5856,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -6021,7 +6021,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -6097,7 +6097,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", "termcolor", ] @@ -6201,7 +6201,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -6316,7 +6316,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -6350,7 +6350,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6979,7 +6979,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs index 53658067aa..aa59205169 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -116,12 +116,12 @@ pub async fn generate_gateway_bonding_msg_payload( #[tauri::command] pub async fn generate_nym_node_bonding_msg_payload( - nym_node: NymNode, + nymnode: NymNode, cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { - nym_node_bonding_msg_payload(nym_node, cost_params, pledge, state).await + nym_node_bonding_msg_payload(nymnode, cost_params, pledge, state).await } #[tauri::command] diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx index 6ad0bcadbc..525545b91d 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx @@ -2,15 +2,15 @@ import React, { createContext, useContext, useMemo, useState } from 'react'; import { CurrencyDenom } from '@nymproject/types'; import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types'; -const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = { +const defaultNymNodeValues: TBondNymNodeArgs['nymnode'] = { identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9', - custom_http_port: 1, + custom_http_port: null, host: '1.1.1.1', }; const defaultCostParams = (denom: CurrencyDenom): TBondNymNodeArgs['costParams'] => ({ interval_operating_cost: { amount: '40', denom }, - profit_margin_percent: '10', + profit_margin_percent: '40', }); const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({ @@ -21,14 +21,14 @@ const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({ interface FormContextType { step: 1 | 2 | 3 | 4; setStep: React.Dispatch>; - nymNodeData: TBondNymNodeArgs['nymNode']; - setNymNodeData: React.Dispatch>; + nymNodeData: TBondNymNodeArgs['nymnode']; + setNymNodeData: React.Dispatch>; costParams: TBondNymNodeArgs['costParams']; setCostParams: React.Dispatch>; amountData: TBondMixNodeArgs['pledge']; setAmountData: React.Dispatch>; - signature?: string; - setSignature: React.Dispatch>; + signature: string; + setSignature: React.Dispatch>; onError: (e: string) => void; } @@ -41,9 +41,8 @@ const FormContext = createContext({ setCostParams: () => {}, amountData: defaultAmount('nym'), setAmountData: () => {}, - signature: undefined, + signature: '', setSignature: () => {}, - onError: () => {}, }); @@ -52,10 +51,10 @@ const FormContextProvider = ({ children }: { children: React.ReactNode }) => { const denom = 'nym'; const [step, setStep] = useState<1 | 2 | 3 | 4>(1); - const [nymNodeData, setNymNodeData] = useState(defaultNymNodeValues); + const [nymNodeData, setNymNodeData] = useState(defaultNymNodeValues); const [costParams, setCostParams] = useState(defaultCostParams(denom)); const [amountData, setAmountData] = useState(defaultAmount(denom)); - const [signature, setSignature] = useState(); + const [signature, setSignature] = useState(''); const onError = (e: string) => { console.error(e); diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx index 550dd639ad..e95d0ae934 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx @@ -1,22 +1,12 @@ import React from 'react'; import { Stack, TextField, Box, FormHelperText } from '@mui/material'; import { useForm } from 'react-hook-form'; -import { TBondNymNodeArgs } from 'src/types'; import { yupResolver } from '@hookform/resolvers/yup'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { checkHasEnoughFunds } from 'src/utils'; import { nymNodeAmountSchema } from './amountValidationSchema'; - -const defaultNymNodeCostParamValues: TBondNymNodeArgs['costParams'] = { - profit_margin_percent: '10', - interval_operating_cost: { amount: '40', denom: 'nym' }, -}; - -const defaultNymNodePledgeValue: TBondNymNodeArgs['pledge'] = { - amount: '100', - denom: 'nym', -}; +import { useFormContext } from './FormContext'; type NymNodeDataProps = { onClose: () => void; @@ -26,6 +16,7 @@ type NymNodeDataProps = { }; const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => { + const { setAmountData, setCostParams, amountData, costParams } = useFormContext(); const { formState: { errors }, register, @@ -36,21 +27,26 @@ const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => { } = useForm({ mode: 'all', defaultValues: { - pledge: defaultNymNodePledgeValue, - ...defaultNymNodeCostParamValues, + pledge: amountData, + ...costParams, }, resolver: yupResolver(nymNodeAmountSchema()), }); - console.log(errors, 'errors'); - const handleRequestValidation = async () => { const values = getValues(); const hasSufficientTokens = await checkHasEnoughFunds(values.pledge.amount); if (hasSufficientTokens) { - handleSubmit(onNext)(); + handleSubmit((args) => { + setAmountData(args.pledge); + setCostParams({ + profit_margin_percent: args.profit_margin_percent, + interval_operating_cost: args.interval_operating_cost, + }); + onNext(); + })(); } else { setError('pledge.amount', { message: 'Not enough tokens' }); } @@ -77,8 +73,8 @@ const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => { setValue('pledge.amount', newValue.amount, { shouldValidate: true }); }} validationError={errors.pledge?.amount?.message} - denom={defaultNymNodePledgeValue.denom} - initialValue={defaultNymNodePledgeValue.amount} + denom={amountData.denom} + initialValue={amountData.amount} /> @@ -90,8 +86,8 @@ const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => { setValue('interval_operating_cost', newValue, { shouldValidate: true }); }} validationError={errors.interval_operating_cost?.amount?.message} - denom={defaultNymNodeCostParamValues.interval_operating_cost.denom} - initialValue={defaultNymNodeCostParamValues.interval_operating_cost.amount} + denom={costParams.interval_operating_cost.denom} + initialValue={costParams.interval_operating_cost.amount} /> Monthly operational costs of running your node. If your node is in the active set the amount will be paid diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx index e32068a9c6..313dd61fa8 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx @@ -1,18 +1,12 @@ import React from 'react'; +import * as yup from 'yup'; import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material'; import { useForm } from 'react-hook-form'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; -import { TBondNymNodeArgs } from 'src/types'; import { yupResolver } from '@hookform/resolvers/yup'; -import * as yup from 'yup'; import { isValidHostname, validateRawPort } from 'src/utils'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; - -const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = { - identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9', - custom_http_port: 1, - host: '1.1.1.1', -}; +import { useFormContext } from './FormContext'; const yupValidationSchema = yup.object().shape({ identity_key: yup.string().required('Identity key is required'), @@ -24,8 +18,13 @@ const yupValidationSchema = yup.object().shape({ custom_http_port: yup .number() - .required('A custom http port is required') - .test('valid-http', 'A valid http port is required', (value) => (value ? validateRawPort(value) : false)), + .nullable() + .test('valid-http', 'A valid http port is required', (value) => { + if (value === null) { + return true; + } + return value ? validateRawPort(value) : false; + }), }); type NymNodeDataProps = { @@ -36,6 +35,7 @@ type NymNodeDataProps = { }; const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { + const { setNymNodeData, nymNodeData } = useFormContext(); const { formState: { errors }, register, @@ -43,14 +43,17 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { handleSubmit, } = useForm({ mode: 'all', - defaultValues: defaultNymNodeValues, + defaultValues: nymNodeData, resolver: yupResolver(yupValidationSchema), }); const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false); const handleNext = async () => { - handleSubmit(onNext)(); + handleSubmit((args) => { + setNymNodeData(args); + onNext(); + })(); }; return ( @@ -69,7 +72,7 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { required fullWidth label="Identity Key" - initialValue={defaultNymNodeValues.identity_key} + initialValue={nymNodeData.identity_key} errorText={errors.identity_key?.message?.toString()} onChanged={(value) => setValue('identity_key', value, { shouldValidate: true })} showTickOnValid={false} diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx index e1e5814d55..c4c809ca82 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import * as yup from 'yup'; import { Stack, TextField, Typography } from '@mui/material'; import { useForm } from 'react-hook-form'; import { CopyToClipboard } from 'src/components/CopyToClipboard'; @@ -7,9 +8,11 @@ import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { useBondingContext } from 'src/context'; import { TBondNymNodeArgs } from 'src/types'; import { Signature } from 'src/pages/bonding/types'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useFormContext } from './FormContext'; const NymNodeSignature = ({ - nymNode, + nymnode, pledge, costParams, step, @@ -17,27 +20,38 @@ const NymNodeSignature = ({ onClose, onBack, }: { - nymNode: TBondNymNodeArgs['nymNode']; + nymnode: TBondNymNodeArgs['nymnode']; pledge: TBondNymNodeArgs['pledge']; costParams: TBondNymNodeArgs['costParams']; step: number; - onNext: (data: Signature) => void; + onNext: () => void; onClose: () => void; onBack: () => void; }) => { const [message, setMessage] = useState(''); const [error, setError] = useState(); const { generateNymNodeMsgPayload } = useBondingContext(); + const { signature, setSignature } = useFormContext(); + + const yupValidationSchema = yup.object().shape({ + signature: yup.string().required('Signature is required'), + }); const { register, formState: { errors }, - } = useForm(); + handleSubmit, + } = useForm({ + defaultValues: { + signature, + }, + resolver: yupResolver(yupValidationSchema), + }); const generateMessage = async () => { try { const msg = await generateNymNodeMsgPayload({ - nymNode, + nymnode, pledge, costParams, }); @@ -53,10 +67,10 @@ const NymNodeSignature = ({ useEffect(() => { generateMessage(); - }, [nymNode, pledge, costParams]); + }, []); - const onSubmit = async (data: Signature) => { - onNext(data); + const handleNext = async () => { + handleSubmit(onNext)(); }; if (error) { @@ -66,11 +80,7 @@ const NymNodeSignature = ({ return ( - onSubmit({ - signature: 'signature', - }) - } + onOk={handleNext} onClose={onClose} header="Bond Nym Node" subHeader={`Step ${step}/3`} @@ -95,10 +105,13 @@ const NymNodeSignature = ({ setSignature(e.target.value)} id="outlined-multiline-static" name="signature" rows={3} placeholder="Paste Signature" + helperText={errors.signature?.message} + error={Boolean(errors.signature)} multiline fullWidth required diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts index 1868a4f2e2..93ab8f1478 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts @@ -5,8 +5,8 @@ import { isLessThan, isGreaterThan, validateAmount } from 'src/utils'; const operatingCostAndPmValidation = (params?: TauriContractStateParams) => { const defaultParams = { profit_margin_percent: { - minimum: parseFloat(params?.profit_margin.minimum || '0%'), - maximum: parseFloat(params?.profit_margin.maximum || '100%'), + minimum: parseFloat(params?.profit_margin.minimum || '20%'), + maximum: parseFloat(params?.profit_margin.maximum || '50%'), }, interval_operating_cost: { diff --git a/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx index bfed31e765..f6288889ab 100644 --- a/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx @@ -2,18 +2,24 @@ import React, { useContext, useEffect } from 'react'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { useGetFee } from 'src/hooks/useGetFee'; -import { Signature } from 'src/pages/bonding/types'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; +import { TBondNymNodeArgs } from 'src/types'; import FormContextProvider, { useFormContext } from '../forms/nym-node/FormContext'; import NymNodeData from '../forms/nym-node/NymNodeData'; import NymNodeAmount from '../forms/nym-node/NymNodeAmount'; import NymNodeSignature from '../forms/nym-node/NymNodeSignature'; -export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => { +export const BondNymNodeModal = ({ + onClose, + onBond, +}: { + onClose: () => void; + onBond: (data: TBondNymNodeArgs) => Promise; +}) => { const { fee, resetFeeState, feeError } = useGetFee(); const { userBalance } = useContext(AppContext); - const { setStep, step, onError, setSignature, amountData, costParams, nymNodeData } = useFormContext(); + const { setStep, step, onError, signature, amountData, costParams, nymNodeData } = useFormContext(); useEffect(() => { if (feeError) { @@ -21,12 +27,17 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => { } }, [feeError]); - const handleUpdateMixnodeData = async () => { + const handleUpdateNymnodeData = async () => { setStep(2); }; - const handleUpdateSignature = async (data: Signature) => { - setSignature(data.signature); + const handleBond = async () => { + onBond({ + nymnode: nymNodeData, + pledge: amountData, + costParams, + msgSignature: signature, + }); }; const handleConfirm = async () => {}; @@ -51,7 +62,7 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => { } if (step === 1) { - return ; + return ; } if (step === 2) { @@ -61,10 +72,10 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => { if (step === 3) { return ( setStep(2)} step={step} @@ -75,14 +86,22 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => { return null; }; -export const BondNymNodeModalWithState = ({ open, onClose }: { open: boolean; onClose: () => void }) => { +export const BondNymNode = ({ + open, + onClose, + onBond, +}: { + open: boolean; + onClose: () => void; + onBond: (data: TBondNymNodeArgs) => Promise; +}) => { if (!open) { return null; } return ( - + ); }; diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx index 51e779a563..de2fbdd1ca 100644 --- a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { useEffect } from 'react'; import { Typography } from '@mui/material'; +import { TBondedNode } from 'src/context'; import { useGetFee } from 'src/hooks/useGetFee'; import { isGateway, isMixnode } from 'src/types'; -import { TBondedGateway } from 'src/requests/gatewayDetails'; -import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { ModalFee } from '../../Modals/ModalFee'; import { ModalListItem } from '../../Modals/ModalListItem'; import { SimpleModal } from '../../Modals/SimpleModal'; @@ -16,7 +15,7 @@ import { } from '../../../requests'; interface Props { - node: TBondedMixnode | TBondedGateway; + node: TBondedNode; onConfirm: () => Promise; onClose: () => void; onError: (e: string) => void; diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 51671e02e6..bbc1047147 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { FeeDetails, TransactionExecuteResult } from '@nymproject/types'; -import { isGateway, isMixnode, TUpdateBondArgs, isNymNode, TNymNodeSignatureArgs } from 'src/types'; +import { isGateway, isMixnode, TUpdateBondArgs, isNymNode, TNymNodeSignatureArgs, TBondNymNodeArgs } from 'src/types'; import { Console } from 'src/utils/console'; import useGetNodeDetails from 'src/hooks/useGetNodeDetails'; import { TBondedNymNode } from 'src/requests/nymNodeDetails'; @@ -20,6 +20,7 @@ import { migrateVestedMixnode as tauriMigrateVestedMixnode, migrateLegacyMixnode as migrateLegacyMixnodeReq, migrateLegacyGateway as migrateLegacyGatewayReq, + bondNymNode, } from '../requests'; export type TBondedNode = TBondedMixnode | TBondedGateway | TBondedNymNode; @@ -31,6 +32,7 @@ export type TBondingContext = { isVestingAccount: boolean; refresh: () => void; unbond: (fee?: FeeDetails) => Promise; + bond: (args: TBondNymNodeArgs) => Promise; updateBondAmount: (data: TUpdateBondArgs) => Promise; redeemRewards: (fee?: FeeDetails) => Promise; generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise; @@ -41,6 +43,9 @@ export type TBondingContext = { export const BondingContext = createContext({ isLoading: true, refresh: async () => undefined, + bond: async () => { + throw new Error('Not implemented'); + }, unbond: async () => { throw new Error('Not implemented'); }, @@ -70,7 +75,11 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen const { userBalance, clientDetails, network } = useContext(AppContext); - const { bondedNode, isLoading: isBondedNodeLoading } = useGetNodeDetails(clientDetails?.client_address, network); + const { + bondedNode, + isLoading: isBondedNodeLoading, + getNodeDetails, + } = useGetNodeDetails(clientDetails?.client_address, network); useEffect(() => { userBalance.fetchBalance(); @@ -91,6 +100,30 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen resetState(); }; + const bond = async (data: TBondNymNodeArgs) => { + let tx; + setIsLoading(true); + + try { + tx = await bondNymNode({ + ...data, + costParams: { + ...data.costParams, + profit_margin_percent: toPercentFloatString(data.costParams.profit_margin_percent), + }, + }); + if (clientDetails?.client_address) { + await getNodeDetails(clientDetails?.client_address); + } + } catch (e) { + Console.warn(e); + setError(`an error occurred: ${e as string}`); + } finally { + setIsLoading(false); + } + return tx; + }; + const unbond = async (fee?: FeeDetails) => { let tx; setIsLoading(true); @@ -143,7 +176,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen try { const message = await generateNymNodeMsgPayloadReq({ - nymNode: data.nymNode, + nymnode: data.nymnode, pledge: data.pledge, costParams: { ...data.costParams, @@ -187,6 +220,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen isLoading: isLoading || isBondedNodeLoading, error, bondedNode, + bond, unbond, refresh, redeemRewards, diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 2ef46237f9..11abb64e6d 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -133,6 +133,14 @@ export const MockBondingContextProvider = ({ return TxResultMock; }; + const bond = async (): Promise => { + setIsLoading(true); + await mockSleep(SLEEP_MS); + setBondedData(bondedMixnodeMock); + setIsLoading(false); + return TxResultMock; + }; + const unbond = async (): Promise => { setIsLoading(true); await mockSleep(SLEEP_MS); @@ -189,6 +197,7 @@ export const MockBondingContextProvider = ({ error, bondMixnode, bondGateway, + bond, unbond, refresh, redeemRewards, diff --git a/nym-wallet/src/hooks/useGetNodeDetails.ts b/nym-wallet/src/hooks/useGetNodeDetails.ts index 9049b87ce5..1c9608a351 100644 --- a/nym-wallet/src/hooks/useGetNodeDetails.ts +++ b/nym-wallet/src/hooks/useGetNodeDetails.ts @@ -63,6 +63,7 @@ const useGetNodeDetails = (clientAddress?: string, network?: string) => { bondedNode, isLoading, isError, + getNodeDetails, }; }; diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 843ada1f5a..b66ffbe58e 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -11,14 +11,14 @@ import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/component import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { AppContext, urls } from 'src/context/main'; -import { isGateway, isMixnode, isNymNode, TUpdateBondArgs } from 'src/types'; +import { isGateway, isMixnode, isNymNode, TBondNymNodeArgs, TUpdateBondArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; import { VestingWarningModal } from 'src/components/VestingWarningModal'; import MigrateLegacyNode from 'src/components/Bonding/modals/MigrateLegacyNode'; import { BondedNymNode } from 'src/components/Bonding/BondedNymNode'; import { UpdateBondAmountNymNode } from 'src/components/Bonding/modals/UpdateBondAmountNymNode'; -import { BondNymNodeModalWithState } from 'src/components/Bonding/modals/BondNymNodeModal'; +import { BondNymNode } from 'src/components/Bonding/modals/BondNymNodeModal'; import { BondingContextProvider, useBondingContext } from '../../context'; export const Bonding = () => { @@ -44,6 +44,7 @@ export const Bonding = () => { redeemRewards, updateBondAmount, refresh, + bond, migrateVestedMixnode, migrateLegacyNode, } = useBondingContext(); @@ -74,6 +75,18 @@ export const Bonding = () => { setShowMigrateLegacyNodeModal(shouldShowMigrateLegacyNodeModal()); }, [bondedNode]); + const handleBondNymNode = async (data: TBondNymNodeArgs) => { + setShowModal(undefined); + const tx = await bond(data); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Bonding successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } + }; + const handleMigrateVestedMixnode = async () => { setShowMigrationModal(false); const tx = await migrateVestedMixnode(); @@ -250,7 +263,7 @@ export const Bonding = () => { /> )} - + {showModal === 'update-bond-oversaturated' && uncappedSaturation && ( Promise; onError: (e: string) => void; } @@ -71,7 +70,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { - {isConfirmed && !isNymNode(bondedNode) && ( + {isConfirmed && ( { diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index 23144fd871..ff58082ab4 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -36,7 +36,7 @@ export type TBondNymNodeArgs = TNymNodeSignatureArgs & { }; export type TNymNodeSignatureArgs = { - nymNode: NymNode; + nymnode: NymNode; costParams: NodeCostParams; pledge: DecCoin; }; From 8b0b70a7279f81242ec99bf3927fd9753b3dbf85 Mon Sep 17 00:00:00 2001 From: Fouad Date: Thu, 31 Oct 2024 09:59:22 +0000 Subject: [PATCH 03/13] allow nym node config updates (#5066) --- .../Bonding/forms/nym-node/FormContext.tsx | 2 +- .../Bonding/forms/nym-node/NymNodeData.tsx | 30 ++++--------- .../nym-node/settingsValidationSchema.ts | 20 +++++++++ nym-wallet/src/context/bonding.tsx | 26 +++++++++++ nym-wallet/src/context/mocks/bonding.tsx | 9 ++++ .../GeneralNymNodeSettings.tsx | 44 +++++++++---------- nym-wallet/src/types/global.ts | 2 +- 7 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx index 525545b91d..d3f95006f6 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx @@ -3,7 +3,7 @@ import { CurrencyDenom } from '@nymproject/types'; import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types'; const defaultNymNodeValues: TBondNymNodeArgs['nymnode'] = { - identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9', + identity_key: '', custom_http_port: null, host: '1.1.1.1', }; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx index 313dd61fa8..4fc8d7fc98 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx @@ -1,31 +1,12 @@ import React from 'react'; -import * as yup from 'yup'; +import * as Yup from 'yup'; import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material'; import { useForm } from 'react-hook-form'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { yupResolver } from '@hookform/resolvers/yup'; -import { isValidHostname, validateRawPort } from 'src/utils'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { useFormContext } from './FormContext'; - -const yupValidationSchema = yup.object().shape({ - identity_key: yup.string().required('Identity key is required'), - host: yup - .string() - .required('A host is required') - .test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || '')) - .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), - - custom_http_port: yup - .number() - .nullable() - .test('valid-http', 'A valid http port is required', (value) => { - if (value === null) { - return true; - } - return value ? validateRawPort(value) : false; - }), -}); +import { settingsValidationSchema } from './settingsValidationSchema'; type NymNodeDataProps = { onClose: () => void; @@ -34,6 +15,11 @@ type NymNodeDataProps = { step: number; }; +const validationSchema = Yup.object().shape({ + identity_key: Yup.string().required('Identity key is required'), + ...settingsValidationSchema.fields, +}); + const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { const { setNymNodeData, nymNodeData } = useFormContext(); const { @@ -44,7 +30,7 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { } = useForm({ mode: 'all', defaultValues: nymNodeData, - resolver: yupResolver(yupValidationSchema), + resolver: yupResolver(validationSchema), }); const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false); diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts new file mode 100644 index 0000000000..316f6f4ad7 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts @@ -0,0 +1,20 @@ +import { isValidHostname, validateRawPort } from 'src/utils'; +import * as Yup from 'yup'; + +const settingsValidationSchema = Yup.object().shape({ + host: Yup.string() + .required('A host is required') + .test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || '')) + .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), + + custom_http_port: Yup.number() + .nullable() + .test('valid-http', 'A valid http port is required', (value) => { + if (value === null) { + return true; + } + return value ? validateRawPort(value) : false; + }), +}); + +export { settingsValidationSchema }; diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index bbc1047147..d5b1e2f118 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -21,6 +21,7 @@ import { migrateLegacyMixnode as migrateLegacyMixnodeReq, migrateLegacyGateway as migrateLegacyGatewayReq, bondNymNode, + updateNymNodeConfig as updateNymNodeConfigReq, } from '../requests'; export type TBondedNode = TBondedMixnode | TBondedGateway | TBondedNymNode; @@ -34,6 +35,10 @@ export type TBondingContext = { unbond: (fee?: FeeDetails) => Promise; bond: (args: TBondNymNodeArgs) => Promise; updateBondAmount: (data: TUpdateBondArgs) => Promise; + updateNymNodeConfig: (data: { + host: string; + custom_http_port: number | null; + }) => Promise; redeemRewards: (fee?: FeeDetails) => Promise; generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise; migrateVestedMixnode: () => Promise; @@ -52,6 +57,9 @@ export const BondingContext = createContext({ updateBondAmount: async () => { throw new Error('Not implemented'); }, + updateNymNodeConfig: async () => { + throw new Error('Not implemented'); + }, redeemRewards: async () => { throw new Error('Not implemented'); }, @@ -140,6 +148,23 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; + const updateNymNodeConfig = async (data: { host: string; custom_http_port: number | null }) => { + let tx; + setIsLoading(true); + try { + tx = await updateNymNodeConfigReq(data); + if (clientDetails?.client_address) { + await getNodeDetails(clientDetails?.client_address); + } + } catch (e) { + Console.warn(e); + setError(`an error occurred: ${e}`); + } finally { + setIsLoading(false); + } + return tx; + }; + const redeemRewards = async (fee?: FeeDetails) => { let tx; setIsLoading(true); @@ -225,6 +250,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen refresh, redeemRewards, updateBondAmount, + updateNymNodeConfig, generateNymNodeMsgPayload, migrateVestedMixnode, migrateLegacyNode, diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 11abb64e6d..0fd08a871f 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -149,6 +149,14 @@ export const MockBondingContextProvider = ({ return TxResultMock; }; + const updateNymNodeConfig = async (): Promise => { + setIsLoading(true); + await mockSleep(SLEEP_MS); + triggerStateUpdate(); + setIsLoading(false); + return TxResultMock; + }; + const redeemRewards = async (): Promise => { setIsLoading(true); await mockSleep(SLEEP_MS); @@ -212,6 +220,7 @@ export const MockBondingContextProvider = ({ isVestingAccount: false, migrateVestedMixnode: async () => undefined, migrateLegacyNode: async () => undefined, + updateNymNodeConfig, }), [isLoading, error, bondedMixnode, bondedGateway, trigger, fee], ); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx index 76721d9937..cedc2891ed 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx @@ -3,22 +3,21 @@ import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { updateNymNodeConfig } from 'src/requests'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { useGetFee } from 'src/hooks/useGetFee'; -import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { BalanceWarning } from 'src/components/FeeWarning'; -import { AppContext } from 'src/context'; +import { AppContext, useBondingContext } from 'src/context'; import { TBondedNymNode } from 'src/requests/nymNodeDetails'; +import { settingsValidationSchema } from 'src/components/Bonding/forms/nym-node/settingsValidationSchema'; export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymNode }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const { fee, resetFeeState } = useGetFee(); const { userBalance } = useContext(AppContext); + const { updateNymNodeConfig } = useBondingContext(); const theme = useTheme(); @@ -27,28 +26,26 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN handleSubmit, formState: { errors, isSubmitting, isDirty, isValid }, } = useForm({ - resolver: yupResolver(bondedInfoParametersValidationSchema), + resolver: yupResolver(settingsValidationSchema), mode: 'onChange', defaultValues: { host: bondedNode.host, - customHttpPort: bondedNode.customHttpPort, + custom_http_port: bondedNode.customHttpPort, }, }); - const onSubmit = async (data: { host?: string; customHttpPort?: number | null }) => { + const onSubmit = async ({ host, custom_http_port }: { host: string; custom_http_port: number | null }) => { resetFeeState(); - const { host, customHttpPort } = data; - if (host && customHttpPort) { + + try { const NymNodeConfigParams = { host, - custom_http_port: customHttpPort, + custom_http_port, }; - try { - await updateNymNodeConfig(NymNodeConfigParams); - setOpenConfirmationModal(true); - } catch (error) { - Console.error(error); - } + await updateNymNodeConfig(NymNodeConfigParams); + setOpenConfirmationModal(true); + } catch (error) { + Console.error(error); } }; @@ -59,7 +56,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN open header="Update node settings" fee={fee} - onConfirm={handleSubmit((d) => onSubmit(d))} + onConfirm={handleSubmit(onSubmit)} onPrev={resetFeeState} onClose={resetFeeState} > @@ -70,7 +67,6 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN )} )} - {isSubmitting && } @@ -93,12 +89,12 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN @@ -134,7 +130,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN size="large" variant="contained" disabled={isSubmitting || !isDirty || !isValid} - onClick={handleSubmit(() => undefined)} + onClick={handleSubmit(onSubmit)} sx={{ m: 3, mr: 0 }} fullWidth > @@ -152,7 +148,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN hideCloseIcon displayInfoIcon onOk={async () => { - await setOpenConfirmationModal(false); + setOpenConfirmationModal(false); }} buttonFullWidth sx={{ diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index ff58082ab4..946b312376 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -86,7 +86,7 @@ export type TNodeDescription = { export type TNodeConfigUpdateArgs = { host: string; - custom_http_port: number; + custom_http_port: number | null; }; export type TDelegateArgs = { From be672340932dcfd912eff969559a9c750e7f8dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 31 Oct 2024 10:32:25 +0000 Subject: [PATCH 04/13] bugfix: credential-proxy obtain-async (#5067) * removed foreign key constraint on deposit table * fixed sql nullability * fixed swagger arguments for '/api/v1/ticketbook/shares/device/{device_id}/credential/{credential_id}' route * fixed missing swagger component definitions --- .../nym-credential-proxy-requests/Cargo.toml | 4 ++-- .../src/api/v1/ticketbook/models.rs | 2 ++ .../nym-credential-proxy/Cargo.toml | 2 +- .../03_blinded_shares_no_foreign_table.sql | 20 +++++++++++++++++++ .../http/router/api/v1/ticketbook/shares.rs | 2 +- .../src/storage/manager.rs | 6 +++++- 6 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index 5f1d6a49a0..462fa8a4c1 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -22,12 +22,12 @@ reqwest = { workspace = true, features = ["json"] } wasm-bindgen = { workspace = true, optional = true } ## openapi: -utoipa = { workspace = true, optional = true } +utoipa = { workspace = true, optional = true, features = ["uuid"] } nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-http-api-common = { path = "../../common/http-api-common", optional = true } -nym-http-api-client = { path = "../../common/http-api-client" } +nym-http-api-client = { path = "../../common/http-api-client" } nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] } [target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs index 6ce58e81af..46a65060ad 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs @@ -40,6 +40,7 @@ pub struct TicketbookRequest { /// you **MUST** provide a valid value otherwise blacklisting won't work #[schemars(with = "String")] #[serde(with = "bs58_ecash")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] pub ecash_pubkey: PublicKeyUser, // needs to be explicit in case user creates request at 23:59:59.999, but it reaches vpn-api at 00:00:00.001 @@ -48,6 +49,7 @@ pub struct TicketbookRequest { pub expiration_date: Date, #[schemars(with = "String")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] pub ticketbook_type: TicketType, pub is_freepass_request: bool, diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index eb852c891e..f04586b452 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-credential-proxy" -version = "0.1.1" +version = "0.1.2" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql b/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql new file mode 100644 index 0000000000..b7bc48ee96 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql @@ -0,0 +1,20 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + + +DROP TABLE blinded_shares; +CREATE TABLE blinded_shares +( + id INTEGER NOT NULL PRIMARY KEY, +-- removed reference to `ticketbook_deposit` as the deposit wouldn't actually have been made before the pending share is inserted + request_uuid TEXT NOT NULL, + status TEXT NOT NULL, + device_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + available_shares INTEGER NOT NULL DEFAULT 0, + error_message TEXT DEFAULT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated TIMESTAMP WITHOUT TIME ZONE NOT NULL +); \ No newline at end of file diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs index 174a419694..a42ac2c870 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs @@ -149,7 +149,7 @@ pub(crate) async fn query_for_shares_by_id( (status = 401, description = "authentication token is missing or is invalid"), (status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"), ), - params(OutputParams), + params(SharesQueryParams), security( ("auth_token" = []) ) diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs index c1dd782e00..68990e5ba5 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs @@ -82,7 +82,11 @@ impl SqliteStorageManager { sqlx::query_as!( MinimalWalletShare, r#" - SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date + SELECT + t1.node_id as "node_id!", + t1.blinded_signature as "blinded_signature!", + t1.epoch_id as "epoch_id!", + t1.expiration_date as "expiration_date!" FROM partial_blinded_wallet as t1 JOIN ticketbook_deposit as t2 on t1.corresponding_deposit = t2.deposit_id From b9d1fc40e7173afbfd24be3772444661eb6a0f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 31 Oct 2024 12:08:58 +0000 Subject: [PATCH 05/13] deprecated old nym-api client methods and replaced them when possible (#5069) --- .../validator-client/src/client.rs | 61 +++++++++++++- .../validator-client/src/connection_tester.rs | 2 +- .../validator-client/src/nym_api/mod.rs | 84 ++++++++++++++++++- .../validator-client/src/nym_api/routes.rs | 2 + .../mixnet/query/query_all_gateways.rs | 24 +++--- .../mixnet/query/query_all_mixnodes.rs | 36 +++----- common/mixnode-common/src/verloc/mod.rs | 21 ++--- explorer-api/src/mix_node/econ_stats.rs | 3 + explorer-api/src/tasks.rs | 2 + gateway/src/node/mod.rs | 8 +- mixnode/src/node/mod.rs | 9 +- nym-api/nym-api-requests/src/models.rs | 6 ++ nym-credential-proxy/Cargo.lock | 32 ++++++- .../src-tauri/src/operations/mixnet/bond.rs | 4 + .../src/operations/mixnet/delegate.rs | 2 + .../src/operations/nym_api/status.rs | 16 ++++ .../src/manager/local_client.rs | 10 +-- 17 files changed, 251 insertions(+), 71 deletions(-) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 8667678b05..6a13e8f186 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -18,7 +18,7 @@ use nym_api_requests::ecash::{ PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{ - GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, + ApiHealthResponse, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; @@ -192,6 +192,8 @@ impl Client { } // validator-api wrappers +// we have to allow the use of deprecated method here as they're calling the deprecated trait methods +#[allow(deprecated)] impl Client { pub fn api_url(&self) -> &Url { self.nym_api.current_url() @@ -201,46 +203,54 @@ impl Client { self.nym_api.change_base_url(new_endpoint) } + #[deprecated] pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes().await?) } + #[deprecated] pub async fn get_cached_mixnodes_detailed( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes_detailed().await?) } + #[deprecated] pub async fn get_cached_mixnodes_detailed_unfiltered( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes_detailed_unfiltered().await?) } + #[deprecated] pub async fn get_cached_rewarded_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_rewarded_mixnodes().await?) } + #[deprecated] pub async fn get_cached_rewarded_mixnodes_detailed( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_rewarded_mixnodes_detailed().await?) } + #[deprecated] pub async fn get_cached_active_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_active_mixnodes().await?) } + #[deprecated] pub async fn get_cached_active_mixnodes_detailed( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_active_mixnodes_detailed().await?) } + #[deprecated] pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } @@ -304,6 +314,8 @@ pub struct NymApiClient { // we could re-implement the communication with the REST API on port 1317 } +// we have to allow the use of deprecated method here as they're calling the deprecated trait methods +#[allow(deprecated)] impl NymApiClient { pub fn new(api_url: Url) -> Self { let nym_api = nym_api::Client::new(api_url, None); @@ -417,6 +429,38 @@ impl NymApiClient { Ok(nodes) } + /// retrieve basic information for nodes are capable of operating as a mixnode + /// this includes legacy mixnodes and nym-nodes + pub async fn get_all_basic_mixing_capable_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_basic_mixing_capable_nodes( + semver_compatibility.clone(), + false, + Some(page), + None, + ) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + /// retrieve basic information for all bonded nodes on the network pub async fn get_all_basic_nodes( &self, @@ -443,26 +487,35 @@ impl NymApiClient { Ok(nodes) } + pub async fn health(&self) -> Result { + Ok(self.nym_api.health().await?) + } + + #[deprecated] pub async fn get_cached_active_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_active_mixnodes().await?) } + #[deprecated] pub async fn get_cached_rewarded_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_rewarded_mixnodes().await?) } + #[deprecated] pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes().await?) } + #[deprecated] pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } + #[deprecated] pub async fn get_cached_described_gateways( &self, ) -> Result, ValidatorClientError> { @@ -511,6 +564,7 @@ impl NymApiClient { Ok(bonds) } + #[deprecated] pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -522,6 +576,7 @@ impl NymApiClient { .await?) } + #[deprecated] pub async fn get_mixnode_core_status_count( &self, mix_id: NodeId, @@ -533,6 +588,7 @@ impl NymApiClient { .await?) } + #[deprecated] pub async fn get_mixnode_status( &self, mix_id: NodeId, @@ -540,6 +596,7 @@ impl NymApiClient { Ok(self.nym_api.get_mixnode_status(mix_id).await?) } + #[deprecated] pub async fn get_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -547,6 +604,7 @@ impl NymApiClient { Ok(self.nym_api.get_mixnode_reward_estimation(mix_id).await?) } + #[deprecated] pub async fn get_mixnode_stake_saturation( &self, mix_id: NodeId, @@ -578,6 +636,7 @@ impl NymApiClient { .await?) } + #[deprecated] pub async fn spent_credentials_filter( &self, ) -> Result { diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 15d2efe250..7876fd1918 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -164,7 +164,7 @@ async fn test_nym_api_connection( ) -> ConnectionResult { let result = match timeout( Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC), - client.get_cached_mixnodes(), + client.health(), ) .await { diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 5de7c73aec..aaad043ad5 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -11,7 +11,8 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ - AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, NymNodeDescription, + AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, + NymNodeDescription, }; use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; use nym_api_requests::pagination::PaginatedResponse; @@ -53,11 +54,25 @@ pub fn rfc_3339_date() -> Vec> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { + async fn health(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::API_STATUS_ROUTES, + routes::HEALTH, + ], + NO_PARAMS, + ) + .await + } + + #[deprecated] async fn get_mixnodes(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } + #[deprecated] async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -71,6 +86,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_gateways_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -84,6 +100,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnodes_detailed_unfiltered( &self, ) -> Result, NymAPIError> { @@ -99,11 +116,13 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } + #[deprecated] async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], @@ -112,6 +131,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], @@ -158,6 +178,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -181,6 +202,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -286,6 +308,47 @@ pub trait NymApiClientExt: ApiClient { .await } + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + async fn get_basic_mixing_capable_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + "nym-nodes", + "skimmed", + "mixnodes", + "all", + ], + ¶ms, + ) + .await + } + async fn get_basic_nodes( &self, semver_compatibility: Option, @@ -318,6 +381,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], @@ -326,6 +390,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -340,6 +405,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], @@ -348,6 +414,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnode_report( &self, mix_id: NodeId, @@ -365,6 +432,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, @@ -382,6 +450,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnode_history( &self, mix_id: NodeId, @@ -399,6 +468,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, @@ -416,6 +486,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, NymAPIError> { @@ -432,6 +503,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -463,6 +535,7 @@ pub trait NymApiClientExt: ApiClient { } } + #[deprecated] async fn get_mixnode_core_status_count( &self, mix_id: NodeId, @@ -495,6 +568,7 @@ pub trait NymApiClientExt: ApiClient { } } + #[deprecated] async fn get_mixnode_status( &self, mix_id: NodeId, @@ -512,6 +586,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -529,6 +604,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn compute_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -548,6 +624,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnode_stake_saturation( &self, mix_id: NodeId, @@ -565,6 +642,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnode_inclusion_probability( &self, mix_id: NodeId, @@ -598,6 +676,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ @@ -612,6 +691,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], @@ -620,6 +700,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED], @@ -676,6 +757,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] async fn double_spending_filter_v1(&self) -> Result { self.get_json( &[ diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index e2a1540dda..e0325c44cf 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -36,6 +36,8 @@ pub mod ecash { } pub const STATUS_ROUTES: &str = "status"; +pub const API_STATUS_ROUTES: &str = "api-status"; +pub const HEALTH: &str = "health"; pub const MIXNODE: &str = "mixnode"; pub const GATEWAY: &str = "gateway"; pub const NYM_NODES: &str = "nym-nodes"; diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index cfe66ffd6c..f03fb61d28 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -2,10 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::QueryClientWithNyxd; -use crate::utils::{pretty_cosmwasm_coin, show_error}; +use crate::utils::show_error; use clap::Parser; use comfy_table::Table; -use nym_validator_client::client::NymApiClientExt; #[derive(Debug, Parser)] pub struct Args { @@ -15,12 +14,11 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.nym_api.get_gateways().await { + match client.get_all_cached_described_nodes().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { - node.gateway - .identity_key + node.ed25519_identity_key() .to_string() .eq_ignore_ascii_case(&identity_key) }); @@ -32,14 +30,16 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { None => { let mut table = Table::new(); - table.set_header(vec!["Identity Key", "Owner", "Host", "Bond", "Version"]); - for node in res { + table.set_header(vec!["Node Id", "Identity Key", "Version", "Is Legacy"]); + for node in res + .into_iter() + .filter(|node| node.description.declared_role.entry) + { table.add_row(vec![ - node.gateway.identity_key.to_string(), - node.owner.to_string(), - node.gateway.host.to_string(), - pretty_cosmwasm_coin(&node.pledge_amount), - node.gateway.version.clone(), + node.node_id.to_string(), + node.ed25519_identity_key().to_base58_string(), + node.description.build_information.build_version, + (!node.contract_node_type.is_nym_node()).to_string(), ]); } diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index 9bb20cc8d4..ef5b4a9548 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -2,10 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::QueryClientWithNyxd; -use crate::utils::{pretty_decimal_with_denom, show_error}; +use crate::utils::show_error; use clap::Parser; use comfy_table::Table; -use nym_validator_client::client::NymApiClientExt; #[derive(Debug, Parser)] pub struct Args { @@ -15,13 +14,11 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.nym_api.get_mixnodes().await { + match client.get_all_cached_described_nodes().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { - node.bond_information - .mix_node - .identity_key + node.ed25519_identity_key() .to_string() .eq_ignore_ascii_case(&identity_key) }); @@ -33,25 +30,16 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { None => { let mut table = Table::new(); - table.set_header(vec![ - "Mix id", - "Identity Key", - "Owner", - "Host", - "Bond", - "Total Delegations", - "Version", - ]); - for node in res { - let denom = &node.bond_information.original_pledge().denom; + table.set_header(vec!["Node Id", "Identity Key", "Version", "Is Legacy"]); + for node in res + .into_iter() + .filter(|node| node.description.declared_role.mixnode) + { table.add_row(vec![ - node.mix_id().to_string(), - node.bond_information.mix_node.identity_key.clone(), - node.bond_information.owner.clone().into_string(), - node.bond_information.mix_node.host.clone(), - pretty_decimal_with_denom(node.rewarding_details.operator, denom), - pretty_decimal_with_denom(node.rewarding_details.delegates, denom), - node.bond_information.mix_node.version, + node.node_id.to_string(), + node.ed25519_identity_key().to_base58_string(), + node.description.build_information.build_version, + (!node.contract_node_type.is_nym_node()).to_string(), ]); } diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs index 68004105d2..7bcea71c9d 100644 --- a/common/mixnode-common/src/verloc/mod.rs +++ b/common/mixnode-common/src/verloc/mod.rs @@ -14,7 +14,6 @@ use nym_task::TaskClient; use rand::seq::SliceRandom; use rand::thread_rng; use std::net::SocketAddr; -use std::net::ToSocketAddrs; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; @@ -313,7 +312,7 @@ impl VerlocMeasurer { info!("Starting verloc measurements"); // TODO: should we also measure gateways? - let all_mixes = match self.validator_client.get_cached_mixnodes().await { + let all_mixes = match self.validator_client.get_all_described_nodes().await { Ok(nodes) => nodes, Err(err) => { error!( @@ -332,22 +331,14 @@ impl VerlocMeasurer { // we only care about address and identity let tested_nodes = all_mixes .into_iter() + .filter(|n| n.description.declared_role.mixnode) .filter_map(|node| { - let mix_node = node.bond_information.mix_node; - // check if the node has sufficient version to be able to understand the packets - let node_version = parse_version(&mix_node.version).ok()?; - if node_version < self.config.minimum_compatible_node_version { - return None; - } - // try to parse the identity and host - let node_identity = - identity::PublicKey::from_base58_string(mix_node.identity_key).ok()?; + let node_identity = node.ed25519_identity_key(); - let verloc_host = (&*mix_node.host, mix_node.verloc_port) - .to_socket_addrs() - .ok()? - .next()?; + let ip = node.description.host_information.ip_address.first()?; + let verloc_port = node.description.verloc_port(); + let verloc_host = SocketAddr::new(*ip, verloc_port); // TODO: possible problem in the future, this does name resolution and theoretically // if a lot of nodes maliciously mis-configured themselves, it might take a while to resolve them all diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 31d5e78c93..3fe054bb5d 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -8,6 +8,9 @@ use nym_contracts_common::truncate_decimal; use nym_mixnet_contract_common::NodeId; use nym_validator_client::client::NymApiClientExt; +// use deprecated method as hopefully this whole API will be sunset soon-enough... +// and we're only getting info for legacy node so the relevant data should still exist +#[allow(deprecated)] pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, mix_id: NodeId, diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index efb03f34a6..fa43bfc9e4 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -17,6 +17,8 @@ pub(crate) struct ExplorerApiTasks { shutdown: TaskClient, } +// allow usage of deprecated methods here as we actually want to be explicitly querying for legacy data +#[allow(deprecated)] impl ExplorerApiTasks { pub(crate) fn new(state: ExplorerApiStateContext, shutdown: TaskClient) -> Self { ExplorerApiTasks { state, shutdown } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index e899919d95..86c07a84cb 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -622,7 +622,7 @@ impl Gateway { // TODO: if anything, this should be getting data directly from the contract // as opposed to the validator API let validator_client = self.random_api_client()?; - let existing_nodes = match validator_client.get_cached_gateways().await { + let existing_nodes = match validator_client.get_all_basic_nodes(None).await { Ok(nodes) => nodes, Err(err) => { error!("failed to grab initial network gateways - {err}\n Please try to startup again in few minutes"); @@ -630,9 +630,9 @@ impl Gateway { } }; - Ok(existing_nodes.iter().any(|node| { - node.gateway.identity_key == self.identity_keypair.public_key().to_base58_string() - })) + Ok(existing_nodes + .iter() + .any(|node| &node.ed25519_identity_pubkey == self.identity_keypair.public_key())) } pub async fn run(mut self) -> Result<(), GatewayError> diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 0043113658..e630007454 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -234,7 +234,7 @@ impl MixNode { // TODO: if anything, this should be getting data directly from the contract // as opposed to the validator API let validator_client = self.random_api_client(); - let existing_nodes = match validator_client.get_cached_mixnodes().await { + let existing_nodes = match validator_client.get_all_basic_nodes(None).await { Ok(nodes) => nodes, Err(err) => { error!( @@ -245,10 +245,9 @@ impl MixNode { } }; - existing_nodes.iter().any(|node| { - node.bond_information.mix_node.identity_key - == self.identity_keypair.public_key().to_base58_string() - }) + existing_nodes + .iter() + .any(|node| &node.ed25519_identity_pubkey == self.identity_keypair.public_key()) } async fn wait_for_interrupt(&self, shutdown: TaskHandle) { diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index a1bf8182e2..34c51dab91 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -897,6 +897,12 @@ pub enum DescribedNodeType { NymNode, } +impl DescribedNodeType { + pub fn is_nym_node(&self) -> bool { + matches!(self, DescribedNodeType::NymNode) + } +} + #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( diff --git a/nym-credential-proxy/Cargo.lock b/nym-credential-proxy/Cargo.lock index c8c6b85b57..01f25f0932 100644 --- a/nym-credential-proxy/Cargo.lock +++ b/nym-credential-proxy/Cargo.lock @@ -214,6 +214,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-client-ip" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +dependencies = [ + "axum", + "forwarded-header-value", + "serde", +] + [[package]] name = "axum-core" version = "0.4.5" @@ -1311,6 +1322,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "futures" version = "0.3.31" @@ -2147,6 +2168,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2315,7 +2342,7 @@ dependencies = [ "digest 0.9.0", "ff", "group", - "itertools 0.12.1", + "itertools 0.13.0", "nym-network-defaults", "nym-pemstore", "rand", @@ -2355,7 +2382,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait", @@ -2540,6 +2567,7 @@ name = "nym-http-api-common" version = "0.1.0" dependencies = [ "axum", + "axum-client-ip", "bytes", "colored", "mime", diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 28075562ec..e624f036d4 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -403,6 +403,8 @@ pub async fn update_gateway_config( )?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn get_mixnode_avg_uptime( state: tauri::State<'_, WalletState>, @@ -605,6 +607,8 @@ pub async fn get_nym_node_description( ) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn get_mixnode_uptime( mix_id: NodeId, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 31af28d199..6d960031fe 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -141,6 +141,8 @@ pub async fn undelegate_all_from_mixnode( Ok(res) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn get_all_mix_delegations( state: tauri::State<'_, WalletState>, diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 8e2ce5a327..a370c8ffd2 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -14,6 +14,8 @@ use nym_validator_client::models::{ MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_core_node_status( mix_id: NodeId, @@ -25,6 +27,8 @@ pub async fn mixnode_core_node_status( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn gateway_core_node_status( identity: IdentityKeyRef<'_>, @@ -36,6 +40,8 @@ pub async fn gateway_core_node_status( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn gateway_report( identity: IdentityKeyRef<'_>, @@ -44,6 +50,8 @@ pub async fn gateway_report( Ok(api_client!(state).get_gateway_report(identity).await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_status( mix_id: NodeId, @@ -52,6 +60,8 @@ pub async fn mixnode_status( Ok(api_client!(state).get_mixnode_status(mix_id).await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_reward_estimation( mix_id: NodeId, @@ -62,6 +72,8 @@ pub async fn mixnode_reward_estimation( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn compute_mixnode_reward_estimation( mix_id: u32, @@ -85,6 +97,8 @@ pub async fn compute_mixnode_reward_estimation( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_stake_saturation( mix_id: NodeId, @@ -95,6 +109,8 @@ pub async fn mixnode_stake_saturation( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_inclusion_probability( mix_id: NodeId, diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index f4afb4961e..c037f53719 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -7,7 +7,6 @@ use crate::manager::network::LoadedNetwork; use crate::manager::NetworkManager; use console::style; use nym_config::{must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR}; -use nym_validator_client::client::NymApiClientExt; use nym_validator_client::NymApiClient; use rand::{thread_rng, RngCore}; use std::fs; @@ -97,8 +96,8 @@ impl NetworkManager { let wait_fut = async { let inner_fut = async { loop { - let mut gateways = match api_client.nym_api.get_basic_gateways(None).await { - Ok(gateways) => gateways, + let mut nodes = match api_client.get_all_basic_nodes(None).await { + Ok(nodes) => nodes, Err(err) => { ctx.println(format!( "❌ {} {err}", @@ -110,8 +109,7 @@ impl NetworkManager { // if we explicitly specified some identity, find THIS node if let Some(identity) = ctx.gateway.as_ref() { - if let Some(node) = gateways - .nodes + if let Some(node) = nodes .iter() .find(|gw| &gw.ed25519_identity_pubkey.to_base58_string() == identity) { @@ -123,7 +121,7 @@ impl NetworkManager { } // otherwise look for ANY node - if let Some(node) = gateways.nodes.pop() { + if let Some(node) = nodes.pop() { return SocketAddr::new(node.ip_addresses[0], node.entry.unwrap().ws_port); } sleep(Duration::from_secs(10)).await; From 9f8bf2d08053a87c5109ab1e20b2eb000b8dc654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 31 Oct 2024 12:23:20 +0000 Subject: [PATCH 06/13] bugfix: wallet backend fixes (#5070) * fixed simulation arguments * make sure 'try_convert_pubkey_to_node_id' checks for native nymnodes --- nym-wallet/src-tauri/src/main.rs | 6 ++-- .../src/operations/simulate/mixnet.rs | 4 +-- nym-wallet/src-tauri/src/utils.rs | 30 +++++++++++++++---- .../components/Delegation/DelegateModal.tsx | 10 +++---- .../components/Delegation/UndelegateModal.tsx | 4 +-- nym-wallet/src/requests/simulate.ts | 12 ++++---- nym-wallet/src/requests/utils.ts | 4 +-- 7 files changed, 44 insertions(+), 26 deletions(-) diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index acaaa6d995..6824afbc15 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -115,7 +115,7 @@ fn main() { utils::owns_mixnode, utils::owns_nym_node, utils::get_env, - utils::try_convert_pubkey_to_mix_id, + utils::try_convert_pubkey_to_node_id, utils::default_mixnode_cost_params, nym_api::status::compute_mixnode_reward_estimation, nym_api::status::gateway_core_node_status, @@ -178,8 +178,8 @@ fn main() { simulate::mixnet::simulate_update_mixnode_config, simulate::mixnet::simulate_update_mixnode_cost_params, simulate::mixnet::simulate_update_gateway_config, - simulate::mixnet::simulate_delegate_to_mixnode, - simulate::mixnet::simulate_undelegate_from_mixnode, + simulate::mixnet::simulate_delegate_to_node, + simulate::mixnet::simulate_undelegate_from_node, simulate::vesting::simulate_vesting_delegate_to_mixnode, simulate::vesting::simulate_vesting_undelegate_from_mixnode, simulate::vesting::simulate_vesting_bond_gateway, diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index afcb0a512f..733f8cb5f5 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -184,7 +184,7 @@ pub async fn simulate_update_gateway_config( } #[tauri::command] -pub async fn simulate_delegate_to_mixnode( +pub async fn simulate_delegate_to_node( node_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, @@ -193,7 +193,7 @@ pub async fn simulate_delegate_to_mixnode( } #[tauri::command] -pub async fn simulate_undelegate_from_mixnode( +pub async fn simulate_undelegate_from_node( node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 9a90861059..1c18d0206a 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -55,16 +55,34 @@ pub async fn owns_nym_node(state: tauri::State<'_, WalletState>) -> Result, mix_identity: IdentityKey, ) -> Result, BackendError> { - let res = nyxd_client!(state) - .get_mixnode_details_by_identity(mix_identity) - .await?; - Ok(res + let guard = state.read().await; + let client = guard.current_client()?; + + // first try native nym-node + if let Some(node) = client + .nyxd + .get_nymnode_details_by_identity(mix_identity.clone()) + .await? + .details + { + return Ok(Some(node.node_id())); + } + + // fallback to legacy mixnode + if let Some(node) = client + .nyxd + .get_mixnode_details_by_identity(mix_identity.clone()) + .await? .mixnode_details - .map(|mixnode_details| mixnode_details.mix_id())) + { + return Ok(Some(node.mix_id())); + } + + Ok(None) } #[tauri::command] diff --git a/nym-wallet/src/components/Delegation/DelegateModal.tsx b/nym-wallet/src/components/Delegation/DelegateModal.tsx index 211b4ae2b9..88b91627c7 100644 --- a/nym-wallet/src/components/Delegation/DelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -5,7 +5,7 @@ import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField' import { CurrencyDenom, FeeDetails, DecCoin, decimalToFloatApproximation } from '@nymproject/types'; import { Console } from 'src/utils/console'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode, tryConvertIdentityToMixId } from 'src/requests'; +import { simulateDelegateToNode, simulateVestingDelegateToMixnode, tryConvertIdentityToNodeId } from 'src/requests'; import { debounce } from 'lodash'; import { AppContext } from 'src/context'; import { SimpleModal } from '../Modals/SimpleModal'; @@ -152,7 +152,7 @@ export const DelegateModal: FCWithChildren<{ } if (tokenPool === 'balance') { - getFee(simulateDelegateToMixnode, { mixId: id, amount: value }); + getFee(simulateDelegateToNode, { nodeId: id, amount: value }); } if (tokenPool === 'locked') { @@ -187,16 +187,16 @@ export const DelegateModal: FCWithChildren<{ } let res; try { - res = await tryConvertIdentityToMixId(idKey); + res = await tryConvertIdentityToNodeId(idKey); } catch (e) { - Console.warn(`failed to resolve mix_id for "${idKey}": ${e}`); + Console.warn(`failed to resolve node_id for "${idKey}": ${e}`); return; } if (res) { setMixId(res); setMixIdError(undefined); } else { - setMixIdError('Mixnode with this identity does not seem to be currently bonded'); + setMixIdError('Node with this identity does not seem to be currently bonded'); } }, 500), [], diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx index 65820eb2bb..1038dbe341 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect } from 'react'; import { Box, SxProps } from '@mui/material'; import { FeeDetails } from '@nymproject/types'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateUndelegateFromMixnode, simulateVestingUndelegateFromMixnode } from 'src/requests'; +import { simulateUndelegateFromNode, simulateVestingUndelegateFromMixnode } from 'src/requests'; import { AppContext } from 'src/context'; import { ModalFee } from '../Modals/ModalFee'; import { ModalListItem } from '../Modals/ModalListItem'; @@ -27,7 +27,7 @@ export const UndelegateModal: FCWithChildren<{ useEffect(() => { if (usesVestingContractTokens) getFee(simulateVestingUndelegateFromMixnode, { mixId }); else { - getFee(simulateUndelegateFromMixnode, mixId); + getFee(simulateUndelegateFromNode, mixId); } }, []); diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index 8f7373bb07..58e704d979 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -28,14 +28,14 @@ export const simulateUpdateMixnodeConfig = async (update: MixNodeConfigUpdate) = export const simulateUpdateGatewayConfig = async (update: GatewayConfigUpdate) => invokeWrapper('simulate_update_gateway_config', { update }); -export const simulateDelegateToMixnode = async (args: { mixId: number; amount: DecCoin }) => - invokeWrapper('simulate_delegate_to_mixnode', args); +export const simulateDelegateToNode = async (args: { nodeId: number; amount: DecCoin }) => + invokeWrapper('simulate_delegate_to_node', args); -export const simulateUndelegateFromMixnode = async (mixId: number) => - invokeWrapper('simulate_undelegate_from_mixnode', { mixId }); +export const simulateUndelegateFromNode = async (nodeId: number) => + invokeWrapper('simulate_undelegate_from_node', { nodeId }); -export const simulateClaimDelegatorReward = async (mixId: number) => - invokeWrapper('simulate_claim_delegator_reward', { mixId }); +export const simulateClaimDelegatorReward = async (nodeId: number) => + invokeWrapper('simulate_claim_delegator_reward', { nodeId }); export const simulateVestingClaimDelegatorReward = async (mixId: number) => invokeWrapper('simulate_vesting_claim_delegator_reward', { mixId }); diff --git a/nym-wallet/src/requests/utils.ts b/nym-wallet/src/requests/utils.ts index f16c2d71b1..82ee989aa6 100644 --- a/nym-wallet/src/requests/utils.ts +++ b/nym-wallet/src/requests/utils.ts @@ -4,8 +4,8 @@ import { invokeWrapper } from './wrapper'; export const getEnv = async () => invokeWrapper('get_env'); -export const tryConvertIdentityToMixId = async (mixIdentity: string) => - invokeWrapper('try_convert_pubkey_to_mix_id', { mixIdentity }); +export const tryConvertIdentityToNodeId = async (mixIdentity: string) => + invokeWrapper('try_convert_pubkey_to_node_id', { mixIdentity }); export const getDefaultNodeCostParams = async (profitMarginPercent: string) => invokeWrapper('default_mixnode_cost_params', { profitMarginPercent }); From cf4fe5f875d0e235117d52737504b37405391668 Mon Sep 17 00:00:00 2001 From: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:52:20 +0100 Subject: [PATCH 07/13] NS API with directory v2 (#5068) * Use unstable explorer client * Clean up stale testruns & logging - log gw identity key - better agent testrun logging - log responses - change response code for agents * Better logging on agent * Testrun stores gw identity key instead of gw pk * Agent 0.1.3 * Agent 0.1.4 * Sqlx offline query data + clippy * Compatible with directory v2 * Point to internal deps + rebase + v0.1.5 * self described field not null * Fix build.rs typo --- common/models/src/ns_api.rs | 7 + nym-node-status-agent/Cargo.toml | 27 + nym-node-status-agent/run.sh | 55 ++ nym-node-status-agent/src/cli.rs | 105 ++++ nym-node-status-agent/src/probe.rs | 60 ++ ...55c69cd5c1a7e7d87073c94600c783a0a3984.json | 20 + ...7dc549cf503409fd6dfedcdc02dbcd61d5454.json | 32 ++ ...f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json | 12 + ...5ceb89b9925cba46efcf4ed79ef0759a01129.json | 26 + ...965acf3bcfb3f84ba8d24ed645d79976cf784.json | 12 + ...e692afc04b51e8782bcbf59f1eb4116112536.json | 12 + ...8632c29c81bfafb043bb8744129cf9e0266ad.json | 38 ++ ...203c8569f03fdd39ce09d7b74177896e52a8c.json | 12 + ...289dd776749dd7a20cda61b0480f2fba60889.json | 50 ++ ...570ee787fc7daf00fcb916f18d1cb7d6c8f08.json | 12 + ...17b40dc01845f554b5479f37855d89b309e6f.json | 32 ++ ...a667e8cacdf677ccb906415b3fe92be0c436b.json | 20 + ...56f816956b0824c66a25da611dce688105d36.json | 50 ++ ...df795147ac3724e89df5b2f24f7215d87dce1.json | 12 + ...f3de5949a5ff2083453cb879af8a1689efe2f.json | 12 + ...29d1320a5eb10797cdb5a435455d22c6aeac1.json | 98 ++++ ...03a2752e28df775d1af8fd943a5fc8d7dc00d.json | 38 ++ ...6c5e41f3c0bb56a27648b5e25466b8634a578.json | 12 + ...d17ca6eb090df7f6d52ef89c5d51564f8b45c.json | 12 + ...86b86d1b9be62336c9eac0eef44987a5451b5.json | 20 + ...90f6c66c14d4267a2cc2ca73354becc2c8bb8.json | 26 + ...c586c4c29c03efb4cf0c40b73a5c76159cf5c.json | 12 + ...ea1f6dc2aed00cedde25f2be3567e47064351.json | 44 ++ ...dadfcf19095935867a51cbd5b8362d1505fcc.json | 32 ++ ...ee2aa37f6163525bbdce1a4cebef4a285f8d9.json | 12 + ...74a5bf4912ee73468e62e7d0d96b1d9074cbe.json | 12 + ...3d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json | 86 +++ ...b925e033ceec4f8112258feb4ac9e96fc5924.json | 12 + ...b5d2f2d08b2e53203aab9f07ea9b52acbd407.json | 26 + nym-node-status-api/Cargo.toml | 62 +++ nym-node-status-api/build.rs | 45 ++ nym-node-status-api/launch_node_status_api.sh | 40 ++ nym-node-status-api/migrations/000_init.sql | 112 ++++ nym-node-status-api/src/db/models.rs | 335 ++++++++++++ .../src/db/queries/gateways.rs | 180 ++++++ nym-node-status-api/src/db/queries/mod.rs | 15 + .../src/db/queries/testruns.rs | 184 +++++++ nym-node-status-api/src/http/api/mod.rs | 94 ++++ nym-node-status-api/src/http/api/testruns.rs | 125 +++++ nym-node-status-api/src/http/error.rs | 48 ++ nym-node-status-api/src/http/models.rs | 76 +++ nym-node-status-api/src/monitor/mod.rs | 511 ++++++++++++++++++ nym-node-status-api/src/testruns/mod.rs | 86 +++ nym-node-status-api/src/testruns/queue.rs | 118 ++++ 49 files changed, 3079 insertions(+) create mode 100644 common/models/src/ns_api.rs create mode 100644 nym-node-status-agent/Cargo.toml create mode 100755 nym-node-status-agent/run.sh create mode 100644 nym-node-status-agent/src/cli.rs create mode 100644 nym-node-status-agent/src/probe.rs create mode 100644 nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json create mode 100644 nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json create mode 100644 nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json create mode 100644 nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json create mode 100644 nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json create mode 100644 nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json create mode 100644 nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json create mode 100644 nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json create mode 100644 nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json create mode 100644 nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json create mode 100644 nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json create mode 100644 nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json create mode 100644 nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json create mode 100644 nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json create mode 100644 nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json create mode 100644 nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json create mode 100644 nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json create mode 100644 nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json create mode 100644 nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json create mode 100644 nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json create mode 100644 nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json create mode 100644 nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json create mode 100644 nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json create mode 100644 nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json create mode 100644 nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json create mode 100644 nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json create mode 100644 nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json create mode 100644 nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json create mode 100644 nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json create mode 100644 nym-node-status-api/Cargo.toml create mode 100644 nym-node-status-api/build.rs create mode 100755 nym-node-status-api/launch_node_status_api.sh create mode 100644 nym-node-status-api/migrations/000_init.sql create mode 100644 nym-node-status-api/src/db/models.rs create mode 100644 nym-node-status-api/src/db/queries/gateways.rs create mode 100644 nym-node-status-api/src/db/queries/mod.rs create mode 100644 nym-node-status-api/src/db/queries/testruns.rs create mode 100644 nym-node-status-api/src/http/api/mod.rs create mode 100644 nym-node-status-api/src/http/api/testruns.rs create mode 100644 nym-node-status-api/src/http/error.rs create mode 100644 nym-node-status-api/src/http/models.rs create mode 100644 nym-node-status-api/src/monitor/mod.rs create mode 100644 nym-node-status-api/src/testruns/mod.rs create mode 100644 nym-node-status-api/src/testruns/queue.rs diff --git a/common/models/src/ns_api.rs b/common/models/src/ns_api.rs new file mode 100644 index 0000000000..5d875420e2 --- /dev/null +++ b/common/models/src/ns_api.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestrunAssignment { + pub testrun_id: i64, + pub gateway_identity_key: String, +} diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml new file mode 100644 index 0000000000..f89065b5b3 --- /dev/null +++ b/nym-node-status-agent/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + + +[package] +name = "nym-node-status-agent" +version = "0.1.4" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true} +clap = { workspace = true, features = ["derive", "env"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +reqwest = { workspace = true, features = ["json"] } +serde_json = { workspace = true } diff --git a/nym-node-status-agent/run.sh b/nym-node-status-agent/run.sh new file mode 100755 index 0000000000..675e39c109 --- /dev/null +++ b/nym-node-status-agent/run.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -eu + +environment="qa" + +source ../envs/${environment}.env + +export RUST_LOG="debug" + +crate_root=$(dirname $(realpath "$0")) +gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core +echo "gateway_probe_src=$gateway_probe_src" +echo "crate_root=$crate_root" + +export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" + +# build & copy over GW probe +function copy_gw_probe() { + pushd $gateway_probe_src + git switch main + git pull + cargo build --release --package nym-gateway-probe + cp target/release/nym-gateway-probe "$crate_root" + $crate_root/nym-gateway-probe --version + popd +} + +function build_agent() { + cargo build --package nym-node-status-agent --release +} + +function swarm() { + local workers=$1 + echo "Running $workers in parallel" + + build_agent + + for ((i = 1; i <= $workers; i++)); do + ../target/release/nym-node-status-agent run-probe & + done + + wait + + echo "All agents completed" +} + +export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +export NODE_STATUS_AGENT_SERVER_PORT="8000" + +copy_gw_probe + +swarm 8 + +# cargo run -- run-probe diff --git a/nym-node-status-agent/src/cli.rs b/nym-node-status-agent/src/cli.rs new file mode 100644 index 0000000000..81e70e2da9 --- /dev/null +++ b/nym-node-status-agent/src/cli.rs @@ -0,0 +1,105 @@ +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_common_models::ns_api::TestrunAssignment; +use std::sync::OnceLock; +use tracing::instrument; + +use crate::probe::GwProbe; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Args { + #[command(subcommand)] + pub(crate) command: Command, + #[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")] + pub(crate) server_address: String, + + #[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")] + pub(crate) server_port: u16, + // TODO dz accept keypair for identification / auth +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Command { + RunProbe { + /// path of binary to run + #[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")] + probe_path: String, + }, +} + +impl Args { + pub(crate) async fn execute(&self) -> anyhow::Result<()> { + match &self.command { + Command::RunProbe { probe_path } => self.run_probe(probe_path).await?, + } + + Ok(()) + } + + async fn run_probe(&self, probe_path: &str) -> anyhow::Result<()> { + let server_address = format!("{}:{}", &self.server_address, self.server_port); + + let probe = GwProbe::new(probe_path.to_string()); + + let version = probe.version().await; + tracing::info!("Probe version:\n{}", version); + + let testrun = request_testrun(&server_address).await?; + + let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key)); + + submit_results(&server_address, testrun.testrun_id, log).await?; + + Ok(()) + } +} + +const URL_BASE: &str = "internal/testruns"; + +#[instrument(level = "debug", skip_all)] +async fn request_testrun(server_addr: &str) -> anyhow::Result { + let target_url = format!("{}/{}", server_addr, URL_BASE); + let client = reqwest::Client::new(); + let res = client + .get(target_url) + .send() + .await + .and_then(|response| response.error_for_status())?; + res.json() + .await + .map(|testrun| { + tracing::info!("Received testrun assignment: {:?}", testrun); + testrun + }) + .map_err(|err| { + tracing::error!("err"); + err.into() + }) +} + +#[instrument(level = "debug", skip(probe_outcome))] +async fn submit_results( + server_addr: &str, + testrun_id: i64, + probe_outcome: String, +) -> anyhow::Result<()> { + let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id); + let client = reqwest::Client::new(); + + let res = client + .post(target_url) + .body(probe_outcome) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted results: {})", res.status()); + Ok(()) +} diff --git a/nym-node-status-agent/src/probe.rs b/nym-node-status-agent/src/probe.rs new file mode 100644 index 0000000000..f779f3af53 --- /dev/null +++ b/nym-node-status-agent/src/probe.rs @@ -0,0 +1,60 @@ +use tracing::error; + +pub(crate) struct GwProbe { + path: String, +} + +impl GwProbe { + pub(crate) fn new(probe_path: String) -> Self { + Self { path: probe_path } + } + + pub(crate) async fn version(&self) -> String { + let mut command = tokio::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + command.arg("--version"); + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output().await { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get probe version".to_string() + } + Err(e) => { + error!("Failed to get probe version: {}", e); + "Failed to get probe version".to_string() + } + } + } + + pub(crate) fn run_and_get_log(&self, gateway_key: &Option) -> String { + let mut command = std::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + + if let Some(gateway_id) = gateway_key { + command.arg("--gateway").arg(gateway_id); + } + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output() { + if !output.status.success() { + let out = String::from_utf8_lossy(&output.stdout); + let err = String::from_utf8_lossy(&output.stderr); + tracing::error!("Probe exited with {:?}:\n{}\n{}", output.status, out, err); + } + + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get log from test run".to_string() + } + Err(e) => { + error!("Failed to spawn test: {}", e); + "Failed to spawn test run task".to_string() + } + } + } +} diff --git a/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json b/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json new file mode 100644 index 0000000000..ad57224826 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n gateway_identity_key\n FROM\n gateways\n WHERE\n id = ?", + "describe": { + "columns": [ + { + "name": "gateway_identity_key", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984" +} diff --git a/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json b/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json new file mode 100644 index 0000000000..8b69daa6a3 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n key as \"key!\",\n value_json as \"value_json!\",\n last_updated_utc as \"last_updated_utc!\"\n FROM summary", + "describe": { + "columns": [ + { + "name": "key!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "value_json!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "last_updated_utc!", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + true, + false + ] + }, + "hash": "1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454" +} diff --git a/nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json b/nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json new file mode 100644 index 0000000000..9bc2966149 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE mixnodes\n SET bonded = ?, last_updated_utc = ?\n WHERE id = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea" +} diff --git a/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json b/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json new file mode 100644 index 0000000000..d9bf0dd6aa --- /dev/null +++ b/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n gateway_identity_key\n FROM gateways\n WHERE id = ?\n LIMIT 1", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_identity_key", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129" +} diff --git a/nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json b/nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json new file mode 100644 index 0000000000..56898ca5ad --- /dev/null +++ b/nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784" +} diff --git a/nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json b/nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json new file mode 100644 index 0000000000..10158436b6 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways\n SET bonded = ?, last_updated_utc = ?\n WHERE id = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536" +} diff --git a/nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json b/nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json new file mode 100644 index 0000000000..2a834fe87e --- /dev/null +++ b/nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_identity_key as \"gateway_identity_key!\",\n self_described as \"self_described?\",\n explorer_pretty_bond as \"explorer_pretty_bond?\"\n FROM gateways\n WHERE gateway_identity_key = ?\n ORDER BY gateway_identity_key\n LIMIT 1", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_identity_key!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "self_described?", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "explorer_pretty_bond?", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + true, + true + ] + }, + "hash": "3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad" +} diff --git a/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json b/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json new file mode 100644 index 0000000000..f9eb3657e7 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE testruns SET status = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c" +} diff --git a/nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json b/nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json new file mode 100644 index 0000000000..2cb8760009 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n timestamp_utc as \"timestamp_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\"\n FROM testruns\n WHERE gateway_id = ? AND status != 2\n ORDER BY id DESC\n LIMIT 1", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_id!", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "status!", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "timestamp_utc!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "ip_address!", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "log!", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889" +} diff --git a/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json b/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json new file mode 100644 index 0000000000..6a9fd9d4eb --- /dev/null +++ b/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08" +} diff --git a/nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json b/nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json new file mode 100644 index 0000000000..8b9c9699f8 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_identity_key as \"identity_key!\",\n bonded as \"bonded: bool\"\n FROM gateways\n WHERE bonded = ?", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "identity_key!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f" +} diff --git a/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json b/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json new file mode 100644 index 0000000000..38fe641a3a --- /dev/null +++ b/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT count(id) FROM mixnodes", + "describe": { + "columns": [ + { + "name": "count(id)", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b" +} diff --git a/nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json b/nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json new file mode 100644 index 0000000000..e12434a85a --- /dev/null +++ b/nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n timestamp_utc as \"timestamp_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\"\n FROM testruns\n WHERE\n id = ?\n AND\n status = ?\n ORDER BY timestamp_utc", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_id!", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "status!", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "timestamp_utc!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "ip_address!", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "log!", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36" +} diff --git a/nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json b/nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json new file mode 100644 index 0000000000..86b61631a6 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO mixnodes\n (mix_id, identity_key, bonded, total_stake,\n host, http_api_port, blacklisted, full_details,\n self_described, last_updated_utc, is_dp_delegatee)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(mix_id) DO UPDATE SET\n bonded=excluded.bonded,\n total_stake=excluded.total_stake, host=excluded.host,\n http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted,\n full_details=excluded.full_details,self_described=excluded.self_described,\n last_updated_utc=excluded.last_updated_utc,\n is_dp_delegatee = excluded.is_dp_delegatee;", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1" +} diff --git a/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json b/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json new file mode 100644 index 0000000000..9d93b219f9 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f" +} diff --git a/nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json b/nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json new file mode 100644 index 0000000000..f061747404 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json @@ -0,0 +1,98 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.blacklisted as \"blacklisted: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n COALESCE(gd.moniker, \"NA\") as \"moniker!\",\n COALESCE(gd.website, \"NA\") as \"website!\",\n COALESCE(gd.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(gd.details, \"NA\") as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key", + "describe": { + "columns": [ + { + "name": "gateway_identity_key!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "blacklisted: bool", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "performance!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "self_described?", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "explorer_pretty_bond?", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "last_probe_result?", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "last_probe_log?", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "last_testrun_utc?", + "ordinal": 8, + "type_info": "Int64" + }, + { + "name": "last_updated_utc!", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "moniker!", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "website!", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "security_contact!", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "details!", + "ordinal": 13, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1" +} diff --git a/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json b/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json new file mode 100644 index 0000000000..9cba203bdb --- /dev/null +++ b/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n date as \"date!\",\n timestamp_utc as \"timestamp_utc!\",\n value_json as \"value_json!\"\n FROM summary_history\n ORDER BY date DESC\n LIMIT 30", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "date!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "timestamp_utc!", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "value_json!", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + false, + false, + true + ] + }, + "hash": "7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d" +} diff --git a/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json b/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json new file mode 100644 index 0000000000..5252ccf2e7 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO summary_history\n (date, timestamp_utc, value_json)\n VALUES (?, ?, ?)\n ON CONFLICT(date) DO UPDATE SET\n timestamp_utc=excluded.timestamp_utc,\n value_json=excluded.value_json;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578" +} diff --git a/nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json b/nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json new file mode 100644 index 0000000000..3f171170db --- /dev/null +++ b/nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways\n SET blacklisted = true\n WHERE gateway_identity_key = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c" +} diff --git a/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json b/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json new file mode 100644 index 0000000000..60583ed8b2 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT count(id) FROM gateways", + "describe": { + "columns": [ + { + "name": "count(id)", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5" +} diff --git a/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json b/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json new file mode 100644 index 0000000000..6c2a194e80 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n gateway_identity_key as \"gateway_identity_key!\",\n bonded as \"bonded: bool\"\n FROM gateways\n ORDER BY last_testrun_utc", + "describe": { + "columns": [ + { + "name": "gateway_identity_key!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8" +} diff --git a/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json b/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json new file mode 100644 index 0000000000..8cc95e72de --- /dev/null +++ b/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c" +} diff --git a/nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json b/nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json new file mode 100644 index 0000000000..2e4f34fd85 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n date_utc as \"date_utc!\",\n packets_received as \"total_packets_received!: i64\",\n packets_sent as \"total_packets_sent!: i64\",\n packets_dropped as \"total_packets_dropped!: i64\",\n total_stake as \"total_stake!: i64\"\n FROM (\n SELECT\n date_utc,\n SUM(packets_received) as packets_received,\n SUM(packets_sent) as packets_sent,\n SUM(packets_dropped) as packets_dropped,\n SUM(total_stake) as total_stake\n FROM mixnode_daily_stats\n GROUP BY date_utc\n ORDER BY date_utc DESC\n LIMIT 30\n )\n GROUP BY date_utc\n ORDER BY date_utc\n ", + "describe": { + "columns": [ + { + "name": "date_utc!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "total_packets_received!: i64", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "total_packets_sent!: i64", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "total_packets_dropped!: i64", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "total_stake!: i64", + "ordinal": 4, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true, + true, + true, + false + ] + }, + "hash": "c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351" +} diff --git a/nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json b/nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json new file mode 100644 index 0000000000..4d0edd1e88 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n identity_key as \"identity_key!\",\n bonded as \"bonded: bool\"\n FROM mixnodes\n WHERE bonded = ?", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "identity_key!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc" +} diff --git a/nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json b/nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json new file mode 100644 index 0000000000..94e5513279 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO gateways\n (gateway_identity_key, bonded, blacklisted,\n self_described, explorer_pretty_bond,\n last_updated_utc, performance)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(gateway_identity_key) DO UPDATE SET\n bonded=excluded.bonded,\n blacklisted=excluded.blacklisted,\n self_described=excluded.self_described,\n explorer_pretty_bond=excluded.explorer_pretty_bond,\n last_updated_utc=excluded.last_updated_utc,\n performance = excluded.performance;", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9" +} diff --git a/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json b/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json new file mode 100644 index 0000000000..5f7443bd50 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO summary\n (key, value_json, last_updated_utc)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value_json=excluded.value_json,\n last_updated_utc=excluded.last_updated_utc;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe" +} diff --git a/nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json b/nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json new file mode 100644 index 0000000000..d3f7611ba2 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json @@ -0,0 +1,86 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n mn.mix_id as \"mix_id!\",\n mn.bonded as \"bonded: bool\",\n mn.blacklisted as \"blacklisted: bool\",\n mn.is_dp_delegatee as \"is_dp_delegatee: bool\",\n mn.total_stake as \"total_stake!\",\n mn.full_details as \"full_details!\",\n mn.self_described as \"self_described\",\n mn.last_updated_utc as \"last_updated_utc!\",\n COALESCE(md.moniker, \"NA\") as \"moniker!\",\n COALESCE(md.website, \"NA\") as \"website!\",\n COALESCE(md.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(md.details, \"NA\") as \"details!\"\n FROM mixnodes mn\n LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id\n ORDER BY mn.mix_id", + "describe": { + "columns": [ + { + "name": "mix_id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "bonded: bool", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "blacklisted: bool", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "is_dp_delegatee: bool", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "total_stake!", + "ordinal": 4, + "type_info": "Int64" + }, + { + "name": "full_details!", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "self_described", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "last_updated_utc!", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "moniker!", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "website!", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "security_contact!", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "details!", + "ordinal": 11, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35" +} diff --git a/nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json b/nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json new file mode 100644 index 0000000000..06d098bebc --- /dev/null +++ b/nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE\n testruns\n SET\n status = ?\n WHERE\n status = ?\n AND\n timestamp_utc < ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924" +} diff --git a/nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json b/nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json new file mode 100644 index 0000000000..24e735e96d --- /dev/null +++ b/nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "UPDATE testruns\n SET status = ?\n WHERE rowid =\n (\n SELECT rowid\n FROM testruns\n WHERE status = ?\n ORDER BY timestamp_utc asc\n LIMIT 1\n )\n RETURNING\n id as \"id!\",\n gateway_id\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_id", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + false + ] + }, + "hash": "ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407" +} diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml new file mode 100644 index 0000000000..0969d31045 --- /dev/null +++ b/nym-node-status-api/Cargo.toml @@ -0,0 +1,62 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-node-status-api" +version = "0.1.5" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +chrono = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } +cosmwasm-std = { workspace = true } +envy = { workspace = true } +futures-util = { workspace = true } +moka = { workspace = true, features = ["future"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +nym-explorer-client = { path = "../explorer-api/explorer-client" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-task = { path = "../common/task" } +nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde_json_path = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-log = { workspace = true } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +# TODO dz `cargo update async-trait` +# for automatic schema detection, which was merged, but not released yet +# https://github.com/ProbablyClem/utoipauto/pull/38 +# utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" } +utoipauto = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs new file mode 100644 index 0000000000..025e755088 --- /dev/null +++ b/nym-node-status-api/build.rs @@ -0,0 +1,45 @@ +use anyhow::{anyhow, Result}; +use sqlx::{Connection, SqliteConnection}; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; +use tokio::{fs::File, io::AsyncWriteExt}; + +const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; + +/// If you need to re-run migrations or reset the db, just run +/// cargo clean -p nym-node-status-api +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let out_dir = read_env_var("OUT_DIR")?; + let database_path = format!("{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); + + write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; + let mut conn = SqliteConnection::connect(&database_path).await?; + sqlx::migrate!("./migrations").run(&mut conn).await?; + + #[cfg(target_family = "unix")] + println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) +} + +fn read_env_var(var: &str) -> Result { + std::env::var(var).map_err(|_| anyhow!("You need to set {} env var", var)) +} + +/// use `./enter_db.sh` to inspect DB +async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { + let mut file = File::create("enter_db.sh").await?; + let _ = file.write(b"#!/bin/bash\n").await?; + file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes()) + .await?; + + file.set_permissions(Permissions::from_mode(0o755)) + .await + .map_err(From::from) +} diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh new file mode 100755 index 0000000000..5d92675412 --- /dev/null +++ b/nym-node-status-api/launch_node_status_api.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -e + +export RUST_LOG=${RUST_LOG:-debug} + +export NYM_API_CLIENT_TIMEOUT=60 +export EXPLORER_CLIENT_TIMEOUT=60 +export NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=60 + +export ENVIRONMENT="qa.env" + +function run_bare() { + # export necessary env vars + set -a + source ../envs/$ENVIRONMENT + set +a + export RUST_LOG=debug + + # --conection-url is provided in build.rs + cargo run --package nym-node-status-api +} + +function run_docker() { + cargo build --package nym-node-status-api --release + cp ../target/release/nym-node-status-api . + + cd .. + docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev . + docker run --env-file envs/${ENVIRONMENT} \ + -e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \ + -e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \ + -e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \ + -e RUST_LOG=${RUST_LOG} node-status-api + +} + +run_bare + +# run_docker diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql new file mode 100644 index 0000000000..4f9fd7da60 --- /dev/null +++ b/nym-node-status-api/migrations/000_init.sql @@ -0,0 +1,112 @@ +CREATE TABLE gateways +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR NOT NULL UNIQUE, + self_described VARCHAR NOT NULL, + explorer_pretty_bond VARCHAR, + last_probe_result VARCHAR, + last_probe_log VARCHAR, + config_score INTEGER NOT NULL DEFAULT (0), + config_score_successes REAL NOT NULL DEFAULT (0), + config_score_samples REAL NOT NULL DEFAULT (0), + routing_score INTEGER NOT NULL DEFAULT (0), + routing_score_successes REAL NOT NULL DEFAULT (0), + routing_score_samples REAL NOT NULL DEFAULT (0), + test_run_samples REAL NOT NULL DEFAULT (0), + last_testrun_utc INTEGER, + last_updated_utc INTEGER NOT NULL, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + blacklisted INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + performance INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_gateway_description_gateway_identity_key ON gateways (gateway_identity_key); + + +CREATE TABLE mixnodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identity_key VARCHAR NOT NULL UNIQUE, + mix_id INTEGER NOT NULL UNIQUE, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + total_stake INTEGER NOT NULL, + host VARCHAR NOT NULL, + http_api_port INTEGER NOT NULL, + blacklisted INTEGER CHECK (blacklisted in (0, 1)) NOT NULL DEFAULT 0, + full_details VARCHAR, + self_described VARCHAR, + last_updated_utc INTEGER NOT NULL + , is_dp_delegatee INTEGER CHECK (is_dp_delegatee IN (0, 1)) NOT NULL DEFAULT 0); +CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id); +CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key); + +CREATE TABLE + mixnode_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id) + ); + +-- Indexes for description table +CREATE INDEX idx_mixnode_description_mix_id ON mixnode_description (mix_id); + + +CREATE TABLE summary +( + key VARCHAR PRIMARY KEY, + value_json VARCHAR, + last_updated_utc INTEGER NOT NULL +); + + +CREATE TABLE summary_history +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date VARCHAR UNIQUE NOT NULL, + timestamp_utc INTEGER NOT NULL, + value_json VARCHAR +); +CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc); +CREATE INDEX idx_summary_history_date ON summary_history (date); + + +CREATE TABLE gateway_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (gateway_identity_key) REFERENCES gateways (gateway_identity_key) + ); + + +CREATE TABLE + mixnode_daily_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER NOT NULL, + total_stake BIGINT NOT NULL, + date_utc VARCHAR NOT NULL, + packets_received INTEGER DEFAULT 0, + packets_sent INTEGER DEFAULT 0, + packets_dropped INTEGER DEFAULT 0, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), + UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index + ); + + +CREATE TABLE testruns +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_id INTEGER NOT NULL, + status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete + timestamp_utc INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + log VARCHAR NOT NULL, + FOREIGN KEY (gateway_id) REFERENCES gateways (id) +); diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs new file mode 100644 index 0000000000..a5511787f9 --- /dev/null +++ b/nym-node-status-api/src/db/models.rs @@ -0,0 +1,335 @@ +use crate::{ + http::{self, models::SummaryHistory}, + monitor::NumericalCheckedCast, +}; +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, FromRepr}; +use utoipa::ToSchema; + +pub(crate) struct GatewayRecord { + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) self_described: String, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_updated_utc: i64, + pub(crate) performance: u8, +} + +#[derive(Debug, Clone)] +pub(crate) struct GatewayDto { + pub(crate) gateway_identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) performance: i64, + pub(crate) self_described: Option, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_probe_result: Option, + pub(crate) last_probe_log: Option, + pub(crate) last_testrun_utc: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) security_contact: String, + pub(crate) details: String, + pub(crate) website: String, +} + +impl TryFrom for http::models::Gateway { + type Error = anyhow::Error; + + fn try_from(value: GatewayDto) -> Result { + // Instead of using routing_score_successes / routing_score_samples, we use the + // number of successful testruns in the last 24h. + let routing_score = 0f32; + let config_score = 0u32; + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let last_testrun_utc = value + .last_testrun_utc + .and_then(|i| i.cast_checked().ok()) + .map(|t| timestamp_as_utc(t).to_rfc3339()); + + let self_described = value.self_described.clone().unwrap_or("null".to_string()); + let explorer_pretty_bond = value + .explorer_pretty_bond + .clone() + .unwrap_or("null".to_string()); + let last_probe_result = value + .last_probe_result + .clone() + .unwrap_or("null".to_string()); + let last_probe_log = value.last_probe_log.clone(); + + let self_described = serde_json::from_str(&self_described).unwrap_or(None); + let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None); + let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None); + + let bonded = value.bonded; + let blacklisted = value.blacklisted; + let performance = value.performance as u8; + + let description = NodeDescription { + moniker: value.moniker.clone(), + website: value.website.clone(), + security_contact: value.security_contact.clone(), + details: value.details.clone(), + }; + + Ok(http::models::Gateway { + gateway_identity_key: value.gateway_identity_key.clone(), + bonded, + blacklisted, + performance, + self_described, + explorer_pretty_bond, + description, + last_probe_result, + last_probe_log, + routing_score, + config_score, + last_testrun_utc, + last_updated_utc, + }) + } +} + +fn timestamp_as_utc(unix_timestamp: u64) -> chrono::DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} + +pub(crate) struct MixnodeRecord { + pub(crate) mix_id: u32, + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) total_stake: i64, + pub(crate) host: String, + pub(crate) http_port: u16, + pub(crate) blacklisted: bool, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) is_dp_delegatee: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct MixnodeDto { + pub(crate) mix_id: i64, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) is_dp_delegatee: bool, + pub(crate) total_stake: i64, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) website: String, + pub(crate) security_contact: String, + pub(crate) details: String, +} + +impl TryFrom for http::models::Mixnode { + type Error = anyhow::Error; + + fn try_from(value: MixnodeDto) -> Result { + let mix_id = value.mix_id.cast_checked()?; + let full_details = value.full_details.clone(); + let full_details = serde_json::from_str(&full_details).unwrap_or(None); + + let self_described = value + .self_described + .clone() + .map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null)); + + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let blacklisted = value.blacklisted; + let is_dp_delegatee = value.is_dp_delegatee; + let moniker = value.moniker.clone(); + let website = value.website.clone(); + let security_contact = value.security_contact.clone(); + let details = value.details.clone(); + + Ok(http::models::Mixnode { + mix_id, + bonded: value.bonded, + blacklisted, + is_dp_delegatee, + total_stake: value.total_stake, + full_details, + description: NodeDescription { + moniker, + website, + security_contact, + details, + }, + self_described, + last_updated_utc, + }) + } +} + +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) struct BondedStatusDto { + pub(crate) id: i64, + pub(crate) identity_key: String, + pub(crate) bonded: bool, +} + +#[allow(unused)] +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryDto { + pub(crate) key: String, + pub(crate) value_json: String, + pub(crate) last_updated_utc: i64, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryHistoryDto { + #[allow(dead_code)] + pub id: i64, + pub date: String, + pub value_json: String, + pub timestamp_utc: i64, +} + +impl TryFrom for SummaryHistory { + type Error = anyhow::Error; + + fn try_from(value: SummaryHistoryDto) -> Result { + let value_json = serde_json::from_str(&value.value_json).unwrap_or_default(); + Ok(SummaryHistory { + value_json, + date: value.date.clone(), + timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(), + }) + } +} + +pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; +pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; +pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; +pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; +pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + +pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; +pub(crate) const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; +pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + +pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; +pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + +// `utoipa`` goes crazy if you use module-qualified prefix as field type so we +// have to import it +use gateway::GatewaySummary; +use mixnode::MixnodeSummary; + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct NetworkSummary { + pub(crate) mixnodes: MixnodeSummary, + pub(crate) gateways: GatewaySummary, +} + +pub(crate) mod mixnode { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummary { + pub(crate) bonded: MixnodeSummaryBonded, + pub(crate) blacklisted: MixnodeSummaryBlacklisted, + pub(crate) historical: MixnodeSummaryHistorical, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryBonded { + pub(crate) count: i32, + pub(crate) active: i32, + pub(crate) inactive: i32, + pub(crate) reserve: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +pub(crate) mod gateway { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummary { + pub(crate) bonded: GatewaySummaryBonded, + pub(crate) blacklisted: GatewaySummaryBlacklisted, + pub(crate) historical: GatewaySummaryHistorical, + pub(crate) explorer: GatewaySummaryExplorer, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryExplorer { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryBonded { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +#[allow(dead_code)] // not dead code, this is SQL data model +#[derive(Debug, Clone)] +pub struct TestRunDto { + pub id: i64, + pub gateway_id: i64, + pub status: i64, + pub timestamp_utc: i64, + pub ip_address: String, + pub log: String, +} + +#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)] +#[repr(u8)] +pub(crate) enum TestRunStatus { + Complete = 2, + InProgress = 1, + Queued = 0, +} + +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayInfoDto { + pub id: i64, + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, +} diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs new file mode 100644 index 0000000000..bcf9c2d6ca --- /dev/null +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -0,0 +1,180 @@ +use crate::{ + db::{ + models::{BondedStatusDto, GatewayDto, GatewayRecord}, + DbPool, + }, + http::models::Gateway, +}; +use futures_util::TryStreamExt; +use nym_validator_client::models::NymNodeDescription; +use sqlx::{pool::PoolConnection, Sqlite}; +use tracing::error; + +pub(crate) async fn select_gateway_identity( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result { + let record = sqlx::query!( + r#"SELECT + gateway_identity_key + FROM + gateways + WHERE + id = ?"#, + gateway_pk + ) + .fetch_one(conn.as_mut()) + .await?; + + Ok(record.gateway_identity_key) +} + +pub(crate) async fn insert_gateways( + pool: &DbPool, + gateways: Vec, +) -> anyhow::Result<()> { + let mut db = pool.acquire().await?; + for record in gateways { + sqlx::query!( + "INSERT INTO gateways + (gateway_identity_key, bonded, blacklisted, + self_described, explorer_pretty_bond, + last_updated_utc, performance) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(gateway_identity_key) DO UPDATE SET + bonded=excluded.bonded, + blacklisted=excluded.blacklisted, + self_described=excluded.self_described, + explorer_pretty_bond=excluded.explorer_pretty_bond, + last_updated_utc=excluded.last_updated_utc, + performance = excluded.performance;", + record.identity_key, + record.bonded, + record.blacklisted, + record.self_described, + record.explorer_pretty_bond, + record.last_updated_utc, + record.performance + ) + .execute(&mut *db) + .await?; + } + + Ok(()) +} + +pub(crate) async fn write_blacklisted_gateways_to_db<'a, I>( + pool: &DbPool, + gateways: I, +) -> anyhow::Result<()> +where + I: Iterator, +{ + let mut conn = pool.acquire().await?; + for gateway_identity_key in gateways { + sqlx::query!( + "UPDATE gateways + SET blacklisted = true + WHERE gateway_identity_key = ?;", + gateway_identity_key, + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +/// Ensure all gateways that are set as bonded, are still bonded +pub(crate) async fn ensure_gateways_still_bonded( + pool: &DbPool, + gateways: &[&NymNodeDescription], +) -> anyhow::Result { + let bonded_gateways_rows = get_all_bonded_gateways_row_ids_by_status(pool, true).await?; + let unbonded_gateways_rows = bonded_gateways_rows.iter().filter(|v| { + !gateways + .iter() + .any(|bonded| *bonded.ed25519_identity_key().to_base58_string() == v.identity_key) + }); + + let recently_unbonded_gateways = unbonded_gateways_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_gateways_rows { + sqlx::query!( + "UPDATE gateways + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_gateways) +} + +async fn get_all_bonded_gateways_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + gateway_identity_key as "identity_key!", + bonded as "bonded: bool" + FROM gateways + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} + +pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + GatewayDto, + r#"SELECT + gw.gateway_identity_key as "gateway_identity_key!", + gw.bonded as "bonded: bool", + gw.blacklisted as "blacklisted: bool", + gw.performance as "performance!", + gw.self_described as "self_described?", + gw.explorer_pretty_bond as "explorer_pretty_bond?", + gw.last_probe_result as "last_probe_result?", + gw.last_probe_log as "last_probe_log?", + gw.last_testrun_utc as "last_testrun_utc?", + gw.last_updated_utc as "last_updated_utc!", + COALESCE(gd.moniker, "NA") as "moniker!", + COALESCE(gd.website, "NA") as "website!", + COALESCE(gd.security_contact, "NA") as "security_contact!", + COALESCE(gd.details, "NA") as "details!" + FROM gateways gw + LEFT JOIN gateway_description gd + ON gw.gateway_identity_key = gd.gateway_identity_key + ORDER BY gw.gateway_identity_key"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items: Vec = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + tracing::trace!("Fetched {} gateways from DB", items.len()); + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs new file mode 100644 index 0000000000..fe22ec27aa --- /dev/null +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -0,0 +1,15 @@ +mod gateways; +mod misc; +mod mixnodes; +mod summary; +pub(crate) mod testruns; + +pub(crate) use gateways::{ + ensure_gateways_still_bonded, get_all_gateways, insert_gateways, select_gateway_identity, + write_blacklisted_gateways_to_db, +}; +pub(crate) use misc::insert_summaries; +pub(crate) use mixnodes::{ + ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes, +}; +pub(crate) use summary::{get_summary, get_summary_history}; diff --git a/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/src/db/queries/testruns.rs new file mode 100644 index 0000000000..91a3a86b13 --- /dev/null +++ b/nym-node-status-api/src/db/queries/testruns.rs @@ -0,0 +1,184 @@ +use crate::db::DbPool; +use crate::http::models::TestrunAssignment; +use crate::{ + db::models::{TestRunDto, TestRunStatus}, + testruns::now_utc, +}; +use anyhow::Context; +use chrono::Duration; +use sqlx::{pool::PoolConnection, Sqlite}; + +pub(crate) async fn get_in_progress_testrun_by_id( + conn: &mut PoolConnection, + testrun_id: i64, +) -> anyhow::Result { + sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE + id = ? + AND + status = ? + ORDER BY timestamp_utc"#, + testrun_id, + TestRunStatus::InProgress as i64, + ) + .fetch_one(conn.as_mut()) + .await + .context(format!("Couldn't retrieve testrun {testrun_id}")) +} + +pub(crate) async fn update_testruns_older_than(db: &DbPool, age: Duration) -> anyhow::Result { + let mut conn = db.acquire().await?; + let previous_run = now_utc() - age; + let cutoff_timestamp = previous_run.timestamp(); + + let res = sqlx::query!( + r#"UPDATE + testruns + SET + status = ? + WHERE + status = ? + AND + timestamp_utc < ? + "#, + TestRunStatus::Queued as i64, + TestRunStatus::InProgress as i64, + cutoff_timestamp + ) + .execute(conn.as_mut()) + .await?; + + let stale_testruns = res.rows_affected(); + if stale_testruns > 0 { + tracing::debug!( + "Refreshed {} stale testruns, scheduled before {} but not yet finished", + stale_testruns, + previous_run + ); + } + + Ok(stale_testruns) +} + +pub(crate) async fn get_oldest_testrun_and_make_it_pending( + conn: &mut PoolConnection, +) -> anyhow::Result> { + // find & mark as "In progress" in the same transaction to avoid race conditions + let returning = sqlx::query!( + r#"UPDATE testruns + SET status = ? + WHERE rowid = + ( + SELECT rowid + FROM testruns + WHERE status = ? + ORDER BY timestamp_utc asc + LIMIT 1 + ) + RETURNING + id as "id!", + gateway_id + "#, + TestRunStatus::InProgress as i64, + TestRunStatus::Queued as i64, + ) + .fetch_optional(conn.as_mut()) + .await?; + + if let Some(testrun) = returning { + let gw_identity = sqlx::query!( + r#" + SELECT + id, + gateway_identity_key + FROM gateways + WHERE id = ? + LIMIT 1"#, + testrun.gateway_id + ) + .fetch_one(conn.as_mut()) + .await?; + + Ok(Some(TestrunAssignment { + testrun_id: testrun.id, + gateway_identity_key: gw_identity.gateway_identity_key, + })) + } else { + Ok(None) + } +} + +pub(crate) async fn update_testrun_status( + conn: &mut PoolConnection, + testrun_id: i64, + status: TestRunStatus, +) -> anyhow::Result<()> { + let status = status as u32; + sqlx::query!( + "UPDATE testruns SET status = ? WHERE id = ?", + status, + testrun_id + ) + .execute(conn.as_mut()) + .await?; + + Ok(()) +} + +pub(crate) async fn update_gateway_last_probe_log( + conn: &mut PoolConnection, + gateway_pk: i64, + log: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + log, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_last_probe_result( + conn: &mut PoolConnection, + gateway_pk: i64, + result: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + result, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_score( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result<()> { + let now = now_utc().timestamp(); + sqlx::query!( + "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + now, + now, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs new file mode 100644 index 0000000000..4e4693ac1f --- /dev/null +++ b/nym-node-status-api/src/http/api/mod.rs @@ -0,0 +1,94 @@ +use anyhow::anyhow; +use axum::{response::Redirect, Router}; +use tokio::net::ToSocketAddrs; +use tower_http::{ + cors::CorsLayer, + trace::{DefaultOnResponse, TraceLayer}, +}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::{server::HttpServer, state::AppState}; + +pub(crate) mod gateways; +pub(crate) mod mixnodes; +pub(crate) mod services; +pub(crate) mod summary; +pub(crate) mod testruns; + +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + pub(crate) fn with_default_routes() -> Self { + let router = Router::new() + .merge( + SwaggerUi::new("/swagger") + .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), + ) + .route( + "/", + axum::routing::get(|| async { Redirect::permanent("/swagger") }), + ) + .nest( + "/v2", + Router::new() + .nest("/gateways", gateways::routes()) + .nest("/mixnodes", mixnodes::routes()) + .nest("/services", services::routes()) + .nest("/summary", summary::routes()), + ) + .nest( + "/internal", + Router::new().nest("/testruns", testruns::routes()), + ); + + Self { + unfinished_router: router, + } + } + + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + fn finalize_routes(self) -> Router { + // layers added later wrap earlier layers + self.unfinished_router + // CORS layer needs to wrap other API layers + .layer(setup_cors()) + // logger should be outermost layer + .layer( + TraceLayer::new_for_http() + .on_response(DefaultOnResponse::new().level(tracing::Level::DEBUG)), + ) + } +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: A, + ) -> anyhow::Result { + tokio::net::TcpListener::bind(bind_address) + .await + .map(|listener| HttpServer::new(self.router, listener)) + .map_err(|err| anyhow!("Couldn't bind to address due to {}", err)) + } +} + +fn setup_cors() -> CorsLayer { + use axum::http::Method; + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs new file mode 100644 index 0000000000..e55e462110 --- /dev/null +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -0,0 +1,125 @@ +use axum::extract::DefaultBodyLimit; +use axum::Json; +use axum::{ + extract::{Path, State}, + Router, +}; +use reqwest::StatusCode; + +use crate::db::models::TestRunStatus; +use crate::db::queries; +use crate::{ + db, + http::{ + error::{HttpError, HttpResult}, + models::TestrunAssignment, + state::AppState, + }, +}; + +// TODO dz consider adding endpoint to trigger testrun scan for a given gateway_id +// like in H< src/http/testruns.rs + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(request_testrun)) + .route("/:testrun_id", axum::routing::post(submit_testrun)) + .layer(DefaultBodyLimit::max(1024 * 1024 * 5)) +} + +#[tracing::instrument(level = "debug", skip_all)] +async fn request_testrun(State(state): State) -> HttpResult> { + // TODO dz log agent's key + // TODO dz log agent's network probe version + tracing::debug!("Agent requested testrun"); + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + return match db::queries::testruns::get_oldest_testrun_and_make_it_pending(&mut conn).await { + Ok(res) => { + if let Some(testrun) = res { + tracing::debug!( + "🏃‍ Assigned testrun row_id {} gateway {} to agent", + &testrun.testrun_id, + testrun.gateway_identity_key + ); + Ok(Json(testrun)) + } else { + Err(HttpError::no_available_testruns()) + } + } + Err(err) => Err(HttpError::internal_with_logging(err)), + }; +} + +// TODO dz accept testrun_id as query parameter +#[tracing::instrument(level = "debug", skip_all)] +async fn submit_testrun( + Path(testrun_id): Path, + State(state): State, + body: String, +) -> HttpResult { + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + let testrun = queries::testruns::get_in_progress_testrun_by_id(&mut conn, testrun_id) + .await + .map_err(|e| { + tracing::error!("{e}"); + HttpError::not_found(testrun_id) + })?; + + let gw_identity = db::queries::select_gateway_identity(&mut conn, testrun.gateway_id) + .await + .map_err(|_| { + // should never happen: + HttpError::internal_with_logging("No gateway found for testrun") + })?; + tracing::debug!( + "Agent submitted testrun {} for gateway {} ({} bytes)", + testrun_id, + gw_identity, + body.len(), + ); + + // TODO dz this should be part of a single transaction: commit after everything is done + queries::testruns::update_testrun_status(&mut conn, testrun_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_last_probe_log(&mut conn, testrun.gateway_id, &body) + .await + .map_err(HttpError::internal_with_logging)?; + let result = get_result_from_log(&body); + queries::testruns::update_gateway_last_probe_result(&mut conn, testrun.gateway_id, &result) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_score(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + // TODO dz log gw identity key + + tracing::info!( + "✅ Testrun row_id {} for gateway {} complete", + testrun.id, + gw_identity + ); + + Ok(StatusCode::CREATED) +} + +fn get_result_from_log(log: &str) -> String { + let re = regex::Regex::new(r"\n\{\s").unwrap(); + let result: Vec<_> = re.splitn(log, 2).collect(); + if result.len() == 2 { + let res = format!("{} {}", "{", result[1]).to_string(); + return res; + } + "".to_string() +} diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs new file mode 100644 index 0000000000..808ace9cec --- /dev/null +++ b/nym-node-status-api/src/http/error.rs @@ -0,0 +1,48 @@ +use std::fmt::Display; + +pub(crate) type HttpResult = Result; + +pub(crate) struct HttpError { + message: String, + status: axum::http::StatusCode, +} + +impl HttpError { + pub(crate) fn invalid_input(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::BAD_REQUEST, + } + } + + pub(crate) fn internal_with_logging(msg: impl Display) -> Self { + tracing::error!("{}", msg.to_string()); + Self::internal() + } + + pub(crate) fn internal() -> Self { + Self { + message: serde_json::json!({"message": "Internal server error"}).to_string(), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn no_available_testruns() -> Self { + Self { + message: serde_json::json!({"message": "No available testruns"}).to_string(), + status: axum::http::StatusCode::SERVICE_UNAVAILABLE, + } + } + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::NOT_FOUND, + } + } +} + +impl axum::response::IntoResponse for HttpError { + fn into_response(self) -> axum::response::Response { + (self.status, self.message).into_response() + } +} diff --git a/nym-node-status-api/src/http/models.rs b/nym-node-status-api/src/http/models.rs new file mode 100644 index 0000000000..82011fc286 --- /dev/null +++ b/nym-node-status-api/src/http/models.rs @@ -0,0 +1,76 @@ +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) use nym_common_models::ns_api::TestrunAssignment; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Gateway { + pub gateway_identity_key: String, + pub bonded: bool, + pub blacklisted: bool, + pub performance: u8, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub description: NodeDescription, + pub last_probe_result: Option, + pub last_probe_log: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct GatewaySkinny { + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub last_probe_result: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, + pub performance: u8, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Mixnode { + pub mix_id: u32, + pub bonded: bool, + pub blacklisted: bool, + pub is_dp_delegatee: bool, + pub total_stake: i64, + pub full_details: Option, + pub self_described: Option, + pub description: NodeDescription, + pub last_updated_utc: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct DailyStats { + pub date_utc: String, + pub total_packets_received: i64, + pub total_packets_sent: i64, + pub total_packets_dropped: i64, + pub total_stake: i64, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Service { + pub gateway_identity_key: String, + pub last_updated_utc: String, + pub routing_score: f32, + pub service_provider_client_id: Option, + pub ip_address: Option, + pub hostname: Option, + pub mixnet_websockets: Option, + pub last_successful_ping_utc: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct SummaryHistory { + pub date: String, + pub value_json: serde_json::Value, + pub timestamp_utc: String, +} diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs new file mode 100644 index 0000000000..a9f3761f35 --- /dev/null +++ b/nym-node-status-api/src/monitor/mod.rs @@ -0,0 +1,511 @@ +use crate::db::models::{ + gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, + GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, + MIXNODES_BLACKLISTED_COUNT, MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_COUNT, +}; +use crate::db::{queries, DbPool}; +use anyhow::anyhow; +use cosmwasm_std::Decimal; +use nym_explorer_client::{ExplorerClient, PrettyDetailedGatewayBond}; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::{ + LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription, +}; +use nym_validator_client::nym_nodes::SkimmedNode; +use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; +use nym_validator_client::nyxd::{AccountId, NyxdClient}; +use nym_validator_client::NymApiClient; +use reqwest::Url; +use std::collections::HashSet; +use std::str::FromStr; +use tokio::time::Duration; +use tracing::instrument; + +// TODO dz should be configurable +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); + +static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; + +// TODO dz: query many NYM APIs: +// multiple instances running directory cache, ask sachin +#[instrument(level = "debug", name = "data_monitor", skip_all)] +pub(crate) async fn spawn_in_background( + db_pool: DbPool, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, + refresh_interval: Duration, +) { + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + tracing::info!("Refreshing node info..."); + + if let Err(e) = run( + &db_pool, + &network_defaults, + explorer_client_timeout, + nym_api_client_timeout, + nyxd_addr, + ) + .await + { + tracing::error!( + "Monitor run failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + // TODO dz implement some sort of backoff + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Info successfully collected, sleeping for {}s...", + refresh_interval.as_secs() + ); + tokio::time::sleep(refresh_interval).await; + } + } +} + +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, +) -> anyhow::Result<()> { + let default_api_url = network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); + let default_explorer_url = network_details.explorer_api.clone().map(|url| { + url.parse() + .expect("rust sdk mainnet default explorer url not parseable") + }); + + // TODO dz replace explorer api with ipinfo.io + let default_explorer_url = + default_explorer_url.expect("explorer url missing in network config"); + let explorer_client = + ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?; + let explorer_gateways = explorer_client + .unstable_get_gateways() + .await + .log_error("unstable_get_gateways")?; + + let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); + + let all_nodes = api_client + .get_all_described_nodes() + .await + .log_error("get_all_described_nodes")?; + tracing::debug!("Fetched {} total nodes", all_nodes.len()); + + let gateways = all_nodes + .iter() + .filter(|node| node.description.declared_role.entry) + .collect::>(); + tracing::debug!("Of those, {} gateways", gateways.len()); + for gw in gateways.iter() { + tracing::debug!("{}", gw.ed25519_identity_key().to_base58_string()); + } + + let mixnodes = all_nodes + .iter() + .filter(|node| node.description.declared_role.mixnode) + .collect::>(); + tracing::debug!("Of those, {} mixnodes", mixnodes.len()); + + log_gw_in_explorer_not_api(explorer_gateways.as_slice(), gateways.as_slice()); + + let all_skimmed_nodes = api_client + .get_all_basic_nodes(None) + .await + .log_error("get_all_basic_nodes")?; + + let mixnodes = api_client + .get_cached_mixnodes() + .await + .log_error("get_cached_mixnodes")?; + tracing::debug!("Fetched {} mixnodes", mixnodes.len()); + + // let gateways_blacklisted = gateways.iter().filter(|gw|gw.) + let gateways_blacklisted = all_skimmed_nodes + .iter() + .filter_map(|node| { + if node.performance.round_to_integer() <= 50 && node.supported_roles.entry { + Some(node.ed25519_identity_pubkey.to_base58_string()) + } else { + None + } + }) + .collect::>(); + + // Cached mixnodes don't include blacklisted nodes + // We need that to calculate the total locked tokens later + let mixnodes = api_client + .nym_api + .get_mixnodes_detailed_unfiltered() + .await + .log_error("get_mixnodes_detailed_unfiltered")?; + let mixnodes_described = api_client + .nym_api + .get_mixnodes_described() + .await + .log_error("get_mixnodes_described")?; + let mixnodes_active = api_client + .nym_api + .get_active_mixnodes() + .await + .log_error("get_active_mixnodes")?; + let delegation_program_members = + get_delegation_program_details(network_details, nyxd_addr).await?; + + // keep stats for later + let count_bonded_mixnodes = mixnodes.len(); + let count_bonded_gateways = gateways.len(); + let count_explorer_gateways = explorer_gateways.len(); + let count_bonded_mixnodes_active = mixnodes_active.len(); + + let gateway_records = prepare_gateway_data( + &gateways, + &gateways_blacklisted, + explorer_gateways, + all_skimmed_nodes, + )?; + queries::insert_gateways(pool, gateway_records) + .await + .map(|_| { + tracing::debug!("Gateway info written to DB!"); + })?; + + // instead of counting blacklisted GWs returned from API cache, count from the active set + let count_gateways_blacklisted = gateways + .iter() + .filter(|gw| { + let gw_identity = gw.ed25519_identity_key().to_base58_string(); + gateways_blacklisted.contains(&gw_identity) + }) + .count(); + + if count_gateways_blacklisted > 0 { + queries::write_blacklisted_gateways_to_db(pool, gateways_blacklisted.iter()) + .await + .map(|_| { + tracing::debug!( + "Gateway blacklist info written to DB! {} blacklisted by Nym API", + count_gateways_blacklisted + ) + })?; + } + + let mixnode_records = + prepare_mixnode_data(&mixnodes, mixnodes_described, delegation_program_members)?; + queries::insert_mixnodes(pool, mixnode_records) + .await + .map(|_| { + tracing::debug!("Mixnode info written to DB!"); + })?; + + let count_mixnodes_blacklisted = mixnodes.iter().filter(|elem| elem.blacklisted).count(); + + let recently_unbonded_gateways = queries::ensure_gateways_still_bonded(pool, &gateways).await?; + let recently_unbonded_mixnodes = queries::ensure_mixnodes_still_bonded(pool, &mixnodes).await?; + + let count_bonded_mixnodes_reserve = 0; // TODO: NymAPI doesn't report the reserve set size + let count_bonded_mixnodes_inactive = count_bonded_mixnodes - count_bonded_mixnodes_active; + + let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(pool).await?; + + // + // write summary keys and values to table + // + + let nodes_summary = vec![ + (MIXNODES_BONDED_COUNT, &count_bonded_mixnodes), + (MIXNODES_BONDED_ACTIVE, &count_bonded_mixnodes_active), + (MIXNODES_BONDED_INACTIVE, &count_bonded_mixnodes_inactive), + (MIXNODES_BONDED_RESERVE, &count_bonded_mixnodes_reserve), + (MIXNODES_BLACKLISTED_COUNT, &count_mixnodes_blacklisted), + (GATEWAYS_BONDED_COUNT, &count_bonded_gateways), + (GATEWAYS_EXPLORER_COUNT, &count_explorer_gateways), + (MIXNODES_HISTORICAL_COUNT, &all_historical_mixnodes), + (GATEWAYS_HISTORICAL_COUNT, &all_historical_gateways), + (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), + ]; + + let last_updated = chrono::offset::Utc::now(); + let last_updated_utc = last_updated.timestamp().to_string(); + let network_summary = NetworkSummary { + mixnodes: mixnode::MixnodeSummary { + bonded: mixnode::MixnodeSummaryBonded { + count: count_bonded_mixnodes.cast_checked()?, + active: count_bonded_mixnodes_active.cast_checked()?, + inactive: count_bonded_mixnodes_inactive.cast_checked()?, + reserve: count_bonded_mixnodes_reserve.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: mixnode::MixnodeSummaryBlacklisted { + count: count_mixnodes_blacklisted.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: mixnode::MixnodeSummaryHistorical { + count: all_historical_mixnodes.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + gateways: gateway::GatewaySummary { + bonded: gateway::GatewaySummaryBonded { + count: count_bonded_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: gateway::GatewaySummaryBlacklisted { + count: count_gateways_blacklisted.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: gateway::GatewaySummaryHistorical { + count: all_historical_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + explorer: gateway::GatewaySummaryExplorer { + count: count_explorer_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + }; + + queries::insert_summaries(pool, &nodes_summary, &network_summary, last_updated).await?; + + let mut log_lines: Vec = vec![]; + for (key, value) in nodes_summary.iter() { + log_lines.push(format!("{} = {}", key, value)); + } + log_lines.push(format!( + "recently_unbonded_mixnodes = {}", + recently_unbonded_mixnodes + )); + log_lines.push(format!( + "recently_unbonded_gateways = {}", + recently_unbonded_gateways + )); + + tracing::info!("Directory summary: \n{}", log_lines.join("\n")); + + Ok(()) +} + +fn prepare_gateway_data( + gateways: &[&NymNodeDescription], + gateways_blacklisted: &HashSet, + explorer_gateways: Vec, + skimmed_gateways: Vec, +) -> anyhow::Result> { + let mut gateway_records = Vec::new(); + + for gateway in gateways { + let identity_key = gateway.ed25519_identity_key().to_base58_string(); + let bonded = true; + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let blacklisted = gateways_blacklisted.contains(&identity_key); + + let self_described = serde_json::to_string(&gateway.description)?; + + let explorer_pretty_bond = explorer_gateways + .iter() + .find(|g| g.gateway.identity_key.eq(&identity_key)); + let explorer_pretty_bond = explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + + let performance = skimmed_gateways + .iter() + .find(|g| { + g.ed25519_identity_pubkey + .to_base58_string() + .eq(&identity_key) + }) + .map(|g| g.performance) + .unwrap_or_default() + .round_to_integer(); + + gateway_records.push(GatewayRecord { + identity_key: identity_key.to_owned(), + bonded, + blacklisted, + self_described, + explorer_pretty_bond, + last_updated_utc, + performance, + }); + } + + Ok(gateway_records) +} + +fn prepare_mixnode_data( + mixnodes: &[MixNodeBondAnnotated], + mixnodes_described: Vec, + delegation_program_members: Vec, +) -> anyhow::Result> { + let mut mixnode_records = Vec::new(); + + for mixnode in mixnodes { + let mix_id = mixnode.mix_id(); + let identity_key = mixnode.identity_key(); + let bonded = true; + let total_stake = decimal_to_i64(mixnode.mixnode_details.total_stake()); + let blacklisted = mixnode.blacklisted; + let node_info = mixnode.mix_node(); + let host = node_info.host.clone(); + let http_port = node_info.http_api_port; + // Contains all the information including what's above + let full_details = serde_json::to_string(&mixnode)?; + + let mixnode_described = mixnodes_described.iter().find(|m| m.bond.mix_id == mix_id); + let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); + let is_dp_delegatee = delegation_program_members.contains(&mix_id); + + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + + mixnode_records.push(MixnodeRecord { + mix_id, + identity_key: identity_key.to_owned(), + bonded, + total_stake, + host, + http_port, + blacklisted, + full_details, + self_described, + last_updated_utc, + is_dp_delegatee, + }); + } + + Ok(mixnode_records) +} + +fn log_gw_in_explorer_not_api( + explorer: &[PrettyDetailedGatewayBond], + api_gateways: &[&NymNodeDescription], +) { + let api_gateways = api_gateways + .iter() + .map(|gw| gw.ed25519_identity_key().to_base58_string()) + .collect::>(); + let explorer_only = explorer + .iter() + .filter(|gw| !api_gateways.contains(&gw.gateway.identity_key.to_string())) + .collect::>(); + + tracing::debug!( + "Gateways listed by explorer but not by Nym API: {}", + explorer_only.len() + ); + for gw in explorer_only.iter() { + tracing::debug!("{}", gw.gateway.identity_key.to_string()); + } +} + +// TODO dz is there a common monorepo place this can be put? +pub trait NumericalCheckedCast +where + T: TryFrom, + >::Error: std::error::Error, + Self: std::fmt::Display + Copy, +{ + fn cast_checked(self) -> anyhow::Result { + T::try_from(self).map_err(|e| { + anyhow::anyhow!( + "Couldn't cast {} to {}: {}", + self, + std::any::type_name::(), + e + ) + }) + } +} + +impl NumericalCheckedCast for T +where + U: TryFrom, + >::Error: std::error::Error, + T: std::fmt::Display + Copy, +{ +} + +async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> { + let mut conn = pool.acquire().await?; + + let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) + .fetch_one(&mut *conn) + .await? + .cast_checked()?; + + let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) + .fetch_one(&mut *conn) + .await? + .cast_checked()?; + + Ok((all_historical_gateways, all_historical_mixnodes)) +} + +async fn get_delegation_program_details( + network_details: &NymNetworkDetails, + nyxd_addr: &Url, +) -> anyhow::Result> { + let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; + + let client = NyxdClient::connect(config, nyxd_addr.as_str()) + .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; + + let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) + .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; + + let delegations = client.get_all_delegator_delegations(&account_id).await?; + + let mix_ids: Vec = delegations + .iter() + .map(|delegation| delegation.node_id) + .collect(); + + Ok(mix_ids) +} + +fn decimal_to_i64(decimal: Decimal) -> i64 { + // Convert the underlying Uint128 to a u128 + let atomics = decimal.atomics().u128(); + let precision = 1_000_000_000_000_000_000u128; + + // Get the fractional part + let fractional = atomics % precision; + + // Get the integer part + let integer = atomics / precision; + + // Combine them into a float + let float_value = integer as f64 + (fractional as f64 / 1_000_000_000_000_000_000_f64); + + // Limit to 6 decimal places + let rounded_value = (float_value * 1_000_000.0).round() / 1_000_000.0; + + rounded_value as i64 +} + +trait LogError { + fn log_error(self, msg: &str) -> Result; +} + +impl LogError for anyhow::Result +where + E: std::error::Error, +{ + fn log_error(self, msg: &str) -> Result { + if let Err(e) = &self { + tracing::error!("[{msg}]:\t{e}"); + } + self + } +} diff --git a/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/src/testruns/mod.rs new file mode 100644 index 0000000000..f487523f36 --- /dev/null +++ b/nym-node-status-api/src/testruns/mod.rs @@ -0,0 +1,86 @@ +use crate::db::models::GatewayIdentityDto; +use crate::db::DbPool; +use futures_util::TryStreamExt; +use std::time::Duration; +use tracing::instrument; + +pub(crate) mod models; +mod queue; +pub(crate) use queue::now_utc; + +pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { + tokio::spawn(async move { + loop { + if let Err(e) = refresh_stale_testruns(&pool, refresh_interval).await { + tracing::error!("{e}"); + } + + if let Err(e) = run(&pool).await { + tracing::error!("Assigning testruns failed: {}", e); + } + tracing::debug!("Sleeping for {}s...", refresh_interval.as_secs()); + tokio::time::sleep(refresh_interval).await; + } + }); +} + +// TODO dz make number of max agents configurable + +#[instrument(level = "debug", name = "testrun_queue", skip_all)] +async fn run(pool: &DbPool) -> anyhow::Result<()> { + tracing::info!("Spawning testruns..."); + if pool.is_closed() { + tracing::debug!("DB pool closed, returning early"); + return Ok(()); + } + + let mut conn = pool.acquire().await?; + + let gateways = sqlx::query_as!( + GatewayIdentityDto, + r#"SELECT + gateway_identity_key as "gateway_identity_key!", + bonded as "bonded: bool" + FROM gateways + ORDER BY last_testrun_utc"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + // TODO dz this filtering could be done in SQL + let gateways: Vec = gateways.into_iter().filter(|g| g.bonded).collect(); + + tracing::debug!("Trying to queue {} testruns", gateways.len()); + let mut testruns_created = 0; + for gateway in gateways { + if let Err(e) = queue::try_queue_testrun( + &mut conn, + gateway.gateway_identity_key.clone(), + // TODO dz read from config + "127.0.0.1".to_string(), + ) + .await + // TODO dz measure how many were actually inserted and how many were skipped + { + tracing::debug!( + "Skipping test for identity {} with error {}", + &gateway.gateway_identity_key, + e + ); + } else { + testruns_created += 1; + } + } + tracing::debug!("{} testruns queued in total", testruns_created); + + Ok(()) +} + +#[instrument(level = "debug", skip_all)] +async fn refresh_stale_testruns(pool: &DbPool, refresh_interval: Duration) -> anyhow::Result<()> { + let chrono_duration = chrono::Duration::from_std(refresh_interval)?; + crate::db::queries::testruns::update_testruns_older_than(pool, chrono_duration).await?; + + Ok(()) +} diff --git a/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/src/testruns/queue.rs new file mode 100644 index 0000000000..88804fff1b --- /dev/null +++ b/nym-node-status-api/src/testruns/queue.rs @@ -0,0 +1,118 @@ +use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::testruns::models::TestRun; +use anyhow::anyhow; +use chrono::DateTime; +use futures_util::TryStreamExt; +use sqlx::pool::PoolConnection; +use sqlx::Sqlite; +use std::time::SystemTime; + +pub(crate) async fn try_queue_testrun( + conn: &mut PoolConnection, + identity_key: String, + ip_address: String, +) -> anyhow::Result { + let timestamp = now_utc().timestamp(); + let timestamp_pretty = now_utc_as_rfc3339(); + + let items = sqlx::query_as!( + GatewayInfoDto, + r#"SELECT + id as "id!", + gateway_identity_key as "gateway_identity_key!", + self_described as "self_described?", + explorer_pretty_bond as "explorer_pretty_bond?" + FROM gateways + WHERE gateway_identity_key = ? + ORDER BY gateway_identity_key + LIMIT 1"#, + identity_key, + ) + // TODO dz shoudl call .fetch_one + // TODO dz replace this in other queries as well + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + let gateway = items + .iter() + .find(|g| g.gateway_identity_key == identity_key); + + // TODO dz if let Some() = gateway.first() ... + if gateway.is_none() { + return Err(anyhow!("Unknown gateway {identity_key}")); + } + let gateway_id = gateway.unwrap().id; + + // + // check if there is already a test run for this gateway + // + let items = sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE gateway_id = ? AND status != 2 + ORDER BY id DESC + LIMIT 1"#, + gateway_id, + ) + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + if !items.is_empty() { + let testrun = items.first().unwrap(); + return Ok(TestRun { + id: testrun.id as u32, + identity_key, + status: format!( + "{}", + TestRunStatus::from_repr(testrun.status as u8).unwrap() + ), + log: testrun.log.clone(), + }); + } + + // + // save test run + // + let status = TestRunStatus::Queued as u32; + let log = format!( + "Test for {identity_key} requested at {} UTC\n\n", + timestamp_pretty + ); + + let id = sqlx::query!( + "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + gateway_id, + status, + ip_address, + timestamp, + log, + ) + .execute(conn.as_mut()) + .await? + .last_insert_rowid(); + + Ok(TestRun { + id: id as u32, + identity_key, + status: format!("{}", TestRunStatus::Queued), + log, + }) +} + +// TODO dz do we need these? +pub fn now_utc() -> DateTime { + SystemTime::now().into() +} + +pub fn now_utc_as_rfc3339() -> String { + now_utc().to_rfc3339() +} From 0374626960c4bf7fdbd7c3265785acc79ac2748a Mon Sep 17 00:00:00 2001 From: Fouad Date: Thu, 31 Oct 2024 16:53:55 +0000 Subject: [PATCH 08/13] Allow custom http port to be reset (#5073) * allow custom port to be reset in wallet --- .../Bonding/forms/nym-node/settingsValidationSchema.ts | 9 +++++++++ nym-wallet/src/context/bonding.tsx | 9 +++------ .../general-settings/GeneralNymNodeSettings.tsx | 2 ++ nym-wallet/src/requests/actions.ts | 3 ++- ts-packages/types/src/types/rust/index.ts | 1 + 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts index 316f6f4ad7..eef46e37c7 100644 --- a/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts @@ -9,6 +9,15 @@ const settingsValidationSchema = Yup.object().shape({ custom_http_port: Yup.number() .nullable() + .transform((numberVal, stringVal) => { + if (stringVal === '') { + return null; + } + if (!Number(stringVal)) { + return stringVal; + } + return numberVal; + }) .test('valid-http', 'A valid http port is required', (value) => { if (value === null) { return true; diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index d5b1e2f118..19652b6cff 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { FeeDetails, TransactionExecuteResult } from '@nymproject/types'; +import { FeeDetails, NodeConfigUpdate, TransactionExecuteResult } from '@nymproject/types'; import { isGateway, isMixnode, TUpdateBondArgs, isNymNode, TNymNodeSignatureArgs, TBondNymNodeArgs } from 'src/types'; import { Console } from 'src/utils/console'; import useGetNodeDetails from 'src/hooks/useGetNodeDetails'; @@ -35,10 +35,7 @@ export type TBondingContext = { unbond: (fee?: FeeDetails) => Promise; bond: (args: TBondNymNodeArgs) => Promise; updateBondAmount: (data: TUpdateBondArgs) => Promise; - updateNymNodeConfig: (data: { - host: string; - custom_http_port: number | null; - }) => Promise; + updateNymNodeConfig: (data: NodeConfigUpdate) => Promise; redeemRewards: (fee?: FeeDetails) => Promise; generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise; migrateVestedMixnode: () => Promise; @@ -148,7 +145,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; - const updateNymNodeConfig = async (data: { host: string; custom_http_port: number | null }) => { + const updateNymNodeConfig = async (data: NodeConfigUpdate) => { let tx; setIsLoading(true); try { diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx index cedc2891ed..9578de69db 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx @@ -41,8 +41,10 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN const NymNodeConfigParams = { host, custom_http_port, + restore_default_http_port: custom_http_port === null, }; await updateNymNodeConfig(NymNodeConfigParams); + setOpenConfirmationModal(true); } catch (error) { Console.error(error); diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index 1d5f207bfb..6907cf054a 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -4,6 +4,7 @@ import { SendTxResult, TransactionExecuteResult, MixNodeConfigUpdate, + NodeConfigUpdate, GatewayConfigUpdate, } from '@nymproject/types'; import { TBondGatewayArgs, TBondGatewaySignatureArgs, TNodeConfigUpdateArgs } from '../types'; @@ -18,7 +19,7 @@ export const generateGatewayMsgPayload = async (args: Omit invokeWrapper('update_mixnode_config', { update, fee }); -export const updateNymNodeConfig = async (update: TNodeConfigUpdateArgs, fee?: Fee) => +export const updateNymNodeConfig = async (update: NodeConfigUpdate, fee?: Fee) => invokeWrapper('update_nymnode_config', { update, fee }); export const updateGatewayConfig = async (update: GatewayConfigUpdate, fee?: Fee) => diff --git a/ts-packages/types/src/types/rust/index.ts b/ts-packages/types/src/types/rust/index.ts index e66d1062df..dc80728ba2 100644 --- a/ts-packages/types/src/types/rust/index.ts +++ b/ts-packages/types/src/types/rust/index.ts @@ -32,6 +32,7 @@ export * from './MixNodeDetails'; export * from './MixNodeRewarding'; export * from './MixnodeStatus'; export * from './MixnodeStatusResponse'; +export * from './NodeConfigUpdate'; export * from './NymNodeDetails'; export * from './OriginalVestingResponse'; export * from './PendingEpochEvent'; From 09b68a82041a95a4f9d1d8e9483d45396f3473bc Mon Sep 17 00:00:00 2001 From: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com> Date: Fri, 1 Nov 2024 01:24:41 +0100 Subject: [PATCH 09/13] Cherry pick NS API from develop (#5074) * Revert "NS API with directory v2 (#5068)" This reverts commit cf4fe5f875d0e235117d52737504b37405391668. * Merge pull request #5050 from nymtech/dz-node-status-api Node Status API * Ns agent workflow (#5055) * feat: add dockerfile * add github workflow for node status agent --------- Co-authored-by: Fran Arbanas * NS API with directory v2 (#5058) * Use unstable explorer client * Clean up stale testruns & logging - log gw identity key - better agent testrun logging - log responses - change response code for agents * Better logging on agent * Testrun stores gw identity key instead of gw pk * Agent 0.1.3 * Agent 0.1.4 * Sqlx offline query data + clippy * Compatible with directory v2 * Point to internal deps + rebase + v0.1.5 * self described field not null * Fix build.rs typo * Fix clippy --------- Co-authored-by: Fran Arbanas --- .github/workflows/ci-build.yml | 12 +- .github/workflows/push-node-status-agent.yaml | 56 ++++ .github/workflows/push-node-status-api.yaml | 54 +++- Cargo.lock | 242 +++++++++++++++++- Cargo.toml | 14 +- common/bin-common/Cargo.toml | 1 + .../client-libs/validator-client/Cargo.toml | 2 +- .../validator-client/src/client.rs | 7 + .../validator-client/src/connection_tester.rs | 18 +- .../validator-client/src/nym_api/mod.rs | 46 ++++ .../nyxd/contract_traits/dkg_query_client.rs | 2 +- .../client_traits/query_client.rs | 7 +- .../client_traits/signing_client.rs | 2 +- .../src/nyxd/cosmwasm_client/helpers.rs | 2 +- common/http-api-client/src/lib.rs | 23 +- common/models/Cargo.toml | 14 + common/models/src/lib.rs | 1 + envs/canary.env | 4 +- envs/mainnet.env | 6 +- envs/qa.env | 4 +- envs/sandbox.env | 6 +- explorer-api/explorer-client/Cargo.toml | 2 +- explorer-api/explorer-client/src/lib.rs | 22 +- nym-data-observatory/README_SQLX.md | 1 + nym-node-status-agent/.gitignore | 1 + nym-node-status-agent/Dockerfile | 28 ++ nym-node-status-agent/src/main.rs | 78 ++++++ nym-node-status-api/.gitignore | 6 + nym-node-status-api/Dockerfile | 15 ++ nym-node-status-api/Dockerfile.dev | 8 + nym-node-status-api/src/cli/mod.rs | 77 ++++++ nym-node-status-api/src/db/mod.rs | 35 +++ nym-node-status-api/src/db/queries/misc.rs | 88 +++++++ .../src/db/queries/mixnodes.rs | 177 +++++++++++++ nym-node-status-api/src/db/queries/summary.rs | 209 +++++++++++++++ nym-node-status-api/src/http/api/gateways.rs | 110 ++++++++ nym-node-status-api/src/http/api/mixnodes.rs | 91 +++++++ .../src/http/api/services/json_path.rs | 58 +++++ .../src/http/api/services/mod.rs | 134 ++++++++++ nym-node-status-api/src/http/api/summary.rs | 43 ++++ nym-node-status-api/src/http/api_docs.rs | 15 ++ nym-node-status-api/src/http/mod.rs | 71 +++++ nym-node-status-api/src/http/server.rs | 93 +++++++ nym-node-status-api/src/http/state.rs | 223 ++++++++++++++++ nym-node-status-api/src/logging.rs | 69 +++++ nym-node-status-api/src/main.rs | 53 ++++ nym-node-status-api/src/monitor/mod.rs | 2 + nym-node-status-api/src/testruns/models.rs | 16 ++ 48 files changed, 2187 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/push-node-status-agent.yaml create mode 100644 common/models/Cargo.toml create mode 100644 common/models/src/lib.rs create mode 100644 nym-node-status-agent/.gitignore create mode 100644 nym-node-status-agent/Dockerfile create mode 100644 nym-node-status-agent/src/main.rs create mode 100644 nym-node-status-api/.gitignore create mode 100644 nym-node-status-api/Dockerfile create mode 100644 nym-node-status-api/Dockerfile.dev create mode 100644 nym-node-status-api/src/cli/mod.rs create mode 100644 nym-node-status-api/src/db/mod.rs create mode 100644 nym-node-status-api/src/db/queries/misc.rs create mode 100644 nym-node-status-api/src/db/queries/mixnodes.rs create mode 100644 nym-node-status-api/src/db/queries/summary.rs create mode 100644 nym-node-status-api/src/http/api/gateways.rs create mode 100644 nym-node-status-api/src/http/api/mixnodes.rs create mode 100644 nym-node-status-api/src/http/api/services/json_path.rs create mode 100644 nym-node-status-api/src/http/api/services/mod.rs create mode 100644 nym-node-status-api/src/http/api/summary.rs create mode 100644 nym-node-status-api/src/http/api_docs.rs create mode 100644 nym-node-status-api/src/http/mod.rs create mode 100644 nym-node-status-api/src/http/server.rs create mode 100644 nym-node-status-api/src/http/state.rs create mode 100644 nym-node-status-api/src/logging.rs create mode 100644 nym-node-status-api/src/main.rs create mode 100644 nym-node-status-api/src/testruns/models.rs diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 77d220e633..d7a76f2673 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -57,6 +57,12 @@ jobs: command: fmt args: --all -- --check + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace --all-targets -- -D warnings + - name: Build all binaries uses: actions-rs/cargo@v1 with: @@ -82,9 +88,3 @@ jobs: with: command: test args: --workspace -- --ignored - - - name: Clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --workspace --all-targets -- -D warnings diff --git a/.github/workflows/push-node-status-agent.yaml b/.github/workflows/push-node-status-agent.yaml new file mode 100644 index 0000000000..a66d4dee41 --- /dev/null +++ b/.github/workflows/push-node-status-agent.yaml @@ -0,0 +1,56 @@ +name: Build and upload Node Status agent container to harbor.nymte.ch + +on: + workflow_dispatch: + +env: + WORKING_DIRECTORY: "nym-node-status-agent" + CONTAINER_NAME: "node-status-agent" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.value }} already exists" + fi + + - name: Remove existing tag if exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml index 1a082a1ad2..941a8619f0 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -1,11 +1,55 @@ name: Build and upload Node Status API container to harbor.nymte.ch - on: workflow_dispatch: +env: + WORKING_DIRECTORY: "nym-node-status-api" + CONTAINER_NAME: "node-status-api" + jobs: - my-job: - runs-on: arc-ubuntu-22.04 + build-container: + runs-on: arc-ubuntu-22.04-dind steps: - - name: my-step - run: echo "Hello World!" + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.result }} already exists" + fi + + - name: Remove existing tag if exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/Cargo.lock b/Cargo.lock index aaffaa5570..65bf102040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,10 +318,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -455,6 +466,7 @@ checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core 0.4.5", + "axum-macros", "bytes", "futures-util", "http 1.1.0", @@ -553,6 +565,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "axum-test" version = "16.2.0" @@ -2331,6 +2354,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2373,6 +2405,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "explorer-api" version = "1.1.41" @@ -3593,7 +3646,7 @@ dependencies = [ "crossbeam-utils", "curl", "curl-sys", - "event-listener", + "event-listener 2.5.3", "futures-lite", "http 0.2.12", "log", @@ -4075,6 +4128,30 @@ dependencies = [ "wasm-utils", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version 0.4.0", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "multer" version = "2.1.0" @@ -4920,6 +4997,13 @@ dependencies = [ "nym-multisig-contract-common", ] +[[package]] +name = "nym-common-models" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "nym-compact-ecash" version = "0.1.0" @@ -5235,12 +5319,12 @@ dependencies = [ name = "nym-explorer-client" version = "0.1.0" dependencies = [ - "log", "nym-explorer-api-requests", "reqwest 0.12.4", "serde", "thiserror", "tokio", + "tracing", "url", ] @@ -5894,6 +5978,61 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-node-status-agent" +version = "0.1.4" +dependencies = [ + "anyhow", + "clap 4.5.18", + "nym-bin-common", + "nym-common-models", + "reqwest 0.12.4", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nym-node-status-api" +version = "0.1.5" +dependencies = [ + "anyhow", + "axum 0.7.7", + "chrono", + "clap 4.5.18", + "cosmwasm-std", + "envy", + "futures-util", + "moka", + "nym-bin-common", + "nym-common-models", + "nym-explorer-client", + "nym-network-defaults", + "nym-node-requests", + "nym-task", + "nym-validator-client", + "regex", + "reqwest 0.12.4", + "serde", + "serde_json", + "serde_json_path", + "sqlx", + "strum 0.26.3", + "strum_macros 0.26.4", + "thiserror", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-log 0.2.0", + "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", +] + [[package]] name = "nym-node-tester-utils" version = "0.1.0" @@ -6488,7 +6627,6 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", @@ -6512,6 +6650,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "ts-rs", "url", "wasmtimer", @@ -7421,6 +7560,21 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -7512,6 +7666,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -8361,9 +8524,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -8371,6 +8534,59 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_json_path" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba" +dependencies = [ + "inventory", + "nom", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_json_path_core", + "serde_json_path_macros", + "thiserror", +] + +[[package]] +name = "serde_json_path_core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79" +dependencies = [ + "inventory", + "once_cell", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "serde_json_path_macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723" +dependencies = [ + "inventory", + "once_cell", + "serde_json_path_core", + "serde_json_path_macros_internal", +] + +[[package]] +name = "serde_json_path_macros_internal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "serde_path_to_error" version = "0.1.16" @@ -8735,7 +8951,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -9145,6 +9361,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -9917,6 +10139,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index fd480fa59e..1dc988d673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ members = [ "common/ip-packet-requests", "common/ledger", "common/mixnode-common", + "common/models", "common/network-defaults", "common/node-tester-utils", "common/nonexhaustive-delayqueue", @@ -119,6 +120,8 @@ members = [ "nym-node", "nym-node/nym-node-http-api", "nym-node/nym-node-requests", + "nym-node-status-api", + "nym-node-status-agent", "nym-outfox", "nym-validator-rewarder", "tools/echo-server", @@ -146,13 +149,16 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "common/models", "explorer-api", "gateway", "mixnode", "nym-api", "nym-data-observatory", "nym-node", + "nym-node-status-api", "nym-validator-rewarder", + "nym-node-status-api", "service-providers/authenticator", "service-providers/ip-packet-router", "service-providers/network-requester", @@ -234,10 +240,12 @@ dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" etherparse = "0.13.0" +envy = "0.4" eyre = "0.6.9" fastrand = "2.1.1" flate2 = "1.0.34" futures = "0.3.28" +futures-util = "0.3" generic-array = "0.14.7" getrandom = "0.2.10" getset = "0.1.3" @@ -267,6 +275,7 @@ ledger-transport-hid = "0.10.0" log = "0.4" maxminddb = "0.23.0" mime = "0.3.17" +moka = { version = "0.12", features = ["future"] } nix = "0.27.1" notify = "5.1.0" okapi = "0.7.0" @@ -299,7 +308,8 @@ semver = "1.0.23" serde = "1.0.210" serde_bytes = "0.11.15" serde_derive = "1.0" -serde_json = "1.0.128" +serde_json = "1.0.132" +serde_json_path = "0.6.7" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" @@ -308,6 +318,7 @@ si-scale = "0.2.3" sphinx-packet = "0.1.1" sqlx = "0.7.4" strum = "0.26" +strum_macros = "0.26" subtle-encoding = "0.5" syn = "1" sysinfo = "0.30.13" @@ -329,6 +340,7 @@ tracing = "0.1.37" tracing-opentelemetry = "0.19.0" tracing-subscriber = "0.3.16" tracing-tree = "0.2.2" +tracing-log = "0.2" ts-rs = "10.0.0" tungstenite = { version = "0.20.1", default-features = false } url = "2.5" diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index b63631ddaa..11a2c76f2b 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -45,3 +45,4 @@ tracing = [ "opentelemetry", ] clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +models = [] diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index d17b100672..5cd86e2dba 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -25,7 +25,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } nym-http-api-client = { path = "../../../common/http-api-client" } thiserror = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } time = { workspace = true, features = ["formatting"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 6a13e8f186..071fb597bf 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -323,6 +323,13 @@ impl NymApiClient { NymApiClient { nym_api } } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { + let nym_api = nym_api::Client::new(api_url, Some(timeout)); + + NymApiClient { nym_api } + } + pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self { let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) .expect("invalid api url") diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 7876fd1918..3e71a3c000 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -121,36 +121,36 @@ async fn test_nyxd_connection( { Ok(Err(NyxdError::TendermintErrorRpc(e))) => { // If we get a tendermint-rpc error, we classify the node as not contactable - log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); + tracing::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); false } Ok(Err(NyxdError::AbciError { code, log, .. })) => { // We accept the mixnet contract not found as ok from a connection standpoint. This happens // for example on a pre-launch network. - log::debug!( + tracing::debug!( "Checking: nyxd url: {url}: {}, but with abci error: {code}: {log}", "success".green() ); code == 18 } Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => { - log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); + tracing::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); false } Ok(Err(e)) => { // For any other error, we're optimistic and just try anyway. - log::warn!( + tracing::warn!( "Checking: nyxd_url: {url}: {}, but with error: {e}", "success".green() ); true } Ok(Ok(_)) => { - log::debug!("Checking: nyxd_url: {url}: {}", "success".green()); + tracing::debug!("Checking: nyxd_url: {url}: {}", "success".green()); true } Err(e) => { - log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); + tracing::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); false } }; @@ -169,15 +169,15 @@ async fn test_nym_api_connection( .await { Ok(Ok(_)) => { - log::debug!("Checking: api_url: {url}: {}", "success".green()); + tracing::debug!("Checking: api_url: {url}: {}", "success".green()); true } Ok(Err(e)) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } Err(e) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } }; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index aaad043ad5..6556a80102 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -43,6 +43,7 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails}; use time::format_description::BorrowedFormatItem; use time::Date; +use tracing::instrument; pub mod error; pub mod routes; @@ -67,12 +68,14 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -87,6 +90,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -101,6 +105,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed_unfiltered( &self, ) -> Result, NymAPIError> { @@ -117,12 +122,14 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], @@ -132,6 +139,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], @@ -140,6 +148,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[tracing::instrument(level = "debug", skip_all)] async fn get_nodes_described( &self, page: Option, @@ -159,6 +168,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[tracing::instrument(level = "debug", skip_all)] async fn get_nym_nodes( &self, page: Option, @@ -179,6 +189,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -203,6 +214,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -228,6 +240,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_basic_entry_assigned_nodes( &self, semver_compatibility: Option, @@ -269,6 +282,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes( &self, semver_compatibility: Option, @@ -310,6 +324,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_basic_mixing_capable_nodes( &self, semver_compatibility: Option, @@ -348,6 +363,7 @@ pub trait NymApiClientExt: ApiClient { ) .await } + #[instrument(level = "debug", skip(self))] async fn get_basic_nodes( &self, @@ -382,6 +398,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], @@ -391,6 +408,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -406,6 +424,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], @@ -415,6 +434,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_report( &self, mix_id: NodeId, @@ -433,6 +453,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, @@ -451,6 +472,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_history( &self, mix_id: NodeId, @@ -469,6 +491,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, @@ -487,6 +510,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, NymAPIError> { @@ -504,6 +528,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -536,6 +561,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_core_status_count( &self, mix_id: NodeId, @@ -569,6 +595,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_status( &self, mix_id: NodeId, @@ -587,6 +614,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -605,6 +633,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn compute_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -625,6 +654,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_stake_saturation( &self, mix_id: NodeId, @@ -643,6 +673,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_inclusion_probability( &self, mix_id: NodeId, @@ -660,6 +691,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_current_node_performance( &self, node_id: NodeId, @@ -692,6 +724,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], @@ -701,6 +734,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED], @@ -709,6 +743,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -725,6 +760,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn verify_ecash_ticket( &self, request_body: &VerifyEcashTicketBody, @@ -741,6 +777,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn batch_redeem_ecash_tickets( &self, request_body: &BatchRedeemTicketsBody, @@ -758,6 +795,7 @@ pub trait NymApiClientExt: ApiClient { } #[deprecated] + #[instrument(level = "debug", skip(self))] async fn double_spending_filter_v1(&self) -> Result { self.get_json( &[ @@ -770,6 +808,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_expiration_date_signatures( &self, expiration_date: Option, @@ -793,6 +832,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_coin_indices_signatures( &self, epoch_id: Option, @@ -813,6 +853,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_expiration_date_signatures( &self, expiration_date: Option, @@ -836,6 +877,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_coin_indices_signatures( &self, epoch_id: Option, @@ -856,6 +898,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn master_verification_key( &self, epoch_id: Option, @@ -875,6 +918,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -891,6 +935,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credential( &self, credential_id: i64, @@ -907,6 +952,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credentials( &self, credential_ids: Vec, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index 4d8fd3237c..8674d7cb0a 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -8,9 +8,9 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; use cosmwasm_std::Addr; -use log::trace; use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse}; use serde::Deserialize; +use tracing::trace; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; pub use nym_coconut_dkg_common::{ diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 5cea0a1ba2..8feceebdbf 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -29,7 +29,6 @@ use cosmrs::proto::cosmwasm::wasm::v1::{ }; use cosmrs::tendermint::{block, chain, Hash}; use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; -use log::trace; use prost::Message; use serde::{Deserialize, Serialize}; @@ -68,7 +67,7 @@ pub trait CosmWasmClient: TendermintRpcClient { Res: Message + Default, { if let Some(ref abci_path) = path { - trace!("performing query on abci path {abci_path}") + tracing::trace!("performing query on abci path {abci_path}") } let mut buf = Vec::with_capacity(req.encoded_len()); req.encode(&mut buf)?; @@ -297,7 +296,7 @@ pub trait CosmWasmClient: TendermintRpcClient { let start = Instant::now(); loop { - log::debug!( + tracing::debug!( "Polling for result of including {} in a block...", broadcasted.hash ); @@ -522,7 +521,7 @@ pub trait CosmWasmClient: TendermintRpcClient { .make_abci_query::<_, QuerySmartContractStateResponse>(path, req) .await?; - trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); + tracing::trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); Ok(serde_json::from_slice(&res.data)?) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 8d0bb0fb4f..cec29e9c50 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -25,12 +25,12 @@ use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; -use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; +use tracing::debug; fn empty_fee() -> tx::Fee { tx::Fee { diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index d6e42daac7..559ad434a2 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -7,9 +7,9 @@ use base64::Engine; use cosmrs::abci::TxMsgData; use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; -use log::error; use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +use tracing::error; pub use cosmrs::abci::MsgResponse; diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 84ab2d2cb9..ce502c112a 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::time::Duration; use thiserror::Error; -use tracing::warn; +use tracing::{instrument, warn}; use url::Url; pub use reqwest::IntoUrl; @@ -205,6 +205,7 @@ impl Client { self.reqwest_client.get(url) } + #[instrument(level = "debug", skip_all, fields(path=?path))] async fn send_get_request( &self, path: PathSegments<'_>, @@ -215,6 +216,7 @@ impl Client { V: AsRef, E: Display, { + tracing::trace!("Sending GET request"); let url = sanitize_url(&self.base_url, path, params); #[cfg(target_arch = "wasm32")] @@ -280,6 +282,7 @@ impl Client { } } + #[instrument(level = "debug", skip_all)] pub async fn get_json( &self, path: PathSegments<'_>, @@ -515,12 +518,14 @@ pub fn sanitize_url, V: AsRef>( url } +#[tracing::instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> where T: DeserializeOwned, E: DeserializeOwned + Display, { let status = res.status(); + tracing::debug!("Status: {} (success: {})", &status, status.is_success()); if !allow_empty { if let Some(0) = res.content_length() { @@ -529,11 +534,19 @@ where } if res.status().is_success() { - let text = res.text().await?; - match serde_json::from_str(&text) { - Ok(res) => Ok(res), - Err(source) => Err(HttpClientError::ResponseDeserialisationFailure { source }), + #[cfg(debug_assertions)] + { + let text = res.text().await.inspect_err(|err| { + tracing::error!("Couldn't even get response text: {err}"); + })?; + tracing::trace!("Result:\n{:#?}", text); + + serde_json::from_str(&text) + .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) } + + #[cfg(not(debug_assertions))] + Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { diff --git a/common/models/Cargo.toml b/common/models/Cargo.toml new file mode 100644 index 0000000000..acb6e35682 --- /dev/null +++ b/common/models/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-common-models" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } diff --git a/common/models/src/lib.rs b/common/models/src/lib.rs new file mode 100644 index 0000000000..3d85e66947 --- /dev/null +++ b/common/models/src/lib.rs @@ -0,0 +1 @@ +pub mod ns_api; diff --git a/envs/canary.env b/envs/canary.env index c4aac15082..db70a19fd0 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -19,5 +19,5 @@ MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftq COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 EXPLORER_API=https://canary-explorer.performance.nymte.ch/api -NYXD="https://canary-validator.performance.nymte.ch" -NYM_API="https://canary-api.performance.nymte.ch/api" +NYXD=https://canary-validator.performance.nymte.ch +NYM_API=https://canary-api.performance.nymte.ch/api diff --git a/envs/mainnet.env b/envs/mainnet.env index e40e54a471..419547bf97 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -21,8 +21,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mp REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYXD="https://rpc.nymtech.net" -NYM_API="https://validator.nymtech.net/api/" +NYXD=https://rpc.nymtech.net +NYM_API=https://validator.nymtech.net/api/ NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" +EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API="https://nymvpn.com/api" diff --git a/envs/qa.env b/envs/qa.env index e88f845416..34306a62ea 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -19,5 +19,5 @@ VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8 REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api -NYXD="https://qa-validator.qa.nymte.ch" -NYM_API="https://qa-nym-api.qa.nymte.ch/api" +NYXD=https://qa-validator.qa.nymte.ch +NYM_API=https://qa-nym-api.qa.nymte.ch/api diff --git a/envs/sandbox.env b/envs/sandbox.env index 4763269a6f..6310b8fa89 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -20,6 +20,6 @@ ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jl STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" EXPLORER_API=https://sandbox-explorer.nymtech.net/api -NYXD="https://rpc.sandbox.nymtech.net" -NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket" -NYM_API="https://sandbox-nym-api1.nymtech.net/api" +NYXD=https://rpc.sandbox.nymtech.net +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket +NYM_API=https://sandbox-nym-api1.nymtech.net/api diff --git a/explorer-api/explorer-client/Cargo.toml b/explorer-api/explorer-client/Cargo.toml index 2397c83f7b..d32429e9a1 100644 --- a/explorer-api/explorer-client/Cargo.toml +++ b/explorer-api/explorer-client/Cargo.toml @@ -7,12 +7,12 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log.workspace = true nym-explorer-api-requests = { path = "../explorer-api-requests" } reqwest = { workspace = true, features = ["json"] } serde.workspace = true thiserror.workspace = true url.workspace = true +tracing = {workspace = true, features = ["attributes"]} [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index f23d10683d..999f23466b 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -3,6 +3,7 @@ use std::time::Duration; use reqwest::StatusCode; use thiserror::Error; +use tracing::instrument; use url::Url; // Re-export request types @@ -49,27 +50,28 @@ impl ExplorerClient { Ok(Self { client, url }) } - #[cfg(target_arch = "wasm32")] - pub fn new(url: url::Url) -> Result { - let client = reqwest::Client::builder().build()?; - Ok(Self { client, url }) - } - #[cfg(not(target_arch = "wasm32"))] pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result { let client = reqwest::Client::builder().timeout(timeout).build()?; Ok(Self { client, url }) } + #[cfg(target_arch = "wasm32")] + pub fn new(url: url::Url) -> Result { + let client = reqwest::Client::builder().build()?; + Ok(Self { client, url }) + } + async fn send_get_request( &self, paths: &[&str], ) -> Result { let url = combine_url(self.url.clone(), paths)?; - log::trace!("Sending GET request {url:?}"); + tracing::debug!("Sending GET request"); Ok(self.client.get(url).send().await?) } + #[instrument(level = "trace", skip_all, fields(paths=?paths))] async fn query_explorer_api(&self, paths: &[&str]) -> Result where T: std::fmt::Debug, @@ -78,12 +80,14 @@ impl ExplorerClient { let response = self.send_get_request(paths).await?; if response.status().is_success() { let res = response.json::().await?; - log::trace!("Got response: {res:?}"); + tracing::trace!("Got response: {res:?}"); Ok(res) } else if response.status() == StatusCode::NOT_FOUND { Err(ExplorerApiError::NotFound) } else { - Err(ExplorerApiError::RequestFailure(response.text().await?)) + let status = response.status(); + let err_msg = format!("{}: {}", response.text().await?, status); + Err(ExplorerApiError::RequestFailure(err_msg)) } } diff --git a/nym-data-observatory/README_SQLX.md b/nym-data-observatory/README_SQLX.md index 3e1021c8eb..72cca4294f 100644 --- a/nym-data-observatory/README_SQLX.md +++ b/nym-data-observatory/README_SQLX.md @@ -68,6 +68,7 @@ warning: no queries found; do you have the `offline` feature enabled ### Possible solutions - does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`? + + `cargo install -f sqlx-cli --version ` ``` cargo install sqlx-cli --version --force ``` diff --git a/nym-node-status-agent/.gitignore b/nym-node-status-agent/.gitignore new file mode 100644 index 0000000000..bf462f3e05 --- /dev/null +++ b/nym-node-status-agent/.gitignore @@ -0,0 +1 @@ +nym-gateway-probe diff --git a/nym-node-status-agent/Dockerfile b/nym-node-status-agent/Dockerfile new file mode 100644 index 0000000000..0280fc5687 --- /dev/null +++ b/nym-node-status-agent/Dockerfile @@ -0,0 +1,28 @@ +FROM rust:latest AS builder + +RUN apt update && apt install -yy libdbus-1-dev pkg-config libclang-dev + +# Install go +RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz -O go.tar.gz +RUN tar -xzvf go.tar.gz -C /usr/local + +RUN git clone https://github.com/nymtech/nym-vpn-client /usr/src/nym-vpn-client +ENV PATH=/go/bin:/usr/local/go/bin:$PATH +WORKDIR /usr/src/nym-vpn-client/nym-vpn-core +RUN cargo build --release --package nym-gateway-probe + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-agent +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./ +COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./ + +ENV NODE_STATUS_AGENT_PROBE_PATH=/nym/nym-gateway-probe +ENTRYPOINT [ "/nym/nym-node-status-agent" ] diff --git a/nym-node-status-agent/src/main.rs b/nym-node-status-agent/src/main.rs new file mode 100644 index 0000000000..133828b7fa --- /dev/null +++ b/nym-node-status-agent/src/main.rs @@ -0,0 +1,78 @@ +use crate::cli::Args; +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +mod cli; +mod probe; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing(); + let args = Args::parse(); + + let server_addr = format!("{}:{}", args.server_address, args.server_port); + test_ns_api_conn(&server_addr).await?; + + args.execute().await?; + + Ok(()) +} + +async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> { + reqwest::get(server_addr) + .await + .map(|res| { + tracing::info!( + "Testing connection to NS API at {server_addr}: {}", + res.status() + ); + }) + .map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err)) +} + +pub(crate) fn setup_tracing() { + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + } + + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore new file mode 100644 index 0000000000..91459afabe --- /dev/null +++ b/nym-node-status-api/.gitignore @@ -0,0 +1,6 @@ +data/ +enter_db.sh +nym-gateway-probe +nym-node-status-api +*.sqlite +*.sqlite-journal diff --git a/nym-node-status-api/Dockerfile b/nym-node-status-api/Dockerfile new file mode 100644 index 0000000000..ceab7c9392 --- /dev/null +++ b/nym-node-status-api/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-api + +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/Dockerfile.dev b/nym-node-status-api/Dockerfile.dev new file mode 100644 index 0000000000..2967a6e605 --- /dev/null +++ b/nym-node-status-api/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY nym-node-status-api/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs new file mode 100644 index 0000000000..84ee86577f --- /dev/null +++ b/nym-node-status-api/src/cli/mod.rs @@ -0,0 +1,77 @@ +use clap::Parser; +use nym_bin_common::bin_info; +use reqwest::Url; +use std::{sync::OnceLock, time::Duration}; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Clone, Debug, Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Network name for the network to which we're connecting. + #[clap(long, env = "NETWORK_NAME")] + pub(crate) network_name: String, + + /// Explorer api url. + #[clap(short, long, env = "EXPLORER_API")] + pub(crate) explorer_api: String, + + /// Nym api url. + #[clap(short, long, env = "NYM_API")] + pub(crate) nym_api: String, + + /// TTL for the http cache. + #[clap( + long, + default_value_t = 30, + env = "NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL" + )] + pub(crate) nym_http_cache_ttl: u64, + + /// HTTP port on which to run node status api. + #[clap(long, default_value_t = 8000, env = "NYM_NODE_STATUS_API_HTTP_PORT")] + pub(crate) http_port: u16, + + /// Nyxd address. + #[clap(long, env = "NYXD")] + pub(crate) nyxd_addr: Url, + + /// Nym api client timeout. + #[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) nym_api_client_timeout: Duration, + + /// Explorer api client timeout. + #[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) explorer_client_timeout: Duration, + + /// Connection url for the database. + #[clap(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) monitor_refresh_interval: Duration, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) testruns_refresh_interval: Duration, +} + +fn parse_duration(arg: &str) -> Result { + let seconds = arg.parse()?; + Ok(std::time::Duration::from_secs(seconds)) +} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs new file mode 100644 index 0000000000..86e25e9227 --- /dev/null +++ b/nym-node-status-api/src/db/mod.rs @@ -0,0 +1,35 @@ +use anyhow::{anyhow, Result}; +use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; +use std::str::FromStr; + +pub(crate) mod models; +pub(crate) mod queries; + +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + +pub(crate) type DbPool = SqlitePool; + +pub(crate) struct Storage { + pool: DbPool, +} + +impl Storage { + pub async fn init(connection_url: String) -> Result { + let connect_options = SqliteConnectOptions::from_str(&connection_url)? + .create_if_missing(true) + .disable_statement_logging(); + + let pool = sqlx::SqlitePool::connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + MIGRATOR.run(&pool).await?; + + Ok(Storage { pool }) + } + + /// Cloning pool is cheap, it's the same underlying set of connections + pub fn pool_owned(&self) -> DbPool { + self.pool.clone() + } +} diff --git a/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/src/db/queries/misc.rs new file mode 100644 index 0000000000..2aa6356051 --- /dev/null +++ b/nym-node-status-api/src/db/queries/misc.rs @@ -0,0 +1,88 @@ +use crate::db::{models::NetworkSummary, DbPool}; +use chrono::{DateTime, Utc}; + +/// take `last_updated` instead of calculating it so that `summary` matches +/// `daily_summary` +pub(crate) async fn insert_summaries( + pool: &DbPool, + summaries: &[(&str, &usize)], + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + insert_summary(pool, summaries, last_updated).await?; + + insert_summary_history(pool, summary, last_updated).await?; + + Ok(()) +} + +async fn insert_summary( + pool: &DbPool, + summaries: &[(&str, &usize)], + last_updated: DateTime, +) -> anyhow::Result<()> { + let timestamp = last_updated.timestamp(); + let mut tx = pool.begin().await?; + + for (kind, value) in summaries { + let value = value.to_string(); + sqlx::query!( + "INSERT INTO summary + (key, value_json, last_updated_utc) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value_json=excluded.value_json, + last_updated_utc=excluded.last_updated_utc;", + kind, + value, + timestamp + ) + .execute(&mut *tx) + .await + .map_err(|err| { + tracing::error!("Failed to insert data for {kind}: {err}, aborting transaction",); + err + })?; + } + + tx.commit().await?; + + Ok(()) +} + +/// For ``, `summary_history` is updated with fresh data on every +/// iteration. +/// +/// After UTC midnight, summary is inserted for `` and last entry for +/// `` stays there forever. +/// +/// This is not aggregate data, it's a set of latest data points +async fn insert_summary_history( + pool: &DbPool, + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + let value_json = serde_json::to_string(&summary)?; + let timestamp = last_updated.timestamp(); + let now_rfc3339 = last_updated.to_rfc3339(); + // YYYY-MM-DD, without time + let date = &now_rfc3339[..10]; + + sqlx::query!( + "INSERT INTO summary_history + (date, timestamp_utc, value_json) + VALUES (?, ?, ?) + ON CONFLICT(date) DO UPDATE SET + timestamp_utc=excluded.timestamp_utc, + value_json=excluded.value_json;", + date, + timestamp, + value_json + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs new file mode 100644 index 0000000000..58af9bd429 --- /dev/null +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -0,0 +1,177 @@ +use futures_util::TryStreamExt; +use nym_validator_client::models::MixNodeBondAnnotated; +use tracing::error; + +use crate::{ + db::{ + models::{BondedStatusDto, MixnodeDto, MixnodeRecord}, + DbPool, + }, + http::models::{DailyStats, Mixnode}, +}; + +pub(crate) async fn insert_mixnodes( + pool: &DbPool, + mixnodes: Vec, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + for record in mixnodes.iter() { + // https://www.sqlite.org/lang_upsert.html + sqlx::query!( + "INSERT INTO mixnodes + (mix_id, identity_key, bonded, total_stake, + host, http_api_port, blacklisted, full_details, + self_described, last_updated_utc, is_dp_delegatee) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(mix_id) DO UPDATE SET + bonded=excluded.bonded, + total_stake=excluded.total_stake, host=excluded.host, + http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted, + full_details=excluded.full_details,self_described=excluded.self_described, + last_updated_utc=excluded.last_updated_utc, + is_dp_delegatee = excluded.is_dp_delegatee;", + record.mix_id, + record.identity_key, + record.bonded, + record.total_stake, + record.host, + record.http_port, + record.blacklisted, + record.full_details, + record.self_described, + record.last_updated_utc, + record.is_dp_delegatee + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + MixnodeDto, + r#"SELECT + mn.mix_id as "mix_id!", + mn.bonded as "bonded: bool", + mn.blacklisted as "blacklisted: bool", + mn.is_dp_delegatee as "is_dp_delegatee: bool", + mn.total_stake as "total_stake!", + mn.full_details as "full_details!", + mn.self_described as "self_described", + mn.last_updated_utc as "last_updated_utc!", + COALESCE(md.moniker, "NA") as "moniker!", + COALESCE(md.website, "NA") as "website!", + COALESCE(md.security_contact, "NA") as "security_contact!", + COALESCE(md.details, "NA") as "details!" + FROM mixnodes mn + LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id + ORDER BY mn.mix_id"# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +/// We fetch the latest 30 days of data as a subquery and then +/// return it in ascending order, so we don't break existing UI +pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + DailyStats, + r#" + SELECT + date_utc as "date_utc!", + packets_received as "total_packets_received!: i64", + packets_sent as "total_packets_sent!: i64", + packets_dropped as "total_packets_dropped!: i64", + total_stake as "total_stake!: i64" + FROM ( + SELECT + date_utc, + SUM(packets_received) as packets_received, + SUM(packets_sent) as packets_sent, + SUM(packets_dropped) as packets_dropped, + SUM(total_stake) as total_stake + FROM mixnode_daily_stats + GROUP BY date_utc + ORDER BY date_utc DESC + LIMIT 30 + ) + GROUP BY date_utc + ORDER BY date_utc + "# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} + +/// Ensure all mixnodes that are set as bonded, are still bonded +pub(crate) async fn ensure_mixnodes_still_bonded( + pool: &DbPool, + mixnodes: &[MixNodeBondAnnotated], +) -> anyhow::Result { + let bonded_mixnodes_rows = get_all_bonded_mixnodes_row_ids_by_status(pool, true).await?; + let unbonded_mixnodes_rows = bonded_mixnodes_rows.iter().filter(|v| { + !mixnodes + .iter() + .any(|bonded| *bonded.mixnode_details.bond_information.identity() == v.identity_key) + }); + + let recently_unbonded_mixnodes = unbonded_mixnodes_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_mixnodes_rows { + sqlx::query!( + "UPDATE mixnodes + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_mixnodes) +} + +async fn get_all_bonded_mixnodes_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + identity_key as "identity_key!", + bonded as "bonded: bool" + FROM mixnodes + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/src/db/queries/summary.rs new file mode 100644 index 0000000000..103712a9a4 --- /dev/null +++ b/nym-node-status-api/src/db/queries/summary.rs @@ -0,0 +1,209 @@ +use chrono::{DateTime, Utc}; +use futures_util::TryStreamExt; +use std::collections::HashMap; +use tracing::error; + +use crate::{ + db::{ + models::{ + gateway::{ + GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded, + GatewaySummaryExplorer, GatewaySummaryHistorical, + }, + mixnode::{ + MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded, + MixnodeSummaryHistorical, + }, + NetworkSummary, SummaryDto, SummaryHistoryDto, + }, + DbPool, + }, + http::{ + error::{HttpError, HttpResult}, + models::SummaryHistory, + }, +}; + +pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + SummaryHistoryDto, + r#"SELECT + id as "id!", + date as "date!", + timestamp_utc as "timestamp_utc!", + value_json as "value_json!" + FROM summary_history + ORDER BY date DESC + LIMIT 30"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +async fn get_summary_dto(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + Ok(sqlx::query_as!( + SummaryDto, + r#"SELECT + key as "key!", + value_json as "value_json!", + last_updated_utc as "last_updated_utc!" + FROM summary"# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?) +} + +pub(crate) async fn get_summary(pool: &DbPool) -> HttpResult { + let items = get_summary_dto(pool).await.map_err(|err| { + tracing::error!("Couldn't get Summary from DB: {err}"); + HttpError::internal() + })?; + from_summary_dto(items).await +} + +async fn from_summary_dto(items: Vec) -> HttpResult { + const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; + const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; + const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; + const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; + const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; + const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; + const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; + const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + + // convert database rows into a map by key + let mut map = HashMap::new(); + for item in items { + map.insert(item.key.clone(), item); + } + + // check we have all the keys we are expecting, and build up a map of errors for missing one + let keys = [ + GATEWAYS_BONDED_COUNT, + GATEWAYS_EXPLORER_COUNT, + GATEWAYS_HISTORICAL_COUNT, + GATEWAYS_BLACKLISTED_COUNT, + MIXNODES_BLACKLISTED_COUNT, + MIXNODES_BONDED_ACTIVE, + MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, + MIXNODES_BONDED_RESERVE, + MIXNODES_HISTORICAL_COUNT, + ]; + + let mut errors: Vec<&str> = vec![]; + for key in keys { + if !map.contains_key(key) { + errors.push(key); + } + } + + // return an error if anything is missing, with a nice list + if !errors.is_empty() { + tracing::error!("Summary value missing: {}", errors.join(", ")); + return Err(HttpError::internal()); + } + + // strip the options and use default values (anything missing is trapped above) + let mixnodes_bonded_count: SummaryDto = + map.get(MIXNODES_BONDED_COUNT).cloned().unwrap_or_default(); + let mixnodes_bonded_active: SummaryDto = + map.get(MIXNODES_BONDED_ACTIVE).cloned().unwrap_or_default(); + let mixnodes_bonded_inactive: SummaryDto = map + .get(MIXNODES_BONDED_INACTIVE) + .cloned() + .unwrap_or_default(); + let mixnodes_bonded_reserve: SummaryDto = map + .get(MIXNODES_BONDED_RESERVE) + .cloned() + .unwrap_or_default(); + let mixnodes_blacklisted_count: SummaryDto = map + .get(MIXNODES_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_bonded_count: SummaryDto = + map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default(); + let gateways_explorer_count: SummaryDto = map + .get(GATEWAYS_EXPLORER_COUNT) + .cloned() + .unwrap_or_default(); + let mixnodes_historical_count: SummaryDto = map + .get(MIXNODES_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_historical_count: SummaryDto = map + .get(GATEWAYS_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_blacklisted_count: SummaryDto = map + .get(GATEWAYS_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + + Ok(NetworkSummary { + mixnodes: MixnodeSummary { + bonded: MixnodeSummaryBonded { + count: to_count_i32(&mixnodes_bonded_count), + active: to_count_i32(&mixnodes_bonded_active), + reserve: to_count_i32(&mixnodes_bonded_reserve), + inactive: to_count_i32(&mixnodes_bonded_inactive), + last_updated_utc: to_timestamp(&mixnodes_bonded_count), + }, + blacklisted: MixnodeSummaryBlacklisted { + count: to_count_i32(&mixnodes_blacklisted_count), + last_updated_utc: to_timestamp(&mixnodes_blacklisted_count), + }, + historical: MixnodeSummaryHistorical { + count: to_count_i32(&mixnodes_historical_count), + last_updated_utc: to_timestamp(&mixnodes_historical_count), + }, + }, + gateways: GatewaySummary { + bonded: GatewaySummaryBonded { + count: to_count_i32(&gateways_bonded_count), + last_updated_utc: to_timestamp(&gateways_bonded_count), + }, + blacklisted: GatewaySummaryBlacklisted { + count: to_count_i32(&gateways_blacklisted_count), + last_updated_utc: to_timestamp(&gateways_blacklisted_count), + }, + historical: GatewaySummaryHistorical { + count: to_count_i32(&gateways_historical_count), + last_updated_utc: to_timestamp(&gateways_historical_count), + }, + explorer: GatewaySummaryExplorer { + count: to_count_i32(&gateways_explorer_count), + last_updated_utc: to_timestamp(&gateways_explorer_count), + }, + }, + }) +} + +fn to_count_i32(value: &SummaryDto) -> i32 { + value.value_json.parse::().unwrap_or_default() +} + +fn to_timestamp(value: &SummaryDto) -> String { + timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339() +} + +fn timestamp_as_utc(unix_timestamp: u64) -> DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} diff --git a/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/src/http/api/gateways.rs new file mode 100644 index 0000000000..c1f4073767 --- /dev/null +++ b/nym-node-status-api/src/http/api/gateways.rs @@ -0,0 +1,110 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{Gateway, GatewaySkinny}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(gateways)) + .route("/skinny", axum::routing::get(gateways_skinny)) + .route("/:identity_key", axum::routing::get(get_gateway)) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways", + responses( + (status = 200, body = PagedGateway) + ) +)] +async fn gateways( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways/skinny", + responses( + (status = 200, body = PagedGatewaySkinny) + ) +)] +async fn gateways_skinny( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + let res: Vec = res + .iter() + .filter(|g| g.bonded) + .map(|g| GatewaySkinny { + gateway_identity_key: g.gateway_identity_key.clone(), + self_described: g.self_described.clone(), + performance: g.performance, + explorer_pretty_bond: g.explorer_pretty_bond.clone(), + last_probe_result: g.last_probe_result.clone(), + last_testrun_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + routing_score: g.routing_score, + config_score: g.config_score, + }) + .collect(); + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdentityKeyParam { + identity_key: String, +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + IdentityKeyParam + ), + path = "/v2/gateways/{identity_key}", + responses( + (status = 200, body = Gateway) + ) +)] +async fn get_gateway( + Path(IdentityKeyParam { identity_key }): Path, + State(state): State, +) -> HttpResult> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + match res + .iter() + .find(|item| item.gateway_identity_key == identity_key) + { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(identity_key)), + } +} diff --git a/nym-node-status-api/src/http/api/mixnodes.rs b/nym-node-status-api/src/http/api/mixnodes.rs new file mode 100644 index 0000000000..f42d0bf91c --- /dev/null +++ b/nym-node-status-api/src/http/api/mixnodes.rs @@ -0,0 +1,91 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use tracing::instrument; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{DailyStats, Mixnode}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(mixnodes)) + .route("/:mix_id", axum::routing::get(get_mixnodes)) + .route("/stats", axum::routing::get(get_stats)) +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + Pagination + ), + path = "/v2/mixnodes", + responses( + (status = 200, body = PagedMixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))] +async fn mixnodes( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_mixnodes_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct MixIdParam { + mix_id: String, +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + MixIdParam + ), + path = "/v2/mixnodes/{mix_id}", + responses( + (status = 200, body = Mixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(mix_id = mix_id))] +async fn get_mixnodes( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> HttpResult> { + match mix_id.parse::() { + Ok(parsed_mix_id) => { + let res = state.cache().get_mixnodes_list(state.db_pool()).await; + + match res.iter().find(|item| item.mix_id == parsed_mix_id) { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(mix_id)), + } + } + Err(_e) => Err(HttpError::invalid_input(mix_id)), + } +} + +#[utoipa::path( + tag = "Mixnodes", + get, + path = "/v2/mixnodes/stats", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_stats(State(state): State) -> HttpResult>> { + let stats = state.cache().get_mixnode_stats(state.db_pool()).await; + Ok(Json(stats)) +} diff --git a/nym-node-status-api/src/http/api/services/json_path.rs b/nym-node-status-api/src/http/api/services/json_path.rs new file mode 100644 index 0000000000..caefc6489d --- /dev/null +++ b/nym-node-status-api/src/http/api/services/json_path.rs @@ -0,0 +1,58 @@ +use serde_json_path::JsonPath; + +use crate::http::models::Gateway; + +pub(super) struct ParseJsonPaths { + pub(super) path_ip_address: JsonPath, + pub(super) path_hostname: JsonPath, + pub(super) path_service_provider_client_id: JsonPath, +} + +impl ParseJsonPaths { + pub fn new() -> Result { + Ok(ParseJsonPaths { + path_ip_address: JsonPath::parse("$.host_information.ip_address[0]")?, + path_hostname: JsonPath::parse("$.host_information.hostname")?, + path_service_provider_client_id: JsonPath::parse("$.network_requester.address")?, + }) + } +} + +pub(super) struct ParsedDetails { + pub(super) ip_address: Option, + pub(super) hostname: Option, + pub(super) service_provider_client_id: Option, +} + +impl ParsedDetails { + fn get_string_from_json_path( + value: &Option, + path: &JsonPath, + ) -> Option { + match value { + Some(value) => path + .query(value) + .exactly_one() + .map(|v2| v2.as_str().map(|v3| v3.to_string())) + .ok() + .flatten(), + None => None, + } + } + pub fn new(paths: &ParseJsonPaths, g: &Gateway) -> ParsedDetails { + ParsedDetails { + hostname: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_hostname, + ), + ip_address: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_ip_address, + ), + service_provider_client_id: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_service_provider_client_id, + ), + } + } +} diff --git a/nym-node-status-api/src/http/api/services/mod.rs b/nym-node-status-api/src/http/api/services/mod.rs new file mode 100644 index 0000000000..5650684c43 --- /dev/null +++ b/nym-node-status-api/src/http/api/services/mod.rs @@ -0,0 +1,134 @@ +use crate::http::{ + error::{HttpError, HttpResult}, + models::Service, + state::AppState, + PagedResult, Pagination, +}; +use axum::{ + extract::{Query, State}, + Json, Router, +}; +use json_path::{ParseJsonPaths, ParsedDetails}; +use tracing::instrument; + +mod json_path; + +pub(crate) fn routes() -> Router { + Router::new().route("/", axum::routing::get(mixnodes)) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct ServicesQueryParams { + size: Option, + page: Option, + wss: Option, + hostname: Option, + entry: Option, +} + +#[utoipa::path( + tag = "Services", + get, + params( + ServicesQueryParams, + ), + path = "/v2/services", + responses( + (status = 200, body = PagedService) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip(state))] +async fn mixnodes( + Query(ServicesQueryParams { + size, + page, + wss, + hostname, + entry, + }): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let cache = state.cache(); + + let show_only_wss = wss.unwrap_or(false); + let show_only_with_hostname = hostname.unwrap_or(false); + let show_entry_gateways_only = entry.unwrap_or(false); + + let paths = ParseJsonPaths::new().map_err(|e| { + tracing::error!("Invalidly configured ParseJsonPaths: {e}"); + HttpError::internal() + })?; + let res = cache.get_gateway_list(db).await; + let res: Vec = res + .iter() + .map(|g| { + let details = ParsedDetails::new(&paths, g); + + let s = Service { + gateway_identity_key: g.gateway_identity_key.clone(), + ip_address: details.ip_address, + service_provider_client_id: details.service_provider_client_id, + hostname: details.hostname, + last_successful_ping_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + // routing_score: g.routing_score, + routing_score: 1f32, + mixnet_websockets: g + .self_described + .clone() + .and_then(|s| s.get("mixnet_websockets").cloned()), + }; + + let f = ServiceFilter::new(&s); + + (s, f) + }) + .filter(|(_, f)| { + let mut keep = f.has_network_requester_sp; + + if show_entry_gateways_only { + keep = true; + } + + if show_only_wss { + keep &= f.has_wss; + } + if show_only_with_hostname { + keep &= f.has_hostname; + } + + keep + }) + .map(|(s, _)| s) + .collect(); + + Ok(Json(PagedResult::paginate(Pagination { size, page }, res))) +} + +struct ServiceFilter { + has_wss: bool, + has_network_requester_sp: bool, + has_hostname: bool, +} + +impl ServiceFilter { + fn new(s: &Service) -> Self { + let has_wss = match &s.mixnet_websockets { + Some(v) => v.get("wss_port").map(|v2| !v2.is_null()).unwrap_or(false), + None => false, + }; + let has_hostname = s.hostname.is_some(); + let has_network_requester_sp = match &s.service_provider_client_id { + Some(v) => !v.is_empty(), + None => false, + }; + + ServiceFilter { + has_wss, + has_hostname, + has_network_requester_sp, + } + } +} diff --git a/nym-node-status-api/src/http/api/summary.rs b/nym-node-status-api/src/http/api/summary.rs new file mode 100644 index 0000000000..729141509c --- /dev/null +++ b/nym-node-status-api/src/http/api/summary.rs @@ -0,0 +1,43 @@ +use axum::{extract::State, Json, Router}; +use tracing::instrument; + +use crate::{ + db::models::NetworkSummary, + http::{error::HttpResult, models::SummaryHistory, state::AppState}, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(summary)) + .route("/history", axum::routing::get(summary_history)) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary", + responses( + (status = 200, body = NetworkSummary) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary(State(state): State) -> HttpResult> { + crate::db::queries::get_summary(state.db_pool()) + .await + .map(Json) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary/history", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary_history(State(state): State) -> HttpResult>> { + Ok(Json( + state.cache().get_summary_history(state.db_pool()).await, + )) +} diff --git a/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/src/http/api_docs.rs new file mode 100644 index 0000000000..9ac5238fc4 --- /dev/null +++ b/nym-node-status-api/src/http/api_docs.rs @@ -0,0 +1,15 @@ +use crate::http::{Gateway, GatewaySkinny, Mixnode, Service}; +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// manually import external structs which are behind feature flags because they +// can't be automatically discovered +// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829 +#[utoipauto(paths = "./nym-node-status-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Node Status API"), + tags(), + components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,)) +)] +pub(super) struct ApiDoc; diff --git a/nym-node-status-api/src/http/mod.rs b/nym-node-status-api/src/http/mod.rs new file mode 100644 index 0000000000..1cc317337f --- /dev/null +++ b/nym-node-status-api/src/http/mod.rs @@ -0,0 +1,71 @@ +use models::{Gateway, GatewaySkinny, Mixnode, Service}; + +pub(crate) mod api; +pub(crate) mod api_docs; +pub(crate) mod error; +pub(crate) mod models; +pub(crate) mod server; +pub(crate) mod state; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] +// exclude generic from auto-discovery +#[utoipauto::utoipa_ignore] +// https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#generic-schemas-with-aliases +// Generic structs can only be included via aliases, not directly, because they +// it would cause an error in generated Swagger docs. +// Instead, you have to manually monomorphize each generic struct that +// you wish to document +#[aliases( + PagedGateway = PagedResult, + PagedGatewaySkinny = PagedResult, + PagedMixnode = PagedResult, + PagedService = PagedResult, +)] +pub struct PagedResult { + pub page: usize, + pub size: usize, + pub total: usize, + pub items: Vec, +} + +impl PagedResult { + pub fn paginate(pagination: Pagination, res: Vec) -> Self { + let total = res.len(); + let (size, mut page) = pagination.intoto_inner_values(); + + if page * size > total { + page = total / size; + } + + let chunks: Vec<&[T]> = res.chunks(size).collect(); + + PagedResult { + page, + size, + total, + items: chunks.get(page).cloned().unwrap_or_default().into(), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct Pagination { + size: Option, + page: Option, +} + +impl Pagination { + // unwrap stored values or use predefined defaults + pub(crate) fn intoto_inner_values(self) -> (usize, usize) { + const SIZE_DEFAULT: usize = 10; + const SIZE_MAX: usize = 200; + + const PAGE_DEFAULT: usize = 0; + + ( + self.size.unwrap_or(SIZE_DEFAULT).min(SIZE_MAX), + self.page.unwrap_or(PAGE_DEFAULT), + ) + } +} diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs new file mode 100644 index 0000000000..17d5d64ab0 --- /dev/null +++ b/nym-node-status-api/src/http/server.rs @@ -0,0 +1,93 @@ +use axum::Router; +use core::net::SocketAddr; +use tokio::{net::TcpListener, task::JoinHandle}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + +use crate::{ + db::DbPool, + http::{api::RouterBuilder, state::AppState}, +}; + +/// Return handles that allow for graceful shutdown of server + awaiting its +/// background tokio task +pub(crate) async fn start_http_api( + db_pool: DbPool, + http_port: u16, + nym_http_cache_ttl: u64, +) -> anyhow::Result { + let router_builder = RouterBuilder::with_default_routes(); + + let state = AppState::new(db_pool, nym_http_cache_ttl); + let router = router_builder.with_state(state); + + // TODO dz do we need this to be configurable? + let bind_addr = format!("0.0.0.0:{}", http_port); + tracing::info!("Binding server to {bind_addr}"); + let server = router.build_server(bind_addr).await?; + + Ok(start_server(server)) +} + +fn start_server(server: HttpServer) -> ShutdownHandles { + // one copy is stored to trigger a graceful shutdown later + let shutdown_button = CancellationToken::new(); + // other copy is given to server to listen for a shutdown + let shutdown_receiver = shutdown_button.clone(); + let shutdown_receiver = shutdown_receiver.cancelled_owned(); + + let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await }); + + ShutdownHandles { + server_handle, + shutdown_button, + } +} + +pub(crate) struct ShutdownHandles { + server_handle: JoinHandle>, + shutdown_button: CancellationToken, +} + +impl ShutdownHandles { + /// Send graceful shutdown signal to server and wait for server task to complete + pub(crate) async fn shutdown(self) -> anyhow::Result<()> { + self.shutdown_button.cancel(); + + match self.server_handle.await { + Ok(Ok(_)) => { + tracing::info!("HTTP server shut down without errors"); + } + Ok(Err(err)) => { + tracing::error!("HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + tracing::error!("Server task panicked: {err}"); + } + }; + + Ok(()) + } +} + +pub(crate) struct HttpServer { + router: Router, + listener: TcpListener, +} + +impl HttpServer { + pub(crate) fn new(router: Router, listener: TcpListener) -> Self { + Self { router, listener } + } + + pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/nym-node-status-api/src/http/state.rs b/nym-node-status-api/src/http/state.rs new file mode 100644 index 0000000000..6bccea39a1 --- /dev/null +++ b/nym-node-status-api/src/http/state.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use moka::{future::Cache, Entry}; +use tokio::sync::RwLock; + +use crate::{ + db::DbPool, + http::models::{DailyStats, Gateway, Mixnode, SummaryHistory}, +}; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + db_pool: DbPool, + cache: HttpCache, +} + +impl AppState { + pub(crate) fn new(db_pool: DbPool, cache_ttl: u64) -> Self { + Self { + db_pool, + cache: HttpCache::new(cache_ttl), + } + } + + pub(crate) fn db_pool(&self) -> &DbPool { + &self.db_pool + } + + pub(crate) fn cache(&self) -> &HttpCache { + &self.cache + } +} + +static GATEWAYS_LIST_KEY: &str = "gateways"; +static MIXNODES_LIST_KEY: &str = "mixnodes"; +static MIXSTATS_LIST_KEY: &str = "mixstats"; +static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; + +#[derive(Debug, Clone)] +pub(crate) struct HttpCache { + gateways: Cache>>>, + mixnodes: Cache>>>, + mixstats: Cache>>>, + history: Cache>>>, +} + +impl HttpCache { + pub fn new(ttl_seconds: u64) -> Self { + HttpCache { + gateways: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixnodes: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixstats: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + history: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + } + } + + pub async fn upsert_gateway_list( + &self, + new_gateway_list: Vec, + ) -> Entry>>> { + self.gateways + .entry_by_ref(GATEWAYS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_gateway_list; + v.clone() + } else { + Arc::new(RwLock::new(new_gateway_list)) + } + }) + .await + } + + pub async fn get_gateway_list(&self, db: &DbPool) -> Vec { + match self.gateways.get(GATEWAYS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + // the key is missing so populate it + tracing::warn!("No gateways in cache, refreshing cache from DB..."); + + let gateways = crate::db::queries::get_all_gateways(db) + .await + .unwrap_or_default(); + self.upsert_gateway_list(gateways.clone()).await; + + if gateways.is_empty() { + tracing::warn!("Database contains 0 gateways"); + } + + gateways + } + } + } + + pub async fn upsert_mixnode_list( + &self, + new_mixnode_list: Vec, + ) -> Entry>>> { + self.mixnodes + .entry_by_ref(MIXNODES_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_mixnode_list; + v.clone() + } else { + Arc::new(RwLock::new(new_mixnode_list)) + } + }) + .await + } + + pub async fn get_mixnodes_list(&self, db: &DbPool) -> Vec { + match self.mixnodes.get(MIXNODES_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + tracing::warn!("No mixnodes in cache, refreshing cache from DB..."); + + let mixnodes = crate::db::queries::get_all_mixnodes(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_list(mixnodes.clone()).await; + + if mixnodes.is_empty() { + tracing::warn!("Database contains 0 mixnodes"); + } + + mixnodes + } + } + } + + pub async fn upsert_mixnode_stats( + &self, + mixnode_stats: Vec, + ) -> Entry>>> { + self.mixstats + .entry_by_ref(MIXSTATS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = mixnode_stats; + v.clone() + } else { + Arc::new(RwLock::new(mixnode_stats)) + } + }) + .await + } + + pub async fn get_mixnode_stats(&self, db: &DbPool) -> Vec { + match self.mixstats.get(MIXSTATS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let mixnode_stats = crate::db::queries::get_daily_stats(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_stats(mixnode_stats.clone()).await; + mixnode_stats + } + } + } + + pub async fn get_summary_history(&self, db: &DbPool) -> Vec { + match self.history.get(SUMMARY_HISTORY_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let summary_history = crate::db::queries::get_summary_history(db) + .await + .unwrap_or(vec![]); + self.upsert_summary_history(summary_history.clone()).await; + summary_history + } + } + } + + pub async fn upsert_summary_history( + &self, + summary_history: Vec, + ) -> Entry>>> { + self.history + .entry_by_ref(SUMMARY_HISTORY_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = summary_history; + v.clone() + } else { + Arc::new(RwLock::new(summary_history)) + } + }) + .await + } +} diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs new file mode 100644 index 0000000000..6bdad3bae5 --- /dev/null +++ b/nym-node-status-api/src/logging.rs @@ -0,0 +1,69 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +// TODO dz you can get the tracing-subscriber via basic-tracing feature on nym-bin-common +pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { + fn directive_checked(directive: impl Into) -> anyhow::Result { + directive.into().parse().map_err(From::from) + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy(); + + // these crates are more granularly filtered + let warn_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in warn_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + } + + let log_level_hint = filter.max_level_hint(); + + // debug or higher granularity (e.g. trace) + let debug_or_higher = std::cmp::max( + log_level_hint.unwrap_or(LevelFilter::DEBUG), + LevelFilter::DEBUG, + ); + filter = filter.add_directive(directive_checked(format!( + "nym_bin_common={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_explorer_client={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_network_defaults={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_validator_client={}", + debug_or_higher + ))?); + + log_builder.with_env_filter(filter).init(); + tracing::info!("Log level: {:?}", log_level_hint); + + Ok(()) +} diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs new file mode 100644 index 0000000000..b2c68b391b --- /dev/null +++ b/nym-node-status-api/src/main.rs @@ -0,0 +1,53 @@ +use clap::Parser; +use nym_task::signal::wait_for_signal; + +mod cli; +mod db; +mod http; +mod logging; +mod monitor; +mod testruns; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logging::setup_tracing_logger()?; + + let args = cli::Cli::parse(); + + let connection_url = args.database_url.clone(); + tracing::debug!("Using config:\n{:#?}", args); + + let storage = db::Storage::init(connection_url).await?; + let db_pool = storage.pool_owned(); + let args_clone = args.clone(); + tokio::spawn(async move { + monitor::spawn_in_background( + db_pool, + args_clone.explorer_client_timeout, + args_clone.nym_api_client_timeout, + &args_clone.nyxd_addr, + args_clone.monitor_refresh_interval, + ) + .await; + tracing::info!("Started monitor task"); + }); + testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await; + + let shutdown_handles = http::server::start_http_api( + storage.pool_owned(), + args.http_port, + args.nym_http_cache_ttl, + ) + .await + .expect("Failed to start server"); + + tracing::info!("Started HTTP server on port {}", args.http_port); + + wait_for_signal().await; + + if let Err(err) = shutdown_handles.shutdown().await { + tracing::error!("{err}"); + }; + + Ok(()) +} diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index a9f3761f35..d0d1c5f638 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, diff --git a/nym-node-status-api/src/testruns/models.rs b/nym-node-status-api/src/testruns/models.rs new file mode 100644 index 0000000000..fe4b33384c --- /dev/null +++ b/nym-node-status-api/src/testruns/models.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct TestRun { + pub id: u32, + pub identity_key: String, + pub status: String, + pub log: String, +} From b73561f1c9386a7d1e8e95054986fbe8083291f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Fri, 1 Nov 2024 15:40:22 +0200 Subject: [PATCH 10/13] Fix gateway decreasing bandwidth (#5075) * Update storage peers after periodic check * Reset storage bytes on restart * Fix clippy --- common/wireguard/src/lib.rs | 15 ++++++++++++++- common/wireguard/src/peer_handle.rs | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 6567c2102c..ecd1576926 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -99,12 +99,25 @@ pub async fn start_wireguard let peers = all_peers .into_iter() .map(Peer::try_from) - .collect::, _>>()?; + .collect::, _>>()? + .into_iter() + .map(|mut peer| { + // since WGApi doesn't set those values on init, let's set them to 0 + peer.rx_bytes = 0; + peer.tx_bytes = 0; + peer + }) + .collect::>(); for peer in peers.iter() { let bandwidth_manager = PeerController::generate_bandwidth_manager(storage.clone(), &peer.public_key) .await? .map(|bw_m| Arc::new(RwLock::new(bw_m))); + // Update storage with *x_bytes set to 0, as in kernel peers we can't set those values + // so we need to restart counting. Hopefully the bandwidth was counted in available_bandwidth + storage + .insert_wireguard_peer(peer, bandwidth_manager.is_some()) + .await?; peer_bandwidth_managers.insert(peer.public_key.clone(), bandwidth_manager); } wg_api.create_interface()?; diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 9b737c53d0..3b6f13d960 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -75,8 +75,8 @@ impl PeerHandle { async fn active_peer( &mut self, - storage_peer: WireguardPeer, - kernel_peer: Peer, + storage_peer: &WireguardPeer, + kernel_peer: &Peer, ) -> Result { if let Some(bandwidth_manager) = &self.bandwidth_storage_manager { let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes) @@ -136,9 +136,12 @@ impl PeerHandle { log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key); return Ok(()); }; - if !self.active_peer(storage_peer, kernel_peer).await? { + if !self.active_peer(&storage_peer, &kernel_peer).await? { log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key); return Ok(()); + } else { + // Update storage values + self.storage.insert_wireguard_peer(&kernel_peer, self.bandwidth_storage_manager.is_some()).await?; } } From f9b363648f5ad1c632079295afb15959175658da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Fri, 1 Nov 2024 16:01:24 +0200 Subject: [PATCH 11/13] Fix expiration date as today + 7 days (#5076) --- common/credential-verification/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 0ffe090a06..066953fc55 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use time::{Date, OffsetDateTime}; use tracing::*; -use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::Storage; @@ -131,7 +131,7 @@ impl CredentialVerifier { let bandwidth = Bandwidth::ticket_amount(credential_type.into()); self.bandwidth_storage_manager - .increase_bandwidth(bandwidth, spend_date) + .increase_bandwidth(bandwidth, cred_exp_date()) .await?; Ok(self From 80b590d50dd6a5a2c82abcd32f17b70a6745fd1f Mon Sep 17 00:00:00 2001 From: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> Date: Fri, 1 Nov 2024 20:48:04 +0000 Subject: [PATCH 12/13] bug-fix: nym-credential-proxy webhook request is the correct shape and added reporting errors via webhook (#5077) Co-authored-by: Mark Sinclair --- nym-credential-proxy/Cargo.lock | 9 ++- .../src/api/v1/ticketbook/models.rs | 18 +++-- .../nym-credential-proxy/Cargo.toml | 2 +- .../src/credentials/ticketbook/mod.rs | 79 +++++++++++++++++-- .../src/http/router/api/v1/openapi.rs | 4 +- .../src/http/router/api/v1/ticketbook/mod.rs | 7 +- nym-wallet/Cargo.lock | 2 +- 7 files changed, 98 insertions(+), 23 deletions(-) diff --git a/nym-credential-proxy/Cargo.lock b/nym-credential-proxy/Cargo.lock index 01f25f0932..50e39249dc 100644 --- a/nym-credential-proxy/Cargo.lock +++ b/nym-credential-proxy/Cargo.lock @@ -2382,7 +2382,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.1" +version = "0.1.3" dependencies = [ "anyhow", "async-trait", @@ -2691,7 +2691,6 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", @@ -2715,6 +2714,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "url", "wasmtimer", "zeroize", @@ -3791,9 +3791,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -4917,6 +4917,7 @@ dependencies = [ "quote", "regex", "syn 2.0.79", + "uuid", ] [[package]] diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs index 46a65060ad..1d88b28d00 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs @@ -10,7 +10,7 @@ use schemars::schema::Schema; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; -use time::Date; +use time::{Date, OffsetDateTime}; #[cfg(feature = "query-types")] use nym_http_api_common::Output; @@ -236,22 +236,28 @@ pub struct TicketbookWalletSharesAsyncResponse { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] -pub struct BlindedWalletSharesResponse { +pub struct WebhookTicketbookWalletShares { pub id: i64, pub status: String, pub device_id: String, pub credential_id: String, pub data: Option, pub error_message: Option, - pub created: String, - pub updated: String, + + #[schemars(with = "String")] + #[serde(with = "time::serde::rfc3339")] + pub created: OffsetDateTime, + + #[schemars(with = "String")] + #[serde(with = "time::serde::rfc3339")] + pub updated: OffsetDateTime, } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] -pub struct WebhookBlindedSharesResponse { - pub blinded_shares: BlindedWalletSharesResponse, +pub struct WebhookTicketbookWalletSharesRequest { + pub ticketbook_wallet_shares: WebhookTicketbookWalletShares, pub secret: String, } diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index f04586b452..143e163f54 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-credential-proxy" -version = "0.1.2" +version = "0.1.3" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 217aa6e89b..3a91724a4f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -3,10 +3,12 @@ use crate::error::VpnApiError; use crate::http::state::ApiState; +use crate::storage::models::BlindedShares; use futures::{stream, StreamExt}; use nym_credential_proxy_requests::api::v1::ticketbook::models::{ TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, - TicketbookWalletSharesResponse, WalletShare, + TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, + WebhookTicketbookWalletSharesRequest, }; use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::Base58; @@ -217,11 +219,13 @@ async fn try_obtain_blinded_ticketbook_async_inner( requested_on: OffsetDateTime, request_data: TicketbookAsyncRequest, params: TicketbookObtainQueryParams, + pending: &BlindedShares, ) -> Result<(), VpnApiError> { let epoch_id = state.current_epoch_id().await?; let device_id = &request_data.device_id; let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); // 1. try to obtain global data let ( @@ -259,19 +263,70 @@ async fn try_obtain_blinded_ticketbook_async_inner( error!(uuid = %request, "failed to update db with issued information: {err}") } - // 4. build the response - let response = TicketbookWalletSharesResponse { + // 4. build the webhook request body + let data = Some(TicketbookWalletSharesResponse { epoch_id, shares, master_verification_key, aggregated_coin_index_signatures, aggregated_expiration_date_signatures, + }); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: pending.status.to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data, + error_message: None, + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, }; // 5. call the webhook state .zk_nym_web_hook() - .try_trigger(request, &response) + .try_trigger(request, &webhook_request) + .await; + + Ok(()) +} + +async fn try_trigger_webhook_request_for_error( + state: &ApiState, + request: Uuid, + request_data: TicketbookAsyncRequest, + pending: &BlindedShares, + error_message: String, +) -> Result<(), VpnApiError> { + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: "error".to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data: None, + error_message: Some(error_message), + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + state + .zk_nym_web_hook() + .try_trigger(request, &webhook_request) .await; Ok(()) @@ -285,16 +340,30 @@ pub(crate) async fn try_obtain_blinded_ticketbook_async( requested_on: OffsetDateTime, request_data: TicketbookAsyncRequest, params: TicketbookObtainQueryParams, + pending: BlindedShares, ) { if let Err(err) = try_obtain_blinded_ticketbook_async_inner( &state, request, requested_on, - request_data, + request_data.clone(), params, + &pending, ) .await { + // post to the webhook to notify of errors on this side + if let Err(webhook_err) = try_trigger_webhook_request_for_error( + &state, + request, + request_data, + &pending, + format!("Failed to get ticketbook: {err}"), + ) + .await + { + error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}") + } error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") } else { info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs index 9769ab415c..2315a658a1 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs @@ -84,8 +84,8 @@ pub(crate) struct ApiDoc; api_requests::v1::ticketbook::models::WalletShare, api_requests::v1::ticketbook::models::TicketbookWalletSharesResponse, api_requests::v1::ticketbook::models::TicketbookWalletSharesAsyncResponse, - api_requests::v1::ticketbook::models::BlindedWalletSharesResponse, - api_requests::v1::ticketbook::models::WebhookBlindedSharesResponse, + api_requests::v1::ticketbook::models::WebhookTicketbookWalletShares, + api_requests::v1::ticketbook::models::WebhookTicketbookWalletSharesRequest, api_requests::v1::ticketbook::models::TicketbookObtainQueryParams, api_requests::v1::ticketbook::models::SharesQueryParams, api_requests::v1::ticketbook::models::PlaceholderJsonSchemaImpl, diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs index 670b6aaba9..2dccc8d349 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs @@ -192,6 +192,7 @@ pub(crate) async fn obtain_ticketbook_shares_async( } Ok(pending) => pending, }; + let id = pending.id; // 3. try to spawn a new task attempting to resolve the request if state @@ -201,6 +202,7 @@ pub(crate) async fn obtain_ticketbook_shares_async( requested_on, payload, params, + pending, )) .is_none() { @@ -213,10 +215,7 @@ pub(crate) async fn obtain_ticketbook_shares_async( } // 4. in the meantime, return the id to the user - Ok(output.to_response(TicketbookWalletSharesAsyncResponse { - id: pending.id, - uuid, - })) + Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) } /// Obtain the current value of the bandwidth voucher deposit diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index fe3e2b65d8..159d861a3f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3428,7 +3428,6 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", @@ -3452,6 +3451,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "url", "wasmtimer", "zeroize", From 5cefa7fdd45dd257d11e9ef0c2c5c021f177ee89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 4 Nov 2024 13:15:27 +0200 Subject: [PATCH 13/13] Don't increase bandwidth again (#5081) --- .../authenticator/src/mixnet_listener.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 679d8a1b9e..8fc5c62582 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -27,7 +27,7 @@ use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::EcashManager, BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; +use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::Storage; @@ -362,7 +362,6 @@ impl MixnetListener { "bandwidth entry should have just been created".to_string(), ))?; - let t_type = credential.payment.t_type; let client_bandwidth = ClientBandwidth::new(bandwidth.into()); let mut verifier = CredentialVerifier::new( CredentialSpendingRequest::new(credential), @@ -375,20 +374,7 @@ impl MixnetListener { true, ), ); - verifier.verify().await?; - - let amount = TicketType::try_from_encoded(t_type) - .map_err(|e| { - AuthenticatorError::CredentialVerificationError( - nym_credential_verification::Error::UnknownTicketType(e), - ) - })? - .to_repr() - .bandwidth_value() as i64; - let available_bandwidth = ecash_verifier - .storage() - .increase_bandwidth(client_id, amount) - .await?; + let available_bandwidth = verifier.verify().await?; Ok(AuthenticatorResponse::new_topup_bandwidth( RemainingBandwidthData {