Remove semi-colons

This commit is contained in:
Pedro Pombeiro 2018-03-19 22:37:38 +01:00
parent 9597cde0a2
commit 1429a1af7b
No known key found for this signature in database
GPG Key ID: A65DEB11E4BBC647
7 changed files with 188 additions and 188 deletions

View File

@ -1,52 +1,52 @@
'use strict' 'use strict'
const https = require('https'); const https = require('https')
const config = require('../config'); const config = require('../config')
// Returns the url for getting the labels of a request (Github API v3) // Returns the url for getting the labels of a request (Github API v3)
// req has req.issue.labels_url // req has req.issue.labels_url
const getLabelsURL = function (req) { const getLabelsURL = function (req) {
let url = req.body.issue.labels_url; let url = req.body.issue.labels_url
// Make the URL generic removing the name of the label // Make the URL generic removing the name of the label
return url.replace('{/name}', ''); return url.replace('{/name}', '')
} }
// Returns the bounty labelNames of the request, only for testing motives // Returns the bounty labelNames of the request, only for testing motives
const getLabelsMock = function (req) { const getLabelsMock = function (req) {
return new Promise((resolve, reject) => { resolve(req.body.issue.labels) }); return new Promise((resolve, reject) => { resolve(req.body.issue.labels) })
} }
// Returns all the bounty labelNames of a given issue (Github API v3) // Returns all the bounty labelNames of a given issue (Github API v3)
const getLabels = function (req) { const getLabels = function (req) {
if (config.debug) { if (config.debug) {
return getLabelsMock(req); return getLabelsMock(req)
} else { } else {
const path = getLabelsURL(req).replace('https://api.github.com', ''); const path = getLabelsURL(req).replace('https://api.github.com', '')
const options = { const options = {
hostname: 'api.github.com', hostname: 'api.github.com',
path: path, path: path,
headers: { 'User-Agent': config.githubUsername } headers: { 'User-Agent': config.githubUsername }
}; }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = https.get(options, (response) => { const request = https.get(options, (response) => {
// handle http errors // handle http errors
if (response.statusCode < 200 || response.statusCode > 299) { if (response.statusCode < 200 || response.statusCode > 299) {
bot.error(response, 'Failed to load page, status code: ' + response.statusCode); bot.error(response, 'Failed to load page, status code: ' + response.statusCode)
reject(new Error('Failed to load page, status code: ' + response.statusCode)); reject(new Error('Failed to load page, status code: ' + response.statusCode))
} }
// temporary data holder // temporary data holder
const body = []; const body = []
// on every content chunk, push it to the data array // on every content chunk, push it to the data array
response.on('data', (chunk) => body.push(chunk)); response.on('data', (chunk) => body.push(chunk))
// we are done, resolve promise with those joined chunks // we are done, resolve promise with those joined chunks
response.on('end', () => { response.on('end', () => {
const labels = JSON.parse(body.join('')).map(labelObj => labelObj.name); const labels = JSON.parse(body.join('')).map(labelObj => labelObj.name)
resolve(labels); resolve(labels)
}); })
}); })
// handle connection errors of the request // handle connection errors of the request
request.on('error', (err) => reject(err)) request.on('error', (err) => reject(err))
}); })
} }
} }

View File

@ -1,16 +1,16 @@
const winston = require('winston'); const winston = require('winston')
const ethers = require('ethers'); const ethers = require('ethers')
const Wallet = ethers.Wallet; const Wallet = ethers.Wallet
const Contract = ethers.Contract; const Contract = ethers.Contract
const providers = ethers.providers; const providers = ethers.providers
const utils = ethers.utils; const utils = ethers.utils
const prices = require('./prices'); const prices = require('./prices')
const config = require('../config'); const config = require('../config')
const github = require('./github'); const github = require('./github')
const contractAddressString = 'Contract address:'; const contractAddressString = 'Contract address:'
const logger = winston.createLogger({ const logger = winston.createLogger({
@ -21,7 +21,7 @@ const logger = winston.createLogger({
new winston.transports.File({ filename: './log/info.log', level: 'info' }), new winston.transports.File({ filename: './log/info.log', level: 'info' }),
// new winston.transports.File({ filename: './log/combined.log' }) // new winston.transports.File({ filename: './log/combined.log' })
] ]
}); })
const needsFunding = function (req) { const needsFunding = function (req) {
@ -30,22 +30,22 @@ const needsFunding = function (req) {
} else if (req.body.comment.user.login !== config.githubUsername) { } else if (req.body.comment.user.login !== config.githubUsername) {
return false return false
} else if (!hasAddress(req)) { } else if (!hasAddress(req)) {
return false; return false
} }
return true return true
} }
const hasAddress = function (req) { const hasAddress = function (req) {
const commentBody = req.body.comment.body; const commentBody = req.body.comment.body
if (commentBody.search(contractAddressString) === -1) { if (commentBody.search(contractAddressString) === -1) {
return false; return false
} else { } else {
return true; return true
} }
} }
const getAddress = function (req) { const getAddress = function (req) {
const commentBody = req.body.comment.body; const commentBody = req.body.comment.body
const index = commentBody.search(contractAddressString) + 19 const index = commentBody.search(contractAddressString) + 19
return commentBody.substring(index, index + 42) return commentBody.substring(index, index + 42)
} }
@ -54,76 +54,76 @@ const getLabel = function (req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
github.getLabels(req) github.getLabels(req)
.then(labels => { .then(labels => {
const bountyLabels = labels.filter(label => config.bountyLabels.hasOwnProperty(label.name)); const bountyLabels = labels.filter(label => config.bountyLabels.hasOwnProperty(label.name))
if (bountyLabels.length === 1) { if (bountyLabels.length === 1) {
resolve(bountyLabels[0]); resolve(bountyLabels[0])
} else { } else {
reject('Error getting bounty labels'); reject('Error getting bounty labels')
} }
}).catch(err => { }).catch(err => {
reject(err); reject(err)
}); })
}); })
} }
const getAmountMock = function (req) { const getAmountMock = function (req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
resolve(10); resolve(10)
}); })
} }
const getAmount = function (req) { const getAmount = function (req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const labelPromise = getLabel(req); const labelPromise = getLabel(req)
const tokenPricePromise = prices.getTokenPrice(config.token); const tokenPricePromise = prices.getTokenPrice(config.token)
Promise.all([labelPromise, tokenPricePromise]) Promise.all([labelPromise, tokenPricePromise])
.then(function (values) { .then(function (values) {
let label = values[0]; let label = values[0]
let tokenPrice = values[1]; let tokenPrice = values[1]
let amountToPayDollar = config.priceHour * config.bountyLabels[label.name]; let amountToPayDollar = config.priceHour * config.bountyLabels[label.name]
resolve(amountToPayDollar / tokenPrice); resolve(amountToPayDollar / tokenPrice)
}) })
.catch((err) => { .catch((err) => {
reject(err); reject(err)
}); })
}); })
} }
// Logging functions // Logging functions
const logTransaction = function (tx) { const logTransaction = function (tx) {
logger.info("[OK] Succesfully funded bounty with transaction " + tx.hash); logger.info("[OK] Succesfully funded bounty with transaction " + tx.hash)
logger.info(" * From: " + tx.from); logger.info(" * From: " + tx.from)
logger.info(" * To: " + tx.to); logger.info(" * To: " + tx.to)
logger.info(" * Amount: " + tx.value); logger.info(" * Amount: " + tx.value)
logger.info(" * Gas Price: " + tx.gasPrice); logger.info(" * Gas Price: " + tx.gasPrice)
logger.info("===================================================="); logger.info("====================================================")
} }
const info = function (msg) { const info = function (msg) {
logger.info(msg); logger.info(msg)
} }
const error = function (errorMessage) { const error = function (errorMessage) {
logger.error("[ERROR] Request processing failed: " + errorMessage); logger.error("[ERROR] Request processing failed: " + errorMessage)
} }
const sendTransaction = async function (to, amount, gasPrice) { const sendTransaction = async function (to, amount, gasPrice) {
var chainId = providers.Provider.chainId.ropsten; var chainId = providers.Provider.chainId.ropsten
var chainName = providers.networks.ropsten; var chainName = providers.networks.ropsten
if (config.realTransaction) { if (config.realTransaction) {
chainId = providers.Provider.chainId.homestead; chainId = providers.Provider.chainId.homestead
chainName = providers.networks.homestead; chainName = providers.networks.homestead
} }
const wallet = new Wallet(config.privateKey); const wallet = new Wallet(config.privateKey)
const provider = ethers.providers.getDefaultProvider(chainName); const provider = ethers.providers.getDefaultProvider(chainName)
wallet.provider = provider; wallet.provider = provider
if (config.token === 'ETH') { if (config.token === 'ETH') {
const transaction = { const transaction = {
gasLimit: config.gasLimit, gasLimit: config.gasLimit,
@ -131,22 +131,22 @@ const sendTransaction = async function (to, amount, gasPrice) {
to: to, to: to,
value: amount, value: amount,
chainId: chainId chainId: chainId
}; }
return await wallet.sendTransaction(transaction); return await wallet.sendTransaction(transaction)
} else { } else {
let hash = null; let hash = null
async function getAddress() { return wallet.address; } async function getAddress() { return wallet.address }
async function sign(transaction) { return wallet.sign(transaction); } async function sign(transaction) { return wallet.sign(transaction) }
async function resolveName(addressOrName) { return await provider.resolveName(addressOrName); } async function resolveName(addressOrName) { return await provider.resolveName(addressOrName) }
async function estimateGas(transaction) { return await provider.estimateGas(transaction); } async function estimateGas(transaction) { return await provider.estimateGas(transaction) }
async function getGasPrice() { return await provider.getGasPrice(); } async function getGasPrice() { return await provider.getGasPrice() }
async function getTransactionCount(blockTag) { return await provider.getTransactionCount(blockTag); } async function getTransactionCount(blockTag) { return await provider.getTransactionCount(blockTag) }
async function sendTransaction(transaction) { async function sendTransaction(transaction) {
hash = await provider.sendTransaction(transaction); hash = await provider.sendTransaction(transaction)
return hash; return hash
} }
const customSigner = { const customSigner = {
@ -161,13 +161,13 @@ const sendTransaction = async function (to, amount, gasPrice) {
sign: sign sign: sign
} }
const tokenContract = config.tokenContracts[config.token]; const tokenContract = config.tokenContracts[config.token]
const contractAddress = tokenContract.address; const contractAddress = tokenContract.address
const contract = new Contract(contractAddress, tokenContract.abi, customSigner); const contract = new Contract(contractAddress, tokenContract.abi, customSigner)
const bigNumberAmount = ethers.utils.bigNumberify(amount); const bigNumberAmount = ethers.utils.bigNumberify(amount)
await contract.transfer(to, bigNumberAmount); await contract.transfer(to, bigNumberAmount)
return hash; return hash
} }
} }

View File

@ -1,67 +1,67 @@
"use strict" "use strict"
const https = require("https"); const https = require("https")
const config = require("../config"); const config = require("../config")
const getGasPrice = function () { const getGasPrice = function () {
const url = 'https://ethgasstation.info/json/ethgasAPI.json'; const url = 'https://ethgasstation.info/json/ethgasAPI.json'
// return new pending promise // return new pending promise
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// select http or https module, depending on reqested url // select http or https module, depending on reqested url
const lib = url.startsWith('https') ? require('https') : require('http'); const lib = url.startsWith('https') ? require('https') : require('http')
const request = lib.get(url, (response) => { const request = lib.get(url, (response) => {
// handle http errors // handle http errors
if (response.statusCode < 200 || response.statusCode > 299) { if (response.statusCode < 200 || response.statusCode > 299) {
reject('Failed to load page, status code: ' + response.statusCode); reject('Failed to load page, status code: ' + response.statusCode)
} }
// temporary data holder // temporary data holder
const body = []; const body = []
// on every content chunk, push it to the data array // on every content chunk, push it to the data array
response.on('data', (chunk) => body.push(chunk)); response.on('data', (chunk) => body.push(chunk))
// we are done, resolve promise with those joined chunks // we are done, resolve promise with those joined chunks
response.on('end', () => { response.on('end', () => {
// safeLowWait returns GWei (10^10 Wei). // safeLowWait returns GWei (10^10 Wei).
let jsonBody = JSON.parse(body.join('')); let jsonBody = JSON.parse(body.join(''))
let gasPriceWei = parseInt(jsonBody['safeLowWait']) * Math.pow(10, 10); let gasPriceWei = parseInt(jsonBody['safeLowWait']) * Math.pow(10, 10)
resolve(gasPriceWei); resolve(gasPriceWei)
}); })
}); })
// handle connection errors of the request // handle connection errors of the request
request.on('error', (err) => reject(err)); request.on('error', (err) => reject(err))
}) })
}; }
const getTokenPriceMock = function () { const getTokenPriceMock = function () {
return new Promise((resolve, reject) => resolve(0.35)); return new Promise((resolve, reject) => resolve(0.35))
} }
const getTokenPrice = function (token) { const getTokenPrice = function (token) {
if (config.debug) { if (config.debug) {
return getTokenPriceMock(); return getTokenPriceMock()
} }
const currency = 'USD' const currency = 'USD'
const url = 'https://min-api.cryptocompare.com/data/price?fsym=' + token + '&tsyms=' + currency; const url = 'https://min-api.cryptocompare.com/data/price?fsym=' + token + '&tsyms=' + currency
// return new pending promise // return new pending promise
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// select http or https module, depending on reqested url // select http or https module, depending on reqested url
const lib = url.startsWith('https') ? require('https') : require('http'); const lib = url.startsWith('https') ? require('https') : require('http')
const request = lib.get(url, (response) => { const request = lib.get(url, (response) => {
// handle http errors // handle http errors
if (response.statusCode < 200 || response.statusCode > 299) { if (response.statusCode < 200 || response.statusCode > 299) {
reject('Failed to load page, status code: ' + response.statusCode); reject('Failed to load page, status code: ' + response.statusCode)
} }
// temporary data holder // temporary data holder
const body = []; const body = []
// on every content chunk, push it to the data array // on every content chunk, push it to the data array
response.on('data', (chunk) => body.push(chunk)); response.on('data', (chunk) => body.push(chunk))
// we are done, resolve promise with those joined chunks // we are done, resolve promise with those joined chunks
response.on('end', () => { response.on('end', () => {
let jsonBody = JSON.parse(body.join('')); let jsonBody = JSON.parse(body.join(''))
let tokenPrice = parseFloat(jsonBody[currency]); let tokenPrice = parseFloat(jsonBody[currency])
resolve(tokenPrice); resolve(tokenPrice)
}); })
}); })
// handle connection errors of the request // handle connection errors of the request
request.on('error', (err) => reject(err)) request.on('error', (err) => reject(err))
}) })

View File

@ -30,7 +30,7 @@ const ERC20_ABI = [
"payable": false, "payable": false,
"type": "function" "type": "function"
} }
]; ]
const TOKEN_CONTRACTS = { const TOKEN_CONTRACTS = {
'STT': { 'STT': {

View File

@ -1,4 +1,4 @@
var _ = require("lodash"); var _ = require("lodash")
var defaults = require("./default.js"); var defaults = require("./default.js")
var config = require("./" + (process.env.NODE_ENV || "default") + ".js"); var config = require("./" + (process.env.NODE_ENV || "default") + ".js")
module.exports = _.merge({}, defaults, config); module.exports = _.merge({}, defaults, config)

View File

@ -7,9 +7,9 @@
* Depends on commiteth version as of 2017-06-10. * Depends on commiteth version as of 2017-06-10.
*/ */
const config = require('./config'); const config = require('./config')
const bot = require('./bot'); const bot = require('./bot')
const crypto = require('crypto'); const crypto = require('crypto')
var express = require('express'), var express = require('express'),
@ -17,104 +17,104 @@ var express = require('express'),
helmet = require('helmet'), helmet = require('helmet'),
app = express(), app = express(),
bodyParser = require('body-parser'), bodyParser = require('body-parser'),
jsonParser = bodyParser.json(); jsonParser = bodyParser.json()
app.use(cors()); app.use(cors())
app.use(helmet()); app.use(helmet())
// Receive a POST request at the url specified by an env. var. // Receive a POST request at the url specified by an env. var.
app.post(`${config.urlEndpoint}`, jsonParser, function (req, res, next) { app.post(`${config.urlEndpoint}`, jsonParser, function (req, res, next) {
if (!req.body || !req.body.action) { if (!req.body || !req.body.action) {
return res.sendStatus(400); return res.sendStatus(400)
} else if (!bot.needsFunding(req)) { } else if (!bot.needsFunding(req)) {
return res.sendStatus(204); return res.sendStatus(204)
} }
validation = validateRequest(req); validation = validateRequest(req)
if (validation.correct) { if (validation.correct) {
setTimeout(() => { setTimeout(() => {
processRequest(req) processRequest(req)
.then(() => { .then(() => {
bot.info('issue well funded: ' + req.body.issue.url); bot.info('issue well funded: ' + req.body.issue.url)
}) })
.catch((err) => { .catch((err) => {
bot.error('Error processing request: ' + req.body.issue.url); bot.error('Error processing request: ' + req.body.issue.url)
bot.error('Error: ' + err); bot.error('Error: ' + err)
bot.error('Dump: ', req.body); bot.error('Dump: ', req.body)
}); })
}, config.delayInMiliSeconds); }, config.delayInMiliSeconds)
} else { } else {
bot.error('Error validating issue: ' + req.body.issue.url); bot.error('Error validating issue: ' + req.body.issue.url)
bot.error('Error: ' + validation.error); bot.error('Error: ' + validation.error)
} }
return res.sendStatus(200); return res.sendStatus(200)
}); })
const validateRequest = function (req) { const validateRequest = function (req) {
validation = {correct: false, error: ''}; validation = {correct: false, error: ''}
webhookSecret = process.env.WEBHOOK_SECRET; webhookSecret = process.env.WEBHOOK_SECRET
if(!webhookSecret) { if(!webhookSecret) {
validation.error = 'Github Webhook Secret key not found. ' + validation.error = 'Github Webhook Secret key not found. ' +
'Please set env variable WEBHOOK_SECRET to github\'s webhook secret value'; 'Please set env variable WEBHOOK_SECRET to github\'s webhook secret value'
} else { } else {
const blob = JSON.stringify(req.body); const blob = JSON.stringify(req.body)
const hmac = crypto.createHmac('sha1', webhookSecret); const hmac = crypto.createHmac('sha1', webhookSecret)
const ourSignature = `sha1=${hmac.update(blob).digest('hex')}`; const ourSignature = `sha1=${hmac.update(blob).digest('hex')}`
const theirSignature = req.get('X-Hub-Signature'); const theirSignature = req.get('X-Hub-Signature')
const bufferA = Buffer.from(ourSignature, 'utf8'); const bufferA = Buffer.from(ourSignature, 'utf8')
const bufferB = Buffer.from(theirSignature, 'utf8'); const bufferB = Buffer.from(theirSignature, 'utf8')
const safe = crypto.timingSafeEqual(bufferA, bufferB); const safe = crypto.timingSafeEqual(bufferA, bufferB)
if (safe) { if (safe) {
validation.correct = true; validation.correct = true
} else { } else {
validation.error = 'Invalid signature. Check that WEBHOOK_SECRET ' + validation.error = 'Invalid signature. Check that WEBHOOK_SECRET ' +
'env variable matches github\'s webhook secret value'; 'env variable matches github\'s webhook secret value'
} }
} }
return validation; return validation
} }
const processRequest = function (req) { const processRequest = function (req) {
// const wallet = bot.wallet; // const wallet = bot.wallet
const from = config.sourceAddress; const from = config.sourceAddress
const to = bot.getAddress(req); const to = bot.getAddress(req)
// Asynchronous requests for Gas Price and Amount // Asynchronous requests for Gas Price and Amount
const amountPromise = bot.getAmount(req); const amountPromise = bot.getAmount(req)
const gasPricePromise = bot.getGasPrice(); const gasPricePromise = bot.getGasPrice()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Promise.all([amountPromise, gasPricePromise]) Promise.all([amountPromise, gasPricePromise])
.then(function (results) { .then(function (results) {
const amount = results[0]; const amount = results[0]
const gasPrice = results[1]; const gasPrice = results[1]
bot.sendTransaction(to, amount, gasPrice) bot.sendTransaction(to, amount, gasPrice)
.then(function (hash) { .then(function (hash) {
bot.logTransaction(hash); bot.logTransaction(hash)
resolve(); resolve()
}) })
.catch(function (err) { .catch(function (err) {
reject(err); reject(err)
}); })
}) })
.catch(function (err) { .catch(function (err) {
reject(err); reject(err)
}); })
}); })
} }
const port = process.env.PORT || 8181 const port = process.env.PORT || 8181
app.listen(port, function () { app.listen(port, function () {
bot.info('Autobounty listening on port', port); bot.info('Autobounty listening on port', port)
}); })

View File

@ -1,7 +1,7 @@
const chai = require('chai'); const chai = require('chai')
const expect = require('chai').expect; const expect = require('chai').expect
const assert = require('chai').assert; const assert = require('chai').assert
const should = require('chai').should; const should = require('chai').should
const config = require('../config') const config = require('../config')
const bot = require('../bot') const bot = require('../bot')
@ -15,42 +15,42 @@ let requests = [
{ body: { action: 'edited', comment: { body: 'Editing my comment', user: { login: 'RandomUser' } } } }, { body: { action: 'edited', comment: { body: 'Editing my comment', user: { login: 'RandomUser' } } } },
{ body: { action: 'edited', comment: { body: sob_comment, user: { login: 'status-open-bounty' } } } }, { body: { action: 'edited', comment: { body: sob_comment, user: { login: 'status-open-bounty' } } } },
{ body: { action: 'created', issue: { labels: ['bounty', 'bounty-s'] }, comment: { body: sob_comment, user: { login: 'status-open-bounty' } } } } { body: { action: 'created', issue: { labels: ['bounty', 'bounty-s'] }, comment: { body: sob_comment, user: { login: 'status-open-bounty' } } } }
]; ]
describe('Bot behavior', function () { describe('Bot behavior', function () {
describe('#needsFunding()', function () { describe('#needsFunding()', function () {
it('should return false because the comment is not from status-open-bounty', function () { it('should return false because the comment is not from status-open-bounty', function () {
assert.isFalse(bot.needsFunding(requests[0])); assert.isFalse(bot.needsFunding(requests[0]))
}); })
it('should return false because a user is editing a comment', function () { it('should return false because a user is editing a comment', function () {
assert.isFalse(bot.needsFunding(requests[1])); assert.isFalse(bot.needsFunding(requests[1]))
}); })
it('should return false because status-open-bounty edited a comment', function () { it('should return false because status-open-bounty edited a comment', function () {
assert.isFalse(bot.needsFunding(requests[2])); assert.isFalse(bot.needsFunding(requests[2]))
}); })
it('should return true, it is all right and we should fund', function () { it('should return true, it is all right and we should fund', function () {
assert.isTrue(bot.needsFunding(requests[3])); assert.isTrue(bot.needsFunding(requests[3]))
}); })
}); })
describe('#getAddress', function () { describe('#getAddress', function () {
it('should return the address from a status-open-bounty bot comment', function () { it('should return the address from a status-open-bounty bot comment', function () {
assert.equal(bot.getAddress(requests[3]), '0x3645fe42b1a744ad98cc032c22472388806f86f9'); assert.equal(bot.getAddress(requests[3]), '0x3645fe42b1a744ad98cc032c22472388806f86f9')
}); })
}); })
describe('#getAmount', function () { describe('#getAmount', function () {
it('should return the amount for the issue given the price per hour and the bounty label for this issue', (done) => { it('should return the amount for the issue given the price per hour and the bounty label for this issue', (done) => {
bot.getAmount(requests[3]) bot.getAmount(requests[3])
.then(function (amount) { .then(function (amount) {
let label = 'bounty-s'; let label = 'bounty-s'
let tokenPrice = 0.35; let tokenPrice = 0.35
let priceInDollars = config.priceHour * config.bountyLabels[label]; let priceInDollars = config.priceHour * config.bountyLabels[label]
expected_amount = priceInDollars / tokenPrice; expected_amount = priceInDollars / tokenPrice
assert.equal(amount, expected_amount); assert.equal(amount, expected_amount)
done(); done()
}) })
.catch(() => { console.log('error'), done() }); .catch(() => { console.log('error'), done() })
}); })
}); })
}); })