From 98362bb7cd8972d83d7a628b2db4fb06831871c0 Mon Sep 17 00:00:00 2001 From: bigboydiamonds <57741810+bigboydiamonds@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:33:47 -0700 Subject: [PATCH 1/4] feat(api): bridge limits [SLT-165] (#3179) * adds `/bridgeLimits` route, controller * fetch best sdk quote for min/max origin amounts * add tests * implement middleware to normalize addresses * adds swagger doc --- packages/rest-api/package.json | 1 + .../src/controllers/bridgeLimitsController.ts | 108 ++++++++++++++ .../rest-api/src/routes/bridgeLimitsRoute.ts | 138 ++++++++++++++++++ packages/rest-api/src/routes/index.ts | 2 + .../src/tests/bridgeLimitsRoute.test.ts | 81 ++++++++++ 5 files changed, 330 insertions(+) create mode 100644 packages/rest-api/src/controllers/bridgeLimitsController.ts create mode 100644 packages/rest-api/src/routes/bridgeLimitsRoute.ts create mode 100644 packages/rest-api/src/tests/bridgeLimitsRoute.test.ts diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index 4d0f56ef06..79c289ccd0 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -18,6 +18,7 @@ "test:coverage": "jest --collect-coverage" }, "dependencies": { + "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/providers": "^5.7.2", "@ethersproject/units": "5.7.0", diff --git a/packages/rest-api/src/controllers/bridgeLimitsController.ts b/packages/rest-api/src/controllers/bridgeLimitsController.ts new file mode 100644 index 0000000000..1ef292338f --- /dev/null +++ b/packages/rest-api/src/controllers/bridgeLimitsController.ts @@ -0,0 +1,108 @@ +import { validationResult } from 'express-validator' +import { BigNumber } from 'ethers' +import { parseUnits } from '@ethersproject/units' + +import { Synapse } from '../services/synapseService' +import { tokenAddressToToken } from '../utils/tokenAddressToToken' +import { formatBNToString } from '../utils/formatBNToString' + +export const bridgeLimitsController = async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }) + } + try { + const { fromChain, fromToken, toChain, toToken } = req.query + + const fromTokenInfo = tokenAddressToToken(fromChain, fromToken) + const toTokenInfo = tokenAddressToToken(toChain, toToken) + + const upperLimitValue = parseUnits('1000000', fromTokenInfo.decimals) + const upperLimitBridgeQuotes = await Synapse.allBridgeQuotes( + Number(fromChain), + Number(toChain), + fromTokenInfo.address, + toTokenInfo.address, + upperLimitValue + ) + + const lowerLimitValues = ['0.01', '10'] + let lowerLimitBridgeQuotes = null + + for (const limit of lowerLimitValues) { + const lowerLimitAmount = parseUnits(limit, fromTokenInfo.decimals) + + lowerLimitBridgeQuotes = await Synapse.allBridgeQuotes( + Number(fromChain), + Number(toChain), + fromTokenInfo.address, + toTokenInfo.address, + lowerLimitAmount + ) + + if (lowerLimitBridgeQuotes && lowerLimitBridgeQuotes.length > 0) { + break + } + } + + const maxBridgeAmountQuote = upperLimitBridgeQuotes.reduce( + (maxQuote, currentQuote) => { + const currentMaxAmount = currentQuote.maxAmountOut + const maxAmount = maxQuote ? maxQuote.maxAmountOut : BigNumber.from(0) + + return currentMaxAmount.gt(maxAmount) ? currentQuote : maxQuote + }, + null + ) + + const minBridgeAmountQuote = lowerLimitBridgeQuotes.reduce( + (minQuote, currentQuote) => { + const currentFeeAmount = currentQuote.feeAmount + const minFeeAmount = minQuote ? minQuote.feeAmount : null + + return !minFeeAmount || currentFeeAmount.lt(minFeeAmount) + ? currentQuote + : minQuote + }, + null + ) + + if (!maxBridgeAmountQuote || !minBridgeAmountQuote) { + return res.json({ + maxOriginAmount: null, + minOriginAmount: null, + }) + } + + const maxAmountOriginQueryTokenOutInfo = tokenAddressToToken( + toChain, + maxBridgeAmountQuote.destQuery.tokenOut + ) + + const minAmountOriginQueryTokenOutInfo = tokenAddressToToken( + fromChain, + minBridgeAmountQuote.originQuery.tokenOut + ) + + const maxOriginAmount = formatBNToString( + maxBridgeAmountQuote.maxAmountOut, + maxAmountOriginQueryTokenOutInfo.decimals + ) + + const minOriginAmount = formatBNToString( + minBridgeAmountQuote.feeAmount, + minAmountOriginQueryTokenOutInfo.decimals + ) + + return res.json({ + maxOriginAmount, + minOriginAmount, + }) + } catch (err) { + res.status(500).json({ + error: + 'An unexpected error occurred in /bridgeLimits. Please try again later.', + details: err.message, + }) + } +} diff --git a/packages/rest-api/src/routes/bridgeLimitsRoute.ts b/packages/rest-api/src/routes/bridgeLimitsRoute.ts new file mode 100644 index 0000000000..14ab637671 --- /dev/null +++ b/packages/rest-api/src/routes/bridgeLimitsRoute.ts @@ -0,0 +1,138 @@ +import express from 'express' +import { check } from 'express-validator' + +import { CHAINS_ARRAY } from '../constants/chains' +import { showFirstValidationError } from '../middleware/showFirstValidationError' +import { bridgeLimitsController } from '../controllers/bridgeLimitsController' +import { isTokenSupportedOnChain } from './../utils/isTokenSupportedOnChain' +import { isTokenAddress } from '../utils/isTokenAddress' +import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress' +import { checksumAddresses } from '../middleware/checksumAddresses' + +const router = express.Router() + +/** + * @openapi + * /bridgeLimits: + * get: + * summary: Get min/max origin values for bridge quote + * description: Retrieve min/max bridgeable amounts to bridge from source chain to destination chain. Returns null for min/max amounts if limits are unavailable. + * parameters: + * - in: query + * name: fromChain + * required: true + * schema: + * type: integer + * description: The source chain ID. + * - in: query + * name: toChain + * required: true + * schema: + * type: integer + * description: The destination chain ID. + * - in: query + * name: fromToken + * required: true + * schema: + * type: string + * description: The address of the token on the source chain. + * - in: query + * name: toToken + * required: true + * schema: + * type: string + * description: The address of the token on the destination chain. + * responses: + * 200: + * description: Successful response containing min and max origin amounts. + * content: + * application/json: + * schema: + * type: object + * properties: + * maxOriginAmount: + * type: string + * description: Maximum amount of tokens that can be bridged from the origin chain. + * minOriginAmount: + * type: string + * description: Minimum amount of tokens that can be bridged from the origin chain. + * example: + * maxOriginAmount: "999600" + * minOriginAmount: "4" + * 400: + * description: Invalid input + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: object + * properties: + * value: + * type: string + * message: + * type: string + * field: + * type: string + * location: + * type: string + * example: + * error: + * value: "999" + * message: "Unsupported fromChain" + * field: "fromChain" + * location: "query" + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * details: + * type: string + */ +router.get( + '/', + normalizeNativeTokenAddress(['fromToken', 'toToken']), + checksumAddresses(['fromToken', 'toToken']), + [ + check('fromChain') + .exists() + .withMessage('fromChain is required') + .isNumeric() + .custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value))) + .withMessage('Unsupported fromChain'), + check('toChain') + .exists() + .withMessage('toChain is required') + .isNumeric() + .custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value))) + .withMessage('Unsupported toChain'), + check('fromToken') + .exists() + .withMessage('fromToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid fromToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.fromChain as string) + ) + .withMessage('Token not supported on specified chain'), + check('toToken') + .exists() + .withMessage('toToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid toToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.toChain as string) + ) + .withMessage('Token not supported on specified chain'), + ], + showFirstValidationError, + bridgeLimitsController +) + +export default router diff --git a/packages/rest-api/src/routes/index.ts b/packages/rest-api/src/routes/index.ts index 1bbcf3ea51..2c5e1c547e 100644 --- a/packages/rest-api/src/routes/index.ts +++ b/packages/rest-api/src/routes/index.ts @@ -10,6 +10,7 @@ import bridgeTxStatusRoute from './bridgeTxStatusRoute' import destinationTxRoute from './destinationTxRoute' import tokenListRoute from './tokenListRoute' import destinationTokensRoute from './destinationTokensRoute' +import bridgeLimitsRoute from './bridgeLimitsRoute' const router = express.Router() @@ -18,6 +19,7 @@ router.use('/swap', swapRoute) router.use('/swapTxInfo', swapTxInfoRoute) router.use('/bridge', bridgeRoute) router.use('/bridgeTxInfo', bridgeTxInfoRoute) +router.use('/bridgeLimits', bridgeLimitsRoute) router.use('/synapseTxId', synapseTxIdRoute) router.use('/bridgeTxStatus', bridgeTxStatusRoute) router.use('/destinationTx', destinationTxRoute) diff --git a/packages/rest-api/src/tests/bridgeLimitsRoute.test.ts b/packages/rest-api/src/tests/bridgeLimitsRoute.test.ts new file mode 100644 index 0000000000..921393773b --- /dev/null +++ b/packages/rest-api/src/tests/bridgeLimitsRoute.test.ts @@ -0,0 +1,81 @@ +import request from 'supertest' +import express from 'express' + +import bridgeLimitsRoute from '../routes/bridgeLimitsRoute' +import { USDC, ETH } from '../constants/bridgeable' + +const app = express() +app.use('/bridgeLimits', bridgeLimitsRoute) + +describe('Get Bridge Limits Route', () => { + it('should return min/max origin amounts bridging USDC', async () => { + const response = await request(app).get('/bridgeLimits').query({ + fromChain: 1, + fromToken: USDC.addresses[1], + toChain: 10, + toToken: USDC.addresses[10], + }) + + expect(response.status).toBe(200) + expect(response.body).toHaveProperty('maxOriginAmount') + expect(response.body).toHaveProperty('minOriginAmount') + }, 10_000) + + it('should return min/max origin amounts bridging ETH', async () => { + const response = await request(app).get('/bridgeLimits').query({ + fromChain: 1, + fromToken: ETH.addresses[1], + toChain: 10, + toToken: ETH.addresses[10], + }) + + expect(response.status).toBe(200) + expect(response.body).toHaveProperty('maxOriginAmount') + expect(response.body).toHaveProperty('minOriginAmount') + }, 10_000) + + it('should return 400 for unsupported fromChain', async () => { + const response = await request(app).get('/bridgeLimits').query({ + fromChain: '999', + toChain: '137', + fromToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', + toToken: USDC.addresses[137], + }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty( + 'message', + 'Unsupported fromChain' + ) + }, 10_000) + + it('should return 400 for unsupported toChain', async () => { + const response = await request(app).get('/bridgeLimits').query({ + fromChain: '137', + toChain: '999', + fromToken: USDC.addresses[137], + toToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', + }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty('message', 'Unsupported toChain') + }, 10_000) + + it('should return 400 for missing fromToken', async () => { + const response = await request(app).get('/bridgeLimits').query({ + fromChain: '1', + toChain: '137', + toToken: USDC.addresses[137], + }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty('field', 'fromToken') + }, 10_000) + + it('should return 400 for missing toToken', async () => { + const response = await request(app).get('/bridgeLimits').query({ + fromChain: '1', + toChain: '137', + fromToken: USDC.addresses[1], + }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty('field', 'toToken') + }, 10_000) +}) From 67a04cd3f00c3f45bf5fcdc4b84e36d20f1aff4b Mon Sep 17 00:00:00 2001 From: bigboydiamonds Date: Tue, 24 Sep 2024 19:38:04 +0000 Subject: [PATCH 2/4] Publish - @synapsecns/rest-api@1.2.0 --- packages/rest-api/CHANGELOG.md | 11 +++++++++++ packages/rest-api/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/rest-api/CHANGELOG.md b/packages/rest-api/CHANGELOG.md index 286079ef3d..ac53ec49bf 100644 --- a/packages/rest-api/CHANGELOG.md +++ b/packages/rest-api/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.2.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.1.5...@synapsecns/rest-api@1.2.0) (2024-09-24) + + +### Features + +* **api:** bridge limits [SLT-165] ([#3179](https://github.com/synapsecns/sanguine/issues/3179)) ([98362bb](https://github.com/synapsecns/sanguine/commit/98362bb7cd8972d83d7a628b2db4fb06831871c0)) + + + + + ## [1.1.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.1.4...@synapsecns/rest-api@1.1.5) (2024-09-23) diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index 79c289ccd0..e6836b39f0 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/rest-api", - "version": "1.1.5", + "version": "1.2.0", "private": "true", "engines": { "node": ">=18.17.0" From c15ec8691902817f096589553bac360b53ba40cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:29:04 +0200 Subject: [PATCH 3/4] fix(contracts-rfq): limit the amount of solhint warnings [SLT-245] (#3182) * ci: limit the amount of solhint warnings * refactor: move the errors into the separate interface * refactor: errors imports in tests --- .../contracts-rfq/contracts/FastBridgeV2.sol | 4 ++-- .../interfaces/IFastBridgeV2Errors.sol | 19 +++++++++++++++++++ packages/contracts-rfq/package.json | 4 ++-- .../contracts-rfq/test/FastBridgeV2.Dst.t.sol | 2 -- .../test/FastBridgeV2.Parity.t.sol | 6 ++++-- .../contracts-rfq/test/FastBridgeV2.Src.t.sol | 13 ------------- .../contracts-rfq/test/FastBridgeV2.t.sol | 3 ++- 7 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index 7f938f28f1..50acfca74c 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -3,14 +3,14 @@ pragma solidity 0.8.24; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./libs/Errors.sol"; import {UniversalTokenLib} from "./libs/UniversalToken.sol"; import {Admin} from "./Admin.sol"; import {IFastBridge} from "./interfaces/IFastBridge.sol"; import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; +import {IFastBridgeV2Errors} from "./interfaces/IFastBridgeV2Errors.sol"; -contract FastBridgeV2 is Admin, IFastBridgeV2 { +contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { using SafeERC20 for IERC20; using UniversalTokenLib for address; diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol new file mode 100644 index 0000000000..70fd3d0e39 --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IFastBridgeV2Errors { + error AmountIncorrect(); + error ChainIncorrect(); + error MsgValueIncorrect(); + error SenderIncorrect(); + error StatusIncorrect(); + error ZeroAddress(); + + error DeadlineExceeded(); + error DeadlineNotExceeded(); + error DeadlineTooShort(); + error DisputePeriodNotPassed(); + error DisputePeriodPassed(); + + error TransactionRelayed(); +} diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index d868a0738e..9fb2606e9f 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -24,7 +24,7 @@ "lint:check": "forge fmt --check && npm run solhint:check", "ci:lint": "npm run lint:check", "build:go": "./flatten.sh contracts/*.sol test/*.sol", - "solhint": "solhint '{contracts,script,test}/**/*.sol' --fix --noPrompt", - "solhint:check": "solhint '{contracts,script,test}/**/*.sol'" + "solhint": "solhint '{contracts,script,test}/**/*.sol' --fix --noPrompt --max-warnings 3", + "solhint:check": "solhint '{contracts,script,test}/**/*.sol' --max-warnings 3" } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol index 926f05c8c0..b9957b85ca 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {ChainIncorrect, DeadlineExceeded, TransactionRelayed} from "../contracts/libs/Errors.sol"; - import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; // solhint-disable func-name-mixedcase, ordering diff --git a/packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol index d92e4f8ac5..dd4b3a4fa2 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Parity.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; -import {FastBridgeTest, SenderIncorrect} from "./FastBridge.t.sol"; +import {IFastBridgeV2Errors} from "../contracts/interfaces/IFastBridgeV2Errors.sol"; + +import {FastBridgeTest} from "./FastBridge.t.sol"; // solhint-disable func-name-mixedcase, ordering -contract FastBridgeV2ParityTest is FastBridgeTest { +contract FastBridgeV2ParityTest is FastBridgeTest, IFastBridgeV2Errors { address public anotherRelayer = makeAddr("Another Relayer"); function deployFastBridge() internal virtual override returns (address) { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 8a0106d0c0..b5177aae6a 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -1,19 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import { - AmountIncorrect, - ChainIncorrect, - DisputePeriodNotPassed, - DisputePeriodPassed, - DeadlineNotExceeded, - DeadlineTooShort, - MsgValueIncorrect, - SenderIncorrect, - StatusIncorrect, - ZeroAddress -} from "../contracts/libs/Errors.sol"; - import {FastBridgeV2, FastBridgeV2Test, IFastBridge} from "./FastBridgeV2.t.sol"; // solhint-disable func-name-mixedcase, ordering diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index 39881c5151..8517a2dbf8 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; +import {IFastBridgeV2Errors} from "../contracts/interfaces/IFastBridgeV2Errors.sol"; import {FastBridgeV2} from "../contracts/FastBridgeV2.sol"; import {MockERC20} from "./MockERC20.sol"; @@ -11,7 +12,7 @@ import {Test} from "forge-std/Test.sol"; import {stdStorage, StdStorage} from "forge-std/Test.sol"; // solhint-disable no-empty-blocks, ordering -abstract contract FastBridgeV2Test is Test { +abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { using stdStorage for StdStorage; uint32 public constant SRC_CHAIN_ID = 1337; From 11ac6502799730a682345c30c11da9ff8f51de6e Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Wed, 25 Sep 2024 11:33:32 +0000 Subject: [PATCH 4/4] Publish - @synapsecns/contracts-rfq@0.5.1 --- packages/contracts-rfq/CHANGELOG.md | 11 +++++++++++ packages/contracts-rfq/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index e70355a860..5a569c7584 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.5.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.5.0...@synapsecns/contracts-rfq@0.5.1) (2024-09-25) + + +### Bug Fixes + +* **contracts-rfq:** limit the amount of solhint warnings [SLT-245] ([#3182](https://github.com/synapsecns/sanguine/issues/3182)) ([c15ec86](https://github.com/synapsecns/sanguine/commit/c15ec8691902817f096589553bac360b53ba40cf)) + + + + + # 0.5.0 (2024-09-24) diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index 9fb2606e9f..eaf3c456be 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.5.0", + "version": "0.5.1", "description": "FastBridge contracts.", "private": true, "files": [