From 7ae38906926dc09bc10670c361af0d2bf0050426 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Sat, 19 Jan 2019 18:42:04 +0800 Subject: Update dependency packages --- packages/testnet-faucets/src/ts/configs.ts | 7 - packages/testnet-faucets/src/ts/constants.ts | 5 - packages/testnet-faucets/src/ts/dispatch_queue.ts | 53 ------ .../testnet-faucets/src/ts/dispense_asset_tasks.ts | 73 -------- packages/testnet-faucets/src/ts/error_reporter.ts | 37 ---- packages/testnet-faucets/src/ts/global.d.ts | 6 - packages/testnet-faucets/src/ts/handler.ts | 200 --------------------- .../src/ts/parameter_transformer.ts | 28 --- packages/testnet-faucets/src/ts/rpc_urls.ts | 12 -- packages/testnet-faucets/src/ts/server.ts | 52 ------ packages/testnet-faucets/src/ts/tokens.ts | 44 ----- 11 files changed, 517 deletions(-) delete mode 100644 packages/testnet-faucets/src/ts/configs.ts delete mode 100644 packages/testnet-faucets/src/ts/constants.ts delete mode 100644 packages/testnet-faucets/src/ts/dispatch_queue.ts delete mode 100644 packages/testnet-faucets/src/ts/dispense_asset_tasks.ts delete mode 100644 packages/testnet-faucets/src/ts/error_reporter.ts delete mode 100644 packages/testnet-faucets/src/ts/global.d.ts delete mode 100644 packages/testnet-faucets/src/ts/handler.ts delete mode 100644 packages/testnet-faucets/src/ts/parameter_transformer.ts delete mode 100644 packages/testnet-faucets/src/ts/rpc_urls.ts delete mode 100644 packages/testnet-faucets/src/ts/server.ts delete mode 100644 packages/testnet-faucets/src/ts/tokens.ts (limited to 'packages/testnet-faucets/src/ts') diff --git a/packages/testnet-faucets/src/ts/configs.ts b/packages/testnet-faucets/src/ts/configs.ts deleted file mode 100644 index 038c8e22a..000000000 --- a/packages/testnet-faucets/src/ts/configs.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const configs = { - DISPENSER_ADDRESS: (process.env.DISPENSER_ADDRESS as string).toLowerCase(), - DISPENSER_PRIVATE_KEY: process.env.DISPENSER_PRIVATE_KEY, - ENVIRONMENT: process.env.FAUCET_ENVIRONMENT, - INFURA_API_KEY: process.env.INFURA_API_KEY, - ROLLBAR_ACCESS_KEY: process.env.FAUCET_ROLLBAR_ACCESS_KEY, -}; diff --git a/packages/testnet-faucets/src/ts/constants.ts b/packages/testnet-faucets/src/ts/constants.ts deleted file mode 100644 index c6370e3f6..000000000 --- a/packages/testnet-faucets/src/ts/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const constants = { - SUCCESS_STATUS: 200, - SERVICE_UNAVAILABLE_STATUS: 503, - BAD_REQUEST_STATUS: 400, -}; diff --git a/packages/testnet-faucets/src/ts/dispatch_queue.ts b/packages/testnet-faucets/src/ts/dispatch_queue.ts deleted file mode 100644 index 3d0958fbf..000000000 --- a/packages/testnet-faucets/src/ts/dispatch_queue.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { intervalUtils, logUtils } from '@0x/utils'; -import * as _ from 'lodash'; - -import { errorReporter } from './error_reporter'; - -const MAX_QUEUE_SIZE = 500; -const DEFAULT_QUEUE_INTERVAL_MS = 1000; - -export class DispatchQueue { - private readonly _queueIntervalMs: number; - private readonly _queue: Array<() => Promise>; - private _queueIntervalIdIfExists?: NodeJS.Timer; - constructor() { - this._queueIntervalMs = DEFAULT_QUEUE_INTERVAL_MS; - this._queue = []; - this._start(); - } - public add(taskAsync: () => Promise): boolean { - if (this.isFull()) { - return false; - } - this._queue.push(taskAsync); - return true; - } - public size(): number { - return this._queue.length; - } - public isFull(): boolean { - return this.size() >= MAX_QUEUE_SIZE; - } - public stop(): void { - if (!_.isUndefined(this._queueIntervalIdIfExists)) { - intervalUtils.clearAsyncExcludingInterval(this._queueIntervalIdIfExists); - } - } - private _start(): void { - this._queueIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval( - async () => { - const taskAsync = this._queue.shift(); - if (_.isUndefined(taskAsync)) { - return Promise.resolve(); - } - await taskAsync(); - }, - this._queueIntervalMs, - (err: Error) => { - logUtils.log(`Unexpected err: ${err} - ${JSON.stringify(err)}`); - // tslint:disable-next-line:no-floating-promises - errorReporter.reportAsync(err); - }, - ); - } -} diff --git a/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts b/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts deleted file mode 100644 index 58caeeeaa..000000000 --- a/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ERC20TokenWrapper } from '0x.js'; -import { BigNumber, logUtils } from '@0x/utils'; -import { Web3Wrapper } from '@0x/web3-wrapper'; -import * as _ from 'lodash'; - -import { configs } from './configs'; -import { TOKENS_BY_NETWORK } from './tokens'; - -const DISPENSE_AMOUNT_ETHER = 0.1; -const DISPENSE_AMOUNT_TOKEN = 1; -const DISPENSE_MAX_AMOUNT_TOKEN = 100; -const DISPENSE_MAX_AMOUNT_ETHER = 2; - -type AsyncTask = () => Promise; - -export const dispenseAssetTasks = { - dispenseEtherTask(recipientAddress: string, web3Wrapper: Web3Wrapper): AsyncTask { - return async () => { - logUtils.log(`Processing ETH ${recipientAddress}`); - const userBalance = await web3Wrapper.getBalanceInWeiAsync(recipientAddress); - const maxAmountInWei = Web3Wrapper.toWei(new BigNumber(DISPENSE_MAX_AMOUNT_ETHER)); - if (userBalance.isGreaterThanOrEqualTo(maxAmountInWei)) { - logUtils.log( - `User exceeded ETH balance maximum (${maxAmountInWei}) ${recipientAddress} ${userBalance} `, - ); - return; - } - const txHash = await web3Wrapper.sendTransactionAsync({ - from: configs.DISPENSER_ADDRESS, - to: recipientAddress, - value: Web3Wrapper.toWei(new BigNumber(DISPENSE_AMOUNT_ETHER)), - }); - logUtils.log(`Sent ${DISPENSE_AMOUNT_ETHER} ETH to ${recipientAddress} tx: ${txHash}`); - }; - }, - dispenseTokenTask( - recipientAddress: string, - tokenSymbol: string, - networkId: number, - erc20TokenWrapper: ERC20TokenWrapper, - ): AsyncTask { - return async () => { - logUtils.log(`Processing ${tokenSymbol} ${recipientAddress}`); - const amountToDispense = new BigNumber(DISPENSE_AMOUNT_TOKEN); - const tokenIfExists = _.get(TOKENS_BY_NETWORK, [networkId, tokenSymbol]); - if (_.isUndefined(tokenIfExists)) { - throw new Error(`Unsupported asset type: ${tokenSymbol}`); - } - const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountToDispense, tokenIfExists.decimals); - const userBalanceBaseUnits = await erc20TokenWrapper.getBalanceAsync( - tokenIfExists.address, - recipientAddress, - ); - const maxAmountBaseUnits = Web3Wrapper.toBaseUnitAmount( - new BigNumber(DISPENSE_MAX_AMOUNT_TOKEN), - tokenIfExists.decimals, - ); - if (userBalanceBaseUnits.isGreaterThanOrEqualTo(maxAmountBaseUnits)) { - logUtils.log( - `User exceeded token balance maximum (${maxAmountBaseUnits}) ${recipientAddress} ${userBalanceBaseUnits} `, - ); - return; - } - const txHash = await erc20TokenWrapper.transferAsync( - tokenIfExists.address, - configs.DISPENSER_ADDRESS, - recipientAddress, - baseUnitAmount, - ); - logUtils.log(`Sent ${amountToDispense} ${tokenSymbol} to ${recipientAddress} tx: ${txHash}`); - }; - }, -}; diff --git a/packages/testnet-faucets/src/ts/error_reporter.ts b/packages/testnet-faucets/src/ts/error_reporter.ts deleted file mode 100644 index d5358aed0..000000000 --- a/packages/testnet-faucets/src/ts/error_reporter.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { logUtils } from '@0x/utils'; -import * as express from 'express'; -import rollbar = require('rollbar'); - -import { configs } from './configs'; - -export const errorReporter = { - setup(): void { - rollbar.init(configs.ROLLBAR_ACCESS_KEY, { - environment: configs.ENVIRONMENT, - }); - rollbar.handleUncaughtExceptions(configs.ROLLBAR_ACCESS_KEY); - process.on('unhandledRejection', async (err: Error) => { - logUtils.log(`Uncaught exception ${err}. Stack: ${err.stack}`); - await errorReporter.reportAsync(err); - process.exit(1); - }); - }, - async reportAsync(err: Error, req?: express.Request): Promise { - if (configs.ENVIRONMENT === 'development') { - return; // Do not log development environment errors - } - return new Promise((resolve, reject) => { - rollbar.handleError(err, req, (rollbarErr: Error) => { - if (rollbarErr) { - logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`); - reject(rollbarErr); - } else { - resolve(); - } - }); - }); - }, - errorHandler(): any { - return rollbar.errorHandler(configs.ROLLBAR_ACCESS_KEY); - }, -}; diff --git a/packages/testnet-faucets/src/ts/global.d.ts b/packages/testnet-faucets/src/ts/global.d.ts deleted file mode 100644 index 94e63a32d..000000000 --- a/packages/testnet-faucets/src/ts/global.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module '*.json' { - const json: any; - /* tslint:disable */ - export default json; - /* tslint:enable */ -} diff --git a/packages/testnet-faucets/src/ts/handler.ts b/packages/testnet-faucets/src/ts/handler.ts deleted file mode 100644 index 533e1f8b3..000000000 --- a/packages/testnet-faucets/src/ts/handler.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { - assetDataUtils, - BigNumber, - ContractWrappers, - generatePseudoRandomSalt, - Order, - orderHashUtils, - Provider, - RPCSubprovider, - signatureUtils, - SignedOrder, - Web3ProviderEngine, -} from '0x.js'; -import { NonceTrackerSubprovider, PrivateKeyWalletSubprovider } from '@0x/subproviders'; -import { logUtils } from '@0x/utils'; -import { Web3Wrapper } from '@0x/web3-wrapper'; -import * as express from 'express'; -import * as _ from 'lodash'; - -import { configs } from './configs'; -import { constants } from './constants'; -import { DispatchQueue } from './dispatch_queue'; -import { dispenseAssetTasks } from './dispense_asset_tasks'; -import { rpcUrls } from './rpc_urls'; -import { TOKENS_BY_NETWORK } from './tokens'; - -interface NetworkConfig { - dispatchQueue: DispatchQueue; - web3Wrapper: Web3Wrapper; - contractWrappers: ContractWrappers; - networkId: number; -} - -interface ItemByNetworkId { - [networkId: string]: T; -} - -enum RequestedAssetType { - ETH = 'ETH', // tslint:disable-line:enum-naming - WETH = 'WETH', // tslint:disable-line:enum-naming - ZRX = 'ZRX', // tslint:disable-line:enum-naming -} - -const FIVE_DAYS_IN_MS = 4.32e8; // TODO: make this configurable -const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; -const ZERO = new BigNumber(0); -const ASSET_AMOUNT = new BigNumber(0.1); - -export class Handler { - private readonly _networkConfigByNetworkId: ItemByNetworkId = {}; - private static _createProviderEngine(rpcUrl: string): Provider { - if (_.isUndefined(configs.DISPENSER_PRIVATE_KEY)) { - throw new Error('Dispenser Private key not found'); - } - const engine = new Web3ProviderEngine(); - engine.addProvider(new NonceTrackerSubprovider()); - engine.addProvider(new PrivateKeyWalletSubprovider(configs.DISPENSER_PRIVATE_KEY)); - engine.addProvider(new RPCSubprovider(rpcUrl)); - engine.start(); - return engine; - } - constructor() { - _.forIn(rpcUrls, (rpcUrl: string, networkIdString: string) => { - const providerObj = Handler._createProviderEngine(rpcUrl); - const web3Wrapper = new Web3Wrapper(providerObj); - // tslint:disable-next-line:custom-no-magic-numbers - const networkId = parseInt(networkIdString, 10); - const contractWrappersConfig = { - networkId, - }; - const contractWrappers = new ContractWrappers(providerObj, contractWrappersConfig); - const dispatchQueue = new DispatchQueue(); - this._networkConfigByNetworkId[networkId] = { - dispatchQueue, - web3Wrapper, - contractWrappers, - networkId, - }; - }); - } - public getQueueInfo(_req: express.Request, res: express.Response): void { - res.setHeader('Content-Type', 'application/json'); - const queueInfo = _.mapValues(rpcUrls, (_rpcUrl: string, networkId: string) => { - const dispatchQueue = this._networkConfigByNetworkId[networkId].dispatchQueue; - return { - full: dispatchQueue.isFull(), - size: dispatchQueue.size(), - }; - }); - const payload = JSON.stringify(queueInfo); - res.status(constants.SUCCESS_STATUS).send(payload); - } - public dispenseEther(req: express.Request, res: express.Response): void { - this._dispenseAsset(req, res, RequestedAssetType.ETH); - } - public dispenseZRX(req: express.Request, res: express.Response): void { - this._dispenseAsset(req, res, RequestedAssetType.ZRX); - } - public async dispenseWETHOrderAsync(req: express.Request, res: express.Response): Promise { - await this._dispenseOrderAsync(req, res, RequestedAssetType.WETH); - } - public async dispenseZRXOrderAsync( - req: express.Request, - res: express.Response, - _next: express.NextFunction, - ): Promise { - await this._dispenseOrderAsync(req, res, RequestedAssetType.ZRX); - } - private _dispenseAsset(req: express.Request, res: express.Response, requestedAssetType: RequestedAssetType): void { - const networkId = req.params.networkId; - const recipient = req.params.recipient; - const networkConfig = _.get(this._networkConfigByNetworkId, networkId); - if (_.isUndefined(networkConfig)) { - res.status(constants.BAD_REQUEST_STATUS).send('UNSUPPORTED_NETWORK_ID'); - return; - } - let dispenserTask; - switch (requestedAssetType) { - case RequestedAssetType.ETH: - dispenserTask = dispenseAssetTasks.dispenseEtherTask(recipient, networkConfig.web3Wrapper); - break; - case RequestedAssetType.WETH: - case RequestedAssetType.ZRX: - dispenserTask = dispenseAssetTasks.dispenseTokenTask( - recipient, - requestedAssetType, - networkConfig.networkId, - networkConfig.contractWrappers.erc20Token, - ); - break; - default: - throw new Error(`Unsupported asset type: ${requestedAssetType}`); - } - const didAddToQueue = networkConfig.dispatchQueue.add(dispenserTask); - if (!didAddToQueue) { - res.status(constants.SERVICE_UNAVAILABLE_STATUS).send('QUEUE_IS_FULL'); - return; - } - logUtils.log(`Added ${recipient} to queue: ${requestedAssetType} networkId: ${networkId}`); - res.status(constants.SUCCESS_STATUS).end(); - } - private async _dispenseOrderAsync( - req: express.Request, - res: express.Response, - requestedAssetType: RequestedAssetType, - ): Promise { - const networkConfig = _.get(this._networkConfigByNetworkId, req.params.networkId); - if (_.isUndefined(networkConfig)) { - res.status(constants.BAD_REQUEST_STATUS).send('UNSUPPORTED_NETWORK_ID'); - return; - } - res.setHeader('Content-Type', 'application/json'); - const makerTokenIfExists = _.get(TOKENS_BY_NETWORK, [networkConfig.networkId, requestedAssetType]); - if (_.isUndefined(makerTokenIfExists)) { - throw new Error(`Unsupported asset type: ${requestedAssetType}`); - } - const takerTokenSymbol = - requestedAssetType === RequestedAssetType.WETH ? RequestedAssetType.ZRX : RequestedAssetType.WETH; - const takerTokenIfExists = _.get(TOKENS_BY_NETWORK, [networkConfig.networkId, takerTokenSymbol]); - if (_.isUndefined(takerTokenIfExists)) { - throw new Error(`Unsupported asset type: ${takerTokenSymbol}`); - } - - const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(ASSET_AMOUNT, makerTokenIfExists.decimals); - const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(ASSET_AMOUNT, takerTokenIfExists.decimals); - const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenIfExists.address); - const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenIfExists.address); - const order: Order = { - makerAddress: configs.DISPENSER_ADDRESS, - takerAddress: req.params.recipient as string, - makerFee: ZERO, - takerFee: ZERO, - makerAssetAmount, - takerAssetAmount, - makerAssetData, - takerAssetData, - salt: generatePseudoRandomSalt(), - exchangeAddress: networkConfig.contractWrappers.exchange.address, - feeRecipientAddress: NULL_ADDRESS, - senderAddress: NULL_ADDRESS, - expirationTimeSeconds: new BigNumber(Date.now() + FIVE_DAYS_IN_MS) - // tslint:disable-next-line:custom-no-magic-numbers - .div(1000) - .integerValue(BigNumber.ROUND_FLOOR), - }; - const orderHash = orderHashUtils.getOrderHashHex(order); - const signature = await signatureUtils.ecSignHashAsync( - networkConfig.web3Wrapper.getProvider(), - orderHash, - configs.DISPENSER_ADDRESS, - ); - const signedOrder: SignedOrder = { - ...order, - signature, - }; - const payload = JSON.stringify(signedOrder); - logUtils.log(`Dispensed signed order: ${payload}`); - res.status(constants.SUCCESS_STATUS).send(payload); - } -} diff --git a/packages/testnet-faucets/src/ts/parameter_transformer.ts b/packages/testnet-faucets/src/ts/parameter_transformer.ts deleted file mode 100644 index bed8eb99e..000000000 --- a/packages/testnet-faucets/src/ts/parameter_transformer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { addressUtils } from '@0x/utils'; -import { NextFunction, Request, Response } from 'express'; -import * as _ from 'lodash'; - -import { constants } from './constants'; -import { rpcUrls } from './rpc_urls'; - -const DEFAULT_NETWORK_ID = 42; // kovan - -export const parameterTransformer = { - transform(req: Request, res: Response, next: NextFunction): void { - const recipientAddress = req.params.recipient; - if (_.isUndefined(recipientAddress) || !addressUtils.isAddress(recipientAddress)) { - res.status(constants.BAD_REQUEST_STATUS).send('INVALID_RECIPIENT_ADDRESS'); - return; - } - const lowerCaseRecipientAddress = recipientAddress.toLowerCase(); - req.params.recipient = lowerCaseRecipientAddress; - const networkId = _.get(req.query, 'networkId', DEFAULT_NETWORK_ID); - const rpcUrlIfExists = _.get(rpcUrls, networkId); - if (_.isUndefined(rpcUrlIfExists)) { - res.status(constants.BAD_REQUEST_STATUS).send('UNSUPPORTED_NETWORK_ID'); - return; - } - req.params.networkId = networkId; - next(); - }, -}; diff --git a/packages/testnet-faucets/src/ts/rpc_urls.ts b/packages/testnet-faucets/src/ts/rpc_urls.ts deleted file mode 100644 index d31908257..000000000 --- a/packages/testnet-faucets/src/ts/rpc_urls.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { configs } from './configs'; - -const productionRpcUrls = { - '3': `https://ropsten.infura.io/${configs.INFURA_API_KEY}`, - '42': `https://kovan.infura.io/${configs.INFURA_API_KEY}`, -}; - -const developmentRpcUrls = { - '50': 'http://127.0.0.1:8545', -}; - -export const rpcUrls = configs.ENVIRONMENT === 'development' ? developmentRpcUrls : productionRpcUrls; diff --git a/packages/testnet-faucets/src/ts/server.ts b/packages/testnet-faucets/src/ts/server.ts deleted file mode 100644 index f00562000..000000000 --- a/packages/testnet-faucets/src/ts/server.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as bodyParser from 'body-parser'; -import * as express from 'express'; - -import { constants } from './constants'; -import { errorReporter } from './error_reporter'; -import { Handler } from './handler'; -import { parameterTransformer } from './parameter_transformer'; - -// Setup the errorReporter to catch uncaught exceptions and unhandled rejections -errorReporter.setup(); - -const app = express(); -app.use(bodyParser.json()); // for parsing application/json -// tslint:disable-next-line:no-unused-variable -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); - next(); -}); - -const handler = new Handler(); -// tslint:disable-next-line:no-unused-variable -app.get('/ping', (req: express.Request, res: express.Response) => { - res.status(constants.SUCCESS_STATUS).send('pong'); -}); -app.get('/info', handler.getQueueInfo.bind(handler)); -app.get( - '/ether/:recipient', - parameterTransformer.transform.bind(parameterTransformer), - handler.dispenseEther.bind(handler), -); -app.get( - '/zrx/:recipient', - parameterTransformer.transform.bind(parameterTransformer), - handler.dispenseZRX.bind(handler), -); -app.get( - '/order/weth/:recipient', - parameterTransformer.transform.bind(parameterTransformer), - handler.dispenseWETHOrderAsync.bind(handler), -); -app.get( - '/order/zrx/:recipient', - parameterTransformer.transform.bind(parameterTransformer), - handler.dispenseZRXOrderAsync.bind(handler), -); - -// Log to rollbar any errors unhandled by handlers -app.use(errorReporter.errorHandler()); -const DEFAULT_PORT = 3000; -const port = process.env.PORT || DEFAULT_PORT; -app.listen(port); diff --git a/packages/testnet-faucets/src/ts/tokens.ts b/packages/testnet-faucets/src/ts/tokens.ts deleted file mode 100644 index 4ffb03df4..000000000 --- a/packages/testnet-faucets/src/ts/tokens.ts +++ /dev/null @@ -1,44 +0,0 @@ -interface TokensByNetwork { - [networkId: number]: { [tokenSymbol: string]: { address: string; decimals: number } }; -} - -export const tokens = { - ZRX: { - decimals: 18, - }, - WETH: { - decimals: 18, - }, -}; -export const TOKENS_BY_NETWORK: TokensByNetwork = { - 3: { - ZRX: { - ...tokens.ZRX, - address: '0xff67881f8d12f372d91baae9752eb3631ff0ed00', - }, - WETH: { - ...tokens.WETH, - address: '0xc778417e063141139fce010982780140aa0cd5ab', - }, - }, - 42: { - ZRX: { - ...tokens.ZRX, - address: '0x2002d3812f58e35f0ea1ffbf80a75a38c32175fa', - }, - WETH: { - ...tokens.WETH, - address: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', - }, - }, - 50: { - ZRX: { - ...tokens.ZRX, - address: '0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c', - }, - WETH: { - ...tokens.WETH, - address: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', - }, - }, -}; -- cgit v1.2.3