Skip to content

Commit

Permalink
feat: Send dcl_error_simulate_transaction metric (#122)
Browse files Browse the repository at this point in the history
* feat: Install @wkc/features-component

* refactor: Use new @wkc/features-component

* feat: Send dcl_error_simulate_transaction metric

* fix: prettier

* fix: tests

* fix: Readable comment

* fix: remove unrequired test
  • Loading branch information
cyaiox authored Dec 19, 2024
1 parent 7f14ef2 commit 9740954
Show file tree
Hide file tree
Showing 14 changed files with 925 additions and 847 deletions.
1,358 changes: 880 additions & 478 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@dcl/schemas": "^15.1.0",
"@well-known-components/env-config-provider": "^1.1.1",
"@well-known-components/features-component": "^2.0.0",
"@well-known-components/http-requests-logger-component": "^2.1.0",
"@well-known-components/http-server": "^1.1.6",
"@well-known-components/http-tracer-component": "^1.1.0",
Expand All @@ -38,14 +39,15 @@
"@well-known-components/thegraph-component": "^1.1.0",
"@well-known-components/tracer-component": "^1.2.0",
"ajv": "^8.6.0",
"decentraland-transactions": "^2.6.1",
"decentraland-transactions": "^2.17.0",
"ethers": "^5.6.3",
"node-fetch": "^2.6.1",
"sql-template-strings": "^2.2.2",
"typescript": "^4.3.4"
},
"devDependencies": {
"@types/jest": "^27.0.3",
"@types/node": "^20.3.2",
"@well-known-components/test-helpers": "^1.2.0",
"dcl-tslint-config-standard": "^2.0.0",
"expect": "^26.6.2",
Expand Down
2 changes: 1 addition & 1 deletion src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
Verbosity,
} from '@well-known-components/http-requests-logger-component'
import { createPgComponent } from '@well-known-components/pg-component'
import { createFeaturesComponent } from '@well-known-components/features-component'
import { createContractsComponent } from './ports/contracts/component'
import { createFetchComponent } from './ports/fetcher'
import { createFeaturesComponent } from './ports/features'
import { createTransactionComponent } from './ports/transaction/component'
import { metricDeclarations } from './metrics'
import { AppComponents, GlobalContext } from './types'
Expand Down
5 changes: 5 additions & 0 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export const metricDeclarations = {
type: IMetricsComponent.CounterType,
labelNames: [],
},
dcl_error_simulate_transaction: {
help: 'Count errors of simulate transaction',
type: IMetricsComponent.CounterType,
labelNames: [],
},
...thegraphMetrics,
}

Expand Down
140 changes: 1 addition & 139 deletions src/ports/features.ts
Original file line number Diff line number Diff line change
@@ -1,142 +1,4 @@
import { IFetchComponent } from '@well-known-components/http-server'
import {
IConfigComponent,
ILoggerComponent,
} from '@well-known-components/interfaces'

// Note:
// features component may be encapsulated in @well-known-components/features
// in a future. for the time being, given it's simplicity and ease of configuration
// it can be implemented as follow to enable customizations:

export async function createFeaturesComponent(
components: NeededComponents,
referer: string
): Promise<IFeaturesComponent> {
const { config, fetch, logs } = components
const FF_URL =
(await config.getString('FF_URL')) ??
'https://feature-flags.decentraland.org'

const logger = logs.getLogger('transactions-server')

async function getEnvFeature(
app: string,
feature: string
): Promise<string | undefined> {
return config.getString(`FF_${app}_${feature}`.toUpperCase())
}

async function fetchFeatureFlags(
app: string
): Promise<FeaturesFlagsResponse | null> {
try {
const response = await fetch.fetch(`${FF_URL}/${app}.json`, {
headers: {
Referer: referer,
},
})

if (response.ok) {
return await response.json()
} else {
throw new Error(`Could not fetch features service from ${FF_URL}`)
}
} catch (error) {
logger.error(error as Error)
}

return null
}

async function getIsFeatureEnabled(
app: string,
feature: string
): Promise<boolean> {
const envFeatureFlag = await getEnvFeature(app, feature)

if (envFeatureFlag) {
return envFeatureFlag === '1' ? true : false
}

const featureFlags = await fetchFeatureFlags(app)

return !!featureFlags?.flags[`${app}-${feature}`]
}

async function getFeatureVariant<FeatureFlagVariant>(
app: string,
feature: string
): Promise<FeatureFlagVariant | null> {
const ffKey = `${app}-${feature}`
const featureFlags = await fetchFeatureFlags(app)

if (featureFlags?.flags[ffKey] && featureFlags?.variants[ffKey]) {
return featureFlags.variants[ffKey] as unknown as FeatureFlagVariant
}

return null
}

return {
getEnvFeature,
getIsFeatureEnabled,
getFeatureVariant,
}
}

export type IFeaturesComponent = {
/**
* Helper to get whether a feature flag is enabled or disabled.
* It will first look into your env file for the feature flag, if it is not defined there,
* it will look it in the requested and stored features data.
* The env key will be determined from the application and the flag. For example, if the
* application is "explorer" and the flag is "some-crazy-feature", it will look
* for it as FF_EXPLORER_SOME_CRAZY_FEATURE.
* @param app Appplication name.
* @param feature Feature key without the application name prefix. For example for the "builder-feature".
* @returns Whether the feature is enabled or not and its variant.
*/
getEnvFeature(app: string, feature: string): Promise<string | undefined>
getIsFeatureEnabled(app: string, feature: string): Promise<boolean>
getFeatureVariant(
app: string,
feature: string
): Promise<FeatureFlagVariant | null>
}

export type FeaturesFlagsResponse = {
flags: Record<string, boolean>
variants: Record<string, FeatureFlagVariant>
}

export type FeatureFlagVariant = {
name: string
payload: {
type: string
value: string
}
enabled: boolean
}

export type NeededComponents = {
config: IConfigComponent
fetch: IFetchComponent
logs: ILoggerComponent
}

export enum Feature {
MAX_GAS_PRICE_ALLOWED_IN_WEI = 'max-gas-price-allowed',
GELATO_RELAYER = 'gelato-relayer',
}

export enum ApplicationName {
EXPLORER = 'explorer',
BUILDER = 'builder',
MARKETPLACE = 'marketplace',
ACCOUNT = 'account',
DAO = 'dao',
DAPPS = 'dapps',
EVENTS = 'events',
LANDING = 'landing',
TEST = 'test',
}
3 changes: 2 additions & 1 deletion src/ports/transaction/component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { IDatabase } from '@well-known-components/interfaces'
import { ApplicationName } from '@well-known-components/features-component'
import SQL from 'sql-template-strings'
import { AppComponents } from '../../types'
import { TransactionData } from '../../types/transactions/transactions'
import { ApplicationName, Feature } from '../features'
import { Feature } from '../features'
import {
checkSchema,
checkSalePrice,
Expand Down
9 changes: 4 additions & 5 deletions src/ports/transaction/validation/checkGasPrice.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { ChainId, ChainName } from '@dcl/schemas'
import { BigNumber } from 'ethers'
import { ContractName, getContract } from 'decentraland-transactions'
import { ApplicationName } from '@well-known-components/features-component'
import {
decodeFunctionData,
getMaticChainIdFromChainName,
} from '../../../logic/ethereum'
import { AppComponents } from '../../../types'
import { ApplicationName, Feature } from '../../features'
import { Feature } from '../../features'
import { HighCongestionError } from '../../../types/transactions/errors'
import { IGasPriceValidator } from './types'
import { TransactionData } from '../../../types/transactions/transactions'

const FF_MAX_GAS_PRICE_ALLOWED_IN_WEI = 'max-gas-price-allowed'

export const checkGasPrice: IGasPriceValidator = async (
components,
transactionData
Expand All @@ -22,7 +21,7 @@ export const checkGasPrice: IGasPriceValidator = async (

const isGasPriceAllowedFFEnabled = await features.getIsFeatureEnabled(
ApplicationName.DAPPS,
FF_MAX_GAS_PRICE_ALLOWED_IN_WEI
Feature.MAX_GAS_PRICE_ALLOWED_IN_WEI
)

if (isGasPriceAllowedFFEnabled) {
Expand Down Expand Up @@ -67,7 +66,7 @@ const getMaxGasPriceAllowed = async (
const { features } = components
const gasPriceAllowedVariant = await features.getFeatureVariant(
ApplicationName.DAPPS,
FF_MAX_GAS_PRICE_ALLOWED_IN_WEI
Feature.MAX_GAS_PRICE_ALLOWED_IN_WEI
)

if (!gasPriceAllowedVariant) {
Expand Down
23 changes: 16 additions & 7 deletions src/ports/transaction/validation/checkTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { providers } from 'ethers'
import { isErrorWithMessage } from '../../../logic/errors'
import { SimulateTransactionError } from '../../../types/transactions/errors'
import { ITransactionValidator } from './types'

export const checkTransaction: ITransactionValidator = async (
components,
transactionData
) => {
const { config } = components
const { config, metrics } = components
const rpcURL = await config.requireString('RPC_URL')

const provider = new providers.JsonRpcProvider(rpcURL)

// Estimage Transaction Gas to avoid send a transaction malformed to the providers
await provider.estimateGas({
from: transactionData.from.toLowerCase(),
to: transactionData.params[0].toLowerCase(),
data: transactionData.params[1],
})
try {
// Estimate the transaction gas to avoid sending a malformed transaction to the providers
await provider.estimateGas({
from: transactionData.from.toLowerCase(),
to: transactionData.params[0].toLowerCase(),
data: transactionData.params[1],
})
} catch (error) {
metrics.increment('dcl_error_simulate_transaction')
throw new SimulateTransactionError(
isErrorWithMessage(error) ? error.message : 'Error simulating transaction'
)
}
}
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import type {
IBaseComponent,
IMetricsComponent,
} from '@well-known-components/interfaces'
import { metricDeclarations } from './metrics'
import { IFeaturesComponent } from '@well-known-components/features-component'
import { IPgComponent } from '@well-known-components/pg-component'
import { metricDeclarations } from './metrics'
import { ITestFetchComponent } from './ports/fetcher'
import { IContractsComponent } from './ports/contracts/types'
import { ITransactionComponent } from './ports/transaction/types'
import { IFeaturesComponent } from './ports/features'
import { BiconomyMetaTransactionComponent } from './ports/biconomy'
import { GelatoMetaTransactionComponent } from './ports/gelato'

Expand Down
8 changes: 8 additions & 0 deletions src/types/transactions/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,11 @@ export class RelayerTimeout extends Error {
super(`The relayer took too long to respond: ${message}`)
}
}

export class SimulateTransactionError extends Error {
public code = ErrorCode.INVALID_TRANSACTION

constructor(public message: string) {
super(`Error simulating transaction: ${message}`)
}
}
Loading

0 comments on commit 9740954

Please sign in to comment.