Compare commits

...
Author SHA1 Message Date
Matthew Russell 72a8571372 Merge branch 'feat/4651-persist-study-sizes' into feat/release-preview 2023-09-11 16:19:58 -07:00
Matthew Russell 3f60aae99e Merge branch 'chore/4627-get-rid-off-get-wallet-step' into feat/release-preview 2023-09-11 16:19:15 -07:00
Matthew Russell ae15794739 chore: fix underlines in connect dialog 2023-09-11 16:16:49 -07:00
Matthew Russell f142e37f15 chore: remove hover when button is disabled 2023-09-11 16:06:04 -07:00
Matthew Russell 1847381cf2 chore: uneven spacing in connect dialog 2023-09-11 16:05:30 -07:00
Matthew Russell 5f6e979fd4 chore: change to create studySizes with map 2023-09-11 14:05:25 -07:00
Matthew Russell 6b0942fc48 feat: add sizes lookup so all study types start with persisted size 2023-09-11 13:56:32 -07:00
Matthew Russell b65153b611 chore: reduce the space between candles 2023-09-11 10:51:20 -07:00
Matthew Russell 1a1f1e2daf chore: upgrade pennant 2023-09-11 10:49:45 -07:00
Matthew Russell 58205e976e feat: store and pass study sizes to chart 2023-09-11 10:49:44 -07:00
maciek e55ecc4983 Merge remote-tracking branch 'origin/develop' into chore/4627-get-rid-off-get-wallet-step 2023-09-11 17:31:25 +02:00
maciek ea24d0fdb9 chore: get rid of 'get wallet' step - remove unnecessary condition 2023-09-11 17:30:54 +02:00
maciek e56271bcdb chore: get rid of 'get wallet' step - fix wrong handling of one case 2023-09-11 17:01:31 +02:00
maciek d458559074 chore: get rid of 'get wallet' step - fix linter issue 2023-09-11 16:45:16 +02:00
maciek 414919bbf4 chore: get rid of 'get wallet' step - adjust steps hook 2023-09-11 16:39:42 +02:00
maciek ae5f6d5a14 chore: get rid of 'get wallet' step 2023-09-11 15:16:09 +02:00
12 changed files with 168 additions and 148 deletions
@@ -22,7 +22,7 @@ describe('GetStarted', () => {
};
const checkTicks = (elements: Element[]) => {
elements.forEach((item, i) => {
if (i + 1 < mockStep) {
if (i + 1 < mockStep - 1) {
expect(item.querySelector('[data-testid="icon-tick"]')).toBeTruthy();
}
});
@@ -57,19 +57,9 @@ describe('GetStarted', () => {
'get'
);
navigatorGetter.mockReturnValue('Chrome');
mockStep = 1;
const { rerender, container } = renderComponent();
expect(screen.queryByTestId('icon-tick')).not.toBeInTheDocument();
expect(screen.getByTestId('get-wallet-button')).toBeInTheDocument();
mockStep = 2;
rerender(
<MemoryRouter>
<VegaWalletContext.Provider value={{} as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
);
const { rerender, container } = renderComponent();
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
@@ -93,7 +83,9 @@ describe('GetStarted', () => {
</MemoryRouter>
);
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Ready to trade' })
).toBeInTheDocument();
mockStep = 5;
rerender(
@@ -7,11 +7,7 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
GetWalletButton,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useNavigate } from 'react-router-dom';
@@ -30,7 +26,6 @@ interface Props {
}
const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
const { CHROME_EXTENSION_URL, MOZILLA_EXTENSION_URL } = useEnvironment();
const navigate = useNavigate();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
@@ -47,15 +42,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
let onClickHandle = () => {
openVegaWalletDialog();
};
if (step === OnboardingStep.ONBOARDING_WALLET_STEP) {
return (
<GetWalletButton
className="justify-between"
chromeExtensionUrl={CHROME_EXTENSION_URL}
mozillaExtensionUrl={MOZILLA_EXTENSION_URL}
/>
);
} else if (step === OnboardingStep.ONBOARDING_CONNECT_STEP) {
if (step <= OnboardingStep.ONBOARDING_CONNECT_STEP) {
buttonText = t('Connect');
} else if (step === OnboardingStep.ONBOARDING_DEPOSIT_STEP) {
buttonText = t('Deposit');
@@ -65,7 +52,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
dismiss();
};
} else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) {
buttonText = t('Dismiss');
buttonText = t('Ready to trade');
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Order });
@@ -116,23 +103,18 @@ export const GetStarted = ({ lead }: Props) => {
<ul className="list-none">
<Step
step={1}
text={t('Get a Vega wallet')}
complete={currentStep > OnboardingStep.ONBOARDING_WALLET_STEP}
/>
<Step
step={2}
text={t('Connect')}
complete={Boolean(
currentStep > OnboardingStep.ONBOARDING_CONNECT_STEP || pubKey
)}
/>
<Step
step={3}
step={2}
text={t('Deposit funds')}
complete={currentStep > OnboardingStep.ONBOARDING_DEPOSIT_STEP}
/>
<Step
step={4}
step={3}
text={t('Open a position')}
complete={currentStep > OnboardingStep.ONBOARDING_ORDER_STEP}
/>
@@ -7,8 +7,6 @@ import {
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { depositsProvider } from '@vegaprotocol/deposits';
import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
import { ordersWithMarketProvider } from '@vegaprotocol/orders';
import { positionsDataProvider } from '@vegaprotocol/positions';
@@ -23,7 +21,7 @@ let mockContext: Partial<VegaWalletContextShape> = { pubKey: 'test-pubkey' };
describe('useGetOnboardingStep', () => {
beforeEach(() => {
jest.clearAllMocks();
mockData = [{ id: 'item-id' }];
mockData = [];
mockContext = { pubKey: 'test-pubkey' };
globalThis.window.vega = {} as Vega;
});
@@ -42,13 +40,6 @@ describe('useGetOnboardingStep', () => {
expect(result.current).toEqual(OnboardingStep.ONBOARDING_UNKNOWN_STEP);
});
it('should return properly ONBOARDING_WALLET_STEP', () => {
// @ts-ignore test only purpose
globalThis.window.vega = undefined;
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
expect(result.current).toEqual(OnboardingStep.ONBOARDING_WALLET_STEP);
});
it('should return properly ONBOARDING_CONNECT_STEP', () => {
mockContext = { pubKey: null };
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
@@ -56,15 +47,6 @@ describe('useGetOnboardingStep', () => {
});
it('should return properly ONBOARDING_DEPOSIT_STEP', async () => {
(useDataProvider as jest.Mock).mockImplementation((args) => {
if (
args.dataProvider === depositsProvider ||
args.dataProvider === aggregatedAccountsDataProvider
) {
return { data: [] };
}
return { data: mockData };
});
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
await expect(result.current).toEqual(
OnboardingStep.ONBOARDING_DEPOSIT_STEP
@@ -72,6 +54,7 @@ describe('useGetOnboardingStep', () => {
});
it('should return properly ONBOARDING_ORDER_STEP', async () => {
mockData = [{ id: 'item-id' }];
(useDataProvider as jest.Mock).mockImplementation((args) => {
if (
args.dataProvider === ordersWithMarketProvider ||
@@ -86,6 +69,7 @@ describe('useGetOnboardingStep', () => {
});
it('should return properly ONBOARDING_COMPLETE_STEP', async () => {
mockData = [{ id: 'item-id' }];
(useDataProvider as jest.Mock).mockImplementation(() => {
return { data: mockData };
});
@@ -1,4 +1,4 @@
import { isBrowserWalletInstalled, useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { depositsProvider } from '@vegaprotocol/deposits';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '@vegaprotocol/orders';
@@ -63,20 +63,21 @@ export const useGetOnboardingStep = () => {
collateralData === null ||
positionsData === null)
);
let step = OnboardingStep.ONBOARDING_UNKNOWN_STEP;
if (isLoading) {
return OnboardingStep.ONBOARDING_UNKNOWN_STEP;
}
if (!isBrowserWalletInstalled()) {
return OnboardingStep.ONBOARDING_WALLET_STEP;
return step;
}
if (!pubKey) {
return OnboardingStep.ONBOARDING_CONNECT_STEP;
step = OnboardingStep.ONBOARDING_CONNECT_STEP;
}
if (!deposits && !collaterals) {
return OnboardingStep.ONBOARDING_DEPOSIT_STEP;
if (pubKey) {
step = OnboardingStep.ONBOARDING_DEPOSIT_STEP;
}
if (!orders && !positions) {
return OnboardingStep.ONBOARDING_ORDER_STEP;
if (pubKey && (deposits || collaterals)) {
step = OnboardingStep.ONBOARDING_ORDER_STEP;
}
return OnboardingStep.ONBOARDING_COMPLETE_STEP;
if (pubKey && (orders || positions)) {
step = OnboardingStep.ONBOARDING_COMPLETE_STEP;
}
return step;
};
+1
View File
@@ -82,6 +82,7 @@ html [data-theme='light'] {
--pennant-color-volume-sell: theme(colors.market.red.DEFAULT);
/* reduce space between candles */
--pennant-candlestick-inner-padding: 0.1;
}
+25 -6
View File
@@ -3,6 +3,7 @@ import { CandlestickChart } from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
@@ -22,8 +23,25 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const { interval, chartType, overlays, studies, merge } =
useCandlesChartSettings();
const {
interval,
chartType,
overlays,
studies,
studySizes,
setStudies,
setStudySizes,
setOverlays,
} = useCandlesChartSettings();
const handlePaneChange = useMemo(
() =>
debounce((sizes: number[]) => {
// first number is main pain, which is greedy so we don't store it
setStudySizes(sizes.filter((_, i) => i !== 0));
}, 300),
[setStudySizes]
);
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey);
@@ -45,15 +63,16 @@ export const CandlesChartContainer = ({
initialNumCandlesToDisplay: Math.floor(
width * CANDLES_TO_WIDTH_FACTOR
),
studySize: 150, // default size
studySizes,
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
merge({
overlays: options.overlays,
studies: options.studies,
});
setStudies(options.studies);
setOverlays(options.overlays);
}}
onPaneChanged={handlePaneChange}
/>
</div>
)}
@@ -5,36 +5,45 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
type StudySizes = { [S in Study]?: number };
interface StoredSettings {
interval: Interval;
type: ChartType;
overlays: Overlay[];
studies: Study[];
studySizes: StudySizes;
}
export const STUDY_SIZE = 150;
const STUDY_ORDER: Study[] = [
Study.FORCE_INDEX,
Study.RELATIVE_STRENGTH_INDEX,
Study.ELDAR_RAY,
Study.MACD,
Study.VOLUME,
];
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
studySizes: {},
};
export const useCandlesChartSettingsStore = create<
StoredSettings & {
merge: (settings: Partial<StoredSettings>) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
setOverlays: (overlays?: Overlay[]) => void;
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
}
>()(
persist(
immer((set) => ({
...DEFAULT_CHART_SETTINGS,
merge: (settings: Partial<StoredSettings>) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type) =>
set((state) => {
state.type = type;
@@ -43,14 +52,35 @@ export const useCandlesChartSettingsStore = create<
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays) =>
setOverlays: (overlays) => {
if (!overlays) return;
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies) =>
});
},
setStudies: (studies) => {
if (!studies) return;
// Make sure studies are always returned in the same order
studies.sort((a, b) => {
return STUDY_ORDER.indexOf(a) - STUDY_ORDER.indexOf(b);
});
set((state) => {
state.studies = studies;
}),
});
},
setStudySizes: (sizes) => {
set((state) => {
// for every study find the corresonding size and update
// the size record for that study
state.studies.forEach((s, i) => {
const size = sizes[i];
state.studySizes[s] = size;
});
});
},
})),
{
name: 'vega_candles_chart_store',
@@ -85,15 +115,22 @@ export const useCandlesChartSettings = () => {
[Study.VOLUME]
);
// find the study size
const studySizes = studies.map((s) => {
const size = settings.studySizes[s] || STUDY_SIZE;
return size;
});
return {
interval,
chartType,
overlays,
studies,
studySizes,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
merge: settings.merge,
setStudySizes: settings.setStudySizes,
};
};
@@ -39,17 +39,17 @@ const getClassName = (
},
// colours
{
'bg-vega-yellow hover:bg-vega-yellow-550 dark:bg-vega-yellow dark:hover:bg-vega-yellow-450':
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 dark:bg-vega-yellow dark:enabled:hover:bg-vega-yellow-450':
intent === Intent.Primary,
'bg-vega-clight-500 hover:bg-vega-clight-400 dark:bg-vega-cdark-500 dark:hover:bg-vega-cdark-400':
'bg-vega-clight-500 enabled:hover:bg-vega-clight-400 dark:bg-vega-cdark-500 dark:enabled:hover:bg-vega-cdark-400':
intent === Intent.None,
'bg-vega-blue-350 hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:hover:bg-vega-blue-600':
'bg-vega-blue-350 enabled:hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:enabled:hover:bg-vega-blue-600':
intent === Intent.Info,
'bg-vega-orange-350 hover:bg-vega-orange-400 dark:bg-vega-orange-650 dark:hover:bg-vega-orange-600':
'bg-vega-orange-350 enabled:hover:bg-vega-orange-400 dark:bg-vega-orange-650 dark:enabled:hover:bg-vega-orange-600':
intent === Intent.Warning,
'bg-vega-red-350 hover:bg-vega-red-400 dark:bg-vega-red-650 dark:hover:bg-vega-red-600':
'bg-vega-red-350 enabled:hover:bg-vega-red-400 dark:bg-vega-red-650 dark:enabled:hover:bg-vega-red-600':
intent === Intent.Danger,
'bg-vega-green-350 hover:bg-vega-green-400 dark:bg-vega-green-650 dark:hover:bg-vega-green-600':
'bg-vega-green-350 enabled:hover:bg-vega-green-400 dark:bg-vega-green-650 dark:enabled:hover:bg-vega-green-600':
intent === Intent.Success,
'text-vega-clight-50 dark:text-vega-cdark-50': intent !== Intent.Primary,
'text-vega-clight-900 dark:text-vega-cdark-900':
@@ -73,7 +73,7 @@ const Content = ({
children,
}: Pick<TradingButtonProps, 'icon' | 'subLabel' | 'children'>) => (
<>
<span data-label className="font-alpha leading-none" key="children">
<span data-label className="leading-none font-alpha" key="children">
{children}
</span>
{icon}
@@ -241,51 +241,49 @@ const ConnectorList = ({
'Connect securely, deposit funds and approve or reject transactions with the Vega wallet'
)}
</p>
<div data-testid="connectors-list" className="flex flex-col mt-4 gap-4">
<div>
{isBrowserWalletInstalled() ? (
<ConnectionOptionWithDescription
type="injected"
text={extendedText}
onClick={() => onSelect('injected')}
title={
<>
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs">{t('full featured')}</span>
</>
}
description={t(
`Connect Vega Wallet extension
<div data-testid="connectors-list" className="flex flex-col mt-4 gap-2">
{isBrowserWalletInstalled() ? (
<ConnectionOptionWithDescription
type="injected"
text={extendedText}
onClick={() => onSelect('injected')}
title={
<>
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs">{t('full featured')}</span>
</>
}
description={t(
`Connect Vega Wallet extension
for %s to access all features including key
management and detailed transaction views from your
browser.`,
[browserName]
)}
/>
) : (
<div>
<h1 className="mb-1 text-lg">
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs"> {t('full featured')}</span>
</h1>
<p className="mb-2 text-sm">
{t(
`Install Vega Wallet extension
for %s to access all features including key
management and detailed transaction views from your
browser.`,
[browserName]
)}
</p>
<GetWalletButton
chromeExtensionUrl={links.chromeExtensionUrl}
mozillaExtensionUrl={links.mozillaExtensionUrl}
/>
) : (
<div className="py-2">
<h1 className="px-2 text-lg">
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs"> {t('full featured')}</span>
</h1>
<p className="p-2 text-sm">
{t(
`Install Vega Wallet extension
for %s to access all features including key
management and detailed transaction views from your
browser.`,
[browserName]
)}
</p>
<GetWalletButton
chromeExtensionUrl={links.chromeExtensionUrl}
mozillaExtensionUrl={links.mozillaExtensionUrl}
/>
</div>
)}
</div>
</div>
)}
{connectors['snap'] !== undefined ? (
<div>
{snapStatus === SnapStatus.INSTALLED ? (
@@ -335,7 +333,7 @@ const ConnectorList = ({
}}
/>
{snapStatus === SnapStatus.NOT_SUPPORTED ? (
<p className="pt-1 text-xs leading-tight text-default">
<p className="pt-1 text-xs leading-tight text-muted">
{t('No MetaMask version that supports snaps detected.')}{' '}
{t('Learn more about')}{' '}
<ExternalLink href="https://metamask.io/snaps/">
@@ -348,7 +346,7 @@ const ConnectorList = ({
</div>
) : null}
<div>
<h1 className="my-1 text-md">{t('Advanced / Other options...')}</h1>
<h1 className="mb-1 text-md">{t('Advanced / Other options...')}</h1>
<ConnectionOption
type="view"
text={t('View as party')}
@@ -520,7 +518,7 @@ const ConnectionOptionWithDescription = ({
return (
<div>
<h1 className="text-md">{title}</h1>
<p className="pb-2 text-sm text-gray-60 text-muted">{description}</p>
<p className="mb-2 text-sm text-gray-60 text-muted">{description}</p>
<ConnectionOption
disabled={disabled}
type={type}
@@ -579,10 +577,11 @@ const CustomUrlInput = ({
<div className="flex justify-between mb-1.5">
<p className="text-sm text-secondary">{t('Custom wallet location')}</p>
<button
className="text-sm underline"
className="text-sm text-muted"
onClick={() => setUrlInputExpanded(false)}
>
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
<VegaIcon name={VegaIconNames.ARROW_LEFT} />{' '}
<span className="underline underline-offset-4">{t('Go back')}</span>
</button>
</div>
<TradingFormGroup
@@ -612,18 +611,20 @@ const CustomUrlInput = ({
onClick={() => onSelect('jsonRpc')}
/>
{isDesktopWalletRunning !== null && (
<p className="pt-1 mb-2 text-sm leading-tight">
<div className="pt-1 mb-2 text-sm">
{isDesktopWalletRunning ? (
<button
className="underline text-default"
className="text-muted"
onClick={() => setUrlInputExpanded(true)}
disabled={Boolean(pubKey)}
>
{t('Enter a custom wallet location')}{' '}
<span className="underline underline-offset-4">
{t('Enter a custom wallet location')}
</span>{' '}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</button>
) : (
<>
<p className="leading-tight text-muted">
<span className="text-xs">
{t(
'No running Desktop App/CLI detected. Open your app now to connect or enter a'
@@ -636,9 +637,9 @@ const CustomUrlInput = ({
>
{t('custom wallet location')}
</button>
</>
</p>
)}
</p>
</div>
)}
</>
);
@@ -88,10 +88,13 @@ export function ViewConnectorForm({
<div className="flex justify-end">
<button
onClick={reset}
className="p-2 text-sm underline"
className="p-2 text-sm"
data-testid="back-button"
>
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
<VegaIcon name={VegaIconNames.ARROW_LEFT} />{' '}
<span className="underline underline-offset-4">
{t('Go back')}
</span>
</button>
</div>
)}
+1 -1
View File
@@ -70,7 +70,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "1.11.1",
"pennant": "1.12.0",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+4 -4
View File
@@ -20253,10 +20253,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.11.1.tgz#f47bceaade01db215eeba666cd755840d16fe9ed"
integrity sha512-U26OxjxETWLJAvCFj20oH0y5gzyBfJ5oD4O3dYg1aWnH4giUYDPWuUDFnzlMJ+yXolyYECccXm6S1BkoVcVzDg==
pennant@1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.12.0.tgz#e12707d5f1aac554d81bad060637e608335e0b50"
integrity sha512-xosg5erRf+Ke9iORdqyv+SOGcD3uJX1dgf990q1DvHuzz36w2txCZsfnvcXhRO++HYVKS9sU/YLPRLJgolFGtA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"