diff --git a/.yarn/cache/@graphql-typed-document-node-core-npm-3.2.0-505adb1e90-fa44443acc.zip b/.yarn/cache/@graphql-typed-document-node-core-npm-3.2.0-505adb1e90-fa44443acc.zip new file mode 100644 index 000000000..40c3a9cbb Binary files /dev/null and b/.yarn/cache/@graphql-typed-document-node-core-npm-3.2.0-505adb1e90-fa44443acc.zip differ diff --git a/.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip b/.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip new file mode 100644 index 000000000..3424b4443 Binary files /dev/null and b/.yarn/cache/tslib-npm-2.6.2-4fc8c068d9-329ea56123.zip differ diff --git a/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip b/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip deleted file mode 100644 index 74e59aab9..000000000 Binary files a/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip and /dev/null differ diff --git a/.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip b/.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip new file mode 100644 index 000000000..3b175343d Binary files /dev/null and b/.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip differ diff --git a/.yarn/cache/yaml-npm-2.2.1-b7f7f5e84d-84f68cbe46.zip b/.yarn/cache/yaml-npm-2.2.1-b7f7f5e84d-84f68cbe46.zip deleted file mode 100644 index 0e658b209..000000000 Binary files a/.yarn/cache/yaml-npm-2.2.1-b7f7f5e84d-84f68cbe46.zip and /dev/null differ diff --git a/.yarn/cache/yaml-npm-2.3.3-c5a47b9f8f-cdfd132e7e.zip b/.yarn/cache/yaml-npm-2.3.3-c5a47b9f8f-cdfd132e7e.zip new file mode 100644 index 000000000..160a02342 Binary files /dev/null and b/.yarn/cache/yaml-npm-2.3.3-c5a47b9f8f-cdfd132e7e.zip differ diff --git a/v2/perps-v2/ui/codegen.ts b/v2/perps-v2/ui/codegen.ts index 8c5834dad..306d997ea 100644 --- a/v2/perps-v2/ui/codegen.ts +++ b/v2/perps-v2/ui/codegen.ts @@ -1,11 +1,15 @@ require('dotenv').config({ path: '.env.local', override: true }); + import { CodegenConfig } from '@graphql-codegen/cli'; -import { PERPS_V2_DASHBOARD_GRAPH_GOERLI_URL, PERPS_V2_DASHBOARD_GRAPH_URL } from './src/utils'; import { isStaging } from './src/utils/isStaging'; +import { + PERPS_V2_DASHBOARD_GRAPH_GOERLI_URL, + PERPS_V2_DASHBOARD_GRAPH_URL, +} from './src/utils/constants'; const config: CodegenConfig = { schema: isStaging ? PERPS_V2_DASHBOARD_GRAPH_GOERLI_URL : PERPS_V2_DASHBOARD_GRAPH_URL, - documents: ['src/**/*.ts'], + documents: ['src/queries/**/*.ts'], generates: { './src/__generated__/': { preset: 'client', diff --git a/v2/perps-v2/ui/package.json b/v2/perps-v2/ui/package.json index 16bcc0b01..f92e253cd 100644 --- a/v2/perps-v2/ui/package.json +++ b/v2/perps-v2/ui/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "webpack-cli serve", "build": "NODE_ENV=production webpack-cli", - "compile": "graphql-codegen", + "compile": "graphql-codegen ", "test": "NODE_ENV=test jest", "typecheck": "tsc --noEmit", "standalone-install": "yarn workspaces focus '@synthetixio/perps-v2-dashboard'", diff --git a/v2/perps-v2/ui/src/__generated__/gql.ts b/v2/perps-v2/ui/src/__generated__/gql.ts index 0b92fc3a4..74cbf386d 100644 --- a/v2/perps-v2/ui/src/__generated__/gql.ts +++ b/v2/perps-v2/ui/src/__generated__/gql.ts @@ -20,6 +20,7 @@ const documents = { "\n query StatsQuery($where: DailyStat_filter, $orderBy: DailyStat_orderBy, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n dailyStats(where: $where, orderBy: $orderBy, orderDirection: $orderDirection, first: $first, skip: $skip) {\n id\n timestamp\n cumulativeVolume\n volume\n fees\n cumulativeFees\n day\n existingTraders\n newTraders\n cumulativeTraders\n cumulativeTrades\n trades\n }\n }\n": types.StatsQueryDocument, "\n query MarketsQuery($where: DailyMarketStat_filter, $orderBy: DailyMarketStat_orderBy, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n dailyMarketStats(where: $where, orderBy: $orderBy, orderDirection: $orderDirection, first: $first, skip: $skip) {\n id\n day\n market {\n id\n marketKey\n asset\n isActive\n timestamp\n }\n volume\n }\n }\n": types.MarketsQueryDocument, "\n query MarketsIdQuery {\n futuresMarkets {\n id\n marketKey\n }\n }\n": types.MarketsIdQueryDocument, + "\n query PositionsLiquidated($where: PositionLiquidated_filter, $skip: Int, $first: Int, $orderBy: PositionLiquidated_orderBy, $orderDirection: OrderDirection) {\n positionLiquidateds(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {\n id\n timestamp\n txHash\n size\n price\n futuresPosition {\n leverage\n }\n market {\n asset\n }\n fee\n liquidator\n trader {\n id\n totalLiquidations\n }\n }\n }\n": types.PositionsLiquidatedDocument, "\n query PositionsMarket($where: FuturesPosition_filter, $skip: Int, $first: Int, $orderBy: FuturesPosition_orderBy, $orderDirection: OrderDirection) {\n futuresPositions(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {\n market {\n id\n marketKey\n asset\n }\n trader {\n id\n }\n isOpen\n entryPrice\n avgEntryPrice\n leverage\n feesPaidToSynthetix\n id\n realizedPnl\n unrealizedPnl\n feesPaidToSynthetix\n exitPrice\n lastPrice\n netFunding\n long\n size\n isLiquidated\n }\n }\n": types.PositionsMarketDocument, "\n query Synthetix {\n synthetix(id: \"synthetix\") {\n feesByLiquidations\n feesByPositionModifications\n totalVolume\n totalLiquidations\n totalTraders\n }\n }\n": types.SynthetixDocument, }; @@ -66,6 +67,10 @@ export function gql(source: "\n query MarketsQuery($where: DailyMarketStat_filt * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql(source: "\n query MarketsIdQuery {\n futuresMarkets {\n id\n marketKey\n }\n }\n"): (typeof documents)["\n query MarketsIdQuery {\n futuresMarkets {\n id\n marketKey\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n query PositionsLiquidated($where: PositionLiquidated_filter, $skip: Int, $first: Int, $orderBy: PositionLiquidated_orderBy, $orderDirection: OrderDirection) {\n positionLiquidateds(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {\n id\n timestamp\n txHash\n size\n price\n futuresPosition {\n leverage\n }\n market {\n asset\n }\n fee\n liquidator\n trader {\n id\n totalLiquidations\n }\n }\n }\n"): (typeof documents)["\n query PositionsLiquidated($where: PositionLiquidated_filter, $skip: Int, $first: Int, $orderBy: PositionLiquidated_orderBy, $orderDirection: OrderDirection) {\n positionLiquidateds(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {\n id\n timestamp\n txHash\n size\n price\n futuresPosition {\n leverage\n }\n market {\n asset\n }\n fee\n liquidator\n trader {\n id\n totalLiquidations\n }\n }\n }\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/v2/perps-v2/ui/src/__generated__/graphql.ts b/v2/perps-v2/ui/src/__generated__/graphql.ts index d13b9aa1f..62aed2907 100644 --- a/v2/perps-v2/ui/src/__generated__/graphql.ts +++ b/v2/perps-v2/ui/src/__generated__/graphql.ts @@ -15,6 +15,11 @@ export type Scalars = { BigDecimal: string; BigInt: string; Bytes: string; + /** + * 8 bytes signed integer + * + */ + Int8: any; }; export type BlockChangedFilter = { @@ -1328,6 +1333,7 @@ export type FuturesTrade = { id: Scalars['ID']; margin: Scalars['BigInt']; market: FuturesMarket; + marketOrder: Scalars['Boolean']; netFunding: Scalars['BigInt']; positionClosed: Scalars['Boolean']; positionSize: Scalars['BigInt']; @@ -1418,6 +1424,10 @@ export type FuturesTrade_Filter = { margin_not?: InputMaybe; margin_not_in?: InputMaybe>; market?: InputMaybe; + marketOrder?: InputMaybe; + marketOrder_in?: InputMaybe>; + marketOrder_not?: InputMaybe; + marketOrder_not_in?: InputMaybe>; market_?: InputMaybe; market_contains?: InputMaybe; market_contains_nocase?: InputMaybe; @@ -1581,6 +1591,7 @@ export enum FuturesTrade_OrderBy { Id = 'id', Margin = 'margin', Market = 'market', + MarketOrder = 'marketOrder', MarketAsset = 'market__asset', MarketId = 'market__id', MarketIsActive = 'market__isActive', @@ -2795,6 +2806,17 @@ export type MarketsIdQueryQueryVariables = Exact<{ [key: string]: never; }>; export type MarketsIdQueryQuery = { __typename?: 'Query', futuresMarkets: Array<{ __typename?: 'FuturesMarket', id: string, marketKey: string }> }; +export type PositionsLiquidatedQueryVariables = Exact<{ + where?: InputMaybe; + skip?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; +}>; + + +export type PositionsLiquidatedQuery = { __typename?: 'Query', positionLiquidateds: Array<{ __typename?: 'PositionLiquidated', id: string, timestamp: string, txHash: string, size: string, price: string, fee: string, liquidator: string, futuresPosition: { __typename?: 'FuturesPosition', leverage: string }, market: { __typename?: 'FuturesMarket', asset: string }, trader: { __typename?: 'Trader', id: string, totalLiquidations: string } }> }; + export type PositionsMarketQueryVariables = Exact<{ where?: InputMaybe; skip?: InputMaybe; @@ -2819,5 +2841,6 @@ export const LiquidatedQueryDocument = {"kind":"Document","definitions":[{"kind" export const StatsQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StatsQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyStat_filter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyStat_orderBy"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderDirection"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyStats"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"cumulativeVolume"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"fees"}},{"kind":"Field","name":{"kind":"Name","value":"cumulativeFees"}},{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"existingTraders"}},{"kind":"Field","name":{"kind":"Name","value":"newTraders"}},{"kind":"Field","name":{"kind":"Name","value":"cumulativeTraders"}},{"kind":"Field","name":{"kind":"Name","value":"cumulativeTrades"}},{"kind":"Field","name":{"kind":"Name","value":"trades"}}]}}]}}]} as unknown as DocumentNode; export const MarketsQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MarketsQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyMarketStat_filter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyMarketStat_orderBy"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderDirection"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyMarketStats"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"market"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"marketKey"}},{"kind":"Field","name":{"kind":"Name","value":"asset"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]}}]} as unknown as DocumentNode; export const MarketsIdQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MarketsIdQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"futuresMarkets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"marketKey"}}]}}]}}]} as unknown as DocumentNode; +export const PositionsLiquidatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PositionsLiquidated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PositionLiquidated_filter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PositionLiquidated_orderBy"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderDirection"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"positionLiquidateds"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"futuresPosition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"leverage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"market"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"asset"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"liquidator"}},{"kind":"Field","name":{"kind":"Name","value":"trader"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"totalLiquidations"}}]}}]}}]}}]} as unknown as DocumentNode; export const PositionsMarketDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PositionsMarket"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FuturesPosition_filter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"skip"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FuturesPosition_orderBy"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderDirection"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"futuresPositions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"skip"},"value":{"kind":"Variable","name":{"kind":"Name","value":"skip"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderDirection"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderDirection"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"market"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"marketKey"}},{"kind":"Field","name":{"kind":"Name","value":"asset"}}]}},{"kind":"Field","name":{"kind":"Name","value":"trader"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isOpen"}},{"kind":"Field","name":{"kind":"Name","value":"entryPrice"}},{"kind":"Field","name":{"kind":"Name","value":"avgEntryPrice"}},{"kind":"Field","name":{"kind":"Name","value":"leverage"}},{"kind":"Field","name":{"kind":"Name","value":"feesPaidToSynthetix"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"realizedPnl"}},{"kind":"Field","name":{"kind":"Name","value":"unrealizedPnl"}},{"kind":"Field","name":{"kind":"Name","value":"feesPaidToSynthetix"}},{"kind":"Field","name":{"kind":"Name","value":"exitPrice"}},{"kind":"Field","name":{"kind":"Name","value":"lastPrice"}},{"kind":"Field","name":{"kind":"Name","value":"netFunding"}},{"kind":"Field","name":{"kind":"Name","value":"long"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"isLiquidated"}}]}}]}}]} as unknown as DocumentNode; export const SynthetixDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Synthetix"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"synthetix"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"StringValue","value":"synthetix","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feesByLiquidations"}},{"kind":"Field","name":{"kind":"Name","value":"feesByPositionModifications"}},{"kind":"Field","name":{"kind":"Name","value":"totalVolume"}},{"kind":"Field","name":{"kind":"Name","value":"totalLiquidations"}},{"kind":"Field","name":{"kind":"Name","value":"totalTraders"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/v2/perps-v2/ui/src/components/Header.tsx b/v2/perps-v2/ui/src/components/Header.tsx index 515302625..25a15e999 100644 --- a/v2/perps-v2/ui/src/components/Header.tsx +++ b/v2/perps-v2/ui/src/components/Header.tsx @@ -65,7 +65,9 @@ export const Header: FC = () => { - {['/', '/trades', '/actions', '/markets', '/positions'].includes(location.pathname) && ( + {['/', '/trades', '/actions', '/markets', '/positions', '/liquidations'].includes( + location.pathname + ) && ( @@ -80,6 +82,9 @@ export const Header: FC = () => { Positions + + Liquidations + )} @@ -126,6 +131,13 @@ export const Header: FC = () => { + + + + Liquidations + + + diff --git a/v2/perps-v2/ui/src/components/Liquidations/LiquidationsFilter.tsx b/v2/perps-v2/ui/src/components/Liquidations/LiquidationsFilter.tsx new file mode 100644 index 000000000..d36b233b7 --- /dev/null +++ b/v2/perps-v2/ui/src/components/Liquidations/LiquidationsFilter.tsx @@ -0,0 +1,50 @@ +import { Stack } from '@chakra-ui/react'; +import { MarketSelect, DropdownFilter } from '../Shared'; +import { PageFilter } from '../OpenPositions/OpenPositionsFilter/PageFilter'; +import { useMarketSummaries } from '../../hooks/useMarketSummaries'; + +interface DropdownInterface { + value: string; + display: string; +} + +const ORDER_BY_CATEGORIES: DropdownInterface[] = [ + { value: 'size', display: 'Size' }, + { value: 'unrealizedPnl', display: 'Unrealized PNL' }, + { value: 'realizedPnl', display: 'Realized PNL' }, +]; + +const ORDER_DIRECTIONS: DropdownInterface[] = [ + { value: 'desc', display: 'Desc' }, + { value: 'asc', display: 'Asc' }, +]; + +interface OpenPositionsFilterProps { + route: string; +} + +export const OpenPositionsFilter = ({ route }: OpenPositionsFilterProps) => { + const markets = useMarketSummaries(); + + return ( + + x.asset)} route="liquidations" /> + + + + + + + + ); +}; diff --git a/v2/perps-v2/ui/src/components/Liquidations/LiquidationsLoading.tsx b/v2/perps-v2/ui/src/components/Liquidations/LiquidationsLoading.tsx new file mode 100644 index 000000000..d12cc11c4 --- /dev/null +++ b/v2/perps-v2/ui/src/components/Liquidations/LiquidationsLoading.tsx @@ -0,0 +1,44 @@ +import { Skeleton, Td, Tr, Box } from '@chakra-ui/react'; + +// A loading skeleton with dummy values +export const LiquidationsLoading = () => { + return ( + + + + Lorem Ipsum + + + + + Lorem Ipsum + + + + + Lorem Ipsum + + + + + Lorem Ipsum + + + + + Lorem Ipsum + + + + + Lorem Ipsum + + + + + Lorem Ipsum + + + + ); +}; diff --git a/v2/perps-v2/ui/src/components/Liquidations/LiquidationsTable.tsx b/v2/perps-v2/ui/src/components/Liquidations/LiquidationsTable.tsx new file mode 100644 index 000000000..a2a33e6f3 --- /dev/null +++ b/v2/perps-v2/ui/src/components/Liquidations/LiquidationsTable.tsx @@ -0,0 +1,94 @@ +import { TableContainer, Table, Thead, Tr, Tbody, Flex, Text } from '@chakra-ui/react'; +import { + Currency, + TableHeaderCell, + Market, + Size, + WalletTooltip, + LiquidatorTooltip, + Age, +} from '../Shared'; +import { useLiquidations } from '../../hooks'; +import { LiquidationsLoading } from './LiquidationsLoading'; + +export const LiquidationsTable = () => { + const { loading, data, error } = useLiquidations(); + + return ( + <> + + + + + Market + Age + Price + Size + Fee + Address + Liquidator + + + + {loading && ( + <> + + + + + + + )} + + {data?.map( + ({ + id: liquidationId, + market: { asset }, + liquidator, + timestamp, + price, + size, + trader: { id }, + fee, + futuresPosition: { leverage }, + }) => { + return ( + + + + + + + + + + ); + } + )} + +
+ {((!loading && data?.length === 0) || error) && ( + + + No Liquidations + + + )} +
+ + ); +}; diff --git a/v2/perps-v2/ui/src/components/Liquidations/index.tsx b/v2/perps-v2/ui/src/components/Liquidations/index.tsx new file mode 100644 index 000000000..526fe23cd --- /dev/null +++ b/v2/perps-v2/ui/src/components/Liquidations/index.tsx @@ -0,0 +1,2 @@ +export * from './LiquidationsFilter'; +export * from './LiquidationsTable'; diff --git a/v2/perps-v2/ui/src/components/Shared/Age/Age.tsx b/v2/perps-v2/ui/src/components/Shared/Age/Age.tsx new file mode 100644 index 000000000..66c54c79c --- /dev/null +++ b/v2/perps-v2/ui/src/components/Shared/Age/Age.tsx @@ -0,0 +1,12 @@ +import { Td, Fade } from '@chakra-ui/react'; +import { formatDistance } from 'date-fns'; + +export const Age = ({ timestamp }: { timestamp: string }) => { + const dateLiquidated = new Date(parseInt(timestamp) * 1000); + const dateNow = new Date(); + return ( + + {formatDistance(dateLiquidated, dateNow, { addSuffix: true })} + + ); +}; diff --git a/v2/perps-v2/ui/src/components/Shared/Age/index.tsx b/v2/perps-v2/ui/src/components/Shared/Age/index.tsx new file mode 100644 index 000000000..8172569c4 --- /dev/null +++ b/v2/perps-v2/ui/src/components/Shared/Age/index.tsx @@ -0,0 +1 @@ +export * from './Age'; diff --git a/v2/perps-v2/ui/src/components/Shared/LiquidatorTooltip/LiquidatorTooltip.tsx b/v2/perps-v2/ui/src/components/Shared/LiquidatorTooltip/LiquidatorTooltip.tsx new file mode 100644 index 000000000..a0564156f --- /dev/null +++ b/v2/perps-v2/ui/src/components/Shared/LiquidatorTooltip/LiquidatorTooltip.tsx @@ -0,0 +1,47 @@ +import { Td, useDisclosure, Flex, Popover, PopoverContent, Text, Box } from '@chakra-ui/react'; +import { WreckedIcon } from '../../Icons'; +import { Link as RouterLink } from 'react-router-dom'; +import { optimisticEthercanLink } from '../../../utils'; + +interface LiquidatorTooltipProps { + address: string; +} + +export const LiquidatorTooltip = ({ address }: LiquidatorTooltipProps) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + + return ( + + + + + + + + + + {address} + + + + + + + ); +}; diff --git a/v2/perps-v2/ui/src/components/Shared/LiquidatorTooltip/index.tsx b/v2/perps-v2/ui/src/components/Shared/LiquidatorTooltip/index.tsx new file mode 100644 index 000000000..72ecdae8e --- /dev/null +++ b/v2/perps-v2/ui/src/components/Shared/LiquidatorTooltip/index.tsx @@ -0,0 +1 @@ +export * from './LiquidatorTooltip'; diff --git a/v2/perps-v2/ui/src/components/Shared/SizeSelect/SizeSelect.tsx b/v2/perps-v2/ui/src/components/Shared/SizeSelect/SizeSelect.tsx index a7d81ed4e..be8235803 100644 --- a/v2/perps-v2/ui/src/components/Shared/SizeSelect/SizeSelect.tsx +++ b/v2/perps-v2/ui/src/components/Shared/SizeSelect/SizeSelect.tsx @@ -8,7 +8,7 @@ interface SizeState { max: string; } -export const SizeSelect = () => { +export const SizeSelect = ({ route = 'actions' }: { route?: string }) => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const initialMin = searchParams.get('min') || ''; @@ -41,7 +41,7 @@ export const SizeSelect = () => { const newParams = new URLSearchParams(params); navigate({ - pathname: '/actions', + pathname: `/${route}`, search: `?${newParams.toString()}`, }); } else { @@ -51,8 +51,9 @@ export const SizeSelect = () => { params.push(['markets', markets]); } const newParams = new URLSearchParams(params); + navigate({ - pathname: '/actions', + pathname: `/${route}`, search: `?${newParams.toString()}`, }); } diff --git a/v2/perps-v2/ui/src/components/Shared/index.tsx b/v2/perps-v2/ui/src/components/Shared/index.tsx index 84d322d7c..ecfe89197 100644 --- a/v2/perps-v2/ui/src/components/Shared/index.tsx +++ b/v2/perps-v2/ui/src/components/Shared/index.tsx @@ -20,3 +20,5 @@ export * from './MarketSelect'; export * from './SizeSelect'; export * from './DailyVolumeChange'; export * from './DropdownFilter'; +export * from './LiquidatorTooltip'; +export * from './Age'; diff --git a/v2/perps-v2/ui/src/hooks/index.ts b/v2/perps-v2/ui/src/hooks/index.ts index 141108679..63734c63b 100644 --- a/v2/perps-v2/ui/src/hooks/index.ts +++ b/v2/perps-v2/ui/src/hooks/index.ts @@ -4,3 +4,4 @@ export * from './useStats'; export * from './useMarketStats'; export * from './useMarkets'; export * from './useLargestOpenPosition'; +export * from './useLiquidations'; diff --git a/v2/perps-v2/ui/src/hooks/useKwentaAccount.ts b/v2/perps-v2/ui/src/hooks/useKwentaAccount.ts index bac89f356..d46878c56 100644 --- a/v2/perps-v2/ui/src/hooks/useKwentaAccount.ts +++ b/v2/perps-v2/ui/src/hooks/useKwentaAccount.ts @@ -1,21 +1,13 @@ import { z } from 'zod'; import { KWENTA_SUBGRAPH_URL } from '../utils'; -import { ApolloClient, InMemoryCache, useQuery, gql } from '@apollo/client'; +import { ApolloClient, InMemoryCache, useQuery } from '@apollo/client'; +import { AccountQuery } from './useOwnerBySmartId'; const kwentaClient = new ApolloClient({ uri: KWENTA_SUBGRAPH_URL, cache: new InMemoryCache(), }); -const AccountQuery = gql` - query Account($owner: String) { - smartMarginAccounts(where: { owner: $owner }) { - id - owner - } - } -`; - const SmartMarginAccountSchema = z.array( z .object({ diff --git a/v2/perps-v2/ui/src/hooks/useLiquidations.ts b/v2/perps-v2/ui/src/hooks/useLiquidations.ts new file mode 100644 index 000000000..221f3d374 --- /dev/null +++ b/v2/perps-v2/ui/src/hooks/useLiquidations.ts @@ -0,0 +1,111 @@ +import { useQuery } from '@apollo/client'; +import { POSITIONS_LIQUIDATED_QUERY } from '../queries/liquidated'; +import { useSearchParams } from 'react-router-dom'; +import { useMarketSummaries } from './useMarketSummaries'; +import { generateMarketIds } from './useActions'; +import { + PositionLiquidated_OrderBy, + OrderDirection, + PositionsLiquidatedQuery, +} from '../__generated__/graphql'; +import Wei, { wei } from '@synthetixio/wei'; + +interface QueryLiquidation { + __typename?: 'PositionLiquidated'; + id: string; + timestamp: string; + txHash: string; + size: string; + price: string; + fee: string; + liquidator: string; + futuresPosition: { + __typename?: 'FuturesPosition'; + leverage: string; + }; + market: { + __typename?: 'FuturesMarket'; + asset: string; + }; + trader: { + __typename?: 'Trader'; + id: string; + totalLiquidations: string; + }; +} + +interface Liquidation { + id: string; + timestamp: string; + txHash: string; + fee: Wei; + size: Wei; + price: Wei; + liquidator: string; + futuresPosition: { + leverage: Wei; + }; + market: { + asset: string; + }; + trader: { + id: string; + totalLiquidations: string; + }; +} + +function parsedLiquidationData( + data: PositionsLiquidatedQuery | undefined +): Liquidation[] | undefined { + if (!data?.positionLiquidateds) return undefined; + if (!data.positionLiquidateds.length) return []; + + return data.positionLiquidateds.map((liquidation: QueryLiquidation) => ({ + id: liquidation.id, + timestamp: liquidation.timestamp, + txHash: liquidation.txHash, + fee: wei(liquidation.fee, 18, true), + size: wei(liquidation.size, 18, true), + price: wei(liquidation.price, 18, true), + liquidator: liquidation.liquidator, + futuresPosition: { + leverage: wei(liquidation.futuresPosition.leverage, 18, true), + }, + market: { + asset: liquidation.market.asset, + }, + trader: { + id: liquidation.trader.id, + totalLiquidations: liquidation.trader.totalLiquidations, + }, + })); +} + +export function useLiquidations() { + const [searchParams] = useSearchParams(); + const { data: marketConfigs, isLoading: marketConfigsLoading } = useMarketSummaries(); + + const markets = generateMarketIds(marketConfigs, searchParams.get('markets')); + + const { + loading, + error, + data: queryData, + } = useQuery(POSITIONS_LIQUIDATED_QUERY, { + variables: { + where: { + market_in: markets, + }, + orderBy: PositionLiquidated_OrderBy.Timestamp, + orderDirection: OrderDirection.Desc, + }, + skip: marketConfigsLoading, + pollInterval: 10000, + }); + + return { + data: queryData?.positionLiquidateds ? parsedLiquidationData(queryData) : undefined, + loading, + error, + }; +} diff --git a/v2/perps-v2/ui/src/hooks/useOwnerBySmartId.ts b/v2/perps-v2/ui/src/hooks/useOwnerBySmartId.ts index 8d96685d9..8f81a9c39 100644 --- a/v2/perps-v2/ui/src/hooks/useOwnerBySmartId.ts +++ b/v2/perps-v2/ui/src/hooks/useOwnerBySmartId.ts @@ -12,7 +12,7 @@ const polyClient = new ApolloClient({ cache: new InMemoryCache(), }); -const AccountBySmartIdQuery = gql` +export const AccountBySmartIdQuery = gql` query Account($id: String!) { smartMarginAccount(id: $id) { owner @@ -20,7 +20,7 @@ const AccountBySmartIdQuery = gql` } `; -const AccountQuery = gql` +export const AccountQuery = gql` query SmAccounts($account: String) { logAccountCreateds(where: { account: $account }) { owner diff --git a/v2/perps-v2/ui/src/hooks/usePolynomialAccount.ts b/v2/perps-v2/ui/src/hooks/usePolynomialAccount.ts index a7e00d2ef..fa0bf846c 100644 --- a/v2/perps-v2/ui/src/hooks/usePolynomialAccount.ts +++ b/v2/perps-v2/ui/src/hooks/usePolynomialAccount.ts @@ -1,20 +1,13 @@ import { z } from 'zod'; import { POLYNOMIAL_SUBGRAPH_URL } from '../utils'; -import { ApolloClient, gql, InMemoryCache, useQuery } from '@apollo/client'; +import { ApolloClient, InMemoryCache, useQuery } from '@apollo/client'; +import { AccountQuery } from './useOwnerBySmartId'; const polyClient = new ApolloClient({ uri: POLYNOMIAL_SUBGRAPH_URL, cache: new InMemoryCache(), }); -const AccountQuery = gql` - query SmAccounts($owner: String) { - logAccountCreateds(where: { owner: $owner }) { - owner - account - } - } -`; const PolySmartMarginAccountsSchema = z.array( z.object({ owner: z.string(), diff --git a/v2/perps-v2/ui/src/index.tsx b/v2/perps-v2/ui/src/index.tsx index 5da58b949..9dfc4ac4d 100644 --- a/v2/perps-v2/ui/src/index.tsx +++ b/v2/perps-v2/ui/src/index.tsx @@ -12,7 +12,7 @@ import { DEFAULT_REQUEST_REFRESH_INTERVAL, } from './utils/constants'; import { resolvers, typeDefs } from './queries/resolved'; -import { Dashboard, Actions, Markets, Positions, StatsV3 } from './pages'; +import { Dashboard, Actions, Markets, Positions, Liquidations, StatsV3 } from './pages'; import { isStaging } from './utils/isStaging'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { EthersProvider } from './utils/ProviderContext'; @@ -81,6 +81,15 @@ const router = createBrowserRouter([ ), }, + { + path: '/liquidations', + element: ( + <> +
+ + + ), + }, { path: '/v3', element: ( diff --git a/v2/perps-v2/ui/src/pages/Liquidations.tsx b/v2/perps-v2/ui/src/pages/Liquidations.tsx new file mode 100644 index 000000000..fdec21952 --- /dev/null +++ b/v2/perps-v2/ui/src/pages/Liquidations.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Flex, Heading } from '@chakra-ui/react'; +import { MarketSelect } from '../components'; +import { useMarketSummaries } from '../hooks/useMarketSummaries'; +import { LiquidationsTable } from '../components/Liquidations'; + +export function Liquidations() { + const markets = useMarketSummaries(); + return ( + <> + + + Liquidations + + + x.asset)} route="liquidations" /> + {/* Currently Buggered */} + {/* */} + + + + + ); +} + +export default Liquidations; diff --git a/v2/perps-v2/ui/src/pages/index.tsx b/v2/perps-v2/ui/src/pages/index.tsx index ef3687a1c..b1b3c0f89 100644 --- a/v2/perps-v2/ui/src/pages/index.tsx +++ b/v2/perps-v2/ui/src/pages/index.tsx @@ -4,3 +4,4 @@ export * from './Actions'; export * from './Markets'; export * from './Positions'; export * from './StatsV3'; +export * from './Liquidations'; diff --git a/v2/perps-v2/ui/src/queries/liquidated.ts b/v2/perps-v2/ui/src/queries/liquidated.ts new file mode 100644 index 000000000..caf4488fc --- /dev/null +++ b/v2/perps-v2/ui/src/queries/liquidated.ts @@ -0,0 +1,25 @@ +import { gql } from '../__generated__'; + +export const POSITIONS_LIQUIDATED_QUERY = gql(` + query PositionsLiquidated($where: PositionLiquidated_filter, $skip: Int, $first: Int, $orderBy: PositionLiquidated_orderBy, $orderDirection: OrderDirection) { + positionLiquidateds(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) { + id + timestamp + txHash + size + price + futuresPosition { + leverage + } + market { + asset + } + fee + liquidator + trader { + id + totalLiquidations + } + } + } +`); diff --git a/yarn.lock b/yarn.lock index f1fc455f4..9ba28c9ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4768,7 +4768,7 @@ __metadata: languageName: node linkType: hard -"@graphql-typed-document-node/core@npm:3.1.2, @graphql-typed-document-node/core@npm:^3.1.1, @graphql-typed-document-node/core@npm:^3.1.2": +"@graphql-typed-document-node/core@npm:3.1.2": version: 3.1.2 resolution: "@graphql-typed-document-node/core@npm:3.1.2" peerDependencies: @@ -4777,6 +4777,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.1.1, @graphql-typed-document-node/core@npm:^3.1.2": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -32097,7 +32106,14 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:~2.5.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad + languageName: node + linkType: hard + +"tslib@npm:~2.5.0": version: 2.5.0 resolution: "tslib@npm:2.5.0" checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 @@ -33987,8 +34003,8 @@ __metadata: linkType: hard "ws@npm:^8.12.0, ws@npm:^8.2.3, ws@npm:^8.4.2, ws@npm:^8.5.0, ws@npm:^8.6.0, ws@npm:^8.8.0": - version: 8.13.0 - resolution: "ws@npm:8.13.0" + version: 8.14.2 + resolution: "ws@npm:8.14.2" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -33997,7 +34013,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + checksum: 3ca0dad26e8cc6515ff392b622a1467430814c463b3368b0258e33696b1d4bed7510bc7030f7b72838b9fdeb8dbd8839cbf808367d6aae2e1d668ce741d4308b languageName: node linkType: hard @@ -34123,9 +34139,9 @@ __metadata: linkType: hard "yaml@npm:^2.1.3": - version: 2.2.1 - resolution: "yaml@npm:2.2.1" - checksum: 84f68cbe462d5da4e7ded4a8bded949ffa912bc264472e5a684c3d45b22d8f73a3019963a32164023bdf3d83cfb6f5b58ff7b2b10ef5b717c630f40bd6369a23 + version: 2.3.3 + resolution: "yaml@npm:2.3.3" + checksum: cdfd132e7e0259f948929efe8835923df05c013c273c02bb7a2de9b46ac3af53c2778a35b32c7c0f877cc355dc9340ed564018c0242bfbb1278c2a3e53a0e99e languageName: node linkType: hard