Commit aa8baefd authored by Georgios Konstantopoulos's avatar Georgios Konstantopoulos Committed by GitHub

refactor: remove base-service dependency (#16)

* chore: cleanup gitignore

* fix: adjust package.json and tsconfigs

* feat: expose base-service in core-utils

* chore(l1-ingestion): use base-service from core-utils

* chore(l2-ingestion): use base-service from core-utils

* chore: use base-service from core-utils in remaining services
parent 8f5982d2
node_modules node_modules
**/dist **/dist
results results
temp
.nyc_output .nyc_output
*.tsbuildinfo *.tsbuildinfo
...@@ -5,14 +5,14 @@ ...@@ -5,14 +5,14 @@
"files": [ "files": [
"dist/index" "dist/index"
], ],
"types": "build/src/index.d.ts", "types": "dist/index",
"repository": "git@github.com:ethereum-optimism/core-utils.git", "repository": "git@github.com:ethereum-optimism/core-utils.git",
"author": "Kelvin Fichter <kelvinfichter@gmail.com>", "author": "Kelvin Fichter <kelvinfichter@gmail.com>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"all": "yarn clean && yarn build && yarn test && yarn lint:fix && yarn lint", "all": "yarn clean && yarn build && yarn test && yarn lint:fix && yarn lint",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"clean": "rimraf dist/", "clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo",
"lint": "tslint --format stylish --project .", "lint": "tslint --format stylish --project .",
"lint:fix": "prettier --config prettier-config.json --write '{src,test}/**/*.ts'", "lint:fix": "prettier --config prettier-config.json --write '{src,test}/**/*.ts'",
"test": "ts-mocha test/**/*.spec.ts" "test": "ts-mocha test/**/*.spec.ts"
......
...@@ -12,17 +12,19 @@ type OptionSettings<TOptions> = { ...@@ -12,17 +12,19 @@ type OptionSettings<TOptions> = {
* Base for other "Service" objects. Handles your standard initialization process, can dynamically * Base for other "Service" objects. Handles your standard initialization process, can dynamically
* start and stop. * start and stop.
*/ */
export class BaseService<TServiceOptions> { export class BaseService<T> {
protected name: string protected name: string
protected optionSettings: OptionSettings<TServiceOptions> protected options: T
protected logger: Logger protected logger: Logger
protected initialized: boolean = false protected initialized: boolean = false
protected running: boolean = false protected running: boolean = false
/** constructor(name: string, options: T, optionSettings: OptionSettings<T>) {
* @param options Options to pass to the service. validateOptions(options, optionSettings)
*/ this.name = name
constructor(protected options: TServiceOptions) {} this.options = mergeDefaultOptions(options, optionSettings)
this.logger = new Logger({ name })
}
/** /**
* Initializes the service. * Initializes the service.
...@@ -32,48 +34,24 @@ export class BaseService<TServiceOptions> { ...@@ -32,48 +34,24 @@ export class BaseService<TServiceOptions> {
return return
} }
// Apparently I'm going crazy and just now finding out that class variables are undefined this.logger.info('Service is initializing...')
// within the constructor? Anyway, this means I have to do all of this initialization logic await this._init()
// during a separate init function or everything is undefined. this.logger.info('Service has initialized.')
if (this.logger === undefined) {
// tslint:disable-next-line
this.logger = new Logger({name: this.name})
}
this._mergeDefaultOptions()
this._validateOptions()
this.initialized = true this.initialized = true
try {
this.logger.info('Service is initializing...')
await this._init()
this.logger.info('Service has initialized.')
} catch (err) {
this.initialized = false
throw err
}
} }
/** /**
* Starts the service. * Starts the service (initializes it if needed).
*/ */
public async start(): Promise<void> { public async start(): Promise<void> {
if (this.running) { if (this.running) {
return return
} }
if (this.logger === undefined) {
// tslint:disable-next-line
this.logger = new Logger({name: this.name})
}
this.running = true
this.logger.info('Service is starting...') this.logger.info('Service is starting...')
await this.init() await this.init()
await this._start() await this._start()
this.logger.info('Service has started') this.logger.info('Service has started')
this.running = true
} }
/** /**
...@@ -87,7 +65,6 @@ export class BaseService<TServiceOptions> { ...@@ -87,7 +65,6 @@ export class BaseService<TServiceOptions> {
this.logger.info('Service is stopping...') this.logger.info('Service is stopping...')
await this._stop() await this._stop()
this.logger.info('Service has stopped') this.logger.info('Service has stopped')
this.running = false this.running = false
} }
...@@ -111,58 +88,47 @@ export class BaseService<TServiceOptions> { ...@@ -111,58 +88,47 @@ export class BaseService<TServiceOptions> {
protected async _stop(): Promise<void> { protected async _stop(): Promise<void> {
return return
} }
}
/** /**
* Combines user provided and default options. Honestly there's no point for this function to * Combines user provided and default options.
* live within this class and be all stateful, but I didn't realize that until after I wrote it. */
* So we're gonna have to deal with that for now. Whatever, it's an easy fix if anyone else function mergeDefaultOptions<T>(
* feels like tackling it. options: T,
*/ optionSettings: OptionSettings<T>
private _mergeDefaultOptions(): void { ): T {
if (this.optionSettings === undefined) { for (const optionName of Object.keys(optionSettings)) {
return const optionDefault = optionSettings[optionName].default
if (optionDefault === undefined) {
continue
} }
for (const optionName of Object.keys(this.optionSettings)) { if (options[optionName] !== undefined && options[optionName] !== null) {
const optionDefault = this.optionSettings[optionName].default continue
if (optionDefault === undefined) {
continue
}
if (
this.options[optionName] !== undefined &&
this.options[optionName] !== null
) {
continue
}
// TODO: Maybe make a copy of this default instead of directly assigning?
this.options[optionName] = optionDefault
} }
options[optionName] = optionDefault
} }
/** return options
* Performs option validation against the option settings attached to this class. Another }
* function that really shouldn't be part of this class in particular. Good mini project though!
*/
private _validateOptions(): void {
if (this.optionSettings === undefined) {
return
}
for (const optionName of Object.keys(this.optionSettings)) { /**
const optionValidationFunction = this.optionSettings[optionName].validate * Performs option validation against the option settings
if (optionValidationFunction === undefined) { */
continue function validateOptions<T>(options: T, optionSettings: OptionSettings<T>) {
} for (const optionName of Object.keys(optionSettings)) {
const optionValidationFunction = optionSettings[optionName].validate
if (optionValidationFunction === undefined) {
continue
}
const optionValue = this.options[optionName] const optionValue = options[optionName]
if (optionValidationFunction(optionValue) === false) { if (optionValidationFunction(optionValue) === false) {
throw new Error( throw new Error(
`Provided input for option "${optionName}" is invalid: ${optionValue}` `Provided input for option "${optionName}" is invalid: ${optionValue}`
) )
}
} }
} }
} }
export * from './coders' export * from './coders'
export * from './common' export * from './common'
export * from './watcher' export * from './watcher'
export * from './base-service'
/db/
node_modules/
yarn-error.log
.env
test/temp/
build/
\ No newline at end of file
{ {
"name": "@eth-optimism/data-transport-layer", "name": "@eth-optimism/data-transport-layer",
"version": "0.1.2", "version": "0.1.2",
"main": "build/index.js", "main": "dist/index",
"license": "MIT",
"files": [ "files": [
"build/**/*.js", "dist/index"
"build/**/*.js.map",
"build/**/*.ts"
], ],
"types": "build/index.d.ts", "types": "dist/index",
"scripts": { "scripts": {
"clean": "rimraf ./build", "clean": "rimraf ./dist ./tsconfig.build.tsbuildinfo",
"clean:db": "rimraf ./db", "clean:db": "rimraf ./db",
"lint": "yarn run lint:fix && yarn run lint:check", "lint": "yarn run lint:fix && yarn run lint:check",
"lint:check": "tslint --format stylish --project .", "lint:check": "tslint --format stylish --project .",
...@@ -23,12 +20,10 @@ ...@@ -23,12 +20,10 @@
"dependencies": { "dependencies": {
"@eth-optimism/contracts": "^0.1.6", "@eth-optimism/contracts": "^0.1.6",
"@eth-optimism/core-utils": "^0.1.10", "@eth-optimism/core-utils": "^0.1.10",
"@eth-optimism/service-base": "^1.1.5",
"@ethersproject/providers": "^5.0.21", "@ethersproject/providers": "^5.0.21",
"@types/express": "^4.17.11", "@types/express": "^4.17.11",
"bcfg": "^0.1.6", "bcfg": "^0.1.6",
"browser-or-node": "^1.3.0", "browser-or-node": "^1.3.0",
"colors": "^1.4.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"ethers": "^5.0.26", "ethers": "^5.0.26",
...@@ -38,7 +33,6 @@ ...@@ -38,7 +33,6 @@
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
}, },
"devDependencies": { "devDependencies": {
"@eth-optimism/dev": "^1.1.1",
"@nomiclabs/hardhat-ethers": "^2.0.1", "@nomiclabs/hardhat-ethers": "^2.0.1",
"@types/browser-or-node": "^1.3.0", "@types/browser-or-node": "^1.3.0",
"@types/cors": "^2.8.9", "@types/cors": "^2.8.9",
......
/* Imports: External */ /* Imports: External */
import { BaseService } from '@eth-optimism/service-base' import { fromHexString, BaseService } from '@eth-optimism/core-utils'
import { fromHexString } from '@eth-optimism/core-utils'
import { JsonRpcProvider } from '@ethersproject/providers' import { JsonRpcProvider } from '@ethersproject/providers'
import colors from 'colors/safe'
import { LevelUp } from 'levelup' import { LevelUp } from 'levelup'
/* Imports: Internal */ /* Imports: Internal */
...@@ -30,37 +28,39 @@ export interface L1IngestionServiceOptions ...@@ -30,37 +28,39 @@ export interface L1IngestionServiceOptions
db: LevelUp db: LevelUp
} }
export class L1IngestionService extends BaseService<L1IngestionServiceOptions> { const optionSettings = {
protected name = 'L1 Ingestion Service' db: {
validate: validators.isLevelUP,
protected optionSettings = { },
db: { addressManager: {
validate: validators.isLevelUP, validate: validators.isAddress,
}, },
addressManager: { confirmations: {
validate: validators.isAddress, default: 35,
}, validate: validators.isInteger,
confirmations: { },
default: 35, pollingInterval: {
validate: validators.isInteger, default: 5000,
}, validate: validators.isInteger,
pollingInterval: { },
default: 5000, logsPerPollingInterval: {
validate: validators.isInteger, default: 2000,
}, validate: validators.isInteger,
logsPerPollingInterval: { },
default: 2000, dangerouslyCatchAllErrors: {
validate: validators.isInteger, default: false,
}, validate: validators.isBoolean,
dangerouslyCatchAllErrors: { },
default: false, l1RpcProvider: {
validate: validators.isBoolean, validate: (val: any) => {
}, return validators.isUrl(val) || validators.isJsonRpcProvider(val)
l1RpcProvider: {
validate: (val: any) => {
return validators.isUrl(val) || validators.isJsonRpcProvider(val)
},
}, },
},
}
export class L1IngestionService extends BaseService<L1IngestionServiceOptions> {
constructor(options: L1IngestionServiceOptions) {
super('L1 Ingestion Service', options, optionSettings)
} }
private state: { private state: {
......
/* Imports: External */ /* Imports: External */
import { BaseService } from '@eth-optimism/service-base' import { BaseService } from '@eth-optimism/core-utils'
import { JsonRpcProvider } from '@ethersproject/providers' import { JsonRpcProvider } from '@ethersproject/providers'
import colors from 'colors/safe'
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
import { LevelUp } from 'levelup' import { LevelUp } from 'levelup'
...@@ -16,41 +15,43 @@ export interface L2IngestionServiceOptions ...@@ -16,41 +15,43 @@ export interface L2IngestionServiceOptions
db: LevelUp db: LevelUp
} }
export class L2IngestionService extends BaseService<L2IngestionServiceOptions> { const optionSettings = {
protected name = 'L2 Ingestion Service' db: {
validate: validators.isLevelUP,
protected optionSettings = { },
db: { l2RpcProvider: {
validate: validators.isLevelUP, validate: (val: any) => {
}, return validators.isUrl(val) || validators.isJsonRpcProvider(val)
l2RpcProvider: {
validate: (val: any) => {
return validators.isUrl(val) || validators.isJsonRpcProvider(val)
},
},
l2ChainId: {
validate: validators.isInteger,
},
pollingInterval: {
default: 5000,
validate: validators.isInteger,
},
transactionsPerPollingInterval: {
default: 1000,
validate: validators.isInteger,
},
dangerouslyCatchAllErrors: {
default: false,
validate: validators.isBoolean,
},
legacySequencerCompatibility: {
default: false,
validate: validators.isBoolean,
},
stopL2SyncAtBlock: {
default: Infinity,
validate: validators.isInteger,
}, },
},
l2ChainId: {
validate: validators.isInteger,
},
pollingInterval: {
default: 5000,
validate: validators.isInteger,
},
transactionsPerPollingInterval: {
default: 1000,
validate: validators.isInteger,
},
dangerouslyCatchAllErrors: {
default: false,
validate: validators.isBoolean,
},
legacySequencerCompatibility: {
default: false,
validate: validators.isBoolean,
},
stopL2SyncAtBlock: {
default: Infinity,
validate: validators.isInteger,
},
}
export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
constructor(options: L2IngestionServiceOptions) {
super('L2 Ingestion Service', options, optionSettings)
} }
private state: { private state: {
......
/* Imports: External */ /* Imports: External */
import { BaseService } from '@eth-optimism/service-base' import { BaseService } from '@eth-optimism/core-utils'
import { LevelUp } from 'levelup' import { LevelUp } from 'levelup'
import level from 'level' import level from 'level'
...@@ -29,18 +29,20 @@ export interface L1DataTransportServiceOptions { ...@@ -29,18 +29,20 @@ export interface L1DataTransportServiceOptions {
stopL2SyncAtBlock?: number stopL2SyncAtBlock?: number
} }
export class L1DataTransportService extends BaseService<L1DataTransportServiceOptions> { const optionSettings = {
protected name = 'L1 Data Transport Service' syncFromL1: {
default: true,
validate: validators.isBoolean,
},
syncFromL2: {
default: false,
validate: validators.isBoolean,
},
}
protected optionSettings = { export class L1DataTransportService extends BaseService<L1DataTransportServiceOptions> {
syncFromL1: { constructor(options: L1DataTransportServiceOptions) {
default: true, super('L1 Data Transport Service', options, optionSettings)
validate: validators.isBoolean,
},
syncFromL2: {
default: false,
validate: validators.isBoolean,
},
} }
private state: { private state: {
......
/* Imports: External */ /* Imports: External */
import { BaseService } from '@eth-optimism/service-base' import { BaseService } from '@eth-optimism/core-utils'
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express'
import cors from 'cors' import cors from 'cors'
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
...@@ -26,31 +26,34 @@ export interface L1TransportServerOptions ...@@ -26,31 +26,34 @@ export interface L1TransportServerOptions
db: LevelUp db: LevelUp
} }
export class L1TransportServer extends BaseService<L1TransportServerOptions> { const optionSettings = {
protected name = 'L1 Transport Server' db: {
protected optionSettings = { validate: validators.isLevelUP,
db: { },
validate: validators.isLevelUP, port: {
}, default: 7878,
port: { validate: validators.isInteger,
default: 7878, },
validate: validators.isInteger, hostname: {
}, default: 'localhost',
hostname: { validate: validators.isString,
default: 'localhost', },
validate: validators.isString, confirmations: {
}, validate: validators.isInteger,
confirmations: { },
validate: validators.isInteger, l1RpcProvider: {
}, validate: (val: any) => {
l1RpcProvider: { return validators.isUrl(val) || validators.isJsonRpcProvider(val)
validate: (val: any) => {
return validators.isUrl(val) || validators.isJsonRpcProvider(val)
},
},
showUnconfirmedTransactions: {
validate: validators.isBoolean,
}, },
},
showUnconfirmedTransactions: {
validate: validators.isBoolean,
},
}
export class L1TransportServer extends BaseService<L1TransportServerOptions> {
constructor(options: L1TransportServerOptions) {
super('L1 Transport Server', options, optionSettings)
} }
private state: { private state: {
......
{ {
"extends": "../../tsconfig.build.json", "extends": "../../tsconfig.build.json",
...@@ -7,5 +8,6 @@ ...@@ -7,5 +8,6 @@
"include": [ "include": [
"src/**/*" "src/**/*"
], ]
} }
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
], ],
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"clean": "rimraf hardhat *.d.ts *.map *.js tsconfig.tsbuildinfo", "clean": "rimraf hardhat *.d.ts *.map *.js tsconfig.tsbuildinfo dist",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"lint": "yarn run lint:fix && yarn run lint:check", "lint": "yarn run lint:fix && yarn run lint:check",
"lint:check": "tslint --format stylish --project .", "lint:check": "tslint --format stylish --project .",
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"main": "dist/index", "main": "dist/index",
"types": "dist/index", "types": "dist/index",
"files": [ "files": [
"dist" "dist/index"
], ],
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
"lint": "yarn lint:fix && yarn lint:check", "lint": "yarn lint:fix && yarn lint:check",
"lint:check": "tslint --format stylish --project .", "lint:check": "tslint --format stylish --project .",
"lint:fix": "prettier --config ./prettier-config.json --write \"hardhat.config.ts\" \"{src,test}/**/*.ts\"", "lint:fix": "prettier --config ./prettier-config.json --write \"hardhat.config.ts\" \"{src,test}/**/*.ts\"",
"clean": "rimraf ./artifacts ./cache ./dist" "clean": "rimraf ./artifacts ./cache ./dist ./tsconfig.build.tsbuildinfo"
}, },
"peerDependencies": { "peerDependencies": {
"@nomiclabs/ethereumjs-vm": "^4", "@nomiclabs/ethereumjs-vm": "^4",
......
...@@ -53,18 +53,6 @@ ...@@ -53,18 +53,6 @@
ganache-core "^2.12.1" ganache-core "^2.12.1"
glob "^7.1.6" glob "^7.1.6"
"@eth-optimism/core-utils@0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.1.9.tgz#b63d8dc417b1a27977bdb19d4ca435232f88f944"
integrity sha512-esTG0Fi98pWut9EH+7NKPIJvGY+aTtNvcqtffd/p9oXJjj7Z719zrmqwdvhv3oyymxGIqiZSE38HMPTxVDPVrw==
dependencies:
"@ethersproject/abstract-provider" "^5.0.9"
colors "^1.4.0"
debug "^4.3.1"
ethers "^5.0.31"
pino "^6.11.1"
pino-pretty "^4.7.1"
"@eth-optimism/dev@^1.1.1": "@eth-optimism/dev@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@eth-optimism/dev/-/dev-1.1.1.tgz#7bae95b975c1d6641b4ae550cb3ec631c667a56b" resolved "https://registry.yarnpkg.com/@eth-optimism/dev/-/dev-1.1.1.tgz#7bae95b975c1d6641b4ae550cb3ec631c667a56b"
...@@ -86,14 +74,6 @@ ...@@ -86,14 +74,6 @@
tslint-plugin-prettier "^2.3.0" tslint-plugin-prettier "^2.3.0"
typescript "^4.1.5" typescript "^4.1.5"
"@eth-optimism/service-base@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@eth-optimism/service-base/-/service-base-1.1.5.tgz#fddb8f51f2d082a106fced0826a8e45eabf0a16c"
integrity sha512-0xNwZv5aZknj1LKLiob4VXcGDOUKDLc3YKbW1Cub6j9t0bwIZ8/Y5t2sVJ3/d15wBW/LrSSmGkZRL+tR56ZoNA==
dependencies:
"@eth-optimism/core-utils" "0.1.9"
colors "^1.4.0"
"@ethereum-waffle/chai@^3.3.0": "@ethereum-waffle/chai@^3.3.0":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-3.3.1.tgz#3f20b810d0fa516f19af93c50c3be1091333fa8e" resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-3.3.1.tgz#3f20b810d0fa516f19af93c50c3be1091333fa8e"
...@@ -3575,11 +3555,6 @@ color-name@~1.1.4: ...@@ -3575,11 +3555,6 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colors@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
columnify@^1.5.4: columnify@^1.5.4:
version "1.5.4" version "1.5.4"
resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb"
...@@ -3974,7 +3949,7 @@ debug@3.2.6: ...@@ -3974,7 +3949,7 @@ debug@3.2.6:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@4, debug@4.3.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: debug@4, debug@4.3.1, debug@^4.1.0, debug@^4.1.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment