diff --git a/app/api/get-account-details.ts b/app/api/api.ts similarity index 58% rename from app/api/get-account-details.ts rename to app/api/api.ts index 317787f..4e34059 100644 --- a/app/api/get-account-details.ts +++ b/app/api/api.ts @@ -1,5 +1,9 @@ import axios from 'axios'; -import { TransactionResults } from '@blockstack/stacks-blockchain-sidecar-types'; +import { + Transaction, + TransactionResults, + MempoolTransaction, +} from '@blockstack/stacks-blockchain-sidecar-types'; const api = 'https://sidecar.staging.blockstack.xyz/sidecar'; @@ -23,7 +27,19 @@ async function getAddressTransactions(address: string) { return await axios.get(api + `/v1/address/${address}/transactions`); } +type AnyTransaction = Transaction | MempoolTransaction; + +async function getTxDetails(txid: string) { + return await axios.get(api + `/v1/tx/${txid}/transactions`); +} +// async function getStxPriceInDollars() { +// return await axios.get( +// 'https://api.coingecko.com/api/v3/simple/price?ids=blockstack&vs_currencies=usd' +// ); +// } + export const Api = { getAddressBalance, getAddressTransactions, + getTxDetails, }; diff --git a/app/components/home/transaction-list/transaction-list-item-pending.tsx b/app/components/home/transaction-list/transaction-list-item-pending.tsx index 164447c..e968b16 100644 --- a/app/components/home/transaction-list/transaction-list-item-pending.tsx +++ b/app/components/home/transaction-list/transaction-list-item-pending.tsx @@ -2,16 +2,17 @@ import React, { FC } from 'react'; import { useHover } from 'use-events'; import { Box, Flex, Text } from '@blockstack/ui'; +import { PendingTransaction } from '../../../store/pending-transaction'; import { listHoverProps, EnableBefore } from './transaction-list-item-hover'; import { TransactionIcon } from './transaction-icon'; interface TransactionListItemPendingProps { - txId: string; + tx: PendingTransaction; onSelectTx: (txId: string) => void; } export const TransactionListItemPending: FC = ({ - txId, + tx, onSelectTx, }) => { const [hovered, bind] = useHover(); @@ -23,22 +24,22 @@ export const TransactionListItemPending: FC = ( cursor="pointer" position="relative" _before={listHoverProps(hovered)} - onClick={() => onSelectTx(txId)} - data-txid={txId} + onClick={() => onSelectTx(tx.txId)} + data-txid={tx.txId} {...bind} > - Pending + Sending - {'0x' + txId.substr(0, 14)} + {tx.txId.substr(0, 28)} - xxx STX + {tx.amount} STX Pending diff --git a/app/constants/index.ts b/app/constants/index.ts index 25a4e97..0cc8322 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -1 +1,9 @@ export const MNEMONIC_ENTROPY = 256; + +type Environments = 'development' | 'testing' | 'production'; + +export const ENV = (process.env.NODE_ENV ?? 'production') as Environments; + +export const features = { + stackingEnabled: false, +}; diff --git a/app/crypto/create-stx-tx.ts b/app/crypto/create-stx-tx.ts new file mode 100644 index 0000000..b3f14a2 --- /dev/null +++ b/app/crypto/create-stx-tx.ts @@ -0,0 +1,23 @@ +import BN from 'bn.js'; +import { deriveRootKeychainFromMnemonic } from '@blockstack/keychain'; +import { makeSTXTokenTransfer } from '@blockstack/stacks-transactions'; + +import { stacksNetwork } from './environment'; +import { deriveStxAddressKeychain } from './derive-address-keychain'; + +interface CreateStxTxArgs { + mnemonic: string; + recipient: string; + amount: BN; +} + +export async function createStxTransaction({ mnemonic, recipient, amount }: CreateStxTxArgs) { + const rootNode = await deriveRootKeychainFromMnemonic(mnemonic); + const { privateKey } = deriveStxAddressKeychain(rootNode); + return await makeSTXTokenTransfer({ + recipient, + amount, + senderKey: privateKey, + network: stacksNetwork, + }); +} diff --git a/app/crypto/derive-address-keychain.ts b/app/crypto/derive-address-keychain.ts new file mode 100644 index 0000000..ec0c555 --- /dev/null +++ b/app/crypto/derive-address-keychain.ts @@ -0,0 +1,4 @@ +import { deriveStxAddressChain } from '@blockstack/keychain'; +import { chain } from './environment'; + +export const deriveStxAddressKeychain = deriveStxAddressChain(chain); diff --git a/app/crypto/environment.ts b/app/crypto/environment.ts new file mode 100644 index 0000000..5848a0d --- /dev/null +++ b/app/crypto/environment.ts @@ -0,0 +1,14 @@ +import { + ChainID, + StacksNetwork, + StacksTestnet, + StacksMainnet, +} from '@blockstack/stacks-transactions'; +import { ENV } from '../constants'; + +export { ChainID }; + +export const chain = ENV === 'development' || ENV === 'testing' ? ChainID.Testnet : ChainID.Mainnet; + +export const stacksNetwork: StacksNetwork = + ENV === 'development' || ENV === 'testing' ? new StacksTestnet() : new StacksMainnet(); diff --git a/app/crypto/validate-address-net.ts b/app/crypto/validate-address-net.ts new file mode 100644 index 0000000..d3edc14 --- /dev/null +++ b/app/crypto/validate-address-net.ts @@ -0,0 +1,12 @@ +import { chain, ChainID } from './environment'; + +export function validateAddressChain(address: string) { + const prefix = address.substr(0, 2); + if (chain === ChainID.Testnet) { + return prefix === 'SN' || prefix === 'ST'; + } + if (chain === ChainID.Mainnet) { + return prefix === 'SM' || prefix === 'SP'; + } + return false; +} diff --git a/app/hooks/use-interval.ts b/app/hooks/use-interval.ts index 98c8e2b..b0f18f2 100644 --- a/app/hooks/use-interval.ts +++ b/app/hooks/use-interval.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; export function useInterval(callback: () => void, delay: number) { + // eslint-disable-next-line @typescript-eslint/no-empty-function const savedCallback = useRef(() => {}); useEffect(() => { diff --git a/app/main.dev.ts b/app/main.dev.ts index 37cb03e..e3cc55b 100644 --- a/app/main.dev.ts +++ b/app/main.dev.ts @@ -28,29 +28,7 @@ export default class AppUpdater { } } -contextMenu({ - prepend: (defaultActions, params, browserWindow) => [ - // { - // label: 'Rainbow', - // // Only show it when right-clicking images - // visible: params.mediaType === 'image', - // }, - { - label: 'Search Google for “{selection}”', - // Only show it when right-clicking text - visible: params.selectionText.trim().length > 0, - click: (menuItem, browserWindow, event) => { - console.log({ menuItem, browserWindow, event }); - // shell.openExternal( - // `https://google.com/search?q=${encodeURIComponent(params.selectionText)}` - // ); - }, - }, - // { - // label: 'Open in Explorer', - // } - ], -}); +contextMenu(); let mainWindow: BrowserWindow | null = null; diff --git a/app/modals/receive-stx/receive-stx-modal.tsx b/app/modals/receive-stx/receive-stx-modal.tsx new file mode 100644 index 0000000..89c8eff --- /dev/null +++ b/app/modals/receive-stx/receive-stx-modal.tsx @@ -0,0 +1,64 @@ +import React, { FC, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Qr from 'qrcode.react'; +import { Text, Modal, Button, Flex, Box, useClipboard } from '@blockstack/ui'; + +import { TxModalHeader, TxModalFooter } from '../transaction/transaction-modal-layout'; +import { homeActions, selectReceiveModalOpen } from '../../store/home/home.reducer'; + +interface ReceiveStxModalProps { + address: string; +} + +export const ReceiveStxModal: FC = ({ address }) => { + const dispatch = useDispatch(); + const modalOpen = useSelector(selectReceiveModalOpen); + const copyAddressToClipboard = useClipboard(address); + const [buttonText, setButtonText] = useState('Copy address'); + const onCopyAddress = () => { + copyAddressToClipboard.onCopy(); + setButtonText('Copied'); + setTimeout(() => setButtonText('Copy address'), 1000); + }; + const closeModal = () => dispatch(homeActions.closeReceiveModal()); + if (!modalOpen) return null; + return ( + Receive STX} + footerComponent={ + + + + } + > + + + + + + Wallet address + + + + {address} + + + + + + ); +}; diff --git a/app/modals/transaction/transaction-modal.tsx b/app/modals/transaction/transaction-modal.tsx index b1e63a5..c0deb41 100644 --- a/app/modals/transaction/transaction-modal.tsx +++ b/app/modals/transaction/transaction-modal.tsx @@ -1,13 +1,14 @@ import React, { FC, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { Modal, Text, Button } from '@blockstack/ui'; import { useFormik } from 'formik'; import * as yup from 'yup'; import BN from 'bn.js'; +import { Modal, Text, Button, safeAwait } from '@blockstack/ui'; +import { broadcastTransaction, StacksTransaction } from '@blockstack/stacks-transactions'; -import { RootState } from '../../store'; +import { RootState, Dispatch } from '../../store'; import { validateStacksAddress } from '../../utils/get-stx-transfer-direction'; -import { broadcastStxTransaction } from '../../store/transaction/transaction.actions'; +// import { broadcastStxTransaction } from '../../store/transaction/transaction.actions'; import { selectTxModalOpen, homeActions } from '../../store/home/home.reducer'; import { TxModalForm } from './transaction-form'; import { selectMnemonic } from '../../store/keys'; @@ -19,6 +20,9 @@ import { TxModalPreviewItem, modalStyle, } from './transaction-modal-layout'; +import { stacksNetwork } from '../../crypto/environment'; +import { createStxTransaction } from '../../crypto/create-stx-tx'; +import { validateAddressChain } from '../../crypto/validate-address-net'; interface TxModalProps { balance: string; @@ -30,15 +34,19 @@ enum TxModalStep { PreviewAndSend, } -type ModalComponents = { +type ModalComponents = () => { [component in 'header' | 'body' | 'footer']: JSX.Element; }; export const TransactionModal: FC = ({ balance, address }) => { const dispatch = useDispatch(); const [step, setStep] = useState(TxModalStep.DescribeTx); - const { txModalOpen } = useSelector((state: RootState) => ({ + const [fee, setFee] = useState(new BN(0)); + const [tx, setTx] = useState(null); + const [loading, setLoading] = useState(false); + const { mnemonic, txModalOpen } = useSelector((state: RootState) => ({ txModalOpen: selectTxModalOpen(state), + mnemonic: selectMnemonic(state), })); const form = useFormik({ @@ -49,9 +57,12 @@ export const TransactionModal: FC = ({ balance, address }) => { validationSchema: yup.object().shape({ address: yup .string() - .test('test-is-stx-address', 'Must be a valid Stacks Address', value => + .test('test-is-stx-address', 'Must be a valid Stacks Address', (value = '') => validateStacksAddress(value) ) + .test('test-is-for-valid-chain', 'Address is for incorrect network', (value = '') => + validateAddressChain(value) + ) .test( 'test-is-not-my-address', 'You cannot send Stacks to yourself', @@ -64,7 +75,20 @@ export const TransactionModal: FC = ({ balance, address }) => { .min(1, 'Smallest transaction is 1 STX') .required(), }), - onSubmit: () => setStep(TxModalStep.PreviewAndSend), + onSubmit: async () => { + if (!mnemonic) return; + setLoading(true); + const tx = await createStxTransaction({ + mnemonic, + recipient: form.values.address, + amount: new BN(form.values.amount), + }); + // handle errors + setTx(tx); + setFee(tx.auth.spendingCondition?.fee as BN); + setStep(TxModalStep.PreviewAndSend); + setLoading(false); + }, }); if (!txModalOpen) return null; @@ -75,16 +99,41 @@ export const TransactionModal: FC = ({ balance, address }) => { form.resetForm(); }; - const broadcastTx = () => - dispatch( - broadcastStxTransaction({ - recipient: form.values.address, - amount: new BN(form.values.amount), - }) - ); + interface BroadcastStxTxArgs { + amount: BN; + recipient: string; + } + + function broadcastStxTransaction({ tx }: { tx: StacksTransaction }) { + return async (dispatch: Dispatch, getState: () => RootState) => { + const [error, blockchainResponse] = await safeAwait(broadcastTransaction(tx, stacksNetwork)); + + if (error || !blockchainResponse) return null; + console.log({ error }); + // anything but string of id === error + console.log(blockchainResponse); + if (typeof blockchainResponse !== 'string') { + // setError for ui + return; + } + // dispatch( + // addPendingTransaction({ + // txId: pendingTxId as string, + // amount: amount.toString(), + // time: +new Date(), + // }) + // ); + return blockchainResponse; + }; + } + + const broadcastTx = () => { + if (tx === null) return; + dispatch(broadcastStxTransaction({ tx })); + }; const txFormStepMap: { [step in TxModalStep]: ModalComponents } = { - [TxModalStep.DescribeTx]: { + [TxModalStep.DescribeTx]: () => ({ header: Send STX, body: , footer: ( @@ -92,13 +141,18 @@ export const TransactionModal: FC = ({ balance, address }) => { - ), - }, - [TxModalStep.PreviewAndSend]: { + }), + [TxModalStep.PreviewAndSend]: () => ({ header: Confirm and send, body: ( @@ -106,8 +160,10 @@ export const TransactionModal: FC = ({ balance, address }) => { {form.values.address} {form.values.amount} - 0.0323 STX - 18929 STX + {fee.toString()} µSTX + + {new BN(form.values.amount).add(fee).toString()} µSTX + ), footer: ( @@ -120,10 +176,10 @@ export const TransactionModal: FC = ({ balance, address }) => { ), - }, + }), }; - const { header, body, footer } = txFormStepMap[step]; + const { header, body, footer } = txFormStepMap[step](); return ( diff --git a/app/pages/home/home.tsx b/app/pages/home/home.tsx index 8e8c520..03920dd 100644 --- a/app/pages/home/home.tsx +++ b/app/pages/home/home.tsx @@ -10,10 +10,8 @@ import { import { selectAddress } from '../../store/keys/keys.reducer'; import { getAddressDetails } from '../../store/address/address.actions'; import { selectAddressBalance } from '../../store/address/address.reducer'; -import { - selectTransactions, - selectPendingTransactions, -} from '../../store/transaction/transaction.reducer'; +import { selectTransactions } from '../../store/transaction/transaction.reducer'; +import { selectPendingTransactions } from '../../store/pending-transaction/pending-transaction.reducer'; import { homeActions } from '../../store/home/home.reducer'; import { TransactionList, @@ -23,47 +21,74 @@ import { BalanceCard, } from '../../components/home'; import { TransactionModal } from '../../modals/transaction/transaction-modal'; -import { HomeLayout } from './home-layout'; +import { ReceiveStxModal } from '../../modals/receive-stx/receive-stx-modal'; +import { useInterval } from '../../hooks/use-interval'; import { TransactionListItemPending } from '../../components/home/transaction-list/transaction-list-item-pending'; +import { pendingTransactionSuccessful } from '../../store/transaction/transaction.actions'; +import { Api } from '../../api/api'; +import { safelyFormatHexTxid } from '../../utils/safe-handle-txid'; +import { safeAwait } from '../../utils/safe-await'; +import { HomeLayout } from './home-layout'; export const Home: FC = () => { const dispatch = useDispatch(); - const { address, balance, transactions, pendingTransactions } = useSelector( - (state: RootState) => ({ - address: selectAddress(state), - transactions: selectTransactions(state), - balance: selectAddressBalance(state), - pendingTransactions: selectPendingTransactions(state), - }) - ); + const { address, balance, txs, pendingTxs } = useSelector((state: RootState) => ({ + address: selectAddress(state), + txs: selectTransactions(state), + balance: selectAddressBalance(state), + pendingTxs: selectPendingTransactions(state), + })); + + const checkIfPendingTxIsComplete = async (address: string) => { + console.log({ pending: address }); + const [error, txResponse] = await safeAwait(Api.getTxDetails(address)); + if (error || !txResponse || txResponse.data.tx_status === 'pending') { + console.log('Error, it do not exist'); + return; + } + if (txResponse.data.tx_status === 'success') { + dispatch(pendingTransactionSuccessful(txResponse.data)); + } + }; useInterval(() => { if (!address) return; - dispatch(getAddressTransactions(address)); - dispatch(getAddressDetails(address)); - }, 10_000); + pendingTxs.forEach(tx => void checkIfPendingTxIsComplete(safelyFormatHexTxid(tx.txId))); + // dispatch(getAddressTransactions(address)); + // dispatch(getAddressDetails(address)); + }, 5_000); useEffect(() => { if (!address) return; - // STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6 + // STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6s dispatch(getAddressTransactions(address)); - dispatch(getAddressDetails(address)); + // dispatch(getAddressDetails(address)); }, [dispatch, address]); if (!address) return ; const transactionList = ( - - {transactions.map(tx => ( - - ))} - + <> + + {pendingTxs.map(pTx => ( + + ))} + {txs.map(tx => ( + + ))} + + ); const balanceCard = ( dispatch(homeActions.openModal())} - onSelectReceive={() => ({})} + onSelectSend={() => dispatch(homeActions.openTxModal())} + onSelectReceive={() => dispatch(homeActions.openReceiveModal())} /> ); const stackingPromoCard = ; @@ -73,6 +98,7 @@ export const Home: FC = () => { return ( <> + ('address/fetch-address-done'); diff --git a/app/store/address/address.reducer.ts b/app/store/address/address.reducer.ts index 4064b59..ab53000 100644 --- a/app/store/address/address.reducer.ts +++ b/app/store/address/address.reducer.ts @@ -8,7 +8,8 @@ export interface AddressState { } const initialState: AddressState = { - balance: null, + // balance: null, + balance: '100900', }; export const addressReducer = createReducer(initialState, builder => diff --git a/app/store/home/home.reducer.ts b/app/store/home/home.reducer.ts index 7b8de4a..10fe4b6 100644 --- a/app/store/home/home.reducer.ts +++ b/app/store/home/home.reducer.ts @@ -4,18 +4,22 @@ import { RootState } from '..'; export interface HomeState { txModalOpen: boolean; + receiveModalOpen: boolean; } const initialState: HomeState = { txModalOpen: false, + receiveModalOpen: false, }; export const homeSlice = createSlice({ name: 'home', initialState, reducers: { - openModal: state => ({ ...state, txModalOpen: true }), - closeModal: state => ({ ...state, txModalOpen: false }), + openTxModal: () => ({ txModalOpen: true, receiveModalOpen: false }), + closeTxModal: state => ({ ...state, txModalOpen: false }), + openReceiveModal: () => ({ receiveModalOpen: true, txModalOpen: false }), + closeReceiveModal: state => ({ ...state, receiveModalOpen: false }), }, }); @@ -24,3 +28,7 @@ export const homeActions = homeSlice.actions; export const selectHomeState = (state: RootState) => state.home; export const selectTxModalOpen = createSelector(selectHomeState, state => state.txModalOpen); +export const selectReceiveModalOpen = createSelector( + selectHomeState, + state => state.receiveModalOpen +); diff --git a/app/store/index.ts b/app/store/index.ts index ae3385f..92f7f0b 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -6,11 +6,16 @@ import { KeysState, createKeysReducer } from './keys'; import { TransactionState, transactionReducer } from './transaction/transaction.reducer'; import { addressReducer, AddressState } from './address'; import { HomeState, homeSlice } from './home/home.reducer'; +import { + pendingTransactionReducer, + PendingTransactionState, +} from './pending-transaction/pending-transaction.reducer'; export interface RootState { router: any; keys: KeysState; transaction: TransactionState; + pendingTransaction: PendingTransactionState; address: AddressState; home: HomeState; } @@ -31,6 +36,7 @@ export function createRootReducer({ history, keys }: RootReducerArgs) { router: connectRouter(history), keys: createKeysReducer(keys), transaction: transactionReducer, + pendingTransaction: pendingTransactionReducer, address: addressReducer, home: homeSlice.reducer, }); diff --git a/app/store/pending-transaction/index.ts b/app/store/pending-transaction/index.ts new file mode 100644 index 0000000..e71c603 --- /dev/null +++ b/app/store/pending-transaction/index.ts @@ -0,0 +1 @@ +export * from './pending-transaction.reducer'; diff --git a/app/store/pending-transaction/pending-transaction.reducer.ts b/app/store/pending-transaction/pending-transaction.reducer.ts new file mode 100644 index 0000000..a78924e --- /dev/null +++ b/app/store/pending-transaction/pending-transaction.reducer.ts @@ -0,0 +1,55 @@ +import { createEntityAdapter, EntityState, createSlice } from '@reduxjs/toolkit'; + +import { RootState } from '..'; +import { pendingTransactionSuccessful } from '../transaction'; + +export interface PendingTransaction { + txId: string; + amount: string; + time: number; +} + +export type PendingTransactionState = EntityState; + +const pendingTransactionAdapter = createEntityAdapter({ + selectId: pendingTx => pendingTx.txId, + sortComparer: (pTx1, pTx2) => pTx2.time - pTx1.time, +}); + +const initialState = pendingTransactionAdapter.getInitialState({ + // + // TODO: remove demo content when faucet is working + // ids: ['58bc2c34c70184e36023d55eda7e3fd17c695833c3b4fdc0187cd09d929c15e201'], + // entities: { + // '58bc2c34c70184e36023d55eda7e3fd17c695833c3b4fdc0187cd09d929c15e201': { + // txId: '58bc2c34c70184e36023d55eda7e3fd17c695833c3b4fdc0187cd09d929c15e201', + // time: 2342342, + // amount: '384', + // } as PendingTransaction, + // }, +}); + +const pendingTransactionSlice = createSlice({ + name: 'pendingTransactions', + initialState, + reducers: { + addPendingTransaction: pendingTransactionAdapter.addOne, + removePendingTransaction: pendingTransactionAdapter.removeOne, + }, + extraReducers: { + [pendingTransactionSuccessful.type]: ( + state, + action: ReturnType + ) => pendingTransactionAdapter.removeOne(state, action.payload.tx_id), + }, +}); + +export const pendingTransactionReducer = pendingTransactionSlice.reducer; + +const selectPendingTransactionState = (state: RootState) => state.pendingTransaction; +const selectors = pendingTransactionAdapter.getSelectors(selectPendingTransactionState); + +export const selectPendingTransactions = selectors.selectAll; + +export const addPendingTransaction = pendingTransactionSlice.actions.addPendingTransaction; +export const removePendingTransaction = pendingTransactionSlice.actions.removePendingTransaction; diff --git a/app/store/transaction/index.ts b/app/store/transaction/index.ts index a830f16..f7a942f 100644 --- a/app/store/transaction/index.ts +++ b/app/store/transaction/index.ts @@ -1 +1,2 @@ export * from './transaction.reducer'; +export * from './transaction.actions'; diff --git a/app/store/transaction/transaction.actions.ts b/app/store/transaction/transaction.actions.ts index 86c9ecd..96164af 100644 --- a/app/store/transaction/transaction.actions.ts +++ b/app/store/transaction/transaction.actions.ts @@ -12,10 +12,13 @@ import { import BN from 'bn.js'; import { Dispatch, RootState } from '../index'; -import { Api } from '../../api/get-account-details'; +import { Api } from '../../api/api'; import { selectMnemonic } from '../keys/keys.reducer'; +// import { addPendingTransaction } from '../pending-transaction'; -export const addPendingTransaction = createAction('transactions/add-pending'); +export const pendingTransactionSuccessful = createAction( + 'transactions/pending-transaction-successful' +); const fetchTxName = 'transactions/fetch-transactions'; export const fetchTransactions = createAction(fetchTxName); @@ -41,33 +44,8 @@ export const broadcastTx = createAction('transactions/broadcast-transactions'); export const broadcastTxDone = createAction('transactions/broadcast-transactions-done'); export const broadcastTxFail = createAction('transactions/broadcast-transactions-fail'); -interface BroadcastStxTxArgs { - amount: BN; - recipient: string; -} - -export function broadcastStxTransaction({ amount, recipient }: BroadcastStxTxArgs) { - return async (dispatch: Dispatch, getState: () => RootState) => { - const state = getState(); - const mnemonic = selectMnemonic(state); - if (!mnemonic) throw new Error('Cannot broadcast tx without decrypted mnemonic'); - const rootNode = await deriveRootKeychainFromMnemonic(mnemonic); - const { privateKey } = deriveStxAddressChain(ChainID.Testnet)(rootNode); - const network = new StacksTestnet(); - const txOptions = { - recipient, - amount, - senderKey: privateKey, - network, - }; - const tx = await makeSTXTokenTransfer(txOptions); - // console.log(tx.txid()); - const pendingTransactionId = await broadcastTransaction(tx, network); - console.log(pendingTransactionId); - dispatch(addPendingTransaction(pendingTransactionId)); - }; -} - export async function openInExplorer(txId: string) { - return await shell.openExternal(`https://testnet-explorer.blockstack.org/txid/${txId}`); + return await shell.openExternal( + `https://testnet-explorer.blockstack.org/txid/${txId}?wallet=true` + ); } diff --git a/app/store/transaction/transaction.reducer.ts b/app/store/transaction/transaction.reducer.ts index 102471c..6fdc551 100644 --- a/app/store/transaction/transaction.reducer.ts +++ b/app/store/transaction/transaction.reducer.ts @@ -1,43 +1,25 @@ -import { createEntityAdapter, EntityState, createReducer, createSelector } from '@reduxjs/toolkit'; +import { createEntityAdapter, EntityState, createReducer } from '@reduxjs/toolkit'; import { Transaction } from '@blockstack/stacks-blockchain-sidecar-types'; import { RootState } from '..'; -import { fetchTransactionsDone, addPendingTransaction } from './transaction.actions'; +import { fetchTransactionsDone, pendingTransactionSuccessful } from './transaction.actions'; -export interface TransactionState extends EntityState { - pending: string[]; -} +export type TransactionState = EntityState; const transactionAdapter = createEntityAdapter({ selectId: transaction => transaction.tx_id, sortComparer: (tx1, tx2) => tx2.burn_block_time - tx1.burn_block_time, }); -const initialState = transactionAdapter.getInitialState({ - pending: [] as string[], -}); +const initialState = transactionAdapter.getInitialState(); export const transactionReducer = createReducer(initialState, builder => builder - // .addCase(fetchTransactionsDone, ) - .addCase(fetchTransactionsDone, (state, action) => { - transactionAdapter.addMany(state, action); - console.log(state.pending); - state.pending = state.pending.filter( - id => !action.payload.some(tx => tx.tx_id.replace('0x', '') === id) - ); - }) - .addCase(addPendingTransaction, (state, action) => ({ - ...state, - pending: [...state.pending, action.payload], - })) + .addCase(fetchTransactionsDone, transactionAdapter.addMany) + .addCase(pendingTransactionSuccessful, transactionAdapter.addOne) ); const selectTransactionState = (state: RootState) => state.transaction; const selectors = transactionAdapter.getSelectors(selectTransactionState); export const selectTransactions = selectors.selectAll; -export const selectPendingTransactions = createSelector( - selectTransactionState, - state => state.pending -); diff --git a/app/utils/safe-handle-txid.ts b/app/utils/safe-handle-txid.ts new file mode 100644 index 0000000..b85b953 --- /dev/null +++ b/app/utils/safe-handle-txid.ts @@ -0,0 +1,5 @@ +export function safelyFormatHexTxid(id: string) { + const prefix = '0x'; + if (id.startsWith('0x')) return id; + return prefix + id; +} diff --git a/app/yarn.lock b/app/yarn.lock index 174672f..5c4e058 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@blockstack/keychain@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@blockstack/keychain/-/keychain-0.5.0.tgz#bf4fcaa0055c7aa61370017847c5d224fea153d9" - integrity sha512-21Zu8j5FkI8dzPGaMn1vB0efcNtBCJ8Pg1dYQLgnpFyH8OhfAsVWuHRCReXtS3xxMVjnEZebs0YIIZzfAxZQ0A== +"@blockstack/keychain@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@blockstack/keychain/-/keychain-0.8.5.tgz#b25b38e97db2853f05f3d8905a12f981e74f454b" + integrity sha512-bVlLrgdlst9Beb7hPcJB0C/Hy0sOXDP2zMf8e9ppFEdR6gZStLx2tuEkLa+mGuWluTLHLscmeOXTxbyn9xf1rQ== dependencies: "@blockstack/stacks-transactions" "^0.4.6" bip39 "^3.0.2" diff --git a/package.json b/package.json index 76167e0..b6cf331 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "@babel/register": "7.10.4", "@blockstack/eslint-config": "1.0.5", "@blockstack/prettier-config": "0.0.6", - "@blockstack/stacks-blockchain-sidecar-types": "0.0.19", + "@blockstack/stacks-blockchain-sidecar-types": "0.0.20", "@commitlint/config-conventional": "9.0.1", "@types/bcryptjs": "2.4.2", "@types/bn.js": "4.11.6", @@ -147,6 +147,7 @@ "babel-plugin-transform-react-remove-prop-types": "0.4.24", "browserslist-config-erb": "0.0.1", "chalk": "4.1.0", + "circular-dependency-plugin": "5.2.0", "concurrently": "5.2.0", "cross-env": "7.0.2", "cross-spawn": "7.0.3", @@ -192,7 +193,7 @@ "dependencies": { "@blockstack/rpc-client": "0.2.0-alpha.0", "@blockstack/stacks-transactions": "0.5.1", - "@blockstack/ui": "2.9.4", + "@blockstack/ui": "2.10.7", "@hot-loader/react-dom": "16.13.0", "@reduxjs/toolkit": "1.4.0", "@styled-system/theme-get": "5.1.2", diff --git a/yarn.lock b/yarn.lock index ff14f37..b3643f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1680,10 +1680,10 @@ "@blockstack/stacks-transactions" "0.3.0-alpha.5" cross-fetch "^3.0.4" -"@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-blockchain-sidecar-types@0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@blockstack/stacks-blockchain-sidecar-types/-/stacks-blockchain-sidecar-types-0.0.20.tgz#d84e13dc56a2573707ba88dcb043ab7481d4e91d" + integrity sha512-bEoUh8eyYAempTj48KaF+94ecmXXfYsALlT7eqwhhivIPv+069IcYSGoLK0pvcACKLV6gyomBSu4dv7B+r5fuw== "@blockstack/stacks-transactions@0.3.0-alpha.5": version "0.3.0-alpha.5" @@ -4363,6 +4363,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +circular-dependency-plugin@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz#e09dbc2dd3e2928442403e2d45b41cea06bc0a93" + integrity sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"