feat: add debug mode for transaction signing

This commit is contained in:
Hank Stoever
2020-06-16 19:08:36 -07:00
parent a205e14c97
commit 3c6688714b
86 changed files with 3416 additions and 609 deletions

View File

@@ -2,4 +2,8 @@ module.exports = {
root: true,
reportUnusedDisableDirectives: true,
extends: ['@blockstack/eslint-config'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
}
};

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ dist/
.rts2_cache_umd/
yarn-error.log
.github/workflows/event.json
.npmrc
.npmrc
coverage/

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -2,8 +2,8 @@
"name": "root",
"private": true,
"scripts": {
"typecheck": "lerna run typecheck --parallel",
"dev": "yarn lerna exec --parallel 'yarn dev' --scope test-app --scope @blockstack/app",
"typecheck": "lerna run typecheck --parallel --no-bail --stream",
"dev": "NODE_ENV=development yarn lerna exec --parallel 'yarn dev' --scope test-app --scope @blockstack/app",
"bootstrap": "yarn lerna exec --parallel 'yarn'",
"build:libs": "yarn build:ui && yarn build:keychain && yarn build:connect",
"build:ui": "lerna run build --scope @blockstack/ui",
@@ -39,6 +39,7 @@
"eslint-config-prettier": "^6.11.0",
"eslint-config-react-app": "^5.2.1",
"eslint-plugin-flowtype": "^4.7.0",
"eslint-plugin-import": "^2.21.2 ",
"eslint-plugin-jest": "^23.11.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.3",
@@ -47,7 +48,7 @@
"husky": "^4.2.3",
"lerna": "^3.20.2",
"prettier": "^2.0.5",
"typescript": "^3.8.2"
"typescript": "^3.9.3"
},
"dependencies": {
"@babel/preset-env": "^7.10.3",
@@ -55,6 +56,8 @@
},
"resolutions": {
"@blockstack/eslint-config": "^1.0.5",
"@blockstack/prettier-config": "^0.0.6"
"eslint-plugin-import": "2.21.2",
"@blockstack/prettier-config": "^0.0.6",
"buffer": "5.6.0"
}
}

View File

@@ -155,7 +155,7 @@ module.exports = {
// testResultsProcessor: null,
// This option allows use of a custom test runner
testRunner: "jest-circus/runner",
testRunner: 'jest-circus/runner',
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",

View File

@@ -27,17 +27,20 @@
"@blockstack/connect": "^2.8.3",
"@blockstack/keychain": "^0.7.5",
"@blockstack/prettier-config": "^0.0.6",
"@blockstack/stacks-transactions": "^0.4.6",
"@blockstack/stats": "^0.7.0",
"@blockstack/ui": "^2.9.5",
"@blockstack/rpc-client": "^0.3.0-alpha.0",
"@blockstack/stacks-transactions": "0.5.1",
"@rehooks/document-title": "^1.0.1",
"@types/react-router-dom": "^5.1.3",
"blockstack": "^19.3.0",
"bignumber.js": "^9.0.0",
"blockstack": "21.0.0",
"bn.js": "^5.1.1",
"buffer": "^5.6.0",
"formik": "^2.1.4",
"history": "^5.0.0-beta.4",
"mdi-react": "^6.7.0",
"preact": "^10.4.0",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-chrome-redux": "^2.0.0-alpha.5",
"react-dom": "^16.13.1",
@@ -64,6 +67,7 @@
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@blockstack/prettier-config": "^0.0.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.2.0",
"@schemastore/web-manifest": "^0.0.4",
"@types/chrome": "^0.0.104",
@@ -91,16 +95,13 @@
"playwright": "^1.1.1",
"playwright-chromium": "^1.1.1",
"playwright-core": "^1.1.1",
"playwright-firefox": "^1.1.1",
"playwright-webkit": "^1.1.1",
"prettier": "^2.0.4",
"prettier": "^2.0.5",
"react-dev-utils": "^10.2.0",
"react-refresh": "^0.7.2",
"react-test-renderer": "^16.8.6",
"terser-webpack-plugin": "^2.3.5",
"ts-jest": "^25.2.0",
"ts-loader": "^6.0.4",
"typescript": "3.7.5",
"webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-chrome-extension-reloader": "^1.3.0",
@@ -120,6 +121,9 @@
"prettier": "@blockstack/prettier-config",
"repository": {
"type": "git",
"url": "git://github.com/blockstack/blockstack-app.git"
"url": "git://github.com/blockstack/ux.git"
},
"resolutions": {
"buffer": "5.6.0"
}
}

View File

@@ -1,4 +1,4 @@
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import {
selectIdentities,
selectCurrentWallet,
@@ -10,8 +10,10 @@ import { selectSecretKey } from '@store/onboarding/selectors';
import { decrypt } from '@blockstack/keychain';
import { DEFAULT_PASSWORD } from '@store/onboarding/types';
import { useState, useEffect } from 'react';
import { doStoreSeed } from '@store/wallet';
export const useWallet = () => {
const dispatch = useDispatch();
const identities = useSelector(selectIdentities);
const firstIdentity = useSelector(selectFirstIdentity);
const wallet = useSelector(selectCurrentWallet);
@@ -27,9 +29,20 @@ export const useWallet = () => {
}
};
const updateSTXKeychain = async () => {
if (wallet && !wallet.stacksPrivateKey) {
const decryptedKey = await decrypt(wallet?.encryptedBackupPhrase, DEFAULT_PASSWORD);
dispatch(doStoreSeed(decryptedKey, DEFAULT_PASSWORD));
}
};
useEffect(() => {
void fetchSecretKey();
}, [onboardingSecretKey]);
useEffect(() => {
void updateSTXKeychain();
}, []);
return { identities, firstIdentity, wallet, secretKey, isRestoringWallet, isSignedIn };
};

View File

@@ -45,31 +45,4 @@ const faqs = (appName: string) => {
];
};
const howDataVaultWorks = [
{
icon: '/assets/images/icon-cross-over-eye.svg',
title: 'Private data storage',
body:
'Normally, companies store your data on their servers for them to keep. Data Vault stores your encrypted data independently from the app, so companies like Nurx (and even Data Vault) cant have access.',
},
{
icon: '/assets/images/icon-padlock.svg',
title: 'Encryption thats always on',
body:
'Encryption turns your data into indecipherable text that can be read only using the Secret Key that you control. This keeps everything you do private.',
},
{
icon: '/assets/images/icon-chain-of-blocks.svg',
title: 'Blockchain technology',
body:
'The Secret Key that unlocks your Data Vault is made using blockchain technology. That ensures there is only ever one, and that no one can take it from you. Your data will be private, out of the hands of companies, and only accessible to you.',
},
{
icon: '/assets/images/icon-shapes.svg',
title: 'One Vault works with 100s of apps',
body:
'Youll only ever have to create one Data Vault to use 100s of other apps like Nurx privately.',
},
];
export { faqs, howDataVaultWorks };
export { faqs };

View File

@@ -0,0 +1,57 @@
import { ContractCallArgument, ContractCallArgumentType } from '@blockstack/connect';
import {
uintCV,
intCV,
falseCV,
trueCV,
contractPrincipalCV,
standardPrincipalCV,
bufferCV,
} from '@blockstack/stacks-transactions';
import RPCClient from '@blockstack/rpc-client';
import BigNumber from 'bignumber.js';
export const encodeContractCallArgument = ({ type, value }: ContractCallArgument) => {
switch (type) {
case ContractCallArgumentType.UINT:
return uintCV(value);
case ContractCallArgumentType.INT:
return intCV(value);
case ContractCallArgumentType.BOOL:
if (value === 'false' || value === '0') return falseCV();
else if (value === 'true' || value === '1') return trueCV();
else throw new Error(`Unexpected Clarity bool value: ${JSON.stringify(value)}`);
case ContractCallArgumentType.PRINCIPAL:
if (value.includes('.')) {
const [addr, name] = value.split('.');
return contractPrincipalCV(addr, name);
} else {
return standardPrincipalCV(value);
}
case ContractCallArgumentType.BUFFER:
return bufferCV(Buffer.from(value));
default:
throw new Error(`Unexpected Clarity type: ${type}`);
}
};
export const getRPCClient = () => {
const { origin } = location;
const url = origin.includes('localhost')
? 'http://localhost:3999'
: 'https://sidecar.staging.blockstack.xyz';
return new RPCClient(url);
};
export const stacksValue = ({
value,
fixedDecimals = false,
}: {
value: number;
fixedDecimals?: boolean;
}) => {
const microStacks = new BigNumber(value);
const stacks = microStacks.shiftedBy(-6);
const stxString = fixedDecimals ? stacks.toFormat(6) : stacks.decimalPlaces(6).toFormat();
return `${stxString} STX`;
};

View File

@@ -22,6 +22,10 @@ export const USERNAME_SUBMITTED = 'Submit Username';
export const USERNAME_VALIDATION_ERROR = 'Validation Error Username';
export const USERNAME_SUBMIT_SUCCESS = 'Submit Username Success';
export const TRANSACTION_SIGN_START = 'Start Transaction Sign Screen';
export const TRANSACTION_SIGN_SUBMIT = 'Submit Transaction Sign';
export const TRANSACTION_SIGN_ERROR = 'Fail Transaction Sign';
// Nice page names for Mark to see in Mixpanel
export const pageTrackingNameMap = {
[ScreenPaths.CHOOSE_ACCOUNT]: 'Choose Account',
@@ -53,7 +57,7 @@ export const titleNameMap = {
export const doTrackScreenChange = (
screen: ScreenPaths,
decodedAuthRequest: DecodedAuthRequest | undefined
decodedAuthRequest?: DecodedAuthRequest
) => {
if (titleNameMap[screen]) {
document.title = titleNameMap[screen];

View File

@@ -0,0 +1,149 @@
import {
TransactionVersion,
StacksTransaction,
deserializeCV,
} from '@blockstack/stacks-transactions';
import { Wallet } from '@blockstack/keychain';
import { getRPCClient } from './stacks-utils';
import {
ContractDeployPayload,
ContractCallPayload,
STXTransferPayload,
TransactionPayload,
TransactionTypes,
} from '@blockstack/connect';
import { doTrack, TRANSACTION_SIGN_SUBMIT, TRANSACTION_SIGN_ERROR } from '@common/track';
import { finalizeTxSignature } from './utils';
export const generateContractCallTx = ({
txData,
wallet,
nonce,
}: {
txData: ContractCallPayload;
wallet: Wallet;
nonce: number;
}) => {
const { contractName, contractAddress, functionName, functionArgs } = txData;
const version = TransactionVersion.Testnet;
const args = functionArgs.map(arg => {
return deserializeCV(Buffer.from(arg, 'hex'));
});
return wallet.getSigner().signContractCall({
contractName,
contractAddress,
functionName,
functionArgs: args,
version,
nonce,
postConditionMode: txData.postConditionMode,
postConditions: txData.postConditions,
});
};
export const generateContractDeployTx = ({
txData,
wallet,
nonce,
}: {
txData: ContractDeployPayload;
wallet: Wallet;
nonce: number;
}) => {
const { contractName, codeBody } = txData;
const version = TransactionVersion.Testnet;
return wallet.getSigner().signContractDeploy({
contractName,
codeBody,
version,
nonce,
postConditionMode: txData.postConditionMode,
postConditions: txData.postConditions,
});
};
export const generateSTXTransferTx = ({
txData,
wallet,
nonce,
}: {
txData: STXTransferPayload;
wallet: Wallet;
nonce: number;
}) => {
const { recipient, memo, amount } = txData;
return wallet.getSigner().signSTXTransfer({
recipient,
memo,
amount,
nonce,
postConditionMode: txData.postConditionMode,
postConditions: txData.postConditions,
});
};
export const generateTransaction = async ({
txData,
wallet,
nonce,
}: {
wallet: Wallet;
nonce: number;
txData: TransactionPayload;
}) => {
let tx: StacksTransaction | null = null;
switch (txData.txType) {
case TransactionTypes.ContractCall:
tx = await generateContractCallTx({ txData, wallet, nonce });
break;
case TransactionTypes.ContractDeploy:
tx = await generateContractDeployTx({ txData, wallet, nonce });
break;
case TransactionTypes.STXTransfer:
tx = await generateSTXTransferTx({ txData, wallet, nonce });
break;
default:
break;
}
if (!tx) {
throw new Error(`Invalid Transaction Type: ${txData.txType}`);
}
return tx;
};
export const finishTransaction = async ({
tx,
pendingTransaction,
}: {
tx: StacksTransaction;
pendingTransaction: TransactionPayload;
}) => {
const serialized = tx.serialize();
const txRaw = serialized.toString('hex');
const client = getRPCClient();
const res = await client.broadcastTX(serialized);
if (res.ok) {
doTrack(TRANSACTION_SIGN_SUBMIT, {
txType: pendingTransaction?.txType,
appName: pendingTransaction?.appDetails?.name,
});
const txId: string = await res.json();
finalizeTxSignature({ txId, txRaw });
} else {
const response = await res.json();
if (response.error) {
const error = `${response.error} - ${response.reason}`;
doTrack(TRANSACTION_SIGN_ERROR, {
txType: pendingTransaction?.txType,
appName: pendingTransaction?.appDetails?.name,
error: error,
});
console.error(response.error);
console.error(response.reason);
throw new Error(error);
}
}
};

View File

@@ -1,5 +1,6 @@
import { DecodedAuthRequest } from './dev/types';
import { wordlists } from 'bip39';
import { FinishedTxData } from '@blockstack/connect';
export const getAuthRequestParam = () => {
const { hash } = document.location;
@@ -50,7 +51,6 @@ interface FinalizeAuthParams {
* but using a new tab.
*
*/
export const finalizeAuthResponse = ({
decodedAuthRequest,
authRequest,
@@ -68,7 +68,7 @@ export const finalizeAuthResponse = ({
}
}
window.close();
}, 150);
}, 500);
window.addEventListener('message', event => {
if (authRequest && event.data.authRequest === authRequest) {
const source = getEventSourceWindow(event);
@@ -86,6 +86,23 @@ export const finalizeAuthResponse = ({
}
});
};
export const finalizeTxSignature = (data: FinishedTxData) => {
window.addEventListener('message', event => {
const source = getEventSourceWindow(event);
if (source) {
source.postMessage(
{
...data,
source: 'blockstack-app',
},
event.origin
);
}
window.close();
});
};
export const openPopup = (actionsUrl: string) => {
// window.open(actionsUrl, 'Blockstack', 'scrollbars=no,status=no,menubar=no,width=300px,height=200px,left=0,top=0')
const height = 584;
@@ -93,6 +110,7 @@ export const openPopup = (actionsUrl: string) => {
// width=440,height=584
popupCenter(actionsUrl, 'Blockstack', width, height);
};
// open a popup, centered on the screen, with logic to handle dual-monitor setups
export const popupCenter = (url: string, title: string, w: number, h: number) => {
const dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX;

View File

@@ -4,7 +4,7 @@ import { ScreenBody, ScreenActions, Title } from '@blockstack/connect';
import useOnClickOutside from 'use-onclickoutside';
import { Image } from '@components/image';
import { ConfigApp } from '@blockstack/keychain/wallet';
import { ConfigApp } from '@blockstack/keychain';
interface PreviousAppsProps {
apps: ConfigApp[];

View File

@@ -1,12 +1,14 @@
import React, { useEffect } from 'react';
import { Home } from '../pages/home';
import { Create, SaveKey } from '../pages/sign-up';
import { SignIn, DecryptRecoveryCode } from '../pages/sign-in';
import { Home } from '@pages/home';
import { Create, SaveKey } from '@pages/sign-up';
import { SignIn, DecryptRecoveryCode } from '@pages/sign-in';
import { Username } from '../pages/username';
import { SecretKey } from '../pages/secret-key';
import { Username } from '@pages/username';
import { SecretKey } from '@pages/secret-key';
import { ChooseAccount } from '../pages/connect';
import { ChooseAccount } from '@pages/connect';
import { Transaction } from '@pages/transaction';
import { doSaveAuthRequest } from '@store/onboarding/actions';
import { useDispatch } from '@common/hooks/use-dispatch';
@@ -112,6 +114,8 @@ export const Routes: React.FC = () => {
/>
}
/>
{/* Transactions */}
<Route path="/transaction" element={<Transaction />} />
{/*Error/Misc*/}
<Route
path="/settings/secret-key"

View File

@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { Box, Flex, Text, BoxProps } from '@blockstack/ui';
export interface Tab {
title: string | React.ReactNode;
key: string;
content: React.ReactNode;
hide?: boolean;
}
export interface TabHeaderProps extends BoxProps {
active: boolean;
tab: Tab;
}
const getBorderProps = (active: boolean) => {
if (active) {
return {
borderBottomWidth: '1px',
borderColor: 'blue',
};
}
return {};
};
const getTextProps = (active: boolean) => {
if (active) {
return {
color: 'blue',
fontWeight: 600,
};
}
return {};
};
const TabHeader: React.FC<TabHeaderProps> = ({ tab, active, ...rest }) => {
return (
<Box mr={2} py={3} {...getBorderProps(active)} {...rest}>
<Text fontSize={0} {...getTextProps(active)} cursor="pointer">
{tab.title}
</Text>
</Box>
);
};
interface TabBodyProps extends BoxProps {
tab: Tab;
}
const TabBody: React.FC<TabBodyProps> = ({ tab }) => {
return (
<Box width="100%" px={3} py={3} borderTopWidth="1px" borderColor="gray.light">
{tab.content}
</Box>
);
};
export interface TabbedCardProps extends BoxProps {
tabs: Tab[];
}
export const TabbedCard: React.FC<TabbedCardProps> = ({ tabs, ...rest }) => {
const [activeKey, setActiveKey] = useState(tabs[0].key);
const activeTab = tabs.find(tab => tab.key === activeKey);
const Header = tabs.map(tab => {
if (tab.hide) {
return null;
}
return (
<TabHeader
tab={tab}
active={tab.key === activeKey}
onClick={() => {
setActiveKey(tab.key);
}}
key={tab.key}
/>
);
});
return (
<Flex borderWidth="1px" borderColor="gray.light" borderRadius="8px" {...rest} wrap="wrap">
<Box width="100%">
<Flex px={3} width="100%">
{Header}
</Flex>
</Box>
{activeTab && (
<Box width="100%">
<Flex width="100%">
<TabBody tab={activeTab} />
</Flex>
</Box>
)}
</Flex>
);
};

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Box, Flex, Text } from '@blockstack/ui';
import styled from 'styled-components';
const BannerText = styled(Text)`
font-size: 11px;
position: relative;
top: -2px;
font-weight: 600;
`;
export const TestnetBanner: React.FC = ({ ...rest }) => {
return (
<Flex height="24px" bg="white" align="center" justify="space-between" {...rest}>
<Flex width="100%" align="center">
<Box textAlign="center" width="100%" backgroundColor="rgba(249, 161, 77, 0.12)">
<BannerText color="orange">Testnet mode</BannerText>
</Box>
</Flex>
</Flex>
);
};

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Screen, ScreenBody, ScreenActions } from '@blockstack/connect';
import { Button, Text, Box, Flex, FailedIcon } from '@blockstack/ui';
import { TestnetBanner } from './testnet-banner';
interface TxErrorProps {
message: string;
}
export const TxError: React.FC<TxErrorProps> = ({ message }) => {
return (
<>
<Screen>
{/* TODO: only show testnet banner if in testnet mode */}
<TestnetBanner />
<ScreenBody
mt={6}
body={[
<Box textAlign="center">
<Flex>
<Box mx="auto" width="25%">
<FailedIcon size={64} width={64} maxWidth="100%" />
</Box>
</Flex>
<Text display="block" textStyle="display.large" mb={3}>
Transaction Failed
</Text>
<Text display="block" mb={6}>
{message}
</Text>
</Box>,
]}
/>
<ScreenActions>
<Button
width="100%"
mt={6}
size="lg"
onClick={() => {
window.close();
}}
>
Return to App
</Button>
</ScreenActions>
</Screen>
</>
);
};

View File

@@ -1,4 +1,4 @@
import { BlockstackProvider } from '@blockstack/connect/types';
import { BlockstackProvider } from '@blockstack/connect';
interface Response {
source: 'blockstack-extension';

View File

@@ -8,9 +8,7 @@ import { useSelector } from 'react-redux';
import { AppState, store } from '@store';
import { selectAppName, selectDecodedAuthRequest } from '@store/onboarding/selectors';
import { Drawer } from '@components/drawer';
import { selectCurrentWallet, selectIdentities } from '@store/wallet/selectors';
import { ConfigApp } from '@blockstack/keychain/wallet';
import { Wallet } from '@blockstack/keychain';
import { ConfigApp } from '@blockstack/keychain';
import { gaiaUrl } from '@common/constants';
import {
CHOOSE_ACCOUNT_CHOSEN,
@@ -21,6 +19,8 @@ import {
} from '@common/track';
import { useAnalytics } from '@common/hooks/use-analytics';
import { ScreenPaths } from '@store/onboarding/types';
import { useWallet } from '@common/hooks/use-wallet';
import { Navigate } from '@components/navigate';
interface ChooseAccountProps {
next: (identityIndex: number) => void;
@@ -39,15 +39,18 @@ const SettingsButton = () => {
};
export const ChooseAccount: React.FC<ChooseAccountProps> = ({ next }) => {
const { appName, identities, wallet } = useSelector((state: AppState) => ({
const { appName } = useSelector((state: AppState) => ({
appName: selectAppName(state),
identities: selectIdentities(state),
wallet: selectCurrentWallet(state) as Wallet,
}));
const { wallet, identities } = useWallet();
const [reusedApps, setReusedApps] = React.useState<ConfigApp[]>([]);
const [identityIndex, setIdentityIndex] = React.useState<number | undefined>();
const { doTrack } = useAnalytics();
if (!wallet) {
return <Navigate to={{ pathname: '/', hash: 'sign-up' }} screenPath={ScreenPaths.GENERATION} />;
}
// TODO: refactor into util, create unit tests
const didSelectAccount = ({ identityIndex }: { identityIndex: number }) => {
const state = store.getState();

View File

@@ -0,0 +1,228 @@
import React, { useState, useEffect } from 'react';
import {
Screen,
ScreenBody,
ScreenActions,
Title,
PoweredBy,
ScreenFooter,
TransactionPayload,
TransactionTypes,
} from '@blockstack/connect';
import { ScreenHeader } from '@components/connected-screen-header';
import { Button, Box, Text } from '@blockstack/ui';
import { useLocation } from 'react-router-dom';
import { decodeToken } from 'jsontokens';
import { useWallet } from '@common/hooks/use-wallet';
import { TransactionVersion, StacksTransaction } from '@blockstack/stacks-transactions';
import { TestnetBanner } from '@components/transactions/testnet-banner';
import { TxError } from '@components/transactions/tx-error';
import { TabbedCard, Tab } from '@components/tabbed-card';
import { getRPCClient, stacksValue } from '@common/stacks-utils';
import { Wallet } from '@blockstack/keychain';
import { doTrack, TRANSACTION_SIGN_START, TRANSACTION_SIGN_ERROR } from '@common/track';
import { finishTransaction, generateTransaction } from '@common/transaction-utils';
interface TabContentProps {
json: any;
}
const getInputJSON = (pendingTransaction: TransactionPayload | undefined, wallet: Wallet) => {
if (pendingTransaction && wallet) {
const { appDetails, publicKey, ...rest } = pendingTransaction;
return {
...rest,
'tx-sender': wallet.getSigner().getSTXAddress(TransactionVersion.Testnet),
};
}
return {};
};
const TabContent: React.FC<TabContentProps> = ({ json }) => {
return (
<Box whiteSpace="pre" overflow="scroll" color="gray" maxHeight="200px">
{JSON.stringify(json, null, 2)}
</Box>
);
};
export const Transaction: React.FC = () => {
const location = useLocation();
const { wallet } = useWallet();
const [pendingTransaction, setPendingTransaction] = useState<TransactionPayload | undefined>();
const [signedTransaction, setSignedTransaction] = useState<StacksTransaction | undefined>();
const [loading, setLoading] = useState(true);
const [contractSrc, setContractSrc] = useState('');
const [balance, setBalance] = useState(0);
const [error, setError] = useState<string | null>(null);
const client = getRPCClient();
if (!wallet) {
throw new Error('User must be logged in.');
}
const tabs: Tab[] = [
{
title: 'Inputs',
content: <TabContent json={getInputJSON(pendingTransaction, wallet)} />,
key: 'inputs',
},
{
title: (
<>
View Source
{/* Add this icon when we can link to the explorer */}
{/* <ExternalIcon display="inline-block" width="9px" ml={1} /> */}
</>
),
content: (
<Box whiteSpace="pre" overflow="scroll" color="gray" maxHeight="200px">
{contractSrc}
</Box>
),
key: 'source',
hide: pendingTransaction?.txType === TransactionTypes.STXTransfer,
},
];
const setupAccountInfo = async () => {
const account = await wallet.getSigner().fetchAccount({
version: TransactionVersion.Testnet,
rpcClient: client,
});
setBalance(account.balance.toNumber());
return account;
};
const setupWithState = async (tx: TransactionPayload) => {
if (tx.txType === TransactionTypes.ContractCall) {
const contractSource = await client.fetchContractSource({
contractName: tx.contractName,
contractAddress: tx.contractAddress,
});
if (contractSource) {
setContractSrc(contractSource);
setPendingTransaction(tx);
} else {
doTrack(TRANSACTION_SIGN_ERROR, {
txType: pendingTransaction?.txType,
appName: pendingTransaction?.appDetails?.name,
error: 'Contract not found',
});
setError(`Unable to find contract ${tx.contractAddress}.${tx.contractName}`);
}
} else if (tx.txType === TransactionTypes.ContractDeploy) {
console.log(tx);
setContractSrc(tx.codeBody);
setPendingTransaction(tx);
} else if (tx.txType === TransactionTypes.STXTransfer) {
setPendingTransaction(tx);
}
doTrack(TRANSACTION_SIGN_START, {
txType: tx.txType,
appName: tx.appDetails?.name,
});
return tx;
};
const decodeRequest = async () => {
const urlParams = new URLSearchParams(location.search);
const requestToken = urlParams.get('request');
if (requestToken) {
const token = decodeToken(requestToken);
const reqState = (token.payload as unknown) as TransactionPayload;
try {
const [txData, account] = await Promise.all([setupWithState(reqState), setupAccountInfo()]);
const tx = await generateTransaction({
wallet,
nonce: account.nonce,
txData,
});
setSignedTransaction(tx);
} catch (error) {
const nodeURL = new URL(client.url);
setError(`Unable to connect to a Stacks node at ${nodeURL.hostname}`);
}
setLoading(false);
} else {
setError('Unable to decode request');
console.error('Unable to find contract call parameter');
}
};
useEffect(() => {
if (wallet.stacksPrivateKey) {
decodeRequest();
}
}, [wallet]);
const handleButtonClick = async () => {
if (!pendingTransaction || !signedTransaction) {
// shouldn't be able to get here
setError('Unable to finish transaction');
return;
}
setLoading(true);
try {
await finishTransaction({ tx: signedTransaction, pendingTransaction });
} catch (err) {
setError(err.message);
}
setLoading(false);
};
if (error) {
return <TxError message={error} />;
}
return (
<>
<Screen isLoading={loading}>
{/* TODO: only show testnet banner if in testnet mode */}
<TestnetBanner />
<ScreenHeader
rightContent={
<Text textStyle="caption" color="gray" fontSize={0}>
{stacksValue({ value: balance })} available
</Text>
}
/>
<ScreenBody
mt={6}
body={[
<Title>Confirm Transaction</Title>,
<Text mt={2} display="inline-block">
with {pendingTransaction?.appDetails?.name}
</Text>,
<TabbedCard mt={4} mb={4} tabs={tabs} />,
<Box width="100%" mt={5}>
<Text fontWeight={600}>
<Text>Fee</Text>
<Text style={{ float: 'right' }}>
{stacksValue({
value: signedTransaction?.auth.spendingCondition?.fee?.toNumber() || 0,
})}
</Text>
</Text>
</Box>,
]}
/>
<ScreenActions>
<Button
width="100%"
mt={5}
size="lg"
onClick={async () => {
await handleButtonClick();
}}
>
Confirm Transaction
</Button>
</ScreenActions>
<ScreenFooter>
<PoweredBy />
</ScreenFooter>
</Screen>
</>
);
};

View File

@@ -27,6 +27,7 @@ import { selectIdentities, selectCurrentWallet } from '@store/wallet/selectors';
import { finalizeAuthResponse } from '@common/utils';
import { gaiaUrl } from '@common/constants';
import { doTrackScreenChange } from '@common/track';
import { TransactionVersion } from '@blockstack/stacks-transactions';
export const doSetOnboardingProgress = (status: boolean): OnboardingActions => {
return {
@@ -163,7 +164,7 @@ export function doFinishSignIn(
const currentIdentity = identities[identityIndex];
await currentIdentity.refresh();
const gaiaConfig = await wallet.createGaiaConfig(gaiaUrl);
await wallet.getOrCreateConfig(gaiaConfig);
await wallet.getOrCreateConfig({ gaiaConfig, skipUpload: true });
await wallet.updateConfigWithAuth({
identityIndex,
gaiaConfig,
@@ -175,11 +176,15 @@ export function doFinishSignIn(
name: appName as string,
},
});
const stxAddress = wallet.stacksPrivateKey
? wallet.getSigner().getSTXAddress(TransactionVersion.Testnet)
: undefined;
const authResponse = await currentIdentity.makeAuthResponse({
gaiaUrl,
appDomain: appURL.origin,
transitPublicKey: decodedAuthRequest.public_keys[0],
scopes: decodedAuthRequest.scopes,
stxAddress,
});
finalizeAuthResponse({ decodedAuthRequest, authRequest, authResponse });
dispatch(doSetOnboardingPath(undefined));

View File

@@ -41,7 +41,7 @@ export function doStoreSeed(
): ThunkAction<Promise<Wallet>, {}, {}, WalletActions> {
return async dispatch => {
dispatch(isRestoringWallet());
const wallet = await Wallet.restore(password, secretKey, ChainID.Testnet);
const wallet = await Wallet.restore(password, secretKey, ChainID.Mainnet);
dispatch(didRestoreWallet(wallet));
return wallet;
};
@@ -52,7 +52,7 @@ export function doGenerateWallet(
): ThunkAction<Promise<Wallet>, {}, {}, WalletActions> {
return async dispatch => {
dispatch(isRestoringWallet());
const wallet = await Wallet.generate(password, ChainID.Testnet);
const wallet = await Wallet.generate(password, ChainID.Mainnet);
dispatch(didGenerateWallet(wallet));
return wallet;
};

View File

@@ -36,7 +36,6 @@ export const debug = async (page: Page) => {
CONTROL_D: '\u0004',
ENTER: '\r',
};
console.log('\n\n🕵 Code is paused, press enter to resume');
// Run an infinite promise
return new Promise(resolve => {

View File

@@ -27,10 +27,10 @@
"@containers/*": ["containers/*"],
"@common/*": ["common/*"],
"@blockstack/connect": ["../../connect/src"],
"@blockstack/connect/*": ["../../connect/src/*"],
"@blockstack/ui": ["../../ui/src"],
"@blockstack/keychain": ["../../keychain/src"],
"@blockstack/keychain/*": ["../../keychain/src/*"]
"@blockstack/rpc-client": ["../../rpc-client/src"],
"@pages/*": ["pages/*"]
},
"baseUrl": "src",
"allowSyntheticDefaultImports": true

View File

@@ -185,6 +185,10 @@ module.exports = {
contentBase: './dist',
historyApiFallback: true,
},
node: {
Buffer: true,
BufferReader: true,
},
devtool: getSourceMap(),
watch: false,
plugins: [

View File

@@ -32,8 +32,8 @@
],
"dependencies": {
"@blockstack/ui": "^2.9.5",
"jsontokens": "^3.0.0",
"preact": "^10.4.0",
"prettier": "^2.0.5",
"styled-components": "^5.1.0",
"use-events": "^1.4.1",
"use-onclickoutside": "^0.3.1"
@@ -49,17 +49,16 @@
"@types/react-dom": "^16.9.6",
"@types/styled-components": "^5.1.0",
"babel-loader": "^8.1.0",
"blockstack": "^19.3.0",
"blockstack": "21.0.0",
"bundlesize": "^0.18.0",
"husky": "^4.2.1",
"prettier": "^2.0.4",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"rollup-plugin-peer-deps-external": "^2.2.2",
"terser-webpack-plugin": "^2.3.5",
"tsdx": "^0.12.3",
"tslib": "^1.10.0",
"typescript": "^3.7.5",
"webpack": "^4.41.5",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10"

View File

@@ -1,8 +1,8 @@
import { UserSession, AppConfig } from 'blockstack';
import './types';
import { popupCenter } from './popup';
import { popupCenter, setupListener } from './popup';
const defaultAuthURL = 'https://app.blockstack.org';
export const defaultAuthURL = 'https://app.blockstack.org';
export interface FinishedData {
authResponse: string;
@@ -86,7 +86,7 @@ export const authenticate = async ({
skipPopupFallback: !!window.BlockstackProvider,
});
setupListener({
setupAuthListener({
popup,
authRequest,
onFinish: onFinish || finished,
@@ -111,7 +111,7 @@ interface ListenerParams {
userSession: UserSession;
}
const setupListener = ({
const setupAuthListener = ({
popup,
authRequest,
onFinish,
@@ -119,55 +119,24 @@ const setupListener = ({
authURL,
userSession,
}: ListenerParams) => {
let lastPong: number | null = null;
const interval = setInterval(() => {
if (popup) {
try {
popup.postMessage(
{
authRequest,
method: 'ping',
},
authURL.origin
);
} catch (error) {
console.warn('[Blockstack] Unable to send ping to authentication service');
clearInterval(interval);
setupListener<FinishedEventData>({
popup,
onCancel,
onFinish: async (data: FinishedEventData) => {
if (data.authRequest === authRequest) {
if (onFinish) {
const { authResponse } = data;
await userSession.handlePendingSignIn(authResponse);
onFinish({
authResponse,
userSession,
});
}
}
}
if (lastPong && new Date().getTime() - lastPong > 200) {
onCancel && onCancel();
clearInterval(interval);
}
}, 100);
const receiveMessage = async (event: MessageEvent) => {
const authRequestMatch = event.data.authRequest === authRequest;
if (!authRequestMatch) {
return;
}
if (event.data.method === 'pong') {
lastPong = new Date().getTime();
} else {
const data: FinishedEventData = event.data;
if (onFinish) {
window.focus();
const { authResponse } = data;
await userSession.handlePendingSignIn(authResponse);
onFinish({
authResponse,
userSession,
});
}
window.removeEventListener('message', receiveMessageCallback);
clearInterval(interval);
}
};
const receiveMessageCallback = (event: MessageEvent) => {
void receiveMessage(event);
};
window.addEventListener('message', receiveMessageCallback, false);
},
messageParams: {
authRequest,
},
authURL,
});
};

View File

@@ -3,3 +3,4 @@ export * from './react';
export * from './popup';
export * from './types';
export * from './ui';
export * from './transactions';

View File

@@ -73,3 +73,70 @@ export const popupCenter = ({
}
return window.open(url);
};
interface ListenerParams<FinishedType> {
popup: Window | null;
messageParams: {
[key: string]: any;
};
onFinish: (payload: FinishedType) => void | Promise<void>;
onCancel?: () => void;
authURL: URL;
}
export const setupListener = <T>({
popup,
messageParams,
onFinish,
onCancel,
authURL,
}: ListenerParams<T>) => {
let lastPong: number | null = null;
// Send a message to the authenticator popup at a consistent interval. This allows
// the authenticator to 'respond'.
const pingInterval = 250;
const interval = setInterval(() => {
if (popup) {
try {
console.log('about to ping');
popup.postMessage(
{
method: 'ping',
...messageParams,
},
authURL.origin
);
} catch (error) {
console.warn('[Blockstack] Unable to send ping to authentication service');
clearInterval(interval);
}
} else {
console.warn('[Blockstack] Unable to send ping to authentication service - popup closed');
}
if (lastPong && new Date().getTime() - lastPong > pingInterval * 2) {
onCancel && onCancel();
clearInterval(interval);
}
}, pingInterval);
const receiveMessage = async (event: MessageEvent) => {
if (event.data.method === 'pong') {
lastPong = new Date().getTime();
return;
}
if (event.data.source === 'blockstack-app') {
const data = event.data as T;
await onFinish(data);
window.focus();
window.removeEventListener('message', receiveMessageCallback);
clearInterval(interval);
}
};
const receiveMessageCallback = (event: MessageEvent) => {
void receiveMessage(event);
};
window.addEventListener('message', receiveMessageCallback, false);
};

View File

@@ -1,5 +1,6 @@
import React, { useReducer, createContext } from 'react';
import { AuthOptions, FinishedData } from '../../../auth';
import { UserSession } from 'blockstack/lib';
enum States {
MODAL_OPEN = 'modal/open',
@@ -23,6 +24,7 @@ type State = {
screen: string;
authData?: FinishedData;
authOptions: AuthOptions;
userSession?: UserSession;
};
const initialState: State = {
@@ -30,6 +32,7 @@ const initialState: State = {
isAuthenticating: false,
screen: States.SCREENS_INTRO,
authData: undefined,
userSession: undefined,
authOptions: {
redirectTo: '',
manifestPath: '',

View File

@@ -1,5 +1,11 @@
import { useContext } from 'react';
import { authenticate, AuthOptions, FinishedData } from '../../auth';
import { openContractCall, openContractDeploy, openSTXTransfer } from '../../transactions';
import {
ContractCallOptions,
ContractDeployOptions,
STXTransferOptions,
} from '../../transactions/types';
import { ConnectContext, ConnectDispatchContext, States } from '../components/connect/context';
const useConnectDispatch = () => {
@@ -11,7 +17,9 @@ const useConnectDispatch = () => {
};
export const useConnect = () => {
const { isOpen, isAuthenticating, authData, screen, authOptions } = useContext(ConnectContext);
const { isOpen, isAuthenticating, authData, screen, authOptions, userSession } = useContext(
ConnectContext
);
const dispatch = useConnectDispatch();
const doUpdateAuthOptions = (payload: Partial<AuthOptions>) => {
@@ -64,12 +72,31 @@ export const useConnect = () => {
});
};
const doContractCall = async (opts: ContractCallOptions) =>
openContractCall({
...opts,
appDetails: authOptions.appDetails,
});
const doContractDeploy = async (opts: ContractDeployOptions) =>
openContractDeploy({
...opts,
appDetails: authOptions.appDetails,
});
const doSTXTransfer = async (opts: STXTransferOptions) =>
openSTXTransfer({
...opts,
appDetails: authOptions.appDetails,
});
return {
isOpen,
isAuthenticating,
authData,
authOptions,
screen,
userSession,
doOpenAuth,
doCloseAuth,
doChangeScreen,
@@ -81,5 +108,8 @@ export const useConnect = () => {
doFinishAuth,
doAuth,
authenticate,
doContractCall,
doContractDeploy,
doSTXTransfer,
};
};

View File

@@ -0,0 +1,144 @@
import { UserSession, AppConfig } from 'blockstack';
import { SECP256K1Client, TokenSigner } from 'jsontokens';
import { defaultAuthURL } from '../auth';
import { popupCenter, setupListener } from '../popup';
import {
ContractCallOptions,
ContractCallPayload,
ContractDeployOptions,
ContractDeployPayload,
FinishedTxData,
TransactionPopup,
TransactionOptions,
STXTransferOptions,
STXTransferPayload,
TransactionPayload,
TransactionTypes,
} from './types';
import { serializeCV } from '@blockstack/stacks-transactions';
export * from './types';
const getKeys = (_userSession?: UserSession) => {
let userSession = _userSession;
if (!userSession) {
const appConfig = new AppConfig(['store_write'], document.location.href);
userSession = new UserSession({ appConfig });
}
const privateKey = userSession.loadUserData().appPrivateKey;
const publicKey = SECP256K1Client.derivePublicKey(privateKey);
return { privateKey, publicKey };
};
const signPayload = async (payload: TransactionPayload, privateKey: string) => {
const tokenSigner = new TokenSigner('ES256k', privateKey);
return tokenSigner.signAsync(payload as any);
};
const openTransactionPopup = async ({ token, opts }: TransactionPopup) => {
const extensionURL = await window.BlockstackProvider?.getURL();
const authURL = new URL(extensionURL || opts.authOrigin || defaultAuthURL);
const urlParams = new URLSearchParams();
urlParams.set('request', token);
const popup = popupCenter({
url: `${authURL.origin}/#/transaction?${urlParams.toString()}`,
h: 700,
});
setupListener<FinishedTxData>({
popup,
authURL,
onFinish: data => {
if (opts.finished) {
opts.finished(data);
}
},
messageParams: {},
});
return popup;
};
export const makeContractCallToken = async (opts: ContractCallOptions) => {
const { contractAddress, functionName, contractName, functionArgs, appDetails } = opts;
const { privateKey, publicKey } = getKeys(opts.userSession);
const args: string[] = functionArgs.map(arg => {
if (typeof arg === 'string') {
return arg;
}
return serializeCV(arg).toString('hex');
});
const payload: ContractCallPayload = {
contractAddress,
contractName,
functionName,
functionArgs: args,
txType: TransactionTypes.ContractCall,
publicKey,
};
if (appDetails) {
payload.appDetails = appDetails;
}
return signPayload(payload, privateKey);
};
export const makeContractDeployToken = async (opts: ContractDeployOptions) => {
const { contractName, codeBody, appDetails } = opts;
const { privateKey, publicKey } = getKeys(opts.userSession);
const payload: ContractDeployPayload = {
contractName,
codeBody,
publicKey,
txType: TransactionTypes.ContractDeploy,
};
if (appDetails) {
payload.appDetails = appDetails;
}
return signPayload(payload, privateKey);
};
export const makeSTXTransferToken = async (opts: STXTransferOptions) => {
const { amount, recipient, memo, appDetails } = opts;
const { privateKey, publicKey } = getKeys(opts.userSession);
const payload: STXTransferPayload = {
amount: amount.toString(10),
recipient,
memo,
publicKey,
txType: TransactionTypes.STXTransfer,
};
if (appDetails) {
payload.appDetails = appDetails;
}
return signPayload(payload, privateKey);
};
async function generateTokenAndOpenPopup<T extends TransactionOptions>(
opts: T,
makeTokenFn: (opts: T) => Promise<string>
) {
const token = await makeTokenFn(opts);
return openTransactionPopup({ token, opts });
}
export const openContractCall = async (opts: ContractCallOptions) =>
generateTokenAndOpenPopup(opts, makeContractCallToken);
export const openContractDeploy = async (opts: ContractDeployOptions) =>
generateTokenAndOpenPopup(opts, makeContractDeployToken);
export const openSTXTransfer = async (opts: STXTransferOptions) =>
generateTokenAndOpenPopup(opts, makeSTXTransferToken);

View File

@@ -0,0 +1,120 @@
import { UserSession } from 'blockstack';
import { AuthOptions } from '../auth';
import {
PostConditionMode,
PostCondition,
StacksNetwork,
AnchorMode,
ClarityValue,
} from '@blockstack/stacks-transactions';
import BN from 'bn.js';
export interface TxBase {
appDetails?: AuthOptions['appDetails'];
postConditionMode?: PostConditionMode;
postConditions?: PostCondition[];
network?: StacksNetwork;
anchorMode?: AnchorMode;
senderKey?: string;
nonce?: number;
}
export interface FinishedTxData {
txId: string;
txRaw: string;
}
export enum TransactionTypes {
ContractCall = 'contract_call',
ContractDeploy = 'smart_contract',
STXTransfer = 'token_transfer',
}
/**
* Contract Call
*/
export enum ContractCallArgumentType {
BUFFER = 'buffer',
UINT = 'uint',
INT = 'int',
PRINCIPAL = 'principal',
BOOL = 'bool',
}
export interface ContractCallBase extends TxBase {
contractAddress: string;
contractName: string;
functionName: string;
functionArgs: (string | ClarityValue)[];
}
export interface ContractCallOptions extends ContractCallBase {
authOrigin?: string;
userSession?: UserSession;
finished?: (data: FinishedTxData) => void;
}
export interface ContractCallArgument {
type: ContractCallArgumentType;
value: string;
}
export interface ContractCallPayload extends ContractCallBase {
txType: TransactionTypes.ContractCall;
publicKey: string;
functionArgs: string[];
}
/**
* Contract Deploy
*/
export interface ContractDeployBase extends TxBase {
contractName: string;
codeBody: string;
}
export interface ContractDeployOptions extends ContractDeployBase {
authOrigin?: string;
userSession?: UserSession;
finished?: (data: FinishedTxData) => void;
}
export interface ContractDeployPayload extends ContractDeployOptions {
publicKey: string;
txType: TransactionTypes.ContractDeploy;
}
/**
* STX Transfer
*/
export interface STXTransferBase extends TxBase {
recipient: string;
amount: BN | string;
memo?: string;
}
export interface STXTransferOptions extends STXTransferBase {
authOrigin?: string;
userSession?: UserSession;
finished?: (data: FinishedTxData) => void;
}
export interface STXTransferPayload extends STXTransferOptions {
publicKey: string;
txType: TransactionTypes.STXTransfer;
amount: string;
}
/**
* Transaction Popup
*/
export type TransactionOptions = ContractCallOptions | ContractDeployOptions | STXTransferOptions;
export type TransactionPayload = ContractCallPayload | ContractDeployPayload | STXTransferPayload;
export interface TransactionPopup {
token: string;
opts: TransactionOptions;
}

View File

@@ -38,6 +38,7 @@
"@babel/compat-data": "^7.10.1",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@blockstack/prettier-config": "^0.0.6",
"@types/bn.js": "^4.11.6",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.10",
"@types/triplesec": "^3.0.0",
@@ -55,10 +56,12 @@
"typescript": "^3.7.3"
},
"dependencies": {
"@blockstack/stacks-transactions": "^0.4.6",
"@blockstack/rpc-client": "^0.3.0-alpha.0",
"@blockstack/stacks-transactions": "0.5.1",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.1.6",
"blockstack": "21.0.0-alpha.2",
"blockstack": "21.0.0",
"bn.js": "^5.1.1",
"c32check": "^1.0.1",
"jsontokens": "^3.0.0",
"prettier": "^2.0.5",

View File

@@ -51,13 +51,15 @@ export class Identity {
gaiaUrl,
transitPublicKey,
scopes = [],
stxAddress,
}: {
appDomain: string;
gaiaUrl: string;
transitPublicKey: string;
scopes?: string[];
stxAddress?: string;
}) {
const appPrivateKey = await this.appPrivateKey(appDomain);
const appPrivateKey = this.appPrivateKey(appDomain);
const hubInfo = await getHubInfo(gaiaUrl);
const profileUrl = await this.profileUrl(hubInfo.read_url_prefix);
const profile =
@@ -67,7 +69,7 @@ export class Identity {
profile.apps = {};
}
const challengeSigner = ECPair.fromPrivateKey(Buffer.from(appPrivateKey, 'hex'));
const storageUrl = `${hubInfo.read_url_prefix}${await ecPairToAddress(challengeSigner)}/`;
const storageUrl = `${hubInfo.read_url_prefix}${ecPairToAddress(challengeSigner)}/`;
profile.apps[appDomain] = storageUrl;
if (!profile.appsMeta) {
profile.appsMeta = {};
@@ -76,7 +78,7 @@ export class Identity {
storage: storageUrl,
publicKey: challengeSigner.publicKey.toString('hex'),
};
const gaiaHubConfig = await connectToGaiaHubWithConfig({
const gaiaHubConfig = connectToGaiaHubWithConfig({
hubInfo,
privateKey: this.keyPair.key,
gaiaHubUrl: gaiaUrl,
@@ -90,7 +92,10 @@ export class Identity {
return makeAuthResponse(
this.keyPair.key,
this.profile || {},
{
...(this.profile || {}),
stxAddress,
},
this.defaultUsername || '',
{
profileUrl,
@@ -105,7 +110,7 @@ export class Identity {
);
}
async appPrivateKey(appDomain: string) {
appPrivateKey(appDomain: string) {
const { salt, appsNodeKey } = this.keyPair;
const appsNode = new IdentityAddressOwnerNode(bip32.fromBase58(appsNodeKey), salt);
return appsNode.getAppPrivateKey(appDomain);

View File

@@ -3,6 +3,8 @@ export * from './utils';
export * from './mnemonic';
export * from './address-derivation';
export { default as Wallet } from './wallet';
export * from './wallet';
export * from './wallet/signer';
export { decrypt } from './encryption/decrypt';
export { encrypt } from './encryption/encrypt';
export * from './profiles';

View File

@@ -6,6 +6,7 @@ import { getAddress } from '../utils';
const APPS_NODE_INDEX = 0;
const SIGNING_NODE_INDEX = 1;
const ENCRYPTION_NODE_INDEX = 2;
const STX_NODE_INDEX = 6;
export default class IdentityAddressOwnerNode {
hdNode: BIP32Interface;
@@ -40,7 +41,7 @@ export default class IdentityAddressOwnerNode {
return this.hdNode.deriveHardened(APPS_NODE_INDEX);
}
async getAddress() {
getAddress() {
return getAddress(this.hdNode);
}
@@ -52,20 +53,24 @@ export default class IdentityAddressOwnerNode {
return this.hdNode.deriveHardened(SIGNING_NODE_INDEX);
}
async getAppNode(appDomain: string) {
getSTXNode() {
return this.hdNode.deriveHardened(STX_NODE_INDEX);
}
getAppNode(appDomain: string) {
return getLegacyAppNode(this.hdNode, this.salt, appDomain);
}
async getAppPrivateKey(appDomain: string) {
const appNode = await this.getAppNode(appDomain);
getAppPrivateKey(appDomain: string) {
const appNode = this.getAppNode(appDomain);
if (!appNode.privateKey) {
throw new Error('App node does not have private key');
}
return appNode.privateKey.toString('hex');
}
async getAppAddress(appDomain: string) {
const appNode = await this.getAppNode(appDomain);
getAppAddress(appDomain: string) {
const appNode = this.getAppNode(appDomain);
return publicKeyToAddress(appNode.publicKey);
}
}

View File

@@ -5,8 +5,9 @@ import {
makeProfileZoneFile,
} from 'blockstack';
import { IdentityKeyPair } from './utils';
import { uploadToGaiaHub } from './utils/gaia';
import Identity from './identity';
import { uploadToGaiaHub, GaiaHubConfig } from 'blockstack/lib/storage/hub';
import { GaiaHubConfig } from 'blockstack/lib/storage/hub';
const PERSON_TYPE = 'Person';
const CONTEXT = 'http://schema.org';
@@ -58,11 +59,11 @@ export const registrars = {
},
};
export async function signProfileForUpload(profile: any, keypair: IdentityKeyPair) {
export function signProfileForUpload(profile: Profile, keypair: IdentityKeyPair) {
const privateKey = keypair.key;
const publicKey = keypair.keyID;
const token = await signProfileToken(profile, privateKey, { publicKey });
const token = signProfileToken(profile, privateKey, { publicKey });
const tokenRecord = wrapProfileToken(token);
const tokenRecords = [tokenRecord];
return JSON.stringify(tokenRecords, null, 2);
@@ -77,12 +78,12 @@ export async function uploadProfile(
const identityHubConfig =
gaiaHubConfig || (await connectToGaiaHub(gaiaHubUrl, identity.keyPair.key));
return uploadToGaiaHub(
const uploadResponse = await uploadToGaiaHub(
DEFAULT_PROFILE_FILE_NAME,
signedProfileTokenData,
identityHubConfig,
'application/json'
identityHubConfig
);
return uploadResponse;
}
interface SendToRegistrarParams {
@@ -146,9 +147,8 @@ export const registerSubdomain = async ({
username,
subdomain,
}: RegisterParams) => {
// const profile = identity.profile || DEFAULT_PROFILE
const profile = DEFAULT_PROFILE;
const signedProfileTokenData = await signProfileForUpload(profile, identity.keyPair);
const profile = identity.profile || DEFAULT_PROFILE;
const signedProfileTokenData = signProfileForUpload(profile, identity.keyPair);
const profileUrl = await uploadProfile(gaiaHubUrl, identity, signedProfileTokenData);
const fullUsername = `${username}.${subdomain}`;
const zoneFile = makeProfileZoneFile(fullUsername, profileUrl);
@@ -158,7 +158,6 @@ export const registerSubdomain = async ({
zoneFile,
identity,
});
identity.defaultUsername = fullUsername;
identity.usernames.push(fullUsername);
return identity;
@@ -175,7 +174,7 @@ export const signAndUploadProfile = async ({
identity: Identity;
gaiaHubConfig?: GaiaHubConfig;
}) => {
const signedProfileTokenData = await signProfileForUpload(profile, identity.keyPair);
const signedProfileTokenData = signProfileForUpload(profile, identity.keyPair);
await uploadProfile(gaiaHubUrl, identity, signedProfileTokenData, gaiaHubConfig);
};

View File

@@ -49,18 +49,19 @@ interface ConnectToGaiaOptions {
gaiaHubUrl: string;
}
export const connectToGaiaHubWithConfig = async ({
export const connectToGaiaHubWithConfig = ({
hubInfo,
privateKey,
gaiaHubUrl,
}: ConnectToGaiaOptions): Promise<GaiaHubConfig> => {
}: ConnectToGaiaOptions): GaiaHubConfig => {
const readURL = hubInfo.read_url_prefix;
const token = makeGaiaAuthToken({ hubInfo, privateKey, gaiaHubUrl });
const address = await ecPairToAddress(
const address = ecPairToAddress(
hexStringToECPair(privateKey + (privateKey.length === 64 ? '01' : ''))
);
return {
url_prefix: readURL,
max_file_upload_size_megabytes: 100,
address,
token,
server: gaiaHubUrl,
@@ -75,15 +76,16 @@ interface ReadOnlyGaiaConfigOptions {
/**
* When you already know the Gaia read URL, make a Gaia config that doesn't have to fetch `/hub_info`
*/
export const makeReadOnlyGaiaConfig = async ({
export const makeReadOnlyGaiaConfig = ({
readURL,
privateKey,
}: ReadOnlyGaiaConfigOptions): Promise<GaiaHubConfig> => {
const address = await ecPairToAddress(
}: ReadOnlyGaiaConfigOptions): GaiaHubConfig => {
const address = ecPairToAddress(
hexStringToECPair(privateKey + (privateKey.length === 64 ? '01' : ''))
);
return {
url_prefix: readURL,
max_file_upload_size_megabytes: 100,
address,
token: 'not_used',
server: 'not_used',
@@ -113,3 +115,24 @@ const makeGaiaAuthToken = ({ hubInfo, privateKey, gaiaHubUrl }: ConnectToGaiaOpt
const token = new TokenSigner('ES256K', privateKey).sign(payload);
return `v1:${token}`;
};
export const uploadToGaiaHub = async (
filename: string,
contents: Blob | Buffer | ArrayBufferView | string,
hubConfig: GaiaHubConfig
): Promise<string> => {
const contentType = 'application/json';
const response = await fetch(`${hubConfig.server}/store/${hubConfig.address}/${filename}`, {
method: 'POST',
headers: {
'Content-Type': contentType,
Authorization: `bearer ${hubConfig.token}`,
},
body: contents,
referrer: 'no-referrer',
referrerPolicy: 'no-referrer',
});
const { publicURL } = await response.json();
return publicURL;
};

View File

@@ -62,7 +62,7 @@ export async function getIdentityOwnerAddressNode(
return new IdentityAddressOwnerNode(identityPrivateKeychain.deriveHardened(identityIndex), salt);
}
export async function getAddress(node: BIP32Interface) {
export function getAddress(node: BIP32Interface) {
return publicKeyToAddress(node.publicKey);
}
@@ -71,21 +71,24 @@ export interface IdentityKeyPair {
keyID: string;
address: string;
appsNodeKey: string;
stxNodeKey: string;
salt: string;
}
export async function deriveIdentityKeyPair(
export function deriveIdentityKeyPair(
identityOwnerAddressNode: IdentityAddressOwnerNode
): Promise<IdentityKeyPair> {
const address = await identityOwnerAddressNode.getAddress();
): IdentityKeyPair {
const address = identityOwnerAddressNode.getAddress();
const identityKey = identityOwnerAddressNode.getIdentityKey();
const identityKeyID = identityOwnerAddressNode.getIdentityKeyID();
const appsNode = identityOwnerAddressNode.getAppsNode();
const stxNode = identityOwnerAddressNode.getSTXNode();
const keyPair = {
key: identityKey,
keyID: identityKeyID,
address,
appsNodeKey: appsNode.toBase58(),
stxNodeKey: stxNode.toBase58(),
salt: identityOwnerAddressNode.getSalt(),
};
return keyPair;
@@ -104,7 +107,7 @@ export async function getBlockchainIdentities(
const bitcoinPublicKeychainNode = bitcoinPrivateKeychainNode.neutered();
const bitcoinPublicKeychain = bitcoinPublicKeychainNode.toBase58();
const firstBitcoinAddress = await getAddress(getBitcoinAddressNode(bitcoinPublicKeychainNode));
const firstBitcoinAddress = getAddress(getBitcoinAddressNode(bitcoinPublicKeychainNode));
const identityAddresses: string[] = [];
const identityKeypairs = [];
@@ -135,7 +138,7 @@ export const makeIdentity = async (rootNode: BIP32Interface, index: number) => {
identityPrivateKeychainNode,
index
);
const identityKeyPair = await deriveIdentityKeyPair(identityOwnerAddressNode);
const identityKeyPair = deriveIdentityKeyPair(identityOwnerAddressNode);
const identity = new Identity({
keyPair: identityKeyPair,
address: identityKeyPair.address,

View File

@@ -17,8 +17,6 @@ import {
getPublicKeyFromPrivate,
decryptContent,
} from 'blockstack';
import { GaiaHubConfig, uploadToGaiaHub } from 'blockstack/lib/storage/hub';
import { makeReadOnlyGaiaConfig, DEFAULT_GAIA_HUB } from '../utils/gaia';
import {
AllowedKeyEntropyBits,
generateEncryptedMnemonicRootKeychain,
@@ -26,6 +24,9 @@ import {
encryptMnemonicFormatted,
} from '../mnemonic';
import { deriveStxAddressChain } from '../address-derivation';
import { GaiaHubConfig } from 'blockstack/lib/storage/hub';
import { makeReadOnlyGaiaConfig, DEFAULT_GAIA_HUB, uploadToGaiaHub } from '../utils/gaia';
import { WalletSigner } from './signer';
const CONFIG_INDEX = 45;
@@ -60,7 +61,7 @@ export interface ConstructorOptions {
encryptedBackupPhrase: string;
identities: Identity[];
configPrivateKey: string;
stxAddressKeychain: BIP32Interface;
stacksPrivateKey: string;
walletConfig?: WalletConfig;
}
@@ -74,7 +75,7 @@ export class Wallet {
identityPublicKeychain: string;
identities: Identity[];
configPrivateKey: string;
stxAddressKeychain: BIP32Interface;
stacksPrivateKey: string;
walletConfig?: WalletConfig;
constructor({
@@ -87,7 +88,7 @@ export class Wallet {
identityAddresses,
identities,
configPrivateKey,
stxAddressKeychain,
stacksPrivateKey,
walletConfig,
}: ConstructorOptions) {
this.chain = chain;
@@ -99,7 +100,7 @@ export class Wallet {
this.identityAddresses = identityAddresses;
this.identities = identities.map(identity => new Identity(identity));
this.configPrivateKey = configPrivateKey;
this.stxAddressKeychain = stxAddressKeychain;
this.stacksPrivateKey = stacksPrivateKey;
this.walletConfig = walletConfig;
}
@@ -161,7 +162,7 @@ export class Wallet {
...walletAttrs,
chain,
configPrivateKey,
stxAddressKeychain,
stacksPrivateKey: stxAddressKeychain.toBase58(),
encryptedBackupPhrase,
});
}
@@ -183,7 +184,7 @@ export class Wallet {
rootNode: bip32.BIP32Interface;
gaiaReadURL: string;
}) {
const gaiaConfig = await makeReadOnlyGaiaConfig({
const gaiaConfig = makeReadOnlyGaiaConfig({
readURL: gaiaReadURL,
privateKey: this.configPrivateKey,
});
@@ -243,7 +244,13 @@ export class Wallet {
}
}
async getOrCreateConfig(gaiaConfig: GaiaHubConfig): Promise<WalletConfig> {
async getOrCreateConfig({
gaiaConfig,
skipUpload,
}: {
gaiaConfig: GaiaHubConfig;
skipUpload?: boolean;
}): Promise<WalletConfig> {
if (this.walletConfig) {
return this.walletConfig;
}
@@ -259,14 +266,16 @@ export class Wallet {
})),
};
this.walletConfig = newConfig;
await this.updateConfig(gaiaConfig);
if (!skipUpload) {
await this.updateConfig(gaiaConfig);
}
return newConfig;
}
async updateConfig(gaiaConfig: GaiaHubConfig): Promise<void> {
const publicKey = getPublicKeyFromPrivate(this.configPrivateKey);
const encrypted = await encryptContent(JSON.stringify(this.walletConfig), { publicKey });
await uploadToGaiaHub('wallet-config.json', encrypted, gaiaConfig, 'application/json');
await uploadToGaiaHub('wallet-config.json', encrypted, gaiaConfig);
}
async updateConfigWithAuth({
@@ -312,6 +321,10 @@ export class Wallet {
await this.updateConfig(gaiaConfig);
}
getSigner() {
return new WalletSigner({ privateKey: this.stacksPrivateKey });
}
}
export default Wallet;

View File

@@ -0,0 +1,147 @@
import {
makeContractCall,
makeContractDeploy,
TransactionVersion,
ClarityValue,
StacksTestnet,
makeSTXTokenTransfer,
PostConditionMode,
getAddressFromPrivateKey,
PostCondition,
StacksNetwork,
} from '@blockstack/stacks-transactions';
import RPCClient from '@blockstack/rpc-client';
import { bip32 } from 'bitcoinjs-lib';
import { assertIsTruthy } from '../utils';
import BN from 'bn.js';
interface ContractCallOptions {
contractName: string;
contractAddress: string;
functionName: string;
functionArgs: ClarityValue[];
version: TransactionVersion;
nonce: number;
postConditions?: PostCondition[];
postConditionMode?: PostConditionMode;
network?: StacksNetwork;
}
interface ContractDeployOptions {
contractName: string;
codeBody: string;
version: TransactionVersion;
nonce: number;
postConditions?: PostCondition[];
postConditionMode?: PostConditionMode;
network?: StacksNetwork;
}
interface STXTransferOptions {
recipient: string;
amount: string;
memo?: string;
nonce: number;
postConditions?: PostCondition[];
postConditionMode?: PostConditionMode;
network?: StacksNetwork;
}
export class WalletSigner {
privateKey: string;
constructor({ privateKey }: { privateKey: string }) {
this.privateKey = privateKey;
}
getSTXAddress(version: TransactionVersion) {
return getAddressFromPrivateKey(this.getSTXPrivateKey(), version);
}
getSTXPrivateKey() {
const node = bip32.fromBase58(this.privateKey);
assertIsTruthy<Buffer>(node.privateKey);
return node.privateKey;
}
getNetwork() {
const network = new StacksTestnet();
network.coreApiUrl = 'https://sidecar.staging.blockstack.xyz';
return network;
}
async fetchAccount({
version,
rpcClient,
}: {
version: TransactionVersion;
rpcClient: RPCClient;
}) {
const address = this.getSTXAddress(version);
const account = await rpcClient.fetchAccount(address);
return account;
}
async signContractCall({
contractName,
contractAddress,
functionName,
functionArgs,
nonce,
postConditionMode,
postConditions,
}: ContractCallOptions) {
const tx = await makeContractCall({
contractAddress,
contractName,
functionName,
functionArgs,
senderKey: this.getSTXPrivateKey().toString('hex'),
nonce: new BN(nonce),
network: this.getNetwork(),
postConditionMode,
postConditions,
});
return tx;
}
async signContractDeploy({
contractName,
codeBody,
nonce,
postConditionMode,
postConditions,
}: ContractDeployOptions) {
const tx = await makeContractDeploy({
contractName,
codeBody: codeBody,
senderKey: this.getSTXPrivateKey().toString('hex'),
network: this.getNetwork(),
nonce: new BN(nonce),
postConditionMode,
postConditions,
});
return tx;
}
async signSTXTransfer({
recipient,
amount,
memo,
nonce,
postConditionMode,
postConditions,
}: STXTransferOptions) {
const tx = await makeSTXTokenTransfer({
recipient,
amount: new BN(amount),
memo,
senderKey: this.getSTXPrivateKey().toString('hex'),
network: this.getNetwork(),
nonce: new BN(nonce),
postConditionMode,
postConditions,
});
return tx;
}
}

View File

@@ -5,6 +5,8 @@ import { decodeToken } from 'jsontokens';
import { getIdentity, profileResponse } from './helpers';
import { ecPairToAddress } from 'blockstack';
import { ECPair } from 'bitcoinjs-lib';
import { getAddress } from '../src';
import { TransactionVersion } from '@blockstack/stacks-transactions';
interface Decoded {
[key: string]: any;
@@ -60,9 +62,7 @@ test('adds to apps in profile if publish_data scope', async () => {
expect(apps[appDomain]).not.toBeFalsy();
const appPrivateKey = await decryptPrivateKey(transitPrivateKey, payload.private_key);
const challengeSigner = ECPair.fromPrivateKey(Buffer.from(appPrivateKey as string, 'hex'));
const expectedDomain = `https://gaia.blockstack.org/hub/${await ecPairToAddress(
challengeSigner
)}/`;
const expectedDomain = `https://gaia.blockstack.org/hub/${ecPairToAddress(challengeSigner)}/`;
expect(apps[appDomain]).toEqual(expectedDomain);
expect(appsMeta[appDomain]).not.toBeFalsy();
expect(appsMeta[appDomain].storage).toEqual(expectedDomain);
@@ -72,7 +72,7 @@ test('adds to apps in profile if publish_data scope', async () => {
test('generates an app private key', async () => {
const expectedKey = '6f8b6a170f8b2ee57df5ead49b0f4c8acde05f9e1c4c6ef8223d6a42fabfa314';
const identity = await getIdentity();
const appPrivateKey = await identity.appPrivateKey('https://banter.pub');
const appPrivateKey = identity.appPrivateKey('https://banter.pub');
expect(appPrivateKey).toEqual(expectedKey);
});
@@ -80,7 +80,7 @@ test('generates an app private key for a different seed', async () => {
const identity = await getIdentity(
'monster toilet shoe giggle welcome coyote enact glass copy era shed foam'
);
const appPrivateKey = await identity.appPrivateKey('https://banter.pub');
const appPrivateKey = identity.appPrivateKey('https://banter.pub');
expect(appPrivateKey).toEqual('a7bf3ecf0dd68a23a6621c39780d6cae3776240251a7988fed9ecfda2699ffe8');
});

View File

@@ -13,7 +13,7 @@ import { makeProfileZoneFile } from 'blockstack';
describe('signProfileForUpload', () => {
it('should create a signed JSON string', async () => {
const identity = await getIdentity();
const signedJSON = await signProfileForUpload(DEFAULT_PROFILE, identity.keyPair);
const signedJSON = signProfileForUpload(DEFAULT_PROFILE, identity.keyPair);
const profile = JSON.parse(signedJSON);
expect(profile.length).toEqual(1);
const [data] = profile;

View File

@@ -0,0 +1,18 @@
import './setup';
import { getWallet } from './helpers';
import { TransactionVersion } from '@blockstack/stacks-transactions';
const getSigner = async () => {
const wallet = await getWallet();
return wallet.getSigner();
};
test('can get a STX address', async () => {
const signer = await getSigner();
expect(signer.getSTXAddress(TransactionVersion.Mainnet)).toEqual(
'SP1GZ804XH4240T4JT2GQ34GG0DMT6B3BQ5NV18PD'
);
expect(signer.getSTXAddress(TransactionVersion.Testnet)).toEqual(
'ST1GZ804XH4240T4JT2GQ34GG0DMT6B3BQ5YQX2WX'
);
});

View File

@@ -33,6 +33,8 @@ describe('Restoring a wallet', () => {
'xprvA1y4zBndD83n6PWgVH6ivkTpNQ2WU1UGPg9hWa2q8sCANa7YrYMZFHWMhrbpsarx' +
'XMuQRa4jtaT2YXugwsKrjFgn765tUHu9XjyiDFEjB7f',
salt: 'c15619adafe7e75a195a1a2b5788ca42e585a3fd181ae2ff009c6089de54ed9e',
stxNodeKey:
'xprvA1y4zBndD83nNNFWE1UiWpmc9hpPuk8xjPNwb2j341txeJmCHe8VWT7VKS6FcgnCtbuBP2kzyW34ESdJtJ81AQxCbr9cmQsUHHZ8dtyTxCy',
},
];
@@ -157,7 +159,7 @@ test('creates a config', async () => {
.once(JSON.stringify({ publicUrl: 'asdf' }));
const wallet = await Wallet.generate('password', ChainID.Testnet);
const hubConfig = await wallet.createGaiaConfig('https://gaia.blockstack.org');
const config = await wallet.getOrCreateConfig(hubConfig);
const config = await wallet.getOrCreateConfig({ gaiaConfig: hubConfig });
expect(Object.keys(config.identities[0].apps).length).toEqual(0);
const { body } = fetchMock.mock.calls[2][1];
const decrypted = (await decryptContent(body, { privateKey: wallet.configPrivateKey })) as string;
@@ -179,7 +181,7 @@ test('updates wallet config', async () => {
const wallet = await Wallet.generate('password', ChainID.Testnet);
const gaiaConfig = await wallet.createGaiaConfig('https://gaia.blockstack.org');
await wallet.getOrCreateConfig(gaiaConfig);
await wallet.getOrCreateConfig({ gaiaConfig });
const app: ConfigApp = {
origin: 'http://localhost:5000',
scopes: ['read_write'],
@@ -216,7 +218,7 @@ test('updates config for reusing id warning', async () => {
const wallet = await Wallet.generate('password', ChainID.Testnet);
const gaiaConfig = await wallet.createGaiaConfig('https://gaia.blockstack.org');
await wallet.getOrCreateConfig(gaiaConfig);
await wallet.getOrCreateConfig({ gaiaConfig });
expect(wallet.walletConfig?.hideWarningForReusingIdentity).toBeFalsy();
await wallet.updateConfigForReuseWarning({ gaiaConfig });
expect(wallet.walletConfig?.hideWarningForReusingIdentity).toBeTruthy();

View File

@@ -5,9 +5,13 @@
"moduleResolution": "node",
"declaration": true,
"outDir": "/lib",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true,
"baseUrl": "./src",
"allowSyntheticDefaultImports": true,
"paths": {
"@blockstack/rpc-client": ["../../rpc-client/src"]
},
"lib": [
"es2017",
"dom"

8
packages/rpc-client/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
*.log
.DS_Store
node_modules
.rts2_cache_cjs
.rts2_cache_esm
.rts2_cache_umd
.rts2_cache_system
dist

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Hank Stoever
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,27 @@
# TSDX Bootstrap
This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx).
## Local Development
Below is a list of commands you will probably find useful.
### `npm start` or `yarn start`
Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab.
<img src="https://user-images.githubusercontent.com/4060187/52168303-574d3a00-26f6-11e9-9f3b-71dbec9ebfcb.gif" width="600" />
Your library will be rebuilt if you make edits.
### `npm run build` or `yarn build`
Bundles the package to the `dist` folder.
The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
<img src="https://user-images.githubusercontent.com/4060187/52168322-a98e5b00-26f6-11e9-8cf6-222d716b75ef.gif" width="600" />
### `npm test` or `yarn test`
Runs the test watcher (Jest) in an interactive mode.
By default, runs tests related to files changed since the last commit.

View File

@@ -0,0 +1,41 @@
{
"name": "@blockstack/rpc-client",
"version": "0.3.0-alpha.0",
"license": "MIT",
"author": "Hank Stoever",
"main": "dist/index.js",
"module": "dist/rpc-client.esm.js",
"typings": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test",
"lint": "tsdx lint",
"prepublishOnly": "yarn build"
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"devDependencies": {
"@blockstack/stacks-blockchain-sidecar-types": "^0.0.19",
"@types/bn.js": "^4.11.6",
"@types/jest": "^25.2.1",
"bn.js": "^5.1.1",
"husky": "^4.2.3",
"tsdx": "^0.13.1",
"tslib": "^1.11.1"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@blockstack/stacks-transactions": "0.5.1",
"cross-fetch": "^3.0.4"
}
}

View File

@@ -0,0 +1,143 @@
import BN from 'bn.js';
import { serializeCV, ClarityValue } from '@blockstack/stacks-transactions';
import { TransactionResults } from '@blockstack/stacks-blockchain-sidecar-types';
export interface Account {
balance: BN;
nonce: number;
}
export const toBN = (hex: string) => {
return new BN(hex.slice(2), 16);
};
interface FetchContractInterface {
contractAddress: string;
contractName: string;
}
interface BufferArg {
buffer: {
length: number;
};
}
export interface ContractInterfaceFunctionArg {
name: string;
type: string | BufferArg;
}
export interface ContractInterfaceFunction {
name: string;
access: 'public' | 'private' | 'read_only';
args: ContractInterfaceFunctionArg[];
}
export interface ContractInterface {
functions: ContractInterfaceFunction[];
}
interface CallReadOnly extends FetchContractInterface {
args: ClarityValue[];
functionName: string;
}
export class RPCClient {
url: string;
/**
* @param url The base URL for the RPC server
*/
constructor(url: string) {
this.url = url;
}
async fetchAccount(principal: string): Promise<Account> {
const url = `${this.url}/v2/accounts/${principal}`;
const response = await fetch(url, {
credentials: 'omit',
});
const data = await response.json();
return {
balance: toBN(data.balance),
nonce: data.nonce,
};
}
async broadcastTX(hex: Buffer) {
const url = `${this.url}/v2/transactions`;
const response = await fetch(url, {
method: 'POST',
credentials: 'omit',
headers: {
'Content-Type': 'application/octet-stream',
},
body: hex,
});
return response;
}
async fetchContractInterface({
contractAddress,
contractName,
}: FetchContractInterface) {
const url = `${this.url}/v2/contracts/interface/${contractAddress}/${contractName}`;
const response = await fetch(url);
const contractInterface: ContractInterface = await response.json();
return contractInterface;
}
async callReadOnly({
contractName,
contractAddress,
functionName,
args,
}: CallReadOnly) {
const url = `${this.url}/v2/contracts/call-read/${contractAddress}/${contractName}/${functionName}`;
const argsStrings = args.map((arg) => {
return `0x${serializeCV(arg).toString('hex')}`;
});
const body = {
sender: 'SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0',
arguments: argsStrings,
};
console.log(body);
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Unable to call read-only function.`);
}
const data = await response.json();
console.log(data);
return data;
}
async fetchContractSource({
contractName,
contractAddress,
}: {
contractName: string;
contractAddress: string;
}) {
const url = `${this.url}/v2/contracts/source/${contractAddress}/${contractName}`;
const res = await fetch(url);
if (res.ok) {
const { source }: { source: string } = await res.json();
return source;
}
return null;
}
async fetchAddressTransactions({ address }: { address: string }) {
const url = `${this.url}/sidecar/v1/address/${address}/transactions`;
const res = await fetch(url);
const data: TransactionResults = await res.json();
return data.results;
}
}
export default RPCClient;

View File

@@ -0,0 +1,11 @@
import { fetchAccount } from '../src';
describe('fetchAccount', () => {
it('works', async () => {
const account = await fetchAccount(
'ST2VHM28V9E5QCRD6C73215KAPSBKQGPWTEE5CMQT'
);
expect(account.balance.toNumber()).toEqual(10000);
expect(account.nonce).toEqual(0);
}, 10000);
});

View File

@@ -0,0 +1,30 @@
{
"include": ["src", "types", "test"],
"compilerOptions": {
"target": "es5",
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": "./src",
"paths": {
"*": ["src/*", "node_modules/*"]
},
"jsx": "react",
"esModuleInterop": true
}
}

View File

@@ -0,0 +1,21 @@
import { createContext } from 'react';
import { UserData } from 'blockstack/lib/auth/authApp';
import { UserSession, AppConfig } from 'blockstack';
export interface AppState {
userData: UserData | null;
}
export const defaultState = (): AppState => {
const appConfig = new AppConfig(['store_write'], document.location.href);
const userSession = new UserSession({ appConfig });
if (userSession.isUserSignedIn()) {
return {
userData: userSession.loadUserData(),
};
}
return { userData: null };
};
export const AppContext = createContext<AppState>(defaultState());

View File

@@ -0,0 +1,59 @@
export const SampleContracts: readonly {
readonly contractName: string;
readonly contractSource: string;
}[] = [
{
contractName: 'hello-world-contract',
contractSource: `(define-constant sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)
(define-constant recipient 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G)
(define-fungible-token novel-token-19)
(begin (ft-mint? novel-token-19 u12 sender))
(begin (ft-transfer? novel-token-19 u2 sender recipient))
(define-non-fungible-token hello-nft uint)
(begin (nft-mint? hello-nft u1 sender))
(begin (nft-mint? hello-nft u2 sender))
(begin (nft-transfer? hello-nft u1 sender recipient))
(define-public (test-emit-event)
(begin
(print "Event! Hello world")
(ok u1)))
(begin (test-emit-event))
(define-public (test-event-types)
(begin
(unwrap-panic (ft-mint? novel-token-19 u3 recipient))
(unwrap-panic (nft-mint? hello-nft u2 recipient))
(unwrap-panic (stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))
(unwrap-panic (stx-burn? u20 tx-sender))
(ok u1)))
(define-map store ((key (buff 32))) ((value (buff 32))))
(define-public (get-value (key (buff 32)))
(begin
(match (map-get? store ((key key)))
entry (ok (get value entry))
(err 0))))
(define-public (set-value (key (buff 32)) (value (buff 32)))
(begin
(map-set store ((key key)) ((value value)))
(ok u1)))`,
},
{
contractName: 'kv-store',
contractSource: `
(define-map store ((key (buff 32))) ((value (buff 32))))
(define-public (get-value (key (buff 32)))
(match (map-get? store {key: key})
entry (ok (get value entry))
(err 0)))
(define-public (set-value (key (buff 32)) (value (buff 32)))
(begin
(map-set store {key: key} {value: value})
(ok u1)))`,
},
];

View File

@@ -0,0 +1,83 @@
import { useContext, useState, useEffect } from 'react';
import { AppContext } from '@common/context';
import { getRPCClient } from './utils';
import { useSTXAddress } from './use-stx-address';
export const useFaucet = () => {
const stxAddress = useSTXAddress();
const state = useContext(AppContext);
const [balance, setBalance] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [waiting, setWaiting] = useState(false);
const [txId, setTxId] = useState('');
const [error, setError] = useState('');
const client = getRPCClient();
interface FaucetResponse {
txId?: string;
success: boolean;
}
const submit = async (stxAddress: string) => {
setWaiting(true);
const waitForBalance = async (currentBalance: number, attempts: number) => {
const { balance } = await client.fetchAccount(stxAddress);
if (attempts > 18) {
setError(
"It looks like your transaction still isn't confirmed after a few minutes. Something may have gone wrong."
);
setWaiting(false);
}
if (balance.toNumber() > currentBalance) {
setWaiting(false);
setBalance(balance.toNumber());
return;
}
setTimeout(() => {
waitForBalance(currentBalance, attempts + 1);
}, 10000);
};
try {
const url = `${client.url}/sidecar/v1/debug/faucet?address=${stxAddress}`;
const res = await fetch(url, {
method: 'POST',
});
const data: FaucetResponse = await res.json();
console.log(data);
if (data.txId) {
setTxId(data.txId);
const { balance } = await client.fetchAccount(stxAddress);
await waitForBalance(balance.toNumber(), 0);
} else {
setError('Something went wrong when requesting the faucet.');
}
} catch (e) {
setError('Something went wrong when requesting the faucet.');
setLoading(false);
console.error(e.message);
}
};
useEffect(() => {
const getBalance = async () => {
if (stxAddress) {
setLoading(true);
const { balance } = await client.fetchAccount(stxAddress);
setBalance(balance.toNumber());
if (balance.toNumber() === 0) {
void submit(stxAddress);
}
setLoading(false);
}
};
void getBalance();
}, [state.userData]);
return {
balance,
loading,
waiting,
error,
txId,
};
};

View File

@@ -0,0 +1,7 @@
import { useContext } from 'react';
import { AppContext } from '@common/context';
export const useSTXAddress = (): string | undefined => {
const { userData } = useContext(AppContext);
return userData?.profile?.stxAddress;
};

View File

@@ -0,0 +1,33 @@
import { RPCClient } from '@blockstack/rpc-client';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const getAuthOrigin = () => {
let authOrigin = process.env.AUTH_ORIGIN || 'http://localhost:8080';
// In order to have deploy previews use the same version of the authenticator,
// we detect if this is a 'deploy preview' and change the origin to point to the
// same PR's deploy preview in the authenticator.
const { origin } = location;
if (origin.includes('deploy-preview')) {
// Our netlify sites are called "authenticator-demo" for this app, and
// "stacks-authenticator" for the authenticator.
authOrigin = document.location.origin.replace('authenticator-demo', 'stacks-authenticator');
} else if (origin.includes('authenticator-demo')) {
// TODO: revert this when 301 is merged
authOrigin = 'https://deploy-preview-301--stacks-authenticator.netlify.app';
// authOrigin = 'https://app.blockstack.org';
}
return authOrigin;
};
export const getRPCClient = () => {
const { origin } = location;
const url = origin.includes('localhost')
? 'http://localhost:3999'
: 'https://sidecar.staging.blockstack.xyz';
return new RPCClient(url);
};
export const toRelativeTime = (ts: number): string => dayjs().to(ts);

View File

@@ -1,95 +1,39 @@
import React from 'react';
import { ThemeProvider, Box, theme, Text, Flex, CSSReset, Button, Stack } from '@blockstack/ui';
import { Connect, AuthOptions, useConnect } from '@blockstack/connect';
import React, { useEffect } from 'react';
import { ThemeProvider, theme, Flex, CSSReset } from '@blockstack/ui';
import { Connect, AuthOptions } from '@blockstack/connect';
import { getAuthOrigin } from '@common/utils';
import { UserSession, AppConfig } from 'blockstack';
import { defaultState, AppContext, AppState } from '@common/context';
import { Header } from '@components/header';
import { Home } from '@components/home';
const icon = '/assets/messenger-app-icon.png';
let authOrigin = process.env.AUTH_ORIGIN || 'http://localhost:8080';
// In order to have deploy previews use the same version of the authenticator,
// we detect if this is a 'deploy preview' and change the origin to point to the
// same PR's deploy preview in the authenticator.
const { origin } = location;
if (origin.includes('deploy-preview')) {
// Our netlify sites are called "authenticator-demo" for this app, and
// "stacks-authenticator" for the authenticator.
authOrigin = document.location.origin.replace('authenticator-demo', 'stacks-authenticator');
} else if (origin.includes('authenticator-demo')) {
authOrigin = 'https://app.blockstack.org';
}
const Card: React.FC = props => (
<Flex
border="1px solid"
borderRadius="6px"
borderColor="inherit"
p={6}
direction="column"
boxShadow="mid"
minWidth="420px"
{...props}
/>
);
const AppContent: React.FC = () => {
const { doOpenAuth, doAuth } = useConnect();
return (
<Card>
<Box textAlign="center" pb={6}>
<Text as="h1">Blockstack Connect</Text>
</Box>
<Flex justify="center">
<Stack isInline>
<Button onClick={() => doOpenAuth(false)} data-test="sign-up">
Sign Up
</Button>
<Button onClick={() => doOpenAuth(true)} data-test="sign-in">
Sign In
</Button>
<Button onClick={() => doAuth()} data-test="skip-connect">
Skip Connect
</Button>
</Stack>
</Flex>
</Card>
);
};
interface AppState {
[key: string]: any;
}
const SignedIn = (props: { username: string; handleSignOut: () => void }) => {
return (
<Card>
<Box textAlign="center">
<Text as="h1">Welcome back!</Text>
</Box>
<Box textAlign="center" pt={4}>
<Text as="h2">{props.username}</Text>
</Box>
<Flex mt={6} align="center" justify="center">
<Button mx="auto" onClick={props.handleSignOut}>
Sign out
</Button>
</Flex>
</Card>
);
};
export const App: React.FC = () => {
const [state, setState] = React.useState<AppState>({});
const [state, setState] = React.useState<AppState>(defaultState());
const [authResponse, setAuthResponse] = React.useState('');
const [appPrivateKey, setAppPrivateKey] = React.useState('');
const appConfig = new AppConfig();
const appConfig = new AppConfig(['store_write'], document.location.href);
const userSession = new UserSession({ appConfig });
const signOut = () => {
userSession.signUserOut();
setState({ userData: null });
};
const authOrigin = getAuthOrigin();
useEffect(() => {
if (userSession.isUserSignedIn()) {
const userData = userSession.loadUserData();
setState({ userData });
}
}, []);
const handleRedirectAuth = async () => {
if (userSession.isSignInPending()) {
const userData = await userSession.handlePendingSignIn();
setState(() => userData);
setState({ userData });
}
};
@@ -100,11 +44,13 @@ export const App: React.FC = () => {
const authOptions: AuthOptions = {
manifestPath: '/static/manifest.json',
redirectTo: '/',
userSession,
finished: ({ userSession, authResponse }) => {
const userData = userSession.loadUserData();
setState(() => userData);
// setState(() => userData);
setAppPrivateKey(userSession.loadUserData().appPrivateKey);
setAuthResponse(authResponse);
setState({ userData });
console.log(userData);
},
onCancel: () => {
@@ -117,31 +63,20 @@ export const App: React.FC = () => {
},
};
const handleSignOut = () => {
setState({});
};
const isSignedIn = (state && state.identityAddress) || undefined;
return (
<Connect authOptions={authOptions}>
<ThemeProvider theme={theme}>
<CSSReset />
<Flex
direction="column"
height="100vh"
width="100vw"
align="center"
justify="center"
bg="whitesmoke"
>
{authResponse && <input type="hidden" id="auth-response" value={authResponse} />}
{appPrivateKey && <input type="hidden" id="app-private-key" value={appPrivateKey} />}
<AppContext.Provider value={state}>
<CSSReset />
<Flex direction="column" minHeight="100vh" bg="white">
{authResponse && <input type="hidden" id="auth-response" value={authResponse} />}
{appPrivateKey && <input type="hidden" id="app-private-key" value={appPrivateKey} />}
{!isSignedIn ? (
<AppContent />
) : (
<SignedIn handleSignOut={handleSignOut} username={state.username} />
)}
</Flex>
<Header signOut={signOut} />
<Home />
</Flex>
</AppContext.Provider>
</ThemeProvider>
</Connect>
);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Button, Text, Box, space, ButtonGroup } from '@blockstack/ui';
import { useConnect } from '@blockstack/connect';
export const Auth: React.FC = () => {
const { doOpenAuth } = useConnect();
return (
<Box>
<Text display="block" textStyle="body.large">
Sign in with your Blockstack account to try the demo
</Text>
<ButtonGroup spacing={space('base')} mt={space('base-loose')}>
<Button size="lg" onClick={() => doOpenAuth(true)} data-test="sign-in">
Sign in
</Button>
<Button size="lg" mode="tertiary" onClick={() => doOpenAuth()} data-test="sign-up">
Sign up
</Button>
</ButtonGroup>
</Box>
);
};

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Button, ButtonGroup, Box, Text } from '@blockstack/ui';
import { AppContext } from '@common/context';
import { getAuthOrigin, getRPCClient } from '@common/utils';
import { useConnect } from '@blockstack/connect';
import { deserializeCV, IntCV } from '@blockstack/stacks-transactions';
import { ExplorerLink } from '@components/explorer-link';
export const CounterActions: React.FC = () => {
const { userData } = React.useContext(AppContext);
const [loading, setLoading] = React.useState(false);
const [txId, setTxId] = React.useState('');
const [counter, setCounter] = React.useState<number | null>(null);
const [error, setError] = React.useState('');
const { doContractCall } = useConnect();
const callMethod = async (method: string) => {
setError('');
setLoading(true);
const authOrigin = getAuthOrigin();
await doContractCall({
authOrigin,
contractAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
functionName: method,
functionArgs: [],
contractName: 'counter',
finished: data => {
setTxId(data.txId);
console.log('finished!', data);
setLoading(false);
},
});
};
const getCounter = async () => {
const client = getRPCClient();
setLoading(true);
setError('');
try {
const data = await client.callReadOnly({
contractName: 'counter',
contractAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
args: [],
functionName: 'get-counter',
});
const cv = deserializeCV(Buffer.from(data.result.slice(2), 'hex')) as IntCV;
console.log(cv.value);
setCounter(cv.value.toNumber());
setLoading(false);
} catch (error) {
setError('Unable to get current counter value.');
}
};
return (
<Box>
{!userData && <Text display="block">Log in to change the state of this smart contract.</Text>}
<ButtonGroup spacing={4} my={5}>
<Button mt={3} onClick={() => callMethod('increment')}>
Increase by 1
</Button>
<Button mt={3} onClick={() => callMethod('decrement')}>
Decrease by 1
</Button>
<Button mt={3} onClick={getCounter}>
Get current value
</Button>
</ButtonGroup>
{error && (
<Text display="block" color="red">
{error}
</Text>
)}
{txId && !loading && <ExplorerLink txId={txId} />}
{counter !== null && !loading && (
<Text display="block">Current counter value: {counter}</Text>
)}
</Box>
);
};

View File

@@ -0,0 +1,56 @@
import React, { useEffect, useState } from 'react';
import { space, Box, Text, Flex } from '@blockstack/ui';
import { ExplorerLink } from './explorer-link';
import { CounterActions } from './counter-actions';
import { getRPCClient } from '@common/utils';
import { ContractCallTransaction } from '@blockstack/stacks-blockchain-sidecar-types';
import { TxCard } from '@components/tx-card';
export const Counter = () => {
const [transactions, setTransactions] = useState<ContractCallTransaction[]>([]);
const client = getRPCClient();
useEffect(() => {
const getTransactions = async () => {
const transactions = await client.fetchAddressTransactions({
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.counter',
});
const filtered = transactions.filter(t => {
return t.tx_type === 'contract_call';
});
setTransactions(filtered as ContractCallTransaction[]);
};
void getTransactions();
}, []);
return (
<Box py={6}>
<Text as="h2" textStyle="display.small">
Counter smart contract
</Text>
<Text textStyle="body.large" display="block" my={space('loose')}>
Try a smart contract that keeps a single "counter" state variable. The public methods
"increment" and "decrement" change the value of the counter.
</Text>
<ExplorerLink
txId="STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.counter"
text="View contract in explorer"
skipConfirmCheck
/>
{transactions.length > 0 && (
<>
<Text display="block" my={space('base-loose')} textStyle="body.large.medium">
Latest changes
</Text>
<Flex flexWrap="wrap" justifyContent="left">
{transactions.slice(0, 3).map(t => (
<TxCard tx={t} label={t.contract_call.function_name === 'increment' ? '+1' : '-1'} />
))}
</Flex>
</>
)}
<CounterActions />
</Box>
);
};

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { getAuthOrigin } from '@common/utils';
import { useConnect } from '@blockstack/connect';
import { SampleContracts } from '@common/contracts';
import { Box, Button, Text } from '@blockstack/ui';
import { ExplorerLink } from './explorer-link';
export const Deploy = () => {
const [tx, setTx] = React.useState('');
const authOrigin = getAuthOrigin();
const { doContractDeploy, userSession } = useConnect();
const handleSubmit = () =>
doContractDeploy({
authOrigin,
codeBody: SampleContracts[0].contractSource,
contractName: SampleContracts[0].contractName,
userSession,
finished: data => {
setTx(data.txId);
console.log('finished!', data);
},
});
return (
<Box mb={6} maxWidth="600px" mt={6}>
<Text as="h2" fontSize={5} mt={6}>
Contract Deploy
</Text>
<Text display="block" my={4} textStyle="caption.medium">
Deploy a Clarity smart contract. To keep things simple, we'll provide a contract for you.
</Text>
{tx && <ExplorerLink txId={tx} />}
<Box mt={4}>
<Button onClick={handleSubmit}>Deploy</Button>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Link } from '@blockstack/connect';
import { Box } from '@blockstack/ui';
interface LinkProps {
txId: string;
text?: string;
skipConfirmCheck?: boolean;
}
export const ExplorerLink: React.FC<LinkProps> = ({ txId, text }) => {
let id = txId.replace('"', '');
if (!id.startsWith('0x') && !id.includes('.')) {
id = `0x${id}`;
}
const url = `https://testnet-explorer.blockstack.org/txid/${id}`;
return (
<Box>
<Link onClick={() => window.open(url, '_blank')} color="blue" display="inline-block" my={3}>
{text || 'View transaction in explorer'}
</Link>
</Box>
);
};

View File

@@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { Flex, Box, Button, Input, Text } from '@blockstack/ui';
import { getRPCClient } from '@common/utils';
import { ExplorerLink } from './explorer-link';
interface FaucetResponse {
txId?: string;
success: boolean;
}
export const Faucet = ({ address: _address = '' }: { address: string }) => {
const [address, setAddress] = useState(_address);
const [tx, setTX] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const client = getRPCClient();
const handleInput = (evt: React.FormEvent<HTMLInputElement>) => {
setAddress(evt.currentTarget.value || '');
};
const getServerURL = () => {
const { origin } = location;
if (origin.includes('localhost')) {
return 'http://localhost:3999';
}
return 'https://sidecar.staging.blockstack.xyz';
};
const waitForBalance = async (currentBalance: number, attempts: number) => {
const { balance } = await client.fetchAccount(address);
if (attempts > 18) {
setError(
"It looks like your transaction still isn't confirmed after a few minutes. Something may have gone wrong."
);
setLoading(false);
}
if (balance.toNumber() > currentBalance) {
setLoading(false);
setSuccess(true);
return;
}
setTimeout(() => {
waitForBalance(currentBalance, attempts + 1);
}, 10000);
};
const onSubmit = async () => {
setLoading(true);
setError('');
setTX('');
try {
const url = `${getServerURL()}/sidecar/v1/debug/faucet?address=${address}`;
const res = await fetch(url, {
method: 'POST',
});
const data: FaucetResponse = await res.json();
console.log(data);
if (data.txId) {
setTX(data.txId);
const { balance } = await client.fetchAccount(address);
await waitForBalance(balance.toNumber(), 0);
} else {
setError('Something went wrong when requesting the faucet.');
}
} catch (e) {
setError('Something went wrong when requesting the faucet.');
setLoading(false);
setTX('');
console.error(e.message);
}
};
return (
<Box mb={6} maxWidth="600px" mt={6}>
<Text as="h2" fontSize={5} mt={6}>
Faucet
</Text>
<Text display="block" my={4} textStyle="caption.medium">
Receive some free testnet STX for testing out the network. STX are required to execute smart
contract functions.
</Text>
{tx && <ExplorerLink txId={tx} />}
{error && (
<Text display="inline-block" my={1} fontSize={1} color="red">
{error}
</Text>
)}
<Flex wrap="wrap">
<Box width="100%">
<Input
type="text"
placeholder="Address"
textStyle="body.small"
value={address}
onChange={handleInput}
name="address"
/>
</Box>
<Box width="100%" mt={3}>
<Button
isLoading={loading}
loadingText=" Waiting for TX to Confirm"
isDisabled={success}
onClick={onSubmit}
>
{success ? 'Faucet TX Confirmed' : 'Receive Testnet STX'}
</Button>
</Box>
</Flex>
</Box>
);
};

View File

@@ -0,0 +1,38 @@
import React, { useContext } from 'react';
import { Flex, Box, Text } from '@blockstack/ui';
import { BlockstackIcon } from '@blockstack/ui';
import { AppContext } from '@common/context';
import { Link } from '@blockstack/connect';
interface HeaderProps {
signOut: () => void;
}
export const Header: React.FC<HeaderProps> = ({ signOut }) => {
const state = useContext(AppContext);
return (
<Flex as="nav" justifyContent="space-between" alignItems="center" height="64px" px={6}>
<Box verticalAlign="center">
<BlockstackIcon maxHeight="26px" display="inline-block" ml="-10px" />
<Text display="inline-block" ml={1}>
Blockstack
</Text>
</Box>
{state.userData ? (
<Box>
<Link
display="inline-block"
ml={2}
textStyle="caption.medium"
color="blue"
onClick={() => {
signOut();
}}
>
Sign out
</Link>
</Box>
) : null}
</Flex>
);
};

View File

@@ -0,0 +1,81 @@
import React, { useContext, useState } from 'react';
import { AppContext } from '@common/context';
import { Box, Spinner, Text, Flex, space, BoxProps } from '@blockstack/ui';
import { Auth } from './auth';
import { Tab } from './tab';
import { Status } from './status';
import { Counter } from './counter';
import { useFaucet } from '@common/use-faucet';
import { ExplorerLink } from './explorer-link';
type Tabs = 'status' | 'counter';
const Container: React.FC<BoxProps> = ({ children, ...props }) => {
return (
<Box width="100%" px={6} {...props}>
<Box maxWidth="900px" mx="auto">
{children}
</Box>
</Box>
);
};
export const Home: React.FC = () => {
const state = useContext(AppContext);
const [tab, setTab] = useState<Tabs>('status');
const faucet = useFaucet();
const Page: React.FC = () => {
if (faucet.loading) {
return (
<Container px={6} width="100%">
<Spinner mr={4} />
<Text textStyle="body.large">Setting up...</Text>
</Container>
);
}
if (faucet.balance === 0) {
return (
<Container px={6} width="100%">
<Spinner mr={4} />
<Text textStyle="body.large" display="block">
Please wait while we get you some Stacks tokens to test with
</Text>
<ExplorerLink txId={faucet.txId} />
</Container>
);
}
return (
<>
<Container borderColor="#F0F0F5" borderWidth={0} borderBottomWidth="1px">
<Flex>
<Tab active={tab === 'status'}>
<Text onClick={() => setTab('status')}>Status smart contract</Text>
</Tab>
<Tab active={tab === 'counter'}>
<Text onClick={() => setTab('counter')}>Counter smart contract</Text>
</Tab>
</Flex>
</Container>
<Container>{tab === 'status' ? <Status /> : <Counter />}</Container>
</>
);
};
return (
<Flex flexWrap="wrap">
<Container mt={space('base-loose')}>
<Text as="h1" textStyle="display.large" fontSize={7} mb={space('loose')} display="block">
Testnet Demo
</Text>
</Container>
{state.userData ? (
<Page />
) : (
<Container>
<Auth />
</Container>
)}
</Flex>
);
};

View File

@@ -0,0 +1,203 @@
import React, { useState, useEffect } from 'react';
import { space, Box, Text, Button, Input, Flex } from '@blockstack/ui';
import { ExplorerLink } from './explorer-link';
import { useConnect } from '@blockstack/connect';
import {
PostConditionMode,
standardPrincipalCV,
BufferCV,
deserializeCV,
ClarityType,
bufferCV,
} from '@blockstack/stacks-transactions';
import { getAuthOrigin, getRPCClient } from '@common/utils';
import { ContractCallTransaction } from '@blockstack/stacks-blockchain-sidecar-types';
import { TxCard } from '@components/tx-card';
import { useSTXAddress } from '@common/use-stx-address';
export const Status = () => {
const stxAddress = useSTXAddress();
const [status, setStatus] = useState('');
const [readStatus, setReadStatus] = useState('');
const [address, setAddress] = useState('');
const [txId, setTxId] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [transactions, setTransactions] = useState<ContractCallTransaction[]>([]);
const { doContractCall } = useConnect();
const client = getRPCClient();
useEffect(() => {
const getTransactions = async () => {
const transactions = await client.fetchAddressTransactions({
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.status',
});
const filtered = transactions.filter(t => {
return t.tx_type === 'contract_call';
});
setTransactions(filtered as ContractCallTransaction[]);
};
void getTransactions();
}, []);
const getAddressCV = () => {
try {
return standardPrincipalCV(address);
} catch (error) {
setError('Invalid address.');
return null;
}
};
const onSubmitRead = async () => {
const addressCV = getAddressCV();
if (!addressCV) {
return;
}
const args = [addressCV];
setLoading(true);
try {
const data = await client.callReadOnly({
contractName: 'status',
contractAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
args,
functionName: 'get-status',
});
console.log(data);
const cv = deserializeCV(Buffer.from(data.result.slice(2), 'hex')) as BufferCV;
console.log(cv);
if (cv.type === ClarityType.Buffer) {
const ua = Array.from(cv.buffer);
const str = String.fromCharCode.apply(null, ua);
setReadStatus(str);
console.log(str);
}
} catch (error) {
setError('An error occurred while fetching the status contract.');
}
setLoading(false);
};
const onSubmitWrite = async () => {
const authOrigin = getAuthOrigin();
const statusArg = bufferCV(Buffer.from(status));
await doContractCall({
authOrigin,
contractAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
functionName: 'write-status!',
functionArgs: [statusArg],
contractName: 'status',
finished: data => {
setTxId(data.txId);
console.log('finished!', data);
},
postConditionMode: PostConditionMode.Deny,
});
};
const handleStatus = (evt: React.FormEvent<HTMLInputElement>) => {
setStatus(evt.currentTarget.value || '');
};
const handleAddress = (evt: React.FormEvent<HTMLInputElement>) => {
setAddress(evt.currentTarget.value || '');
};
return (
<Box py={6}>
<Text as="h2" textStyle="display.small">
Status smart contract
</Text>
<Text textStyle="body.large" display="block" my={space('loose')}>
Try a smart contract where anyone can write their public status, like a decentralized
Twitter. You can read someone else's status by entering their address.
</Text>
<ExplorerLink
txId="STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.status"
text="View contract in explorer"
skipConfirmCheck
/>
{transactions.length > 0 && (
<>
<Text display="block" my={space('base-loose')} textStyle="body.large.medium">
Latest statuses
</Text>
<Flex flexWrap="wrap" justifyContent="left">
{transactions.slice(0, 3).map(t => (
<TxCard tx={t} label={JSON.parse(t.tx_result?.repr || '')} />
))}
</Flex>
</>
)}
<Text display="block" my={space('base-loose')} textStyle="body.large.medium">
Write a status
</Text>
<Box width="100%" mt={3}>
<Input
type="text"
placeholder="Enter your status"
textStyle="body.small"
value={status}
onChange={handleStatus}
name="status"
maxWidth="300px"
onKeyUp={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'enter') {
onSubmitWrite();
}
}}
/>
</Box>
{txId && <ExplorerLink txId={txId} />}
<Button my={space('base-loose')} onClick={onSubmitWrite}>
Write status
</Button>
<Text display="block" my={space('base-loose')} textStyle="body.large.medium">
Read a status
</Text>
{stxAddress && (
<Text display="block" my={space('base-loose')} textStyle="body.small">
If you want to read your own status, your address is {stxAddress}.
</Text>
)}
<Box width="100%" mt={3}>
<Input
type="text"
placeholder="Enter an STX address"
textStyle="body.small"
value={address}
onChange={handleAddress}
name="status"
maxWidth="300px"
onKeyUp={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'enter') {
onSubmitRead();
}
}}
/>
</Box>
{readStatus && (
<Text display="block" fontWeight={600} my={3} width="100%">
{readStatus}
</Text>
)}
{error && (
<Text display="block" color="red" width="100%" fontSize={1} mt={2}>
{error}
</Text>
)}
<Button my={space('base-loose')} onClick={onSubmitRead} isLoading={loading}>
Read status
</Button>
</Box>
);
};

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { Flex, Box, Button, Input, Text } from '@blockstack/ui';
import { useConnect } from '@blockstack/connect';
import { ExplorerLink } from '@components/explorer-link';
import { getAuthOrigin } from '@common/utils';
export const STXTransfer = () => {
const [address, setAddress] = useState('');
const [tx, setTX] = useState('');
const [error, setError] = useState('');
const { doSTXTransfer } = useConnect();
const handleInput = (evt: React.FormEvent<HTMLInputElement>) => {
setAddress(evt.currentTarget.value || '');
};
const onSubmit = () => {
setError('');
setTX('');
doSTXTransfer({
recipient: address,
amount: '100',
memo: 'Testing STX Transfer',
authOrigin: getAuthOrigin(),
finished: data => {
setTX(data.txId);
},
});
};
return (
<Box mb={6} maxWidth="600px" mt={6}>
<Text as="h2" fontSize={5} mt={6}>
STX Transfer
</Text>
<Text display="block" my={4} textStyle="caption.medium">
Send a small amount of STX to a different user.
</Text>
{tx && <ExplorerLink txId={tx} />}
{error && (
<Text display="inline-block" my={1} fontSize={1} color="red">
{error}
</Text>
)}
<Flex wrap="wrap">
<Box width="100%">
<Input
type="text"
placeholder="Address"
textStyle="body.small"
value={address}
onChange={handleInput}
name="address"
/>
</Box>
<Box width="100%" mt={3}>
<Button onClick={onSubmit}>Submit</Button>
</Box>
</Flex>
</Box>
);
};

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Box, BoxProps } from '@blockstack/ui';
export const InactiveTab: React.FC = ({ children }) => {
return (
<Box color="ink.400" mr={6} py={3} borderColor="white" borderBottomWidth="2px" cursor="pointer">
{children}
</Box>
);
};
export const ActiveTab: React.FC = ({ children }) => {
return (
<Box color="ink.900" mr={6} py={3} borderColor="blue" borderBottomWidth="2px" cursor="pointer">
{children}
</Box>
);
};
interface TabProps extends BoxProps {
active: boolean;
}
export const Tab: React.FC<TabProps> = ({ active, ...rest }) => {
if (active) {
return <ActiveTab {...rest} />;
}
return <InactiveTab {...rest} />;
};

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { Box, Flex, Text } from '@blockstack/ui';
import { ContractCallTransaction } from '@blockstack/stacks-blockchain-sidecar-types';
import { toRelativeTime } from '@common/utils';
interface TxCardProps {
tx: ContractCallTransaction;
label: string;
}
export const TxCard: React.FC<TxCardProps> = ({ tx, label }) => {
const addr = tx.sender_address;
const shortAddr = `${addr.slice(0, 5)}...${addr.slice(addr.length - 6, addr.length - 1)}`;
return (
<Box
flex="0 1 280px"
mr="10px"
mt={3}
borderColor="#F0F0F5"
borderWidth="1px"
borderRadius="12px"
p={6}
_hover={{
borderColor: 'ink.400',
cursor: 'pointer',
}}
onClick={() => {
const url = `https://testnet-explorer.blockstack.org/txid/${tx.tx_id}`;
window.open(url, '_blank');
}}
>
<Flex>
<Box>
<Text color="ink.600">{shortAddr}</Text>
</Box>
<Box flexGrow={1} textAlign="right">
<Text color="ink.600">{toRelativeTime(tx.burn_block_time * 1000)}</Text>
</Box>
</Flex>
<Text display="block" textStyle="body.large" mt={3}>
{label}
</Text>
</Box>
);
};

View File

@@ -0,0 +1,16 @@
(define-data-var counter int 0)
(define-public (increment)
(begin
(var-set counter (+ (var-get counter) 1))
(print "+1")
(ok (var-get counter))))
(define-public (decrement)
(begin
(var-set counter (- (var-get counter) 1))
(print "-1")
(ok (var-get counter))))
(define-read-only (get-counter)
(var-get counter))

View File

@@ -0,0 +1,31 @@
(define-map statuses
(
(author principal)
)
(
(status (buff 512))
)
)
(define-read-only (get-status (author principal))
(begin
(print author)
(default-to ""
(get status (map-get? statuses {author: author}))
)
)
)
(define-public (write-status!
(status (buff 512))
)
(begin
(print tx-sender)
(print status)
(map-set statuses
((author tx-sender))
((status status))
)
(ok status)
)
)

View File

@@ -0,0 +1,51 @@
;; hello world
(define-public (say-hi)
(ok "hello world")
)
(define-public (echo-number (val int))
(ok val)
)
;; streams
(define-map streams
((id uint))
(
(start-block uint)
(recipient principal)
(sender principal)
)
)
(define-data-var next-stream-id uint u1)
(define-public (make-stream
(recipient principal)
)
(begin
(map-set streams
((id (var-get next-stream-id)))
(
(start-block block-height)
(recipient recipient)
(sender tx-sender)
)
)
(var-set next-stream-id (+ (var-get next-stream-id) u1))
(ok 'true)
)
)
(define-public (get-stream
(stream-id uint)
)
(
ok
(unwrap-panic
(map-get? streams (tuple (id stream-id)))
)
)
)

View File

@@ -0,0 +1,185 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// Respect "browser" field in package.json when resolving modules
// browser: false,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\Boobalay\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
// The directory where Jest should output its coverage files
// coverageDirectory: null,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,
// A path to a custom dependency extractor
// dependencyExtractor: null,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files usin a array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: null,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: null,
// A set of global variables that need to be available in all test environments
globals: {
'ts-jest': {
// https://huafu.github.io/ts-jest/user/config/diagnostics
diagnostics: false,
},
},
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node', 'clar'],
// A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: {
'^@blockstack/keychain': '<rootDir>/../keychain/src',
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: null,
// Run tests from one or more projects
// projects: null,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: null,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: null,
// A list of paths to directories that Jest should use to search for files in
roots: ['<rootDir>/tests', '<rootDir>/src', '<rootDir>/contracts'],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: 'jsdom',
preset: 'ts-jest',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/tests/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
testRegex: '/tests/.*.test.tsx?$',
// This option allows the use of a custom results processor
// testResultsProcessor: null,
// This option allows use of a custom test runner
testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
transform: { '^.+\\.tsx?$': 'ts-jest' },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: null,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

View File

@@ -27,12 +27,14 @@
"@blockstack/keychain": "^0.7.5",
"@blockstack/prettier-config": "^0.0.6",
"@blockstack/ui": "^2.9.5",
"@blockstack/clarity": "0.1.16",
"@blockstack/rpc-client": "^0.3.0-alpha.0",
"@rehooks/document-title": "^1.0.1",
"blockstack": "^21.0.0",
"blockstack": "21.0.0",
"dayjs": "^1.8.29",
"formik": "^2.1.4",
"mdi-react": "^7.0.0",
"preact": "^10.4.0",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-chrome-redux": "^2.0.0-alpha.5",
"react-dom": "^16.13.1",
@@ -57,6 +59,9 @@
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@blockstack/clarity-native-bin": "0.1.13-alpha.6",
"@blockstack/prettier-config": "^0.0.6",
"@blockstack/stacks-blockchain-sidecar-types": "^0.0.19",
"@pmmmwh/react-refresh-webpack-plugin": "^0.2.0",
"@schemastore/web-manifest": "^0.0.4",
"@types/chrome": "^0.0.104",
@@ -77,19 +82,19 @@
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^7.0.0",
"eslint": "^6.7.2",
"dotenv": "^8.2.0",
"eslint": "^7.0.0",
"fork-ts-checker-webpack-plugin": "^4.1.3",
"html-webpack-plugin": "^4.2.0",
"jest": "^25.3.0",
"jest-puppeteer": "^4.3.0",
"prettier": "^2.0.4",
"prettier": "^2.0.5",
"puppeteer": "^3.0.0",
"react-dev-utils": "^10.2.1",
"react-test-renderer": "^16.13.1",
"terser-webpack-plugin": "^2.3.5",
"ts-jest": "^25.3.1",
"ts-loader": "^7.0.0",
"typescript": "3.8.3",
"webpack": "^4.42.1",
"webpack-bundle-analyzer": "^3.7.0",
"webpack-chrome-extension-reloader": "^1.3.0",

View File

@@ -0,0 +1,86 @@
import { Client, Provider, ProviderRegistry, Result, ResultInterface } from '@blockstack/clarity';
function unwrapOkResult(input: ResultInterface<string, unknown>): string {
const { result } = input;
if (!result) {
throw new Error('Unable to parse result');
}
const match = /^\(ok\s0x(\w+)\)$/.exec(result);
// const res = match[1];
if (!match) {
throw new Error('Unable to parse result');
}
return Buffer.from(match[1], 'hex').toString();
}
describe('hello world contract test suite', () => {
let helloWorldClient: Client;
let provider: Provider;
const addresses = [
'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
'S02J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKPVKG2CE',
'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR',
];
// const alice = addresses[0];
const [alice, bob] = addresses;
beforeEach(async () => {
provider = await ProviderRegistry.createProvider();
helloWorldClient = new Client(
'SP3GWX3NE58KXHESRYE4DYQ1S31PQJTCRXB3PE9SB.stream',
'stream',
provider
);
});
it('should have a valid syntax', async () => {
await expect(helloWorldClient.checkContract()).resolves.not.toThrow();
});
describe('deploying an instance of the contract', () => {
beforeEach(async () => {
await helloWorldClient.deployContract();
});
it('should hello world', async () => {
const query = helloWorldClient.createQuery({
method: {
name: 'say-hi',
args: [],
},
});
const receipt = await helloWorldClient.submitQuery(query);
const result = unwrapOkResult(receipt);
expect(result).toEqual('hello world');
});
it('should create a stream', async () => {
const tx = helloWorldClient.createTransaction({
method: {
name: 'make-stream',
args: [`'${bob}`],
},
});
await tx.sign(alice);
const receipt = await helloWorldClient.submitTransaction(tx);
const result = Result.unwrap(receipt);
expect(result).toBeTruthy();
const query = helloWorldClient.createQuery({
method: {
name: 'get-stream',
args: ['u1'],
},
});
const queryReceipt = await helloWorldClient.submitQuery(query);
const queryResult = Result.unwrap(queryReceipt);
// console.log(queryResult);
expect(queryResult).toEqual(
`(ok (tuple (recipient '${bob}) (sender '${alice}) (start-block u2)))`
);
});
});
afterAll(async () => {
await provider.close();
});
});

View File

@@ -46,14 +46,16 @@
"@store/*": ["store/*"],
"@store": ["store/index"],
"@dev/*": ["dev/*"],
"@components/*": ["components/*"],
"@components/*": ["../components/*"],
"@containers/*": ["containers/*"],
"@common/*": ["common/*"],
"@common/*": ["../common/*"],
"@blockstack/connect": ["../../connect/src"],
"@blockstack/connect/*": ["../../connect/src/*"],
"@blockstack/ui": ["../../ui/src"],
"@blockstack/keychain": ["../../keychain/src"],
"@blockstack/keychain/*": ["../../keychain/src/*"],
"@blockstack/keychain/*": ["../../keychain/src/*"],
"@cards/*": ["../components/cards/*"],
"@blockstack/rpc-client": ["../../rpc-client/src"]
},
"baseUrl": "src",
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
@@ -75,6 +77,8 @@
},
"include": [
"./src/**/*",
"./tests/**/*"
"./tests/**/*",
"./components/**/*",
"./common/**/*"
]
}

View File

@@ -189,11 +189,6 @@ module.exports = {
to: path.join(distRootPath, 'assets'),
test: /\.(jpg|jpeg|png|gif|svg)?$/,
},
{
from: path.join(sourceRootPath, 'manifest.json'),
to: path.join(distRootPath, 'manifest.json'),
toType: 'file',
},
]),
new webpack.DefinePlugin({
'process.env.AUTH_ORIGIN': JSON.stringify(process.env.AUTH_ORIGIN),

View File

@@ -69,8 +69,7 @@
"ts-node": "^8.8.2",
"tsdx": "^0.13.2",
"tslint": "^6.1.1",
"tslint-config-prettier": "^1.18.0",
"typescript": "^3.7.5"
"tslint-config-prettier": "^1.18.0"
},
"files": [
"dist"

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import { Svg } from '../svg';
import { BoxProps } from '../box';
export const ExternalIcon = (props: BoxProps) => (
<Svg width="8" height="9" viewBox="0 0 8 9" fill="none" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.30849 0H7.04391H7.99975H7.99981V6.6919H7.04391V1.66447L0.707141 8.00185L0 7.29477L6.33818 0.955986H1.30849V0Z"
fill="#677282"
/>
</Svg>
);

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import { Svg } from '../svg';
import { BoxProps } from '../box';
export const FailedIcon: React.FC<BoxProps> = ({ size = 64, ...props }) => (
<Svg width={size} height={size} viewBox="0 0 64 64" fill="none" {...props}>
<circle cx="32" cy="32" r="30" stroke="#D4001A" strokeWidth="4" strokeLinecap="round" />
<path
d="M23 41L40.9995 23"
stroke="#D4001A"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M41 41L23.0005 23"
stroke="#D4001A"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);

View File

@@ -15,3 +15,5 @@ export * from './plus-circle-icon';
export * from './private-icon';
export * from './union-line-icon';
export * from './close-icon';
export * from './external-icon';
export * from './failed-icon';

View File

@@ -63,7 +63,9 @@ export function Tooltip(props: TooltipProps) {
const baseTooltipProps = getTooltipProps(rest);
const tooltipProps = hasAriaLabel ? omit(baseTooltipProps, ['role', 'id']) : baseTooltipProps;
const { style, ...tooltipProps } = hasAriaLabel
? omit(baseTooltipProps, ['role', 'id'])
: baseTooltipProps;
const hiddenProps = pick(baseTooltipProps, ['role', 'id']);
@@ -82,7 +84,7 @@ export function Tooltip(props: TooltipProps) {
maxWidth="320px"
{...tooltipProps}
style={{
...tooltipProps.style,
...style,
useSelect: 'none',
}}
>

598
yarn.lock
View File

@@ -1598,6 +1598,21 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blockstack/clarity-native-bin@0.1.13-alpha.6":
version "0.1.13-alpha.6"
resolved "https://registry.yarnpkg.com/@blockstack/clarity-native-bin/-/clarity-native-bin-0.1.13-alpha.6.tgz#f2d8c905d4b2b6aac561aa4cf16159ad7747b214"
integrity sha512-iQeSn5/UmiRGz631Hqf+oA8x77++eIqzjHDgZT40GNsuFEduYwUnxQbsRqq7q0yJpkemcqd9bUNLv33qQRGoXA==
dependencies:
fs-extra "^8.0.1"
node-fetch "^2.6.0"
semver "^6.1.1"
tar "^4.4.8"
"@blockstack/clarity@0.1.16":
version "0.1.16"
resolved "https://registry.yarnpkg.com/@blockstack/clarity/-/clarity-0.1.16.tgz#75483365bc28d25eabb6b95df4ebafa5f67954fb"
integrity sha512-3XuPsQCz+wfSQHLVGhdfgiCVsZh2mkSZpDiV+TguoYOWtzAOZJQSoL0CdAkri0rDe5r+AM4s57lpukqgraRTaA==
"@blockstack/eslint-config@^1.0.3", "@blockstack/eslint-config@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@blockstack/eslint-config/-/eslint-config-1.0.5.tgz#a745661123c5e6c2682b5eb7400a4bc72390959d"
@@ -1632,10 +1647,15 @@
resolved "https://registry.yarnpkg.com/@blockstack/prettier-config/-/prettier-config-0.0.6.tgz#8a41cd378ba061b79770987f2a6ad0c92b64bd72"
integrity sha512-ke0MdyblmoUqSJBEutsG8/6G7KAjCB+uOcgZHPJvJr4R+i5yRhT4GSe5nV/wREINuK0jj2GvaA6qlx4PQTKQUA==
"@blockstack/stacks-transactions@^0.4.6":
version "0.4.6"
resolved "https://registry.yarnpkg.com/@blockstack/stacks-transactions/-/stacks-transactions-0.4.6.tgz#b774250fbaadbadf42313a82fbd628b1728cd6ed"
integrity sha512-3Hb+v0ZmG5bVZHasfM9KzlwK+2e5r6oKsKk0eRgavLb5bBMQy/cw0YYoUWmt+ipNqDP5ssZgoOA1KKRhoSWvXg==
"@blockstack/stacks-blockchain-sidecar-types@^0.0.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@blockstack/stacks-blockchain-sidecar-types/-/stacks-blockchain-sidecar-types-0.0.19.tgz#569ed8e94a00c62b1bbfec806d2e06cade9ef464"
integrity sha512-xuXw+TOzxqC/Bcj2Ob7Svp5ukDaKVuoV5OiNDaC3mF4rkt+HvKxj1loGmPrsIT0mbsjHTf0pxfHuEZvvugZdQw==
"@blockstack/stacks-transactions@0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@blockstack/stacks-transactions/-/stacks-transactions-0.5.1.tgz#b58057d52567406ce26669ba0abcaf583cf2d21c"
integrity sha512-szzDyRBHnPDE7hoqm6TJbKvUPE5aHRapS4gqiDetOwZzq/aDqIrFVHSgU1GM7m1d2q2jlN1knW4IiRJUeCxw0Q==
dependencies:
"@types/bn.js" "^4.11.6"
"@types/elliptic" "^6.4.12"
@@ -2255,6 +2275,16 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"
"@jest/types@^25.2.6":
version "25.2.6"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.2.6.tgz#c12f44af9bed444438091e4b59e7ed05f8659cb6"
integrity sha512-myJTTV37bxK7+3NgKc4Y/DlQ5q92/NOwZsZ+Uch7OXdElxOg61QYc72fPYNAjlvbnJ2YvbXLamIsa9tj48BmyQ==
dependencies:
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
"@jest/types@^25.5.0":
version "25.5.0"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d"
@@ -3208,12 +3238,12 @@
tslib "^2.0.0"
"@reach/auto-id@^0.10.0":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.10.4.tgz#ba9c0c96c3fd037f2d3c70e9e2e6a784a83abfab"
integrity sha512-hJIjqOBIYdIdrjefWYfuBAntrUSP2sRp5jj3rJNSXW/Txhv6NUhfk5z5Xwsi6HST9rBS15btM8YaQBvJYicZsQ==
version "0.10.1"
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.10.1.tgz#5c8f7573d6271e0e55df04e459a1d1d1cf11a216"
integrity sha512-xGFW2v+L39M/mafdW7v+NhhsjT1LBnQJCGj64dm37T4IGNgAexlfMkRRwsqHOvuVvV38mR114YOy0xrlkqduRQ==
dependencies:
"@reach/utils" "0.10.4"
tslib "^2.0.0"
"@reach/utils" "^0.10.1"
tslib "^1.11.1"
"@reach/observe-rect@1.1.0":
version "1.1.0"
@@ -3284,6 +3314,15 @@
tslib "^2.0.0"
warning "^4.0.3"
"@reach/utils@^0.10.1":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.10.1.tgz#ee0283f81e161db4126a943b5c908a69f6a66d7e"
integrity sha512-YzwZWVK+rSiUATNVtK7H2/ZkT/GhNKmkRjnj3hnVhSYLGxY9uQdfc+npetOqkh4hTAOXiErDa64ybVClR3h0TA==
dependencies:
"@types/warning" "^3.0.0"
tslib "^1.11.1"
warning "^4.0.3"
"@reach/visually-hidden@0.10.4":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.10.4.tgz#ab390db0adf759393af4d856f84375468b1df676"
@@ -3330,11 +3369,11 @@
resolve "^1.11.1"
"@rollup/plugin-node-resolve@^7.1.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca"
integrity sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q==
version "7.1.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.1.tgz#8c6e59c4b28baf9d223028d0e450e06a485bb2b7"
integrity sha512-14ddhD7TnemeHE97a4rLOhobfYvUVcaYuqTnL8Ti7Jxi9V9Jr5LY7Gko4HZ5k4h4vqQM0gBQt6tsp9xXW94WPA==
dependencies:
"@rollup/pluginutils" "^3.0.8"
"@rollup/pluginutils" "^3.0.6"
"@types/resolve" "0.0.8"
builtin-modules "^3.1.0"
is-module "^1.0.0"
@@ -3348,7 +3387,14 @@
"@rollup/pluginutils" "^3.0.8"
magic-string "^0.25.5"
"@rollup/pluginutils@^3.0.0", "@rollup/pluginutils@^3.0.8":
"@rollup/pluginutils@^3.0.0", "@rollup/pluginutils@^3.0.6":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.0.8.tgz#4e94d128d94b90699e517ef045422960d18c8fde"
integrity sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==
dependencies:
estree-walker "^1.0.1"
"@rollup/pluginutils@^3.0.8":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
@@ -3518,7 +3564,7 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/bn.js@*", "@types/bn.js@^4.11.5", "@types/bn.js@^4.11.6":
"@types/bn.js@*", "@types/bn.js@^4.11.6":
version "4.11.6"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==
@@ -3670,9 +3716,9 @@
"@types/istanbul-lib-report" "*"
"@types/jest-environment-puppeteer@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-4.3.1.tgz#597d16fa0594a5daf1b3c08262bfbb011e146c2d"
integrity sha512-e7G12WRw525gsiz7NW3tKY+YWyNR08r3QvyC31rzMrnn7CkyGbsiskBjnR3koY/6jwIASgxO5knm4xJQ1KtbQQ==
version "4.3.2"
resolved "https://registry.yarnpkg.com/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-4.3.2.tgz#630ad931d433b8197d29e0c6cb42a9faa91f591e"
integrity sha512-QVR49cGaQMOrWRN7CXlvtPMuVAxa3Z+W3APxhWoSQLG/lvz1y03ECPvS7Y9eK+hgfndK+39400rO6IifDJV9YA==
dependencies:
"@jest/environment" "^24"
"@jest/fake-timers" "^24"
@@ -3714,9 +3760,9 @@
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.149":
version "4.14.156"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.156.tgz#cbe30909c89a1feeb7c60803e785344ea0ec82d1"
integrity sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ==
version "4.14.149"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
"@types/mdast@^3.0.0":
version "3.0.3"
@@ -4006,19 +4052,10 @@
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=
"@types/webpack-sources@*":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.0.tgz#e58f1f05f87d39a5c64cf85705bdbdbb94d4d57e"
integrity sha512-c88dKrpSle9BtTqR6ifdaxu1Lvjsl3C5OsfvuUbUwdXymshv1TkufUAXBajCCUM/f/TmnkZC/Esb03MinzSiXQ==
dependencies:
"@types/node" "*"
"@types/source-list-map" "*"
source-map "^0.7.3"
"@types/webpack-sources@^0.1.5":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81"
integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==
"@types/webpack-sources@*", "@types/webpack-sources@^0.1.5":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.6.tgz#3d21dfc2ec0ad0c77758e79362426a9ba7d7cbcb"
integrity sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ==
dependencies:
"@types/node" "*"
"@types/source-list-map" "*"
@@ -5276,6 +5313,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==
binary-extensions@^1.0.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
@@ -5333,10 +5375,10 @@ bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.0:
resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278"
integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==
bitcoinjs-lib@^5.1.2, bitcoinjs-lib@^5.1.6:
version "5.1.10"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-5.1.10.tgz#91ec7cde308007008b22ed9117c5f26970cbad54"
integrity sha512-CesUqtBtnYc+SOMsYN9jWQWhdohW1MpklUkF7Ukn4HiAyN6yxykG+cIJogfRt6x5xcgH87K1Q+Mnoe/B+du1Iw==
bitcoinjs-lib@^5.1.6:
version "5.1.7"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-5.1.7.tgz#dfa023d6ad887eaef8249513d708c9ecd2673a08"
integrity sha512-sNlTQuvhaoIjOdIdyENsX74Dlikv7l6AzO0/uZQscuvfBID6aMANoCz1rooCTH5upTV5rKCj4z3BXBmXJxq23g==
dependencies:
bech32 "^1.1.2"
bip174 "^1.0.1"
@@ -5363,60 +5405,10 @@ bl@^4.0.1:
inherits "^2.0.4"
readable-stream "^3.4.0"
blockstack@21.0.0-alpha.2:
version "21.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/blockstack/-/blockstack-21.0.0-alpha.2.tgz#1f223387df24b5770d5e7ec4031df52b8014ca11"
integrity sha512-I7FQTYU78H/Eok/is0G79dSaQ7tD0DuKGKRCihHDdAKHYGeiTIZyHQ8fyK3NL8yEoRzRoIlCPEefCQgh/no6hg==
dependencies:
"@types/cheerio" "^0.22.13"
"@types/elliptic" "^6.4.10"
"@types/node" "^12.7.12"
"@types/randombytes" "^2.0.0"
ajv "^4.11.5"
bip39 "^3.0.2"
bitcoinjs-lib "^5.1.6"
bn.js "^4.11.8"
cross-fetch "^3.0.4"
elliptic "^6.5.1"
form-data "^2.5.1"
jsontokens "3.0.0-alpha.0"
query-string "^6.8.3"
randombytes "^2.1.0"
request "^2.88.0"
ripemd160-min "0.0.5"
schema-inspector "^1.6.8"
tslib "^1.10.0"
uuid "^3.3.3"
zone-file "^1.0.0"
blockstack@^19.3.0:
version "19.3.0"
resolved "https://registry.yarnpkg.com/blockstack/-/blockstack-19.3.0.tgz#19b3deefcb5555b65f55302326646db42b13e4a8"
integrity sha512-P/HRS5n+buTeIssxs1v479EpDZOFGpfiivRrv9UjbHj/FdSJLxC1onVD8Hiyfm0mB8y7Ah9qT2lGqKX9P6r7+g==
dependencies:
"@types/bn.js" "^4.11.5"
"@types/elliptic" "^6.4.9"
ajv "^4.11.5"
bip39 "^3.0.2"
bitcoinjs-lib "^5.1.2"
bn.js "^4.11.8"
cheerio "^0.22.0"
cross-fetch "^2.2.2"
elliptic "^6.4.1"
form-data "^2.3.3"
jsontokens "^2.0.2"
query-string "^6.3.0"
request "^2.88.0"
ripemd160 "^2.0.2"
schema-inspector "^1.6.8"
triplesec "^3.0.26"
uuid "^3.3.2"
zone-file "^1.0.0"
blockstack@^21.0.0:
version "21.1.0"
resolved "https://registry.yarnpkg.com/blockstack/-/blockstack-21.1.0.tgz#4c0b2678647f7c697efe98f50e24482a1dc0cf09"
integrity sha512-K3n161dRhDqBPzSe1gbg0+O7Xd5u00p6Ort2O+DAPMqs3aIy6XI/NhJgs7pestetL/iaGhaJ9EdDUHP4nU4zVQ==
blockstack@21.0.0:
version "21.0.0"
resolved "https://registry.yarnpkg.com/blockstack/-/blockstack-21.0.0.tgz#28dcf8ecf4878c0273697dee33d4585d5876bdc9"
integrity sha512-KH/eg3vlMZc93T4+rIjGwnSROqye/WOr46cDCrcnmkKdeUd2D2i/g5L6S9zTzIWQGQ7HWWLS1FZ9Xk8GcJdBKg==
dependencies:
"@types/bn.js" "^4.11.6"
"@types/cheerio" "^0.22.13"
@@ -5457,9 +5449,9 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.4.0:
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
bn.js@^5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0"
integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==
version "5.1.1"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.1.tgz#48efc4031a9c4041b9c99c6941d903463ab62eb5"
integrity sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==
body-parser@1.19.0:
version "1.19.0"
@@ -5634,7 +5626,7 @@ browserslist@4.10.0:
node-releases "^1.1.52"
pkg-up "^3.1.0"
browserslist@4.12.0, browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.12.0, browserslist@^4.8.5:
browserslist@4.12.0, browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d"
integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==
@@ -5653,15 +5645,14 @@ browserslist@4.7.0:
electron-to-chromium "^1.3.247"
node-releases "^1.1.29"
browserslist@^4.9.1:
version "4.12.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.2.tgz#76653d7e4c57caa8a1a28513e2f4e197dc11a711"
integrity sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==
browserslist@^4.8.5, browserslist@^4.9.1:
version "4.9.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.9.1.tgz#01ffb9ca31a1aef7678128fc6a2253316aa7287c"
integrity sha512-Q0DnKq20End3raFulq6Vfp1ecB9fh8yUNV55s8sekaDDeqBaCtWlRHCUdaWyUeSSBJM7IbM6HcsyaeYqgeDhnw==
dependencies:
caniuse-lite "^1.0.30001088"
electron-to-chromium "^1.3.483"
escalade "^3.0.1"
node-releases "^1.1.58"
caniuse-lite "^1.0.30001030"
electron-to-chromium "^1.3.363"
node-releases "^1.1.50"
bs-logger@0.x:
version "0.2.6"
@@ -5737,16 +5728,7 @@ buffer-xor@^1.0.3:
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
buffer@^4.3.0:
version "4.9.2"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^5.2.1, buffer@^5.5.0:
buffer@5.6.0, buffer@^4.3.0, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
@@ -5994,15 +5976,20 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001035:
version "1.0.30001087"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001087.tgz#4a0bdc5998a114fcf8b7954e7ba6c2c29831c54a"
integrity sha512-KAQRGtt+eGCQBSp2iZTQibdCf9oe6cNTi5lmpsW38NnxP4WMYzfU6HCRmh4kJyh6LrTM9/uyElK4xcO93kafpg==
caniuse-lite@^1.0.30001088:
version "1.0.30001090"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001090.tgz#ff7766332f60e80fea4903f30d360622e5551850"
integrity sha512-QzPRKDCyp7RhjczTPZaqK3CjPA5Ht2UnXhZhCI4f7QiB5JK6KEuZBxIzyWnB3wO4hgAj4GMRxAhuiacfw0Psjg==
caniuse-lite@^1.0.30001030:
version "1.0.30001093"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001093.tgz#833e80f64b1a0455cbceed2a4a3baf19e4abd312"
integrity sha512-0+ODNoOjtWD5eS9aaIpf4K0gQqZfILNY4WSNuYzeT1sXni+lMrrVjc0odEobJt6wrODofDZUX8XYi/5y7+xl8g==
caniuse-lite@^1.0.30001043:
version "1.0.30001084"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001084.tgz#00e471931eaefbeef54f46aa2203914d3c165669"
integrity sha512-ftdc5oGmhEbLUuMZ/Qp3mOpzfZLCxPYKcvGv6v2dJJ+8EdqcvZRbAGOiLmkM/PV1QGta/uwBs8/nCl6sokDW6w==
capture-exit@^2.0.0:
version "2.0.0"
@@ -6108,28 +6095,6 @@ check-types@^8.0.3:
resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
cheerio@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=
dependencies:
css-select "~1.2.0"
dom-serializer "~0.1.0"
entities "~1.1.1"
htmlparser2 "^3.9.1"
lodash.assignin "^4.0.9"
lodash.bind "^4.1.4"
lodash.defaults "^4.0.1"
lodash.filter "^4.4.0"
lodash.flatten "^4.2.0"
lodash.foreach "^4.3.0"
lodash.map "^4.4.0"
lodash.merge "^4.4.0"
lodash.pick "^4.2.1"
lodash.reduce "^4.4.0"
lodash.reject "^4.4.0"
lodash.some "^4.4.0"
chokidar-cli@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/chokidar-cli/-/chokidar-cli-1.2.3.tgz#28fe28da1c3a12b444f52ddbe8a472358a32279f"
@@ -7014,7 +6979,7 @@ css-select-base-adapter@^0.1.1:
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
css-select@^1.1.0, css-select@~1.2.0:
css-select@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
@@ -7266,6 +7231,11 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dayjs@^1.8.29:
version "1.8.29"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.29.tgz#5d23e341de6bfbd206c01136d2fb0f01877820f5"
integrity sha512-Vm6teig8ZWK7rH/lxzVGxZJCljPdmUr6q/3f4fr5F0VWNGVkZEjZOQJsAN8hUHUqn+NK4XHNEpJZS1MwLyDcLw==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -7657,20 +7627,12 @@ dom-serializer@0, dom-serializer@^0.2.1:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-serializer@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
dependencies:
domelementtype "^1.3.0"
entities "^1.1.1"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
domelementtype@1, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@@ -7755,6 +7717,11 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
dotenv@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
duplexer@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
@@ -7795,15 +7762,20 @@ ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.413:
electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378:
version "1.3.481"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.481.tgz#0d59e72a0aaeb876b43fb1d6e84bf0dfc99617e8"
integrity sha512-q2PeCP2PQXSYadDo9uNY+uHXjdB9PcsUpCVoGlY8TZOPHGlXdevlqW9PkKeqCxn2QBkGB8b6AcMO++gh8X82bA==
electron-to-chromium@^1.3.483:
version "1.3.483"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz#9269e7cfc1c8e72709824da171cbe47ca5e3ca9e"
integrity sha512-+05RF8S9rk8S0G8eBCqBRBaRq7+UN3lDs2DAvnG8SBSgQO3hjy0+qt4CmRk5eiuGbTcaicgXfPmBi31a+BD3lg==
electron-to-chromium@^1.3.363:
version "1.3.484"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.484.tgz#75f5a1eee5fe3168758b7c2cf375ae73c1ccf5e6"
integrity sha512-esh5mmjAGl6HhAaYgHlDZme+jCIc+XIrLrBTwxviE+pM64UBmdLUIHLlrPzJGbit7hQI1TR/oGDQWCvQZ5yrFA==
electron-to-chromium@^1.3.413:
version "1.3.474"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.474.tgz#161af012e11f96795eade84bf03b8ddc039621b9"
integrity sha512-fPkSgT9IBKmVJz02XioNsIpg0WYmkPrvU1lUJblMMJALxyE7/32NGvbJQKKxpNokozPvqfqkuUqVClYsvetcLw==
elliptic@^6.0.0, elliptic@^6.4.0, elliptic@^6.4.1, elliptic@^6.5.1, elliptic@^6.5.2:
version "6.5.3"
@@ -7888,7 +7860,7 @@ enquirer@^2.3.0, enquirer@^2.3.4, enquirer@^2.3.5:
dependencies:
ansi-colors "^3.2.1"
entities@^1.1.1, entities@~1.1.1:
entities@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@@ -8003,11 +7975,6 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3:
d "^1.0.1"
ext "^1.1.2"
escalade@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.1.tgz#52568a77443f6927cd0ab9c73129137533c965ed"
integrity sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -8090,7 +8057,7 @@ eslint-plugin-flowtype@^4.7.0:
dependencies:
lodash "^4.17.15"
eslint-plugin-import@>=2.20.2, eslint-plugin-import@^2.18.2:
eslint-plugin-import@2.21.2, eslint-plugin-import@>=2.20.2, eslint-plugin-import@^2.18.2, "eslint-plugin-import@^2.21.2 ":
version "2.21.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz#8fef77475cc5510801bedc95f84b932f7f334a7c"
integrity sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA==
@@ -8207,7 +8174,7 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
eslint@^6.1.0, eslint@^6.3.0, eslint@^6.7.2:
eslint@^6.1.0, eslint@^6.3.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
@@ -8250,7 +8217,7 @@ eslint@^6.1.0, eslint@^6.3.0, eslint@^6.7.2:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
eslint@^7.0.0, eslint@^7.1.0:
eslint@^7.0.0:
version "7.3.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.3.1.tgz#76392bd7e44468d046149ba128d1566c59acbe19"
integrity sha512-cQC/xj9bhWUcyi/RuMbRtC3I0eW8MH0jhRELSvpKYkWep3C6YZ2OkvcvJVUeO6gcunABmzptbXBuDoXsjHmfTA==
@@ -8292,6 +8259,48 @@ eslint@^7.0.0, eslint@^7.1.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
eslint@^7.1.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.4.0.tgz#4e35a2697e6c1972f9d6ef2b690ad319f80f206f"
integrity sha512-gU+lxhlPHu45H3JkEGgYhWhkR9wLHHEXC9FbWFnTlEkbKyZKWgWRLgf61E8zWmBuI6g5xKBph9ltg3NtZMVF8g==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.0.1"
doctrine "^3.0.0"
enquirer "^2.3.5"
eslint-scope "^5.1.0"
eslint-utils "^2.0.0"
eslint-visitor-keys "^1.2.0"
espree "^7.1.0"
esquery "^1.2.0"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^12.1.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash "^4.17.14"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
progress "^2.0.0"
regexpp "^3.1.0"
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
table "^5.2.3"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^6.1.2:
version "6.2.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
@@ -9018,7 +9027,7 @@ fork-ts-checker-webpack-plugin@^4.0.4, fork-ts-checker-webpack-plugin@^4.1.3:
tapable "^1.0.0"
worker-rpc "^0.1.0"
form-data@^2.3.3, form-data@^2.5.1:
form-data@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
@@ -9899,7 +9908,7 @@ htmlparser2@4.1.0:
domutils "^2.0.0"
entities "^2.0.0"
htmlparser2@^3.3.0, htmlparser2@^3.9.1:
htmlparser2@^3.3.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@@ -11058,7 +11067,17 @@ jest-diff@^24.3.0, jest-diff@^24.9.0:
jest-get-type "^24.9.0"
pretty-format "^24.9.0"
jest-diff@^25.2.1, jest-diff@^25.5.0:
jest-diff@^25.2.1:
version "25.2.6"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.2.6.tgz#a6d70a9ab74507715ea1092ac513d1ab81c1b5e7"
integrity sha512-KuadXImtRghTFga+/adnNrv9s61HudRMR7gVSbP35UKZdn4IK2/0N0PpGZIqtmllK9aUyye54I3nu28OYSnqOg==
dependencies:
chalk "^3.0.0"
diff-sequences "^25.2.6"
jest-get-type "^25.2.6"
pretty-format "^25.2.6"
jest-diff@^25.5.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9"
integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==
@@ -11904,30 +11923,6 @@ jsontokens@3.0.0, jsontokens@^3.0.0:
elliptic "^6.4.1"
sha.js "^2.4.11"
jsontokens@3.0.0-alpha.0:
version "3.0.0-alpha.0"
resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-3.0.0-alpha.0.tgz#16d04a2019a6dbe2392e4eeb489ac47ec3d855d7"
integrity sha512-+2JdFr2d3XBfamUnETQdv76u3AwBl8jmLp2Hgb/uK5NTys0FpoR5KbIIqv3QrXtjn0uIIPJz9JmhqDAnXd2Nhg==
dependencies:
"@types/elliptic" "^6.4.9"
asn1.js "^5.0.1"
base64url "^3.0.1"
ecdsa-sig-formatter "^1.0.11"
elliptic "^6.4.1"
key-encoder "^2.0.3"
jsontokens@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-2.0.2.tgz#fff3a940bc7b4960d395c0c583ac8e2b394e07d1"
integrity sha512-E5W1CIbS7KcVvOJ2CguITvb77GbsjOzzmkFxnuCJqtSLvebgxRXcR1OhFXDK+2Hz+ng7MYJhMylilKnLwlwdYQ==
dependencies:
"@types/elliptic" "^6.4.9"
asn1.js "^5.0.1"
base64url "^3.0.1"
ecdsa-sig-formatter "^1.0.11"
elliptic "^6.4.1"
key-encoder "^2.0.2"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -11946,16 +11941,6 @@ jsx-ast-utils@^2.2.3, jsx-ast-utils@^2.4.1:
array-includes "^3.1.1"
object.assign "^4.1.0"
key-encoder@^2.0.2, key-encoder@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/key-encoder/-/key-encoder-2.0.3.tgz#77073bb48ff1fe2173bb2088b83b91152c8fa4ba"
integrity sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==
dependencies:
"@types/elliptic" "^6.4.9"
asn1.js "^5.0.1"
bn.js "^4.11.8"
elliptic "^6.4.1"
killable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -12188,16 +12173,11 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.assignin@^4.0.9, lodash.assignin@^4.2.0:
lodash.assignin@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI=
lodash.bind@^4.1.4:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@@ -12208,26 +12188,6 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.defaults@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
lodash.filter@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=
lodash.flatten@^4.2.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
lodash.foreach@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -12238,46 +12198,21 @@ lodash.ismatch@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=
lodash.map@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
lodash.memoize@4.x, lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.merge@^4.4.0, lodash.merge@^4.6.2:
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.pick@^4.2.1:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
lodash.reduce@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=
lodash.reject@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
lodash.some@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -13209,7 +13144,7 @@ node-fetch@2.1.2:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
node-fetch@2.6.0, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0:
node-fetch@2.6.0, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
@@ -13305,7 +13240,7 @@ node-notifier@^6.0.0:
shellwords "^0.1.1"
which "^1.3.1"
node-releases@^1.1.29, node-releases@^1.1.52, node-releases@^1.1.53, node-releases@^1.1.58:
node-releases@^1.1.29, node-releases@^1.1.50, node-releases@^1.1.52, node-releases@^1.1.53:
version "1.1.58"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935"
integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==
@@ -14246,38 +14181,6 @@ playwright-core@^1.1.1:
rimraf "^3.0.2"
ws "^6.1.0"
playwright-firefox@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/playwright-firefox/-/playwright-firefox-1.1.1.tgz#9486e5ab695a2e52face4684114735bb098b934b"
integrity sha512-YaRVNxGGfmuAgKD5XDK9vPD82UYYHv5qM1Qn5XwK8zXWhBdDcs+JgaNvW7NnOMefgFAhGy637HHLA1WZ+ajOIA==
dependencies:
debug "^4.1.1"
extract-zip "^2.0.0"
https-proxy-agent "^3.0.0"
jpeg-js "^0.3.7"
mime "^2.4.4"
pngjs "^5.0.0"
progress "^2.0.3"
proxy-from-env "^1.1.0"
rimraf "^3.0.2"
ws "^6.1.0"
playwright-webkit@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/playwright-webkit/-/playwright-webkit-1.1.1.tgz#e3c55b171f32a268b6795772ed414b1bf62e2dbc"
integrity sha512-fv1itK0rz6uqpO22M9CdRaqFyB6L0Y4S3OkWgSCuLLrmMIW4tmSLT1qukM4QXcHYQNdTIVvbtudITxEfIU5p4g==
dependencies:
debug "^4.1.1"
extract-zip "^2.0.0"
https-proxy-agent "^3.0.0"
jpeg-js "^0.3.7"
mime "^2.4.4"
pngjs "^5.0.0"
progress "^2.0.3"
proxy-from-env "^1.1.0"
rimraf "^3.0.2"
ws "^6.1.0"
playwright@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.1.1.tgz#20746124542bddd7e925c128b6ec1ace679ffe6a"
@@ -14766,7 +14669,17 @@ pretty-format@^24.9.0:
ansi-styles "^3.2.0"
react-is "^16.8.4"
pretty-format@^25.2.1, pretty-format@^25.5.0:
pretty-format@^25.2.1, pretty-format@^25.2.6:
version "25.2.6"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.6.tgz#542a1c418d019bbf1cca2e3620443bc1323cb8d7"
integrity sha512-DEiWxLBaCHneffrIT4B+TpMvkV9RNvvJrd3lY9ew1CEQobDzEXmYT1mg0hJhljZty7kCc10z13ohOFAE8jrUDg==
dependencies:
"@jest/types" "^25.2.6"
ansi-regex "^5.0.0"
ansi-styles "^4.0.0"
react-is "^16.12.0"
pretty-format@^25.5.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==
@@ -15043,10 +14956,10 @@ query-string@^4.1.0:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
query-string@^6.3.0, query-string@^6.8.3:
version "6.13.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad"
integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==
query-string@^6.8.3:
version "6.11.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.11.1.tgz#ab021f275d463ce1b61e88f0ce6988b3e8fe7c2c"
integrity sha512-1ZvJOUl8ifkkBxu2ByVM/8GijMIPx+cef7u3yroO3Ogm4DOdZcF5dcrWTIlSHe3Pg/mtlt6/eFjObDfJureZZA==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
@@ -16402,7 +16315,7 @@ semver-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@6.3.0, semver@6.x, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
semver@6.3.0, semver@6.x, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -17821,7 +17734,7 @@ trim@0.0.1:
resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
triplesec@^3.0.26, triplesec@^3.0.27:
triplesec@^3.0.27:
version "3.0.27"
resolved "https://registry.yarnpkg.com/triplesec/-/triplesec-3.0.27.tgz#43ba5a9f0e11ebba20c7563ecca947b2f94e82c5"
integrity sha512-FDhkxa3JYnPOerOd+8k+SBmm7cb7KkyX+xXwNFV3XV6dsQgHuRvjtbnzWfPJ2kimeR8ErjZfPd/6r7RH6epHDw==
@@ -18072,6 +17985,72 @@ tsdx@^0.12.3:
tslib "^1.9.3"
typescript "^3.7.3"
tsdx@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/tsdx/-/tsdx-0.13.1.tgz#6fdfb7f70ecd22462d36c80f538d86f42d364b92"
integrity sha512-BAp96F6eLZVIk5FETP8kSbkC8sqvDQa0pPMz4euZ8am2aFNzeXNX7DbOxfMxYiwzfyemfUsmyb4p5usVbqM4IQ==
dependencies:
"@babel/core" "^7.4.4"
"@babel/helper-module-imports" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.4.4"
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.7.4"
"@babel/plugin-proposal-optional-chaining" "^7.7.5"
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
"@babel/plugin-transform-regenerator" "^7.4.5"
"@babel/plugin-transform-runtime" "^7.6.0"
"@babel/polyfill" "^7.4.4"
"@babel/preset-env" "^7.4.4"
"@rollup/plugin-commonjs" "^11.0.0"
"@rollup/plugin-json" "^4.0.0"
"@rollup/plugin-node-resolve" "^7.1.0"
"@rollup/plugin-replace" "^2.2.1"
"@types/shelljs" "^0.8.5"
"@typescript-eslint/eslint-plugin" "^2.12.0"
"@typescript-eslint/parser" "^2.12.0"
ansi-escapes "^4.2.1"
asyncro "^3.0.0"
babel-eslint "^10.0.3"
babel-plugin-annotate-pure-calls "^0.4.0"
babel-plugin-dev-expression "^0.2.1"
babel-plugin-macros "^2.6.1"
babel-plugin-transform-async-to-promises "^0.8.14"
babel-plugin-transform-rename-import "^2.3.0"
babel-traverse "^6.26.0"
babylon "^6.18.0"
camelcase "^5.0.0"
chalk "^2.4.2"
enquirer "^2.3.4"
eslint "^6.1.0"
eslint-config-prettier "^6.0.0"
eslint-config-react-app "^5.0.2"
eslint-plugin-flowtype "^3.13.0"
eslint-plugin-import "^2.18.2"
eslint-plugin-jsx-a11y "^6.2.3"
eslint-plugin-prettier "^3.1.0"
eslint-plugin-react "^7.14.3"
eslint-plugin-react-hooks "^2.2.0"
execa "3.2.0"
fs-extra "^8.0.1"
jest "^24.8.0"
jest-watch-typeahead "^0.4.0"
jpjs "^1.2.1"
lodash.merge "^4.6.2"
ora "^3.4.0"
pascal-case "^2.0.1"
prettier "^1.19.1"
progress-estimator "^0.2.2"
rollup "^1.32.1"
rollup-plugin-babel "^4.3.2"
rollup-plugin-sourcemaps "^0.4.2"
rollup-plugin-terser "^5.1.2"
rollup-plugin-typescript2 "^0.26.0"
sade "^1.4.2"
shelljs "^0.8.3"
tiny-glob "^0.2.6"
ts-jest "^24.0.2"
tslib "^1.9.3"
typescript "^3.7.3"
tsdx@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/tsdx/-/tsdx-0.13.2.tgz#e6b0d5e52fadb3dd993c26887b9b75acd438cd05"
@@ -18144,10 +18123,10 @@ tslib@1.10.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
tslib@^2.0.0:
version "2.0.0"
@@ -18298,17 +18277,17 @@ typeforce@^1.11.3, typeforce@^1.11.5:
resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc"
integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==
typescript@3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
typescript@3.8.3:
typescript@^3.6.4, typescript@^3.7.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
typescript@^3.6.4, typescript@^3.7.3, typescript@^3.7.5, typescript@^3.8.2, typescript@^3.9.5:
typescript@^3.9.3:
version "3.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a"
integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==
typescript@^3.9.5:
version "3.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
@@ -19359,11 +19338,16 @@ ws@^6.0.0, ws@^6.1.0, ws@^6.1.2, ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
ws@^7.0.0, ws@^7.2.0, ws@^7.2.3:
ws@^7.0.0, ws@^7.2.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
ws@^7.2.3:
version "7.3.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"