From 3c6688714b070a38c2eefe0d93a6218163917c53 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Tue, 16 Jun 2020 19:08:36 -0700 Subject: [PATCH] feat: add debug mode for transaction signing --- .eslintrc.js | 4 + .gitignore | 3 +- .vscode/settings.json | 3 + package.json | 11 +- packages/app/jest.config.js | 2 +- packages/app/package.json | 20 +- packages/app/src/common/hooks/use-wallet.ts | 15 +- packages/app/src/common/onboarding-data.ts | 29 +- packages/app/src/common/stacks-utils.ts | 57 ++ packages/app/src/common/track.ts | 6 +- packages/app/src/common/transaction-utils.ts | 149 +++++ packages/app/src/common/utils.ts | 22 +- packages/app/src/components/drawer/index.tsx | 2 +- packages/app/src/components/routes.tsx | 16 +- packages/app/src/components/tabbed-card.tsx | 98 +++ .../transactions/testnet-banner.tsx | 22 + .../src/components/transactions/tx-error.tsx | 49 ++ packages/app/src/extension/inpage.ts | 2 +- .../app/src/pages/connect/choose-account.tsx | 15 +- packages/app/src/pages/transaction/index.tsx | 228 +++++++ packages/app/src/store/onboarding/actions.ts | 7 +- packages/app/src/store/wallet/actions.ts | 4 +- packages/app/tests/integration/utils.ts | 1 - packages/app/tsconfig.json | 4 +- packages/app/webpack.config.js | 4 + packages/connect/package.json | 7 +- packages/connect/src/auth.ts | 77 +-- packages/connect/src/index.ts | 1 + packages/connect/src/popup.ts | 67 ++ .../src/react/components/connect/context.tsx | 3 + .../connect/src/react/hooks/use-connect.ts | 32 +- packages/connect/src/transactions/index.ts | 144 +++++ packages/connect/src/transactions/types.ts | 120 ++++ packages/keychain/package.json | 7 +- packages/keychain/src/identity.ts | 15 +- packages/keychain/src/index.ts | 2 + .../src/nodes/identity-address-owner-node.ts | 17 +- packages/keychain/src/profiles.ts | 21 +- packages/keychain/src/utils/gaia.ts | 35 +- packages/keychain/src/utils/index.ts | 15 +- packages/keychain/src/wallet/index.ts | 35 +- packages/keychain/src/wallet/signer.ts | 147 +++++ packages/keychain/tests/identity.test.ts | 10 +- packages/keychain/tests/profile.test.ts | 2 +- packages/keychain/tests/wallet-signer.test.ts | 18 + packages/keychain/tests/wallet.test.ts | 8 +- packages/keychain/tsconfig.json | 6 +- packages/rpc-client/.gitignore | 8 + packages/rpc-client/LICENSE | 21 + packages/rpc-client/README.md | 27 + packages/rpc-client/package.json | 41 ++ packages/rpc-client/src/index.ts | 143 +++++ packages/rpc-client/test/index.test.ts | 11 + packages/rpc-client/tsconfig.json | 30 + packages/test-app/common/context.ts | 21 + packages/test-app/common/contracts.ts | 59 ++ packages/test-app/common/use-faucet.ts | 83 +++ packages/test-app/common/use-stx-address.ts | 7 + packages/test-app/common/utils.ts | 33 + packages/test-app/components/app.tsx | 139 ++-- packages/test-app/components/auth.tsx | 22 + .../test-app/components/counter-actions.tsx | 80 +++ packages/test-app/components/counter.tsx | 56 ++ packages/test-app/components/deploy.tsx | 37 ++ .../test-app/components/explorer-link.tsx | 24 + packages/test-app/components/faucet.tsx | 116 ++++ packages/test-app/components/header.tsx | 38 ++ packages/test-app/components/home.tsx | 81 +++ packages/test-app/components/status.tsx | 203 ++++++ packages/test-app/components/stx-transfer.tsx | 63 ++ packages/test-app/components/tab.tsx | 29 + packages/test-app/components/tx-card.tsx | 44 ++ packages/test-app/contracts/counter.clar | 16 + packages/test-app/contracts/status.clar | 31 + packages/test-app/contracts/stream.clar | 51 ++ packages/test-app/jest.config.js | 185 ++++++ packages/test-app/package.json | 15 +- packages/test-app/tests/contracts.test.ts | 86 +++ packages/test-app/tsconfig.json | 12 +- packages/test-app/webpack.config.js | 5 - packages/ui/package.json | 3 +- packages/ui/src/icons/external-icon.tsx | 14 + packages/ui/src/icons/failed-icon.tsx | 23 + packages/ui/src/icons/index.tsx | 2 + packages/ui/src/tooltip/index.tsx | 6 +- yarn.lock | 598 +++++++++--------- 86 files changed, 3416 insertions(+), 609 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 packages/app/src/common/stacks-utils.ts create mode 100644 packages/app/src/common/transaction-utils.ts create mode 100644 packages/app/src/components/tabbed-card.tsx create mode 100644 packages/app/src/components/transactions/testnet-banner.tsx create mode 100644 packages/app/src/components/transactions/tx-error.tsx create mode 100644 packages/app/src/pages/transaction/index.tsx create mode 100644 packages/connect/src/transactions/index.ts create mode 100644 packages/connect/src/transactions/types.ts create mode 100644 packages/keychain/src/wallet/signer.ts create mode 100644 packages/keychain/tests/wallet-signer.test.ts create mode 100644 packages/rpc-client/.gitignore create mode 100644 packages/rpc-client/LICENSE create mode 100644 packages/rpc-client/README.md create mode 100644 packages/rpc-client/package.json create mode 100644 packages/rpc-client/src/index.ts create mode 100644 packages/rpc-client/test/index.test.ts create mode 100644 packages/rpc-client/tsconfig.json create mode 100644 packages/test-app/common/context.ts create mode 100644 packages/test-app/common/contracts.ts create mode 100644 packages/test-app/common/use-faucet.ts create mode 100644 packages/test-app/common/use-stx-address.ts create mode 100644 packages/test-app/common/utils.ts create mode 100644 packages/test-app/components/auth.tsx create mode 100644 packages/test-app/components/counter-actions.tsx create mode 100644 packages/test-app/components/counter.tsx create mode 100644 packages/test-app/components/deploy.tsx create mode 100644 packages/test-app/components/explorer-link.tsx create mode 100644 packages/test-app/components/faucet.tsx create mode 100644 packages/test-app/components/header.tsx create mode 100644 packages/test-app/components/home.tsx create mode 100644 packages/test-app/components/status.tsx create mode 100644 packages/test-app/components/stx-transfer.tsx create mode 100644 packages/test-app/components/tab.tsx create mode 100644 packages/test-app/components/tx-card.tsx create mode 100644 packages/test-app/contracts/counter.clar create mode 100644 packages/test-app/contracts/status.clar create mode 100644 packages/test-app/contracts/stream.clar create mode 100644 packages/test-app/jest.config.js create mode 100644 packages/test-app/tests/contracts.test.ts create mode 100644 packages/ui/src/icons/external-icon.tsx create mode 100644 packages/ui/src/icons/failed-icon.tsx diff --git a/.eslintrc.js b/.eslintrc.js index d97f8c3..707c223 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,4 +2,8 @@ module.exports = { root: true, reportUnusedDisableDirectives: true, extends: ['@blockstack/eslint-config'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json', + } }; diff --git a/.gitignore b/.gitignore index 3e6923c..7cbc623 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ dist/ .rts2_cache_umd/ yarn-error.log .github/workflows/event.json -.npmrc \ No newline at end of file +.npmrc +coverage/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3662b37 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/package.json b/package.json index fa4e690..36d231b 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/packages/app/jest.config.js b/packages/app/jest.config.js index c3e9ae6..99661ac 100755 --- a/packages/app/jest.config.js +++ b/packages/app/jest.config.js @@ -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", diff --git a/packages/app/package.json b/packages/app/package.json index dd5acc4..aad046f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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" } } diff --git a/packages/app/src/common/hooks/use-wallet.ts b/packages/app/src/common/hooks/use-wallet.ts index ac8476a..bb20393 100644 --- a/packages/app/src/common/hooks/use-wallet.ts +++ b/packages/app/src/common/hooks/use-wallet.ts @@ -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 }; }; diff --git a/packages/app/src/common/onboarding-data.ts b/packages/app/src/common/onboarding-data.ts index bc92cc9..98f76fd 100644 --- a/packages/app/src/common/onboarding-data.ts +++ b/packages/app/src/common/onboarding-data.ts @@ -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) can’t have access.', - }, - { - icon: '/assets/images/icon-padlock.svg', - title: 'Encryption that’s 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: - 'You’ll only ever have to create one Data Vault to use 100s of other apps like Nurx privately.', - }, -]; - -export { faqs, howDataVaultWorks }; +export { faqs }; diff --git a/packages/app/src/common/stacks-utils.ts b/packages/app/src/common/stacks-utils.ts new file mode 100644 index 0000000..597ffda --- /dev/null +++ b/packages/app/src/common/stacks-utils.ts @@ -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`; +}; diff --git a/packages/app/src/common/track.ts b/packages/app/src/common/track.ts index 5c04aca..8b579de 100644 --- a/packages/app/src/common/track.ts +++ b/packages/app/src/common/track.ts @@ -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]; diff --git a/packages/app/src/common/transaction-utils.ts b/packages/app/src/common/transaction-utils.ts new file mode 100644 index 0000000..0144ac4 --- /dev/null +++ b/packages/app/src/common/transaction-utils.ts @@ -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); + } + } +}; diff --git a/packages/app/src/common/utils.ts b/packages/app/src/common/utils.ts index 8d447e1..9dc3f8d 100644 --- a/packages/app/src/common/utils.ts +++ b/packages/app/src/common/utils.ts @@ -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; diff --git a/packages/app/src/components/drawer/index.tsx b/packages/app/src/components/drawer/index.tsx index df0885f..a9778db 100644 --- a/packages/app/src/components/drawer/index.tsx +++ b/packages/app/src/components/drawer/index.tsx @@ -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[]; diff --git a/packages/app/src/components/routes.tsx b/packages/app/src/components/routes.tsx index 9c8e9f0..9c2aa5d 100644 --- a/packages/app/src/components/routes.tsx +++ b/packages/app/src/components/routes.tsx @@ -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 */} + } /> {/*Error/Misc*/} { + if (active) { + return { + borderBottomWidth: '1px', + borderColor: 'blue', + }; + } + return {}; +}; + +const getTextProps = (active: boolean) => { + if (active) { + return { + color: 'blue', + fontWeight: 600, + }; + } + return {}; +}; + +const TabHeader: React.FC = ({ tab, active, ...rest }) => { + return ( + + + {tab.title} + + + ); +}; + +interface TabBodyProps extends BoxProps { + tab: Tab; +} + +const TabBody: React.FC = ({ tab }) => { + return ( + + {tab.content} + + ); +}; + +export interface TabbedCardProps extends BoxProps { + tabs: Tab[]; +} + +export const TabbedCard: React.FC = ({ 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 ( + { + setActiveKey(tab.key); + }} + key={tab.key} + /> + ); + }); + return ( + + + + {Header} + + + {activeTab && ( + + + + + + )} + + ); +}; diff --git a/packages/app/src/components/transactions/testnet-banner.tsx b/packages/app/src/components/transactions/testnet-banner.tsx new file mode 100644 index 0000000..ed0ea2f --- /dev/null +++ b/packages/app/src/components/transactions/testnet-banner.tsx @@ -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 ( + + + + Testnet mode + + + + ); +}; diff --git a/packages/app/src/components/transactions/tx-error.tsx b/packages/app/src/components/transactions/tx-error.tsx new file mode 100644 index 0000000..8bf8e9b --- /dev/null +++ b/packages/app/src/components/transactions/tx-error.tsx @@ -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 = ({ message }) => { + return ( + <> + + {/* TODO: only show testnet banner if in testnet mode */} + + + + + + + + + Transaction Failed + + + {message} + + , + ]} + /> + + + + + + ); +}; diff --git a/packages/app/src/extension/inpage.ts b/packages/app/src/extension/inpage.ts index 716e91e..06f0983 100644 --- a/packages/app/src/extension/inpage.ts +++ b/packages/app/src/extension/inpage.ts @@ -1,4 +1,4 @@ -import { BlockstackProvider } from '@blockstack/connect/types'; +import { BlockstackProvider } from '@blockstack/connect'; interface Response { source: 'blockstack-extension'; diff --git a/packages/app/src/pages/connect/choose-account.tsx b/packages/app/src/pages/connect/choose-account.tsx index 1ecdfe0..81707bd 100644 --- a/packages/app/src/pages/connect/choose-account.tsx +++ b/packages/app/src/pages/connect/choose-account.tsx @@ -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 = ({ 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([]); const [identityIndex, setIdentityIndex] = React.useState(); const { doTrack } = useAnalytics(); + if (!wallet) { + return ; + } + // TODO: refactor into util, create unit tests const didSelectAccount = ({ identityIndex }: { identityIndex: number }) => { const state = store.getState(); diff --git a/packages/app/src/pages/transaction/index.tsx b/packages/app/src/pages/transaction/index.tsx new file mode 100644 index 0000000..e8b8dcf --- /dev/null +++ b/packages/app/src/pages/transaction/index.tsx @@ -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 = ({ json }) => { + return ( + + {JSON.stringify(json, null, 2)} + + ); +}; + +export const Transaction: React.FC = () => { + const location = useLocation(); + const { wallet } = useWallet(); + const [pendingTransaction, setPendingTransaction] = useState(); + const [signedTransaction, setSignedTransaction] = useState(); + const [loading, setLoading] = useState(true); + const [contractSrc, setContractSrc] = useState(''); + const [balance, setBalance] = useState(0); + const [error, setError] = useState(null); + const client = getRPCClient(); + + if (!wallet) { + throw new Error('User must be logged in.'); + } + + const tabs: Tab[] = [ + { + title: 'Inputs', + content: , + key: 'inputs', + }, + { + title: ( + <> + View Source + {/* Add this icon when we can link to the explorer */} + {/* */} + + ), + content: ( + + {contractSrc} + + ), + 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 ; + } + + return ( + <> + + {/* TODO: only show testnet banner if in testnet mode */} + + + {stacksValue({ value: balance })} available + + } + /> + Confirm Transaction, + + with {pendingTransaction?.appDetails?.name} + , + , + + + Fee + + {stacksValue({ + value: signedTransaction?.auth.spendingCondition?.fee?.toNumber() || 0, + })} + + + , + ]} + /> + + + + + + + + + ); +}; diff --git a/packages/app/src/store/onboarding/actions.ts b/packages/app/src/store/onboarding/actions.ts index 36e954c..22d6072 100644 --- a/packages/app/src/store/onboarding/actions.ts +++ b/packages/app/src/store/onboarding/actions.ts @@ -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)); diff --git a/packages/app/src/store/wallet/actions.ts b/packages/app/src/store/wallet/actions.ts index db46d51..5cef50c 100644 --- a/packages/app/src/store/wallet/actions.ts +++ b/packages/app/src/store/wallet/actions.ts @@ -41,7 +41,7 @@ export function doStoreSeed( ): ThunkAction, {}, {}, 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, {}, {}, 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; }; diff --git a/packages/app/tests/integration/utils.ts b/packages/app/tests/integration/utils.ts index 9c72bbc..68b9b42 100644 --- a/packages/app/tests/integration/utils.ts +++ b/packages/app/tests/integration/utils.ts @@ -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 => { diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index c16fed1..41e7d9c 100755 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -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 diff --git a/packages/app/webpack.config.js b/packages/app/webpack.config.js index 84d7660..f66d94b 100755 --- a/packages/app/webpack.config.js +++ b/packages/app/webpack.config.js @@ -185,6 +185,10 @@ module.exports = { contentBase: './dist', historyApiFallback: true, }, + node: { + Buffer: true, + BufferReader: true, + }, devtool: getSourceMap(), watch: false, plugins: [ diff --git a/packages/connect/package.json b/packages/connect/package.json index d31082b..880e611 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -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" diff --git a/packages/connect/src/auth.ts b/packages/connect/src/auth.ts index 995afdc..622c8a6 100644 --- a/packages/connect/src/auth.ts +++ b/packages/connect/src/auth.ts @@ -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({ + 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, + }); }; diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index bc707bb..3732245 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -3,3 +3,4 @@ export * from './react'; export * from './popup'; export * from './types'; export * from './ui'; +export * from './transactions'; diff --git a/packages/connect/src/popup.ts b/packages/connect/src/popup.ts index 7e5ebe4..5e8a390 100644 --- a/packages/connect/src/popup.ts +++ b/packages/connect/src/popup.ts @@ -73,3 +73,70 @@ export const popupCenter = ({ } return window.open(url); }; + +interface ListenerParams { + popup: Window | null; + messageParams: { + [key: string]: any; + }; + onFinish: (payload: FinishedType) => void | Promise; + onCancel?: () => void; + authURL: URL; +} + +export const setupListener = ({ + popup, + messageParams, + onFinish, + onCancel, + authURL, +}: ListenerParams) => { + 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); +}; diff --git a/packages/connect/src/react/components/connect/context.tsx b/packages/connect/src/react/components/connect/context.tsx index 008d3d3..473b1e1 100644 --- a/packages/connect/src/react/components/connect/context.tsx +++ b/packages/connect/src/react/components/connect/context.tsx @@ -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: '', diff --git a/packages/connect/src/react/hooks/use-connect.ts b/packages/connect/src/react/hooks/use-connect.ts index f0a0002..39d0232 100644 --- a/packages/connect/src/react/hooks/use-connect.ts +++ b/packages/connect/src/react/hooks/use-connect.ts @@ -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) => { @@ -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, }; }; diff --git a/packages/connect/src/transactions/index.ts b/packages/connect/src/transactions/index.ts new file mode 100644 index 0000000..02b4c8c --- /dev/null +++ b/packages/connect/src/transactions/index.ts @@ -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({ + 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( + opts: T, + makeTokenFn: (opts: T) => Promise +) { + 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); diff --git a/packages/connect/src/transactions/types.ts b/packages/connect/src/transactions/types.ts new file mode 100644 index 0000000..e9085f7 --- /dev/null +++ b/packages/connect/src/transactions/types.ts @@ -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; +} diff --git a/packages/keychain/package.json b/packages/keychain/package.json index 834f8a5..ec9df4c 100644 --- a/packages/keychain/package.json +++ b/packages/keychain/package.json @@ -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", diff --git a/packages/keychain/src/identity.ts b/packages/keychain/src/identity.ts index 2bf0999..f71cafa 100644 --- a/packages/keychain/src/identity.ts +++ b/packages/keychain/src/identity.ts @@ -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); diff --git a/packages/keychain/src/index.ts b/packages/keychain/src/index.ts index caee1b6..f9ed0bc 100644 --- a/packages/keychain/src/index.ts +++ b/packages/keychain/src/index.ts @@ -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'; diff --git a/packages/keychain/src/nodes/identity-address-owner-node.ts b/packages/keychain/src/nodes/identity-address-owner-node.ts index 01cce93..0ca9531 100644 --- a/packages/keychain/src/nodes/identity-address-owner-node.ts +++ b/packages/keychain/src/nodes/identity-address-owner-node.ts @@ -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); } } diff --git a/packages/keychain/src/profiles.ts b/packages/keychain/src/profiles.ts index 4be86e7..3ba445a 100644 --- a/packages/keychain/src/profiles.ts +++ b/packages/keychain/src/profiles.ts @@ -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); }; diff --git a/packages/keychain/src/utils/gaia.ts b/packages/keychain/src/utils/gaia.ts index d2c46a9..56ad752 100644 --- a/packages/keychain/src/utils/gaia.ts +++ b/packages/keychain/src/utils/gaia.ts @@ -49,18 +49,19 @@ interface ConnectToGaiaOptions { gaiaHubUrl: string; } -export const connectToGaiaHubWithConfig = async ({ +export const connectToGaiaHubWithConfig = ({ hubInfo, privateKey, gaiaHubUrl, -}: ConnectToGaiaOptions): Promise => { +}: 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 => { - 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 => { + 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; +}; diff --git a/packages/keychain/src/utils/index.ts b/packages/keychain/src/utils/index.ts index eee38d5..880c308 100644 --- a/packages/keychain/src/utils/index.ts +++ b/packages/keychain/src/utils/index.ts @@ -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 { - 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, diff --git a/packages/keychain/src/wallet/index.ts b/packages/keychain/src/wallet/index.ts index 97e5049..75aa773 100644 --- a/packages/keychain/src/wallet/index.ts +++ b/packages/keychain/src/wallet/index.ts @@ -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 { + async getOrCreateConfig({ + gaiaConfig, + skipUpload, + }: { + gaiaConfig: GaiaHubConfig; + skipUpload?: boolean; + }): Promise { 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 { 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; diff --git a/packages/keychain/src/wallet/signer.ts b/packages/keychain/src/wallet/signer.ts new file mode 100644 index 0000000..ed80ebd --- /dev/null +++ b/packages/keychain/src/wallet/signer.ts @@ -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(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; + } +} diff --git a/packages/keychain/tests/identity.test.ts b/packages/keychain/tests/identity.test.ts index 75343ff..74dbe0c 100644 --- a/packages/keychain/tests/identity.test.ts +++ b/packages/keychain/tests/identity.test.ts @@ -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'); }); diff --git a/packages/keychain/tests/profile.test.ts b/packages/keychain/tests/profile.test.ts index 406d041..b2de438 100644 --- a/packages/keychain/tests/profile.test.ts +++ b/packages/keychain/tests/profile.test.ts @@ -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; diff --git a/packages/keychain/tests/wallet-signer.test.ts b/packages/keychain/tests/wallet-signer.test.ts new file mode 100644 index 0000000..0ca50c5 --- /dev/null +++ b/packages/keychain/tests/wallet-signer.test.ts @@ -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' + ); +}); diff --git a/packages/keychain/tests/wallet.test.ts b/packages/keychain/tests/wallet.test.ts index 87eeee5..f71c1b9 100644 --- a/packages/keychain/tests/wallet.test.ts +++ b/packages/keychain/tests/wallet.test.ts @@ -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(); diff --git a/packages/keychain/tsconfig.json b/packages/keychain/tsconfig.json index f8878c5..98985f0 100644 --- a/packages/keychain/tsconfig.json +++ b/packages/keychain/tsconfig.json @@ -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" diff --git a/packages/rpc-client/.gitignore b/packages/rpc-client/.gitignore new file mode 100644 index 0000000..ff88468 --- /dev/null +++ b/packages/rpc-client/.gitignore @@ -0,0 +1,8 @@ +*.log +.DS_Store +node_modules +.rts2_cache_cjs +.rts2_cache_esm +.rts2_cache_umd +.rts2_cache_system +dist diff --git a/packages/rpc-client/LICENSE b/packages/rpc-client/LICENSE new file mode 100644 index 0000000..11be1a0 --- /dev/null +++ b/packages/rpc-client/LICENSE @@ -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. \ No newline at end of file diff --git a/packages/rpc-client/README.md b/packages/rpc-client/README.md new file mode 100644 index 0000000..7555d82 --- /dev/null +++ b/packages/rpc-client/README.md @@ -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. + + + +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). + + + +### `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. diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json new file mode 100644 index 0000000..2b7c025 --- /dev/null +++ b/packages/rpc-client/package.json @@ -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" + } +} diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts new file mode 100644 index 0000000..cd8f8d2 --- /dev/null +++ b/packages/rpc-client/src/index.ts @@ -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 { + 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; diff --git a/packages/rpc-client/test/index.test.ts b/packages/rpc-client/test/index.test.ts new file mode 100644 index 0000000..d1042c6 --- /dev/null +++ b/packages/rpc-client/test/index.test.ts @@ -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); +}); diff --git a/packages/rpc-client/tsconfig.json b/packages/rpc-client/tsconfig.json new file mode 100644 index 0000000..7b8fdf4 --- /dev/null +++ b/packages/rpc-client/tsconfig.json @@ -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 + } +} diff --git a/packages/test-app/common/context.ts b/packages/test-app/common/context.ts new file mode 100644 index 0000000..4995889 --- /dev/null +++ b/packages/test-app/common/context.ts @@ -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(defaultState()); diff --git a/packages/test-app/common/contracts.ts b/packages/test-app/common/contracts.ts new file mode 100644 index 0000000..049316d --- /dev/null +++ b/packages/test-app/common/contracts.ts @@ -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)))`, + }, +]; diff --git a/packages/test-app/common/use-faucet.ts b/packages/test-app/common/use-faucet.ts new file mode 100644 index 0000000..8a11044 --- /dev/null +++ b/packages/test-app/common/use-faucet.ts @@ -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(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, + }; +}; diff --git a/packages/test-app/common/use-stx-address.ts b/packages/test-app/common/use-stx-address.ts new file mode 100644 index 0000000..1e067f0 --- /dev/null +++ b/packages/test-app/common/use-stx-address.ts @@ -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; +}; diff --git a/packages/test-app/common/utils.ts b/packages/test-app/common/utils.ts new file mode 100644 index 0000000..3ce76d3 --- /dev/null +++ b/packages/test-app/common/utils.ts @@ -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); diff --git a/packages/test-app/components/app.tsx b/packages/test-app/components/app.tsx index 418feef..361e338 100755 --- a/packages/test-app/components/app.tsx +++ b/packages/test-app/components/app.tsx @@ -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 => ( - -); - -const AppContent: React.FC = () => { - const { doOpenAuth, doAuth } = useConnect(); - - return ( - - - Blockstack Connect - - - - - - - - - - ); -}; - -interface AppState { - [key: string]: any; -} - -const SignedIn = (props: { username: string; handleSignOut: () => void }) => { - return ( - - - Welcome back! - - - {props.username} - - - - - - ); -}; - export const App: React.FC = () => { - const [state, setState] = React.useState({}); + const [state, setState] = React.useState(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 ( - - - {authResponse && } - {appPrivateKey && } + + + + {authResponse && } + {appPrivateKey && } - {!isSignedIn ? ( - - ) : ( - - )} - +
+ + + + ); diff --git a/packages/test-app/components/auth.tsx b/packages/test-app/components/auth.tsx new file mode 100644 index 0000000..876be0e --- /dev/null +++ b/packages/test-app/components/auth.tsx @@ -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 ( + + + Sign in with your Blockstack account to try the demo + + + + + + + ); +}; diff --git a/packages/test-app/components/counter-actions.tsx b/packages/test-app/components/counter-actions.tsx new file mode 100644 index 0000000..bbe8864 --- /dev/null +++ b/packages/test-app/components/counter-actions.tsx @@ -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(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 ( + + {!userData && Log in to change the state of this smart contract.} + + + + + + {error && ( + + {error} + + )} + {txId && !loading && } + {counter !== null && !loading && ( + Current counter value: {counter} + )} + + ); +}; diff --git a/packages/test-app/components/counter.tsx b/packages/test-app/components/counter.tsx new file mode 100644 index 0000000..3c2a577 --- /dev/null +++ b/packages/test-app/components/counter.tsx @@ -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([]); + 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 ( + + + Counter smart contract + + + Try a smart contract that keeps a single "counter" state variable. The public methods + "increment" and "decrement" change the value of the counter. + + + + {transactions.length > 0 && ( + <> + + Latest changes + + + {transactions.slice(0, 3).map(t => ( + + ))} + + + )} + + + + ); +}; diff --git a/packages/test-app/components/deploy.tsx b/packages/test-app/components/deploy.tsx new file mode 100644 index 0000000..3aae71b --- /dev/null +++ b/packages/test-app/components/deploy.tsx @@ -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 ( + + + Contract Deploy + + + Deploy a Clarity smart contract. To keep things simple, we'll provide a contract for you. + + {tx && } + + + + + ); +}; diff --git a/packages/test-app/components/explorer-link.tsx b/packages/test-app/components/explorer-link.tsx new file mode 100644 index 0000000..9ad5457 --- /dev/null +++ b/packages/test-app/components/explorer-link.tsx @@ -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 = ({ 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 ( + + window.open(url, '_blank')} color="blue" display="inline-block" my={3}> + {text || 'View transaction in explorer'} + + + ); +}; diff --git a/packages/test-app/components/faucet.tsx b/packages/test-app/components/faucet.tsx new file mode 100644 index 0000000..e1ba2d5 --- /dev/null +++ b/packages/test-app/components/faucet.tsx @@ -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) => { + 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 ( + + + Faucet + + + Receive some free testnet STX for testing out the network. STX are required to execute smart + contract functions. + + {tx && } + {error && ( + + {error} + + )} + + + + + + + + + + ); +}; diff --git a/packages/test-app/components/header.tsx b/packages/test-app/components/header.tsx new file mode 100644 index 0000000..550bf7f --- /dev/null +++ b/packages/test-app/components/header.tsx @@ -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 = ({ signOut }) => { + const state = useContext(AppContext); + return ( + + + + + Blockstack + + + {state.userData ? ( + + { + signOut(); + }} + > + Sign out + + + ) : null} + + ); +}; diff --git a/packages/test-app/components/home.tsx b/packages/test-app/components/home.tsx new file mode 100644 index 0000000..33bfa86 --- /dev/null +++ b/packages/test-app/components/home.tsx @@ -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 = ({ children, ...props }) => { + return ( + + + {children} + + + ); +}; + +export const Home: React.FC = () => { + const state = useContext(AppContext); + const [tab, setTab] = useState('status'); + const faucet = useFaucet(); + + const Page: React.FC = () => { + if (faucet.loading) { + return ( + + + Setting up... + + ); + } + if (faucet.balance === 0) { + return ( + + + + Please wait while we get you some Stacks tokens to test with + + + + ); + } + + return ( + <> + + + + setTab('status')}>Status smart contract + + + setTab('counter')}>Counter smart contract + + + + {tab === 'status' ? : } + + ); + }; + return ( + + + + Testnet Demo + + + {state.userData ? ( + + ) : ( + + + + )} + + ); +}; diff --git a/packages/test-app/components/status.tsx b/packages/test-app/components/status.tsx new file mode 100644 index 0000000..f74daed --- /dev/null +++ b/packages/test-app/components/status.tsx @@ -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([]); + 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) => { + setStatus(evt.currentTarget.value || ''); + }; + + const handleAddress = (evt: React.FormEvent) => { + setAddress(evt.currentTarget.value || ''); + }; + + return ( + + + Status smart contract + + + 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. + + + + {transactions.length > 0 && ( + <> + + Latest statuses + + + {transactions.slice(0, 3).map(t => ( + + ))} + + + )} + + + Write a status + + + + ) => { + if (e.key === 'enter') { + onSubmitWrite(); + } + }} + /> + + {txId && } + + + + Read a status + + + {stxAddress && ( + + If you want to read your own status, your address is {stxAddress}. + + )} + + + ) => { + if (e.key === 'enter') { + onSubmitRead(); + } + }} + /> + + + {readStatus && ( + + {readStatus} + + )} + + {error && ( + + {error} + + )} + + + ); +}; diff --git a/packages/test-app/components/stx-transfer.tsx b/packages/test-app/components/stx-transfer.tsx new file mode 100644 index 0000000..5085dfd --- /dev/null +++ b/packages/test-app/components/stx-transfer.tsx @@ -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) => { + 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 ( + + + STX Transfer + + + Send a small amount of STX to a different user. + + {tx && } + {error && ( + + {error} + + )} + + + + + + + + + + ); +}; diff --git a/packages/test-app/components/tab.tsx b/packages/test-app/components/tab.tsx new file mode 100644 index 0000000..6394017 --- /dev/null +++ b/packages/test-app/components/tab.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Box, BoxProps } from '@blockstack/ui'; + +export const InactiveTab: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const ActiveTab: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +interface TabProps extends BoxProps { + active: boolean; +} + +export const Tab: React.FC = ({ active, ...rest }) => { + if (active) { + return ; + } + return ; +}; diff --git a/packages/test-app/components/tx-card.tsx b/packages/test-app/components/tx-card.tsx new file mode 100644 index 0000000..3e2ef60 --- /dev/null +++ b/packages/test-app/components/tx-card.tsx @@ -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 = ({ tx, label }) => { + const addr = tx.sender_address; + const shortAddr = `${addr.slice(0, 5)}...${addr.slice(addr.length - 6, addr.length - 1)}`; + return ( + { + const url = `https://testnet-explorer.blockstack.org/txid/${tx.tx_id}`; + window.open(url, '_blank'); + }} + > + + + {shortAddr} + + + {toRelativeTime(tx.burn_block_time * 1000)} + + + + {label} + + + ); +}; diff --git a/packages/test-app/contracts/counter.clar b/packages/test-app/contracts/counter.clar new file mode 100644 index 0000000..533420b --- /dev/null +++ b/packages/test-app/contracts/counter.clar @@ -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)) diff --git a/packages/test-app/contracts/status.clar b/packages/test-app/contracts/status.clar new file mode 100644 index 0000000..76c2094 --- /dev/null +++ b/packages/test-app/contracts/status.clar @@ -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) + ) +) diff --git a/packages/test-app/contracts/stream.clar b/packages/test-app/contracts/stream.clar new file mode 100644 index 0000000..08de610 --- /dev/null +++ b/packages/test-app/contracts/stream.clar @@ -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))) + ) + ) +) + diff --git a/packages/test-app/jest.config.js b/packages/test-app/jest.config.js new file mode 100644 index 0000000..68080c2 --- /dev/null +++ b/packages/test-app/jest.config.js @@ -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': '/../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: ['/tests', '/src', '/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, +}; diff --git a/packages/test-app/package.json b/packages/test-app/package.json index d39ca30..dfed149 100755 --- a/packages/test-app/package.json +++ b/packages/test-app/package.json @@ -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", diff --git a/packages/test-app/tests/contracts.test.ts b/packages/test-app/tests/contracts.test.ts new file mode 100644 index 0000000..9fa212a --- /dev/null +++ b/packages/test-app/tests/contracts.test.ts @@ -0,0 +1,86 @@ +import { Client, Provider, ProviderRegistry, Result, ResultInterface } from '@blockstack/clarity'; + +function unwrapOkResult(input: ResultInterface): 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(); + }); +}); diff --git a/packages/test-app/tsconfig.json b/packages/test-app/tsconfig.json index d1f7112..2a0907c 100755 --- a/packages/test-app/tsconfig.json +++ b/packages/test-app/tsconfig.json @@ -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/**/*" ] } diff --git a/packages/test-app/webpack.config.js b/packages/test-app/webpack.config.js index 2a46864..5b36c48 100755 --- a/packages/test-app/webpack.config.js +++ b/packages/test-app/webpack.config.js @@ -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), diff --git a/packages/ui/package.json b/packages/ui/package.json index df06796..663de15 100755 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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" diff --git a/packages/ui/src/icons/external-icon.tsx b/packages/ui/src/icons/external-icon.tsx new file mode 100644 index 0000000..1032c01 --- /dev/null +++ b/packages/ui/src/icons/external-icon.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Svg } from '../svg'; +import { BoxProps } from '../box'; + +export const ExternalIcon = (props: BoxProps) => ( + + + +); diff --git a/packages/ui/src/icons/failed-icon.tsx b/packages/ui/src/icons/failed-icon.tsx new file mode 100644 index 0000000..9eb2247 --- /dev/null +++ b/packages/ui/src/icons/failed-icon.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Svg } from '../svg'; +import { BoxProps } from '../box'; + +export const FailedIcon: React.FC = ({ size = 64, ...props }) => ( + + + + + +); diff --git a/packages/ui/src/icons/index.tsx b/packages/ui/src/icons/index.tsx index 97461ec..3ef899d 100644 --- a/packages/ui/src/icons/index.tsx +++ b/packages/ui/src/icons/index.tsx @@ -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'; diff --git a/packages/ui/src/tooltip/index.tsx b/packages/ui/src/tooltip/index.tsx index f66c282..c744d13 100644 --- a/packages/ui/src/tooltip/index.tsx +++ b/packages/ui/src/tooltip/index.tsx @@ -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', }} > diff --git a/yarn.lock b/yarn.lock index 250dd6e..3ccaa4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"