diff --git a/haskoin-wallet.cabal b/haskoin-wallet.cabal index 57fe7a18..dd407f58 100644 --- a/haskoin-wallet.cabal +++ b/haskoin-wallet.cabal @@ -4,10 +4,10 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 0e1826aa518f8c7890f529498b3362e18f28ddf992adaf9192167af089f6bd44 +-- hash: 7bb228ac472d173832c3ea95377c2776825cd356999e71a2370fb1dd096ce9d6 name: haskoin-wallet -version: 0.8.0 +version: 0.8.1 synopsis: Lightweight command-line wallet for Bitcoin and Bitcoin Cash description: haskoin-wallet (hw) is a lightweight Bitcoin wallet using BIP39 mnemonics and BIP44 account structure. It requires a full blockchain index such as @@ -36,7 +36,7 @@ library Decimal >=0.5.1 , aeson >=1.4.6.0 , aeson-pretty >=0.8.8 - , ansi-terminal >=0.10.3 + , ansi-terminal >=1.0 , base >=4.9 && <5 , base16-bytestring >=1.0.0.0 , base64-bytestring >=1.0.0.3 @@ -62,7 +62,7 @@ library , pretty >=1.1.3.6 , random >=1.1 , raw-strings-qq >=1.1 - , secp256k1-haskell >=1.0.0 + , secp256k1-haskell >=1.1.0 , string-conversions >=0.4.0.1 , text >=1.2.4.0 , time >=1.12.2 @@ -80,6 +80,7 @@ library Haskoin.Wallet.FileIO Haskoin.Wallet.Main Haskoin.Wallet.Parser + Haskoin.Wallet.PrettyPrinter Haskoin.Wallet.Signing Haskoin.Wallet.TxInfo Haskoin.Wallet.Util @@ -96,7 +97,7 @@ executable hw Decimal >=0.5.1 , aeson >=1.4.6.0 , aeson-pretty >=0.8.8 - , ansi-terminal >=0.10.3 + , ansi-terminal >=1.0 , base >=4.9 && <5 , base16-bytestring >=1.0.0.0 , base64-bytestring >=1.0.0.3 @@ -111,7 +112,7 @@ executable hw , haskeline >=0.7.5.0 , haskoin-core >=1.0.0 , haskoin-store-data >=1.0.0 - , haskoin-wallet ==0.8.0 + , haskoin-wallet ==0.8.1 , http-types >=0.12.3 , lens >=4.18.1 , lens-aeson >=1.1 @@ -123,7 +124,7 @@ executable hw , pretty >=1.1.3.6 , random >=1.1 , raw-strings-qq >=1.1 - , secp256k1-haskell >=1.0.0 + , secp256k1-haskell >=1.1.0 , string-conversions >=0.4.0.1 , text >=1.2.4.0 , time >=1.12.2 @@ -154,7 +155,7 @@ test-suite spec , QuickCheck >=2.13.2 , aeson >=1.4.6.0 , aeson-pretty >=0.8.8 - , ansi-terminal >=0.10.3 + , ansi-terminal >=1.0 , base >=4.9 && <5 , base16-bytestring >=1.0.0.0 , base64-bytestring >=1.0.0.3 @@ -169,7 +170,7 @@ test-suite spec , haskeline >=0.7.5.0 , haskoin-core >=1.0.0 , haskoin-store-data >=1.0.0 - , haskoin-wallet ==0.8.0 + , haskoin-wallet ==0.8.1 , hspec >=2.7.1 , http-types >=0.12.3 , lens >=4.18.1 @@ -182,7 +183,7 @@ test-suite spec , pretty >=1.1.3.6 , random >=1.1 , raw-strings-qq >=1.1 - , secp256k1-haskell >=1.0.0 + , secp256k1-haskell >=1.1.0 , string-conversions >=0.4.0.1 , text >=1.2.4.0 , time >=1.12.2 diff --git a/package.yaml b/package.yaml index 4f57fee3..26c30845 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: haskoin-wallet -version: &version 0.8.0 +version: &version 0.8.1 synopsis: Lightweight command-line wallet for Bitcoin and Bitcoin Cash description: ! haskoin-wallet (hw) is a lightweight Bitcoin wallet using BIP39 mnemonics and @@ -24,7 +24,7 @@ bug-reports: http://github.com/haskoin/haskoin-wallet/issues dependencies: base: ">=4.9 && <5" aeson: ">= 1.4.6.0" - ansi-terminal: ">= 0.10.3" + ansi-terminal: ">= 1.0" aeson-pretty: ">= 0.8.8" base16-bytestring: ">= 1.0.0.0" base64-bytestring: ">= 1.0.0.3" @@ -51,7 +51,7 @@ dependencies: pretty: ">= 1.1.3.6" random: ">= 1.1" raw-strings-qq: ">= 1.1" - secp256k1-haskell: ">= 1.0.0" + secp256k1-haskell: ">= 1.1.0" string-conversions: ">= 0.4.0.1" text: ">= 1.2.4.0" time: ">= 1.12.2" diff --git a/src/Haskoin/Wallet/Commands.hs b/src/Haskoin/Wallet/Commands.hs index dac631cb..4496459c 100644 --- a/src/Haskoin/Wallet/Commands.hs +++ b/src/Haskoin/Wallet/Commands.hs @@ -53,7 +53,7 @@ data Response responseMnemonic :: ![Text], responseSplitMnemonic :: ![[Text]] } - | ResponseCreateAcc + | ResponseAccount { responseAccount :: !DBAccount } | ResponseTestAcc @@ -61,22 +61,13 @@ data Response responseResult :: !Bool, responseText :: !Text } - | ResponseImportAcc - { responseAccount :: !DBAccount - } - | ResponseExportAcc - { responseAccount :: !DBAccount, - responseAccountFile :: !FilePath - } - | ResponseRenameAcc - { responseAccount :: !DBAccount, - responseOldName :: !Text, - responseNewName :: !Text + | ResponseFile + { responseTxFile :: !FilePath } | ResponseAccounts { responseAccounts :: ![DBAccount] } - | ResponseReceive + | ResponseAddress { responseAccount :: !DBAccount, responseAddress :: !DBAddress } @@ -84,58 +75,28 @@ data Response { responseAccount :: !DBAccount, responseAddresses :: ![DBAddress] } - | ResponseLabel - { responseAccount :: !DBAccount, - responseAddress :: !DBAddress - } | ResponseTxs { responseAccount :: !DBAccount, responseTxs :: ![TxInfo] } - | ResponsePrepareTx + | ResponseTxInfo { responseAccount :: !DBAccount, responsePendingTx :: !NoSigTxInfo } - | ResponsePendingTxs + | ResponseTxInfos { responseAccount :: !DBAccount, responsePendingTxs :: ![NoSigTxInfo] } - | ResponseReviewTx - { responseAccount :: !DBAccount, - responsePendingTx :: !NoSigTxInfo - } - | ResponseExportTx - { responseTxFile :: !FilePath - } - | ResponseImportTx - { responseAccount :: !DBAccount, - responsePendingTx :: !NoSigTxInfo - } | ResponseDeleteTx - { responseFreedCoins :: !Natural, + { responseNoSigHash :: !TxHash, + responseFreedCoins :: !Natural, responseFreedAddrs :: !Natural } - | ResponseSignTx - { responseAccount :: !DBAccount, - responsePendingTx :: !NoSigTxInfo - } | ResponseCoins { responseAccount :: !DBAccount, responseCoins :: ![JsonCoin] } - | ResponseSendTx - { responseAccount :: !DBAccount, - responseTransaction :: !TxInfo, - responseNetworkTxId :: !TxHash - } - | ResponseSyncAcc - { responseAccount :: !DBAccount, - responseBestBlock :: !BlockHash, - responseBestHeight :: !Natural, - responseTxCount :: !Natural, - responseCoinCount :: !Natural - } - | ResponseDiscoverAcc + | ResponseSync { responseAccount :: !DBAccount, responseBestBlock :: !BlockHash, responseBestHeight :: !Natural, @@ -144,14 +105,6 @@ data Response } | ResponseVersion {responseVersion :: !Text} - | ResponsePrepareSweep - { responseAccount :: !DBAccount, - responsePendingTx :: !NoSigTxInfo - } - | ResponseSignSweep - { responseAccount :: !DBAccount, - responsePendingTx :: !NoSigTxInfo - } | ResponseRollDice { responseRollDice :: ![Natural], responseEntropySource :: !Text @@ -172,9 +125,9 @@ instance MarshalJSON Ctx Response where "mnemonic" .= w, "splitmnemonic" .= ws ] - ResponseCreateAcc a -> + ResponseAccount a -> object - [ "type" .= Json.String "createacc", + [ "type" .= Json.String "account", "account" .= a ] ResponseTestAcc a b t -> @@ -184,32 +137,19 @@ instance MarshalJSON Ctx Response where "result" .= b, "text" .= t ] - ResponseImportAcc a -> - object - [ "type" .= Json.String "importacc", - "account" .= a - ] - ResponseExportAcc a f -> + ResponseFile f -> object - [ "type" .= Json.String "exportacc", - "account" .= a, - "accountfile" .= f - ] - ResponseRenameAcc o n a -> - object - [ "type" .= Json.String "renameacc", - "oldname" .= o, - "newname" .= n, - "account" .= a + [ "type" .= Json.String "file", + "file" .= f ] ResponseAccounts as -> object [ "type" .= Json.String "accounts", "accounts" .= as ] - ResponseReceive a addr -> + ResponseAddress a addr -> object - [ "type" .= Json.String "receive", + [ "type" .= Json.String "address", "account" .= a, "address" .= addr ] @@ -219,64 +159,33 @@ instance MarshalJSON Ctx Response where "account" .= a, "addresses" .= adrs ] - ResponseLabel a adr -> - object - [ "type" .= Json.String "label", - "account" .= a, - "address" .= adr - ] ResponseTxs a txs -> object [ "type" .= Json.String "txs", "account" .= a, "txs" .= (marshalValue (accountNetwork a, ctx) <$> txs) ] - ResponsePrepareTx a t -> do - let net = accountNetwork a - object - [ "type" .= Json.String "preparetx", - "account" .= a, - "pendingtx" .= marshalValue (net, ctx) t - ] - ResponsePendingTxs a ts -> do - let net = accountNetwork a - object - [ "type" .= Json.String "pendingtxs", - "account" .= a, - "pendingtxs" .= (marshalValue (net, ctx) <$> ts) - ] - ResponseReviewTx a tx -> do + ResponseTxInfo a t -> do let net = accountNetwork a object - [ "type" .= Json.String "reviewtx", + [ "type" .= Json.String "txinfo", "account" .= a, - "pendingtx" .= marshalValue (net, ctx) tx - ] - ResponseExportTx f -> - object - [ "type" .= Json.String "exporttx", - "txfile" .= f + "txinfo" .= marshalValue (net, ctx) t ] - ResponseImportTx a tx -> do + ResponseTxInfos a ts -> do let net = accountNetwork a object - [ "type" .= Json.String "importtx", + [ "type" .= Json.String "txinfos", "account" .= a, - "pendingtx" .= marshalValue (net, ctx) tx + "txinfos" .= (marshalValue (net, ctx) <$> ts) ] - ResponseDeleteTx c a -> + ResponseDeleteTx h c a -> object [ "type" .= Json.String "deletetx", + "nosighash" .= h, "freedcoins" .= c, "freedaddrs" .= a ] - ResponseSignTx a t -> do - let net = accountNetwork a - object - [ "type" .= Json.String "signtx", - "account" .= a, - "pendingtx" .= marshalValue (net, ctx) t - ] ResponseCoins a coins -> do let net = accountNetwork a object @@ -284,26 +193,9 @@ instance MarshalJSON Ctx Response where "account" .= a, "coins" .= (marshalValue net <$> coins) ] - ResponseSendTx a t h -> do - let net = accountNetwork a - object - [ "type" .= Json.String "sendtx", - "account" .= a, - "transaction" .= marshalValue (net, ctx) t, - "networktxid" .= h - ] - ResponseSyncAcc as bb bh tc cc -> - object - [ "type" .= Json.String "syncacc", - "account" .= as, - "bestblock" .= bb, - "bestheight" .= bh, - "txupdates" .= tc, - "coinupdates" .= cc - ] - ResponseDiscoverAcc as bb bh tc cc -> + ResponseSync as bb bh tc cc -> object - [ "type" .= Json.String "discoveracc", + [ "type" .= Json.String "sync", "account" .= as, "bestblock" .= bb, "bestheight" .= bh, @@ -312,20 +204,6 @@ instance MarshalJSON Ctx Response where ] ResponseVersion v -> object ["type" .= Json.String "version", "version" .= v] - ResponsePrepareSweep a t -> do - let net = accountNetwork a - object - [ "type" .= Json.String "preparesweep", - "account" .= a, - "pendingtxs" .= marshalValue (net, ctx) t - ] - ResponseSignSweep a t -> do - let net = accountNetwork a - object - [ "type" .= Json.String "signsweep", - "account" .= a, - "pendingtx" .= marshalValue (net, ctx) t - ] ResponseRollDice ns e -> object ["type" .= Json.String "rolldice", "entropysource" .= e, "dice" .= ns] @@ -339,98 +217,55 @@ instance MarshalJSON Ctx Response where <$> o .: "entropysource" <*> o .: "mnemonic" <*> o .: "splitmnemonic" - "createacc" -> - ResponseCreateAcc + "account" -> + ResponseAccount <$> o .: "account" "testacc" -> ResponseTestAcc <$> o .: "account" <*> o .: "result" <*> o .: "text" - "importacc" -> - ResponseImportAcc - <$> o .: "account" - "exportacc" -> - ResponseExportAcc - <$> o .: "account" - <*> o .: "accountfile" - "renameacc" -> - ResponseRenameAcc - <$> o .: "oldname" - <*> o .: "newname" - <*> o .: "account" + "file" -> + ResponseFile + <$> o .: "file" "accounts" -> ResponseAccounts <$> o .: "accounts" - "receive" -> - ResponseReceive + "address" -> + ResponseAddress <$> o .: "account" <*> o .: "address" "addresses" -> ResponseAddresses <$> o .: "account" <*> o .: "addresses" - "label" -> - ResponseLabel - <$> o .: "account" - <*> o .: "address" "txs" -> do a <- o .: "account" let net = accountNetwork a txs <- mapM (unmarshalValue (net, ctx)) =<< o .: "txs" return $ ResponseTxs a txs - "preparetx" -> do - a <- o .: "account" - let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "pendingtx" - return $ ResponsePrepareTx a t - "pendingtxs" -> do - a <- o .: "account" - let net = accountNetwork a - ts <- mapM (unmarshalValue (net, ctx)) =<< o .: "pendingtxs" - return $ ResponsePendingTxs a ts - "reviewtx" -> do + "txinfo" -> do a <- o .: "account" let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "pendingtx" - return $ ResponseReviewTx a t - "exporttx" -> - ResponseExportTx - <$> o .: "txfile" - "importtx" -> do + t <- unmarshalValue (net, ctx) =<< o .: "txinfo" + return $ ResponseTxInfo a t + "txinfos" -> do a <- o .: "account" let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "pendingtx" - return $ ResponseImportTx a t + ts <- mapM (unmarshalValue (net, ctx)) =<< o .: "txinfos" + return $ ResponseTxInfos a ts "deletetx" -> ResponseDeleteTx - <$> o .: "freedcoins" + <$> o .: "nosighash" + <*> o .: "freedcoins" <*> o .: "freedaddrs" - "signtx" -> do - a <- o .: "account" - let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "pendingtx" - return $ ResponseSignTx a t "coins" -> do a <- o .: "account" xs <- o .: "coins" coins <- mapM (unmarshalValue (accountNetwork a)) xs return $ ResponseCoins a coins - "sendtx" -> do - a <- o .: "account" - let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "transaction" - i <- o .: "networktxid" - return $ ResponseSendTx a t i - "syncacc" -> - ResponseSyncAcc - <$> o .: "account" - <*> o .: "bestblock" - <*> o .: "bestheight" - <*> o .: "txupdates" - <*> o .: "coinupdates" - "discoveracc" -> - ResponseDiscoverAcc + "sync" -> + ResponseSync <$> o .: "account" <*> o .: "bestblock" <*> o .: "bestheight" @@ -439,16 +274,6 @@ instance MarshalJSON Ctx Response where "version" -> ResponseVersion <$> o .: "version" - "preparesweep" -> do - a <- o .: "account" - let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "pendingtxs" - return $ ResponsePrepareSweep a t - "signsweep" -> do - a <- o .: "account" - let net = accountNetwork a - t <- unmarshalValue (net, ctx) =<< o .: "pendingtx" - return $ ResponseSignSweep a t "rolldice" -> ResponseRollDice <$> o .: "dice" @@ -475,8 +300,8 @@ catchResponseError m = do Left err -> return $ ResponseError $ cs err Right res -> return res -commandResponse :: Ctx -> Config -> Command -> IO Response -commandResponse ctx cfg cmd = +commandResponse :: Ctx -> Config -> AmountUnit -> Command -> IO Response +commandResponse ctx cfg unit cmd = case cmd of -- Mnemonic and account management CommandMnemonic e d s -> cmdMnemonic e d s @@ -490,11 +315,11 @@ commandResponse ctx cfg cmd = CommandLabel nameM i l -> cmdLabel nameM i l -- Transaction management CommandTxs nameM p -> cmdTxs ctx nameM p - CommandPrepareTx rcpts nameM unit fee dust rcptPay o -> + CommandPrepareTx rcpts nameM fee dust rcptPay o -> cmdPrepareTx ctx cfg rcpts nameM unit fee dust rcptPay o CommandPendingTxs nameM p -> cmdPendingTxs ctx nameM p CommandSignTx nameM h i o s -> cmdSignTx ctx nameM h i o s - CommandDeleteTx nameM h -> cmdDeleteTx ctx nameM h + CommandDeleteTx h -> cmdDeleteTx ctx h CommandCoins nameM p -> cmdCoins nameM p -- Import/export commands CommandExportAcc nameM f -> cmdExportAcc ctx nameM f @@ -503,7 +328,7 @@ commandResponse ctx cfg cmd = CommandExportTx h f -> cmdExportTx h f CommandImportTx nameM file -> cmdImportTx ctx nameM file -- Online commands - CommandSendTx nameM h -> cmdSendTx ctx cfg nameM h + CommandSendTx h -> cmdSendTx ctx cfg h CommandSyncAcc nameM full -> cmdSyncAcc ctx cfg nameM full CommandDiscoverAcc nameM -> cmdDiscoverAccount ctx cfg nameM -- Utilities @@ -535,7 +360,7 @@ cmdCreateAcc ctx name net derivM splitMnemIn = do prvKey <- liftEither $ signingKey net ctx mnem d let xpub = deriveXPubKey ctx prvKey (_, acc) <- insertAccount net ctx walletFP name xpub - return $ ResponseCreateAcc acc + return $ ResponseAccount acc cmdTestAcc :: Ctx -> Maybe Text -> Natural -> IO Response cmdTestAcc ctx nameM splitMnemIn = @@ -568,7 +393,7 @@ cmdImportAcc ctx fp = runDB $ do (PubKeyDoc xpub net name wallet) <- liftEitherIO $ readMarshalFile ctx fp (_, acc) <- insertAccount net ctx wallet name xpub - return $ ResponseImportAcc acc + return $ ResponseAccount acc cmdExportAcc :: Ctx -> Maybe Text -> FilePath -> IO Response cmdExportAcc ctx nameM file = @@ -581,13 +406,13 @@ cmdExportAcc ctx nameM file = wallet = accountWallet acc doc = PubKeyDoc xpub net name wallet liftIO $ writeMarshalFile ctx file doc - return $ ResponseExportAcc acc file + return $ ResponseFile file cmdRenameAcc :: Text -> Text -> IO Response cmdRenameAcc oldName newName = runDB $ do acc <- renameAccount oldName newName - return $ ResponseRenameAcc acc oldName newName + return $ ResponseAccount acc cmdAccounts :: Maybe Text -> IO Response cmdAccounts nameM = @@ -605,7 +430,7 @@ cmdReceive ctx cfg nameM labelM = runDB $ do (accId, acc) <- getAccountByName nameM addr <- genExtAddress ctx cfg accId $ fromMaybe "" labelM - return $ ResponseReceive acc addr + return $ ResponseAddress acc addr cmdAddrs :: Maybe Text -> Page -> IO Response cmdAddrs nameM page = @@ -619,7 +444,7 @@ cmdLabel nameM idx lab = runDB $ do (accId, acc) <- getAccountByName nameM adr <- setAddrLabel accId (fromIntegral idx) lab - return $ ResponseLabel acc adr + return $ ResponseAddress acc adr cmdTxs :: Ctx -> Maybe Text -> Page -> IO Response cmdTxs ctx nameM page = @@ -652,7 +477,7 @@ cmdPrepareTx ctx cfg rcpTxt nameM unit feeByte dust rcptPay fileM = nosigHash <- importPendingTx net ctx accId signDat for_ fileM $ \file -> liftIO $ writeJsonFile file $ Json.toJSON signDat newAcc <- getAccountById accId - return $ ResponsePrepareTx newAcc $ NoSigUnsigned nosigHash txInfoU + return $ ResponseTxInfo newAcc $ NoSigUnsigned nosigHash txInfoU where toRecipient net (a, v) = do addr <- textToAddrE net a @@ -668,13 +493,13 @@ cmdPendingTxs ctx nameM page = let net = accountNetwork acc pub = accountXPubKey ctx acc tsds <- pendingTxPage accId page - txs <- forM tsds $ \(nosigH, tsd@(TxSignData tx _ _ _ signed)) -> do + txs <- forM tsds $ \(nosigH, tsd@(TxSignData tx _ _ _ signed), online) -> do txInfoU <- liftEither $ parseTxSignData net ctx pub tsd return $ if signed - then NoSigSigned nosigH $ unsignedToTxInfo tx txInfoU + then NoSigSigned nosigH (unsignedToTxInfo tx txInfoU) online else NoSigUnsigned nosigH txInfoU - return $ ResponsePendingTxs acc txs + return $ ResponseTxInfos acc txs cmdReviewTx :: Ctx -> Maybe Text -> FilePath -> IO Response cmdReviewTx ctx nameM fp = @@ -687,9 +512,9 @@ cmdReviewTx ctx nameM fp = let txInfo = unsignedToTxInfo tx txInfoU nosigHash = nosigTxHash tx return $ - ResponseReviewTx acc $ + ResponseTxInfo acc $ if signed - then NoSigSigned nosigHash txInfo + then NoSigSigned nosigHash txInfo False else NoSigUnsigned nosigHash txInfoU cmdExportTx :: TxHash -> FilePath -> IO Response @@ -697,10 +522,10 @@ cmdExportTx nosigH fp = runDB $ do pendingTxM <- lift $ getPendingTx nosigH case pendingTxM of - Just (tsd, _) -> do + Just (_,tsd, _) -> do checkPathFree fp liftIO $ writeJsonFile fp $ Json.toJSON tsd - return $ ResponseExportTx fp + return $ ResponseFile fp _ -> throwError "The pending transaction does not exist" cmdImportTx :: Ctx -> Maybe Text -> FilePath -> IO Response @@ -714,18 +539,16 @@ cmdImportTx ctx nameM fp = let txInfo = unsignedToTxInfo tx txInfoU nosigHash <- importPendingTx net ctx accId tsd return $ - ResponseReviewTx acc $ + ResponseTxInfo acc $ if signed - then NoSigSigned nosigHash txInfo + then NoSigSigned nosigHash txInfo False else NoSigUnsigned nosigHash txInfoU -cmdDeleteTx :: Ctx -> Maybe Text -> TxHash -> IO Response -cmdDeleteTx ctx nameM nosigH = +cmdDeleteTx :: Ctx -> TxHash -> IO Response +cmdDeleteTx ctx nosigH = runDB $ do - (accId, acc) <- getAccountByName nameM - let net = accountNetwork acc - (coins, addrs) <- deletePendingTx net ctx accId nosigH - return $ ResponseDeleteTx coins addrs + (coins, addrs) <- deletePendingTx ctx nosigH + return $ ResponseDeleteTx nosigH coins addrs cmdSignTx :: Ctx -> @@ -758,7 +581,7 @@ cmdSignTx ctx nameM nosigHM inputM outputM splitMnemIn = throwError "The nosigHash did not match" when (isJust nosigHM) $ void $ importPendingTx net ctx accId newSignData for_ outputM $ \o -> liftIO $ writeJsonFile o $ Json.toJSON newSignData - return $ ResponseSignTx acc (NoSigSigned nosigH txInfo) + return $ ResponseTxInfo acc (NoSigSigned nosigH txInfo False) parseSignInput :: (MonadUnliftIO m) => @@ -778,7 +601,7 @@ parseSignInput nosigHM inputM outputM = (Just h, _, _) -> do resM <- lift $ getPendingTx h case resM of - Just res -> return res + Just (_,t,b) -> return (t,b) _ -> throwError "The nosigHash does not exist in the wallet" (_, Just i, _) -> do exist <- liftIO $ D.doesFileExist i @@ -793,15 +616,15 @@ cmdCoins nameM page = coins <- coinPage net accId page return $ ResponseCoins acc coins -cmdSendTx :: Ctx -> Config -> Maybe Text -> TxHash -> IO Response -cmdSendTx ctx cfg nameM nosigH = +cmdSendTx :: Ctx -> Config -> TxHash -> IO Response +cmdSendTx ctx cfg nosigH = runDB $ do - (_, acc) <- getAccountByName nameM - let net = accountNetwork acc - pub = accountXPubKey ctx acc tsdM <- lift $ getPendingTx nosigH case tsdM of - Just (tsd@(TxSignData signedTx _ _ _ signed), _) -> do + Just (accId, tsd@(TxSignData signedTx _ _ _ signed), _) -> do + acc <- getAccountById accId + let net = accountNetwork acc + pub = accountXPubKey ctx acc txInfoU <- liftEither $ parseTxSignData net ctx pub tsd let txInfo = unsignedToTxInfo signedTx txInfoU verify = verifyTxInfo net ctx signedTx txInfo @@ -809,8 +632,12 @@ cmdSendTx ctx cfg nameM nosigH = checkHealth ctx net cfg let host = apiHost net cfg Store.TxId netTxId <- liftExcept $ apiCall ctx host (PostTx signedTx) + unless (netTxId == txHash signedTx) $ + throwError $ + "The server returned the wrong TxHash: " + <> cs (txHashToHex netTxId) _ <- lift $ setPendingTxOnline nosigH - return $ ResponseSendTx acc txInfo netTxId + return $ ResponseTxInfo acc $ NoSigSigned nosigH txInfo True _ -> throwError "The nosigHash does not exist in the wallet" cmdSyncAcc :: Ctx -> Config -> Maybe Text -> Bool -> IO Response @@ -818,69 +645,80 @@ cmdSyncAcc ctx cfg nameM full = runDB $ do (accId, acc) <- getAccountByName nameM let net = accountNetwork acc - host = apiHost net cfg - -- Check API health - checkHealth ctx net cfg - -- Get the new best block before starting the sync - best <- liftExcept $ apiCall ctx host (GetBlockBest def) - -- Get the addresses from our local database - (addrPathMap, addrBalMap) <- allAddressesMap net accId - -- Fetch the address balances online - Store.SerialList storeBals <- - liftExcept . apiBatch ctx (configAddrBatch cfg) host $ - GetAddrsBalance (Map.keys addrBalMap) - -- Filter only those addresses whose balances have changed - balsToUpdate <- - if full - then return storeBals - else liftEither $ filterAddresses storeBals addrBalMap - let addrsToUpdate = (.address) <$> balsToUpdate - -- Update balances - updateAddressBalances net balsToUpdate - newAcc <- lift $ updateAccountBalances accId - -- Get a list of our confirmed txs in the local database - -- Use an empty list when doing a full sync - confirmedTxs <- if full then return [] else getConfirmedTxs accId True - -- Fetch the txids of the addresses to update - aTids <- searchAddrTxs net ctx cfg confirmedTxs addrsToUpdate - -- We also want to check if there is any change in unconfirmed txs - uTids <- getConfirmedTxs accId False - let tids = nub $ uTids <> aTids - -- Fetch the full transactions - Store.SerialList txs <- - liftExcept $ apiBatch ctx (configTxFullBatch cfg) host (GetTxs tids) - -- Convert them to TxInfo and store them in the local database - let txInfos = toTxInfo addrPathMap (fromIntegral best.height) <$> txs - resTxInfo <- lift $ forM txInfos $ repsertTxInfo net ctx accId - -- Fetch and update coins - Store.SerialList storeCoins <- - liftExcept . apiBatch ctx (configCoinBatch cfg) host $ - GetAddrsUnspent addrsToUpdate def - (coinCount, newCoins) <- refreshCoins net accId addrsToUpdate storeCoins - -- Get the dependent tranactions of the new coins - depTxsHash <- - if full - then return $ (.outpoint.hash) <$> storeCoins - else mapM (liftEither . coinToTxHash) newCoins - Store.RawResultList rawTxs <- - liftExcept - . apiBatch ctx (configTxFullBatch cfg) host - $ GetTxsRaw - $ nub depTxsHash - lift $ forM_ rawTxs insertRawTx - -- Remove pending transactions if they are online - pendingTids <- pendingTxHashes accId - let toRemove = filter ((`elem` tids) . fst) pendingTids - forM_ toRemove $ \(_, key) -> lift $ deletePendingTxOnline key - -- Update the best block for this network - lift $ updateBest net (headerHash best.header) best.height - return $ - ResponseSyncAcc - newAcc - (headerHash best.header) - (fromIntegral best.height) - (fromIntegral $ length $ filter id $ snd <$> resTxInfo) - (fromIntegral coinCount) + sync ctx cfg net accId full + +sync :: + (MonadUnliftIO m) => + Ctx -> + Config -> + Network -> + DBAccountId -> + Bool -> + ExceptT String (DB m) Response +sync ctx cfg net accId full = do + let host = apiHost net cfg + -- Check API health + checkHealth ctx net cfg + -- Get the new best block before starting the sync + best <- liftExcept $ apiCall ctx host (GetBlockBest def) + -- Get the addresses from our local database + (addrPathMap, addrBalMap) <- allAddressesMap net accId + -- Fetch the address balances online + Store.SerialList storeBals <- + liftExcept . apiBatch ctx (configAddrBatch cfg) host $ + GetAddrsBalance (Map.keys addrBalMap) + -- Filter only those addresses whose balances have changed + balsToUpdate <- + if full + then return storeBals + else liftEither $ filterAddresses storeBals addrBalMap + let addrsToUpdate = (.address) <$> balsToUpdate + -- Update balances + updateAddressBalances net balsToUpdate + newAcc <- lift $ updateAccountBalances accId + -- Get a list of our confirmed txs in the local database + -- Use an empty list when doing a full sync + confirmedTxs <- if full then return [] else getConfirmedTxs accId True + -- Fetch the txids of the addresses to update + aTids <- searchAddrTxs net ctx cfg confirmedTxs addrsToUpdate + -- We also want to check if there is any change in unconfirmed txs + uTids <- getConfirmedTxs accId False + let tids = nub $ uTids <> aTids + -- Fetch the full transactions + Store.SerialList txs <- + liftExcept $ apiBatch ctx (configTxFullBatch cfg) host (GetTxs tids) + -- Convert them to TxInfo and store them in the local database + let txInfos = toTxInfo addrPathMap (fromIntegral best.height) <$> txs + resTxInfo <- lift $ forM txInfos $ repsertTxInfo net ctx accId + -- Fetch and update coins + Store.SerialList storeCoins <- + liftExcept . apiBatch ctx (configCoinBatch cfg) host $ + GetAddrsUnspent addrsToUpdate def + (coinCount, newCoins) <- refreshCoins net accId addrsToUpdate storeCoins + -- Get the dependent tranactions of the new coins + depTxsHash <- + if full + then return $ (.outpoint.hash) <$> storeCoins + else mapM (liftEither . coinToTxHash) newCoins + Store.RawResultList rawTxs <- + liftExcept + . apiBatch ctx (configTxFullBatch cfg) host + $ GetTxsRaw + $ nub depTxsHash + lift $ forM_ rawTxs insertRawTx + -- Remove pending transactions if they are online + pendingTids <- pendingTxHashes accId + let toRemove = filter ((`elem` tids) . fst) pendingTids + forM_ toRemove $ \(_, key) -> lift $ deletePendingTxOnline key + -- Update the best block for this network + lift $ updateBest net (headerHash best.header) best.height + return $ + ResponseSync + newAcc + (headerHash best.header) + (fromIntegral best.height) + (fromIntegral $ length $ filter id $ snd <$> resTxInfo) + (fromIntegral coinCount) coinToTxHash :: DBCoin -> Either String TxHash coinToTxHash coin = @@ -949,7 +787,7 @@ searchAddrTxs net ctx cfg confirmedTxs as cmdDiscoverAccount :: Ctx -> Config -> Maybe Text -> IO Response cmdDiscoverAccount ctx cfg nameM = do - _ <- runDB $ do + runDB $ do (accId, acc) <- getAccountByName nameM let net = accountNetwork acc pub = accountXPubKey ctx acc @@ -959,10 +797,8 @@ cmdDiscoverAccount ctx cfg nameM = do i <- go net pub intDeriv 0 (Page recoveryGap 0) discoverAccGenAddrs ctx cfg accId AddrExternal e discoverAccGenAddrs ctx cfg accId AddrInternal i - return $ ResponseDiscoverAcc acc "" 0 0 0 - -- Perform a full sync after discovery - ResponseSyncAcc a bb bh tc cc <- cmdSyncAcc ctx cfg nameM True - return $ ResponseDiscoverAcc a bb bh tc cc + -- Perform a full sync after discovery + sync ctx cfg net accId True where go net pub path d page@(Page lim off) = do let addrs = addrsDerivPage ctx path page pub @@ -1013,7 +849,7 @@ prepareSweep ctx cfg nameM sweepFromT sweepFromFileM sweepToT outputM feeByte du for_ outputM checkPathFree nosigHash <- importPendingTx net ctx accId tsd for_ outputM $ \file -> liftIO $ writeJsonFile file $ Json.toJSON tsd - return $ ResponsePrepareSweep acc (NoSigUnsigned nosigHash info) + return $ ResponseTxInfo acc (NoSigUnsigned nosigHash info) signSweep :: Ctx -> @@ -1044,7 +880,7 @@ signSweep ctx nameM nosigHM inputM outputM keyFile = throwError "The nosigHash did not match" when (isJust nosigHM) $ void $ importPendingTx net ctx accId newTsd for_ outputM $ \o -> liftIO $ writeJsonFile o $ Json.toJSON newTsd - return $ ResponseSignSweep acc (NoSigSigned nosigH txInfo) + return $ ResponseTxInfo acc (NoSigSigned nosigH txInfo False) rollDice :: Natural -> IO Response rollDice n = do diff --git a/src/Haskoin/Wallet/Database.hs b/src/Haskoin/Wallet/Database.hs index 4c700f16..06e5a951 100644 --- a/src/Haskoin/Wallet/Database.hs +++ b/src/Haskoin/Wallet/Database.hs @@ -1075,22 +1075,24 @@ data TxOnline = TxOnline | TxOffline deriving (Eq, Show) -- Returns (TxSignData, isOnline) -getPendingTx :: (MonadUnliftIO m) => TxHash -> DB m (Maybe (TxSignData, Bool)) +getPendingTx :: + (MonadUnliftIO m) => TxHash -> DB m (Maybe (DBAccountId, TxSignData, Bool)) getPendingTx nosigHash = do let hashT = txHashToHex nosigHash key = DBPendingTxKey hashT resM <- P.get key case resM of - Just (DBPendingTx _ _ _ blob online _) -> do - let tsdM = Json.decode $ BS.fromStrict blob - return $ (,online) <$> tsdM + Just (DBPendingTx wallet accDeriv _ blob online _) -> do + let accId = DBAccountKey wallet accDeriv + tsdM = Json.decode $ BS.fromStrict blob + return $ (accId,,online) <$> tsdM _ -> return Nothing pendingTxPage :: (MonadUnliftIO m) => DBAccountId -> Page -> - ExceptT String (DB m) [(TxHash, TxSignData)] + ExceptT String (DB m) [(TxHash, TxSignData, Bool)] pendingTxPage (DBAccountKey wallet accDeriv) (Page lim off) = do tsds <- lift . select . from $ \p -> do @@ -1108,7 +1110,7 @@ pendingTxPage (DBAccountKey wallet accDeriv) (Page lim off) = do maybeToEither "TxHash" $ hexToTxHash $ dBPendingTxNosigHash res - return (nosigHash, tsd) + return (nosigHash, tsd, dBPendingTxOnline res) -- Returns the TxHash and NoSigHash of pending transactions. They are compared -- during a sync in order to delete pending transactions that are now online. @@ -1145,7 +1147,7 @@ importPendingTx net ctx accId tsd@(TxSignData tx _ _ _ signed) = do bs = BS.toStrict $ Json.encode tsd prevM <- lift $ getPendingTx nosigHash case prevM of - Just (TxSignData prevTx _ _ _ prevSigned, online) -> do + Just (_, TxSignData prevTx _ _ _ prevSigned, online) -> do when online $ throwError "The transaction is already online" when (prevSigned && not signed) $ @@ -1217,23 +1219,22 @@ parseTxInfoU (UnsignedTxInfo _ _ myOps _ myIps _ _ _ _) = -- Delete a pending transaction, unlocks coins and frees internal addresses deletePendingTx :: (MonadUnliftIO m) => - Network -> Ctx -> - DBAccountId -> TxHash -> ExceptT String (DB m) (Natural, Natural) -deletePendingTx net ctx accId nosigHash = do +deletePendingTx ctx nosigHash = do let key = DBPendingTxKey $ txHashToHex nosigHash tsdM <- lift $ getPendingTx nosigHash case tsdM of - Just (_, True) -> do + Just (_, _, True) -> do throwError "This pending transaction has been sent to the network.\ \ Run syncacc to refresh your database." -- We only free coins and addresses if the transaction is offline - Just (tsd, False) -> do + Just (accId, tsd, False) -> do acc <- getAccountById accId - let pub = accountXPubKey ctx acc + let net = accountNetwork acc + pub = accountXPubKey ctx acc txInfoU <- liftEither $ parseTxSignData net ctx pub tsd let (outpoints, outIntAddrs, _) = parseTxInfoU txInfoU outIntAddrsT <- diff --git a/src/Haskoin/Wallet/Main.hs b/src/Haskoin/Wallet/Main.hs index f71179e9..e9dd69a6 100644 --- a/src/Haskoin/Wallet/Main.hs +++ b/src/Haskoin/Wallet/Main.hs @@ -6,14 +6,17 @@ import Haskoin.Wallet.Commands (Response, commandResponse) import Haskoin.Wallet.Config (initConfig) import Haskoin.Wallet.Parser (parserMain) import Haskoin.Wallet.Util (encodeJsonPretty) +import Haskoin.Wallet.PrettyPrinter (prettyPrinter) clientMain :: IO () clientMain = withContext $ \ctx -> do cfg <- initConfig - cmd <- parserMain - res <- commandResponse ctx cfg cmd - jsonPrinter ctx res + (cmd, unit, json) <- parserMain + res <- commandResponse ctx cfg unit cmd + if json + then jsonPrinter ctx res + else prettyPrinter unit res jsonPrinter :: Ctx -> Response -> IO () jsonPrinter ctx = C8.putStrLn . encodeJsonPretty . marshalValue ctx diff --git a/src/Haskoin/Wallet/Parser.hs b/src/Haskoin/Wallet/Parser.hs index a693bd05..cc1c718a 100644 --- a/src/Haskoin/Wallet/Parser.hs +++ b/src/Haskoin/Wallet/Parser.hs @@ -67,7 +67,6 @@ data Command | CommandPrepareTx { commandRecipients :: ![(Text, Text)], commandMaybeAcc :: !(Maybe Text), - commandUnit :: !AmountUnit, commandFeeByte :: !Natural, commandDust :: !Natural, commandRcptPay :: !Bool, @@ -90,9 +89,7 @@ data Command commandFilePath :: !FilePath } | CommandDeleteTx - { commandMaybeAcc :: !(Maybe Text), - commandNoSigHash :: !TxHash - } + { commandNoSigHash :: !TxHash } | CommandSignTx { commandMaybeAcc :: !(Maybe Text), commandNoSigHashMaybe :: !(Maybe TxHash), @@ -105,9 +102,7 @@ data Command commandPage :: !Page } | CommandSendTx - { commandMaybeAcc :: !(Maybe Text), - commandNoSigHash :: !TxHash - } + { commandNoSigHash :: !TxHash } | CommandSyncAcc { commandMaybeAcc :: !(Maybe Text), commandFull :: !Bool @@ -137,15 +132,15 @@ data Command } deriving (Eq, Show) -parserMain :: IO Command +parserMain :: IO (Command, AmountUnit, Bool) parserMain = customExecParser (prefs $ showHelpOnEmpty <> helpIndent 25) programParser -programParser :: ParserInfo Command +programParser :: ParserInfo (Command, AmountUnit, Bool) programParser = do - let cmd = commandParser <**> helper + let cmd = (,,) <$> (commandParser <**> helper) <*> unitOption <*> jsonOption info cmd $ fullDesc <> progDesc @@ -155,6 +150,39 @@ sensitive commands (!) to be run on a separate offline computer. For more information on a command, type "hw COMMAND --help". |] +jsonOption :: Parser Bool +jsonOption = + switch $ + short 'j' + <> long "json" + <> help [r| +Display the result as JSON. Specify this option at the end of your command. +|] + +unitOption :: Parser AmountUnit +unitOption = satoshiOption <|> bitOption + +satoshiOption :: Parser AmountUnit +satoshiOption = + flag UnitBitcoin UnitSatoshi $ + short 's' + <> long "satoshi" + <> help [r| +Use satoshis for parsing and displaying amounts (default: bitcoin). Specify this +option at the end of your command. +|] + + +bitOption :: Parser AmountUnit +bitOption = + flag UnitBitcoin UnitBit $ + short 'b' + <> long "bit" + <> help [r| +Use bits for parsing and displaying amounts (default: bitcoin). Specify this +option at the end of your command. +|] + commandParser :: Parser Command commandParser = asum @@ -550,7 +578,6 @@ prepareTxParser = do CommandPrepareTx <$> some recipientArg <*> accountOption - <*> unitOption <*> feeOption <*> dustOption <*> rcptPayOption @@ -616,23 +643,6 @@ dustOption = <> showDefault <> help "Amount (in satoshi) below which an output is considered dust" -unitOption :: Parser AmountUnit -unitOption = satoshiOption <|> bitOption - -satoshiOption :: Parser AmountUnit -satoshiOption = - flag UnitBitcoin UnitSatoshi $ - short 's' - <> long "satoshi" - <> help "Use satoshis for parsing amounts (default: bitcoin)" - -bitOption :: Parser AmountUnit -bitOption = - flag UnitBitcoin UnitBit $ - short 'b' - <> long "bit" - <> help "Use bits for parsing amounts (default: bitcoin)" - rcptPayOption :: Parser Bool rcptPayOption = switch $ @@ -729,10 +739,7 @@ importTxParser = do deleteTxParser :: ParserInfo Command deleteTxParser = do - let cmd = - CommandDeleteTx - <$> accountOption - <*> nosigHashArg + let cmd = CommandDeleteTx <$> nosigHashArg info cmd $ progDesc "Delete a pending transaction" <> footer @@ -801,10 +808,7 @@ and free up any locked coins. sendTxParser :: ParserInfo Command sendTxParser = do - let cmd = - CommandSendTx - <$> accountOption - <*> nosigHashArg + let cmd = CommandSendTx <$> nosigHashArg info cmd $ progDesc "Send (upload) a signed transaction to the network" <> footer @@ -889,7 +893,7 @@ prepareSweepParser = do given --sweepfrom addresses and sends them to the --sweepto addresses. The typical use case for this command is to migrate an old wallet to a new mnemonic. The addresses can also be parsed from a --addrfile. The best way to pass -multiple addresses on the command line is with the shorthand -s ADDR1 -s ADDR2 +multiple addresses on the command line is with the shorthand -w ADDR1 -w ADDR2 for --sweepfrom addresses and -t ADDR1 -t ADDR2 for --sweepto addresses. You can generate addresses to sweep to with the `receive` command. |] @@ -897,7 +901,7 @@ can generate addresses to sweep to with the `receive` command. sweepFromOption :: Parser Text sweepFromOption = strOption $ - short 's' + short 'w' <> long "sweepfrom" <> metavar "ADDRESS" <> help "Addresses to sweep from" diff --git a/src/Haskoin/Wallet/PrettyPrinter.hs b/src/Haskoin/Wallet/PrettyPrinter.hs new file mode 100644 index 00000000..52f96489 --- /dev/null +++ b/src/Haskoin/Wallet/PrettyPrinter.hs @@ -0,0 +1,555 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Haskoin.Wallet.PrettyPrinter where + +import Control.Monad +import Data.List (intersperse) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Maybe (fromMaybe) +import Data.String.Conversions (cs) +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Time.Format +import Data.Word +import Haskoin +import Haskoin.Wallet.Amounts +import Haskoin.Wallet.Commands +import Haskoin.Wallet.Database +import Haskoin.Wallet.TxInfo +import Haskoin.Wallet.Util +import Numeric.Natural (Natural) +import System.Console.ANSI +import System.Exit +import System.IO + +data Printer + = PConcat !Printer !Printer + | PNewline !Printer + | PNest !Natural !Printer + | PText [SGR] !String + | PEmpty + | PNonEmpty + +instance Semigroup Printer where + a <> PEmpty = a + PEmpty <> b = b + a <> b = PConcat a b + +instance Monoid Printer where + mempty = PEmpty + +isEmptyPrinter :: Printer -> Bool +isEmptyPrinter prt = + case prt of + PEmpty -> True + (PText _ s) -> null s + (PNest _ n) -> isEmptyPrinter n + (PNewline n) -> isEmptyPrinter n + _ -> False + +text :: String -> Printer +text = PText [] + +(<+>) :: Printer -> Printer -> Printer +p1 <+> p2 + | isEmptyPrinter p1 = p2 + | isEmptyPrinter p2 = p1 + | otherwise = p1 <> text " " <> p2 + +vcat :: [Printer] -> Printer +vcat = go . filter (not . isEmptyPrinter) + where + go [] = PEmpty + go [x] = x + go (x : xs) = x <> PNewline (go xs) + +hsep :: [Printer] -> Printer +hsep = go . filter (not . isEmptyPrinter) + where + go [] = PEmpty + go [x] = x + go (x : xs) = x <+> go xs + +nest :: Natural -> Printer -> Printer +nest = PNest + +block :: Natural -> String -> String +block n str = + case n `safeSubtract` fromIntegral (length str) of + Just missing -> str <> replicate (fromIntegral missing) ' ' + _ -> str + +rblock :: Natural -> String -> String +rblock n str = + case n `safeSubtract` fromIntegral (length str) of + Just missing -> replicate (fromIntegral missing) ' ' <> str + _ -> str + +renderIO :: Printer -> IO () +renderIO cp = go 0 0 cp >> putStrLn "" + where + go :: Natural -> Natural -> Printer -> IO Natural + go l n p + | isEmptyPrinter p = return l + | otherwise = + case p of + PConcat p1 p2 -> do + l2 <- go l n p1 + go l2 n p2 + PNewline p1 -> do + putStrLn "" + putStr $ replicate (fromIntegral n) ' ' + go n n p1 + PNest i p1 -> do + putStr $ replicate (fromIntegral i) ' ' + go (l + i) (n + i) p1 + PText xs s -> do + printFormat xs s + return $ l + fromIntegral (length s) + PEmpty -> return l + PNonEmpty -> return l + +formatTitle :: String -> Printer +formatTitle = + PText + [ SetConsoleIntensity BoldIntensity, + SetColor Foreground Vivid White + ] + +formatAccount :: String -> Printer +formatAccount = + PText + [ SetConsoleIntensity BoldIntensity, + SetColor Foreground Vivid Cyan + ] + +formatFilePath :: String -> Printer +formatFilePath = + PText + [ SetItalicized True, + SetColor Foreground Vivid White + ] + +formatKey :: String -> Printer +formatKey = PText [SetColor Foreground Dull White] + +formatValue :: String -> Printer +formatValue = PText [SetColor Foreground Vivid White] + +formatLabel :: String -> Printer +formatLabel = + PText + [ SetItalicized True, + SetColor Foreground Vivid White + ] + +formatMnemonic :: Bool -> String -> Printer +formatMnemonic split = + PText + [ SetConsoleIntensity BoldIntensity, + SetColor Foreground Vivid (if split then Yellow else Cyan) + ] + +formatAddress :: Bool -> String -> Printer +formatAddress False = PText [SetColor Foreground Dull White] +formatAddress True = PText [SetColor Foreground Vivid White] + +formatTxHash :: String -> Printer +formatTxHash = PText [SetColor Foreground Dull White] + +formatNoSigTxHash :: String -> Printer +formatNoSigTxHash = PText [SetColor Foreground Vivid Magenta] + +formatBlockHash :: String -> Printer +formatBlockHash = PText [SetColor Foreground Dull White] + +formatPosAmount :: String -> Printer +formatPosAmount = PText [SetColor Foreground Vivid Green] + +formatNegAmount :: String -> Printer +formatNegAmount = PText [SetColor Foreground Vivid Red] + +formatZeroAmount :: String -> Printer +formatZeroAmount = PText [SetColor Foreground Vivid White] + +formatDice :: String -> Printer +formatDice = + PText + [ SetConsoleIntensity BoldIntensity, + SetColor Foreground Vivid Yellow + ] + +formatTrue :: String -> Printer +formatTrue = PText [SetColor Foreground Vivid Green] + +formatFalse :: String -> Printer +formatFalse = PText [SetColor Foreground Vivid Red] + +formatError :: String -> Printer +formatError = PText [SetColor Foreground Vivid Red] + +printFormat :: [SGR] -> String -> IO () +printFormat sgr str = do + support <- hSupportsANSI stdout + when support $ setSGR sgr + putStr str + when support $ setSGR [] + +{- Pretty Response -} + +mnemonicPrinter :: Bool -> [Text] -> Printer +mnemonicPrinter split ws = + vcat $ + fmap (mconcat . fmap formatWord) $ + chunksOf 4 $ + zip ([1 ..] :: [Natural]) ws + where + formatWord (i, w) = + mconcat + [ formatKey $ block 4 $ show i <> ".", + formatMnemonic split $ block 10 $ cs w + ] + +partsPrinter :: [[Text]] -> [Printer] +partsPrinter [] = mempty +partsPrinter xs = + concatMap formatPart $ zip ([1 ..] :: [Natural]) xs + where + formatPart (i, ws) = + [ formatTitle $ "Private Mnemonic Part #" <> show i, + nest 2 $ mnemonicPrinter True ws + ] + +keyPrinter :: Natural -> Text -> Printer +keyPrinter n txt = formatKey (block n (cs txt)) <> text ": " + +amountPrinter :: AmountUnit -> Word64 -> Printer +amountPrinter unit = integerAmountPrinter unit . fromIntegral + +integerAmountPrinter :: AmountUnit -> Integer -> Printer +integerAmountPrinter unit amnt + | amnt == 0 = integerAmountPrinterWith formatZeroAmount unit amnt + | amnt > 0 = integerAmountPrinterWith formatPosAmount unit amnt + | otherwise = integerAmountPrinterWith formatNegAmount unit amnt + +integerAmountPrinterWith :: + (String -> Printer) -> AmountUnit -> Integer -> Printer +integerAmountPrinterWith f unit amnt = + f (cs $ showIntegerAmount unit amnt) <+> unitPrinter unit amnt + +unitPrinter :: AmountUnit -> Integer -> Printer +unitPrinter unit = text . cs . showUnit unit + +naturalPrinter :: Natural -> Printer +naturalPrinter nat + | nat == 0 = formatZeroAmount $ show nat + | otherwise = formatPosAmount $ show nat + +accountPrinter :: AmountUnit -> DBAccount -> Printer +accountPrinter unit acc = + vcat + [ keyPrinter 8 "Account" <> formatAccount (cs $ dBAccountName acc), + mconcat [keyPrinter 8 "Wallet", formatValue $ cs fp], + mconcat [keyPrinter 8 "Deriv", formatValue deriv], + mconcat [keyPrinter 8 "Network", formatValue net], + mconcat + [keyPrinter 8 "External", formatValue $ show $ dBAccountExternal acc], + mconcat + [keyPrinter 8 "Internal", formatValue $ show $ dBAccountInternal acc], + mconcat [keyPrinter 8 "Created", formatValue created], + formatKey "Balances:", + nest 2 $ + mconcat + [ keyPrinter 11 "Confirmed", + amountPrinter unit $ dBAccountBalanceConfirmed acc + ], + nest 2 $ + mconcat + [ keyPrinter 11 "Unconfirmed", + amountPrinter unit $ dBAccountBalanceUnconfirmed acc + ], + nest 2 $ + mconcat + [ keyPrinter 11 "Coins", + naturalPrinter $ fromIntegral $ dBAccountBalanceCoins acc + ] + ] + where + DBWalletKey fp = dBAccountWallet acc + deriv = cs $ dBAccountDerivation acc + net = (accountNetwork acc).name + utctime = dBAccountCreated acc + created = formatTime defaultTimeLocale "%d %b %Y" utctime + +addressPrinter :: AmountUnit -> Natural -> DBAddress -> Printer +addressPrinter unit pad addr = + vcat + [ mconcat + [ keyPrinter pad $ cs $ show (dBAddressIndex addr), + formatAddress True $ cs $ dBAddressAddress addr + ], + if Text.null $ dBAddressLabel addr + then PEmpty + else + nest 2 $ + mconcat + [ keyPrinter 8 "Label", + formatLabel $ cs $ dBAddressLabel addr + ], + nest 2 $ + mconcat + [ keyPrinter 8 "Txs", + naturalPrinter $ fromIntegral $ dBAddressBalanceTxs addr + ], + nest 2 $ + mconcat + [ keyPrinter 8 "Received", + amountPrinter unit $ dBAddressBalanceReceived addr + ] + ] + +txInfoPrinter :: + Network -> + AmountUnit -> + TxType -> + Integer -> + Natural -> + Natural -> + Map Address MyOutputs -> + Map Address Natural -> + Printer -> + Printer +txInfoPrinter net unit tType amount feeN feeByteN myOutputs otherOutputs custom = + vcat + [ formatTitle (block 9 title) <> text ": " <> total, + custom, + fee, + debit, + credit + ] + where + title = + case tType of + TxDebit -> "Debit" + TxInternal -> "Internal" + TxCredit -> "Credit" + total = integerAmountPrinter unit amount + fee + | tType `elem` [TxDebit, TxInternal] = + keyPrinter 9 "Fee" <> feePrinter unit feeN feeByteN + | otherwise = mempty + debit + | tType /= TxDebit = mempty + | otherwise = + vcat $ + [formatKey "Sending to addresses:"] + <> (nest 2 . addrPrinter False <$> Map.assocs otherOutputs) + credit + | tType /= TxCredit = mempty + | otherwise = + vcat $ + [formatKey "My credited addresses:"] + <> ( nest 2 . addrPrinter True + <$> Map.assocs (Map.map myOutputsValue myOutputs) + ) + addrPrinter isCredit (a, v) = + formatAddress isCredit (parseAddr net a) + <> text ":" + <+> if isCredit + then + integerAmountPrinterWith + formatPosAmount + unit + (fromIntegral v) + else + integerAmountPrinterWith + formatZeroAmount + unit + (fromIntegral v) + +parseAddr :: Network -> Address -> String +parseAddr net = cs . fromMaybe "Invalid Address" . addrToText net + +noSigTxInfoPrinter :: Network -> AmountUnit -> NoSigTxInfo -> Printer +noSigTxInfoPrinter net unit ns = + case ns of + NoSigSigned nosigH TxInfo {..} online -> + txInfoPrinter + net + unit + txInfoType + txInfoAmount + txInfoFee + txInfoFeeByte + txInfoMyOutputs + txInfoOtherOutputs + $ vcat + [ keyPrinter 9 "TxHash" <> formatTxHash (cs $ txHashToHex txInfoHash), + keyPrinter 9 "NoSigHash" + <> formatNoSigTxHash (cs $ txHashToHex nosigH), + keyPrinter 9 "Signed" + <> text "This pending transaction is" + <+> formatTrue "signed", + if online + then + keyPrinter 9 "Online" + <> text "This transaction is" + <+> formatTrue "online" + else mempty + ] + NoSigUnsigned nosigH UnsignedTxInfo {..} -> + txInfoPrinter + net + unit + unsignedTxInfoType + unsignedTxInfoAmount + unsignedTxInfoFee + unsignedTxInfoFeeByte + unsignedTxInfoMyOutputs + unsignedTxInfoOtherOutputs + $ vcat + [ keyPrinter 9 "NoSigHash" + <> formatNoSigTxHash (cs $ txHashToHex nosigH), + keyPrinter 9 "Signed" + <> text "This pending transaction is" + <+> formatFalse "not signed" + ] + +feePrinter :: AmountUnit -> Natural -> Natural -> Printer +feePrinter unit fee feeBytes = + integerAmountPrinterWith formatZeroAmount unit (fromIntegral fee) + <+> text "(" + <> formatValue (show feeBytes) + <+> text "sat/bytes" + <> text ")" + +coinPrinter :: Network -> AmountUnit -> JsonCoin -> Printer +coinPrinter net unit JsonCoin {..} = + vcat + [ formatKey (block 7 "TxHash") + <> text ":" + <+> formatTxHash (cs $ txHashToHex jsonCoinOutpoint.hash), + formatKey (block 7 "Index") + <> text ":" + <+> formatValue (show jsonCoinOutpoint.index), + formatKey (block 7 "Value") + <> text ":" + <+> amountPrinter unit jsonCoinValue, + formatKey (block 7 "Address") + <> text ":" + <+> formatAddress True (parseAddr net jsonCoinAddress), + formatKey (block 7 "Confs") + <> text ":" + <+> formatValue (show jsonCoinConfirmations), + formatKey (block 7 "Status") + <> text ":" + <+> if jsonCoinLocked + then formatFalse "Locked" + else formatTrue "Free" + ] + +prettyPrinter :: AmountUnit -> Response -> IO () +prettyPrinter unit = + \case + ResponseError err -> do + renderIO . mconcat $ [formatError "Error: ", text $ cs err] + exitFailure + ResponseMnemonic orig mnem parts -> + renderIO . vcat $ + [ formatTitle "System Entropy Source", + nest 2 $ formatFilePath $ cs orig, + formatTitle "Private Mnemonic", + nest 2 $ mnemonicPrinter False mnem + ] + <> partsPrinter parts + ResponseAccount acc -> renderIO $ accountPrinter unit acc + ResponseAccounts [] -> renderIO $ text "There are no accounts in the wallet" + ResponseAccounts accs -> + renderIO . vcat $ intersperse (text " ") $ accountPrinter unit <$> accs + ResponseTestAcc _ res txt -> do + let p = + if res + then formatTrue "Success" + else formatFalse "Failure" + renderIO . vcat $ [formatKey "Result:" <+> p, text $ cs txt] + ResponseFile f -> renderIO $ formatKey "File:" <+> formatFilePath f + ResponseAddress _ addr -> do + let l = length $ show $ dBAddressIndex addr + renderIO $ addressPrinter unit (fromIntegral l) addr + ResponseAddresses _ [] -> + renderIO $ text "There are no addresses in the account" + ResponseAddresses _ addrs -> do + let l = length $ show $ maximum $ dBAddressIndex <$> addrs + renderIO $ vcat $ addressPrinter unit (fromIntegral l) <$> addrs + ResponseTxs _ [] -> + renderIO $ text "There are no transactions in the account" + ResponseTxs acc txs -> do + let net = accountNetwork acc + f TxInfo {..} = + txInfoPrinter + net + unit + txInfoType + txInfoAmount + txInfoFee + txInfoFeeByte + txInfoMyOutputs + txInfoOtherOutputs + $ vcat + [ keyPrinter 9 "TxHash" + <> formatTxHash (cs $ txHashToHex txInfoHash), + keyPrinter 9 "Confs" <> formatValue (show txInfoConfirmations) + ] + renderIO $ vcat $ intersperse (text " ") $ f <$> txs + ResponseTxInfo acc txInfo -> do + let net = accountNetwork acc + renderIO $ noSigTxInfoPrinter net unit txInfo + ResponseTxInfos _ [] -> + renderIO $ text "There are no pending transactions in the account" + ResponseTxInfos acc txInfos -> do + let net = accountNetwork acc + renderIO $ + vcat $ + intersperse (text " ") $ + noSigTxInfoPrinter net unit <$> txInfos + ResponseDeleteTx h c a -> + renderIO $ + vcat + [ formatKey "Deleted:" <+> formatNoSigTxHash (cs $ txHashToHex h), + nest 2 $ formatKey "Freed coins:" <+> formatValue (show c), + nest 2 $ + formatKey "Freed internal addresses:" <+> formatValue (show a) + ] + ResponseCoins _ [] -> + renderIO $ text "There are no coins in the account" + ResponseCoins acc coins -> do + let net = accountNetwork acc + renderIO $ vcat $ intersperse (text " ") $ coinPrinter net unit <$> coins + ResponseSync acc bh h t c -> do + renderIO $ + vcat + [ accountPrinter unit acc, + formatTitle "Sync Results:", + nest 2 $ + keyPrinter 12 "Best Block" + <> formatBlockHash (cs $ blockHashToHex bh), + nest 2 $ keyPrinter 12 "Best Height" <> formatValue (show h), + nest 2 $ keyPrinter 12 "Tx updates" <> formatValue (show t), + nest 2 $ keyPrinter 12 "Coin updates" <> formatValue (show c) + ] + ResponseVersion v -> + renderIO $ formatKey "Version:" <+> formatValue (cs v) + ResponseRollDice ds e -> + renderIO . vcat $ + [ formatKey "System Entropy Source:" <+> formatFilePath (cs e), + formatKey "Dice rolls:" + <+> mconcat (intersperse (text ", ") (formatDice . show <$> ds)) + ] diff --git a/src/Haskoin/Wallet/Signing.hs b/src/Haskoin/Wallet/Signing.hs index 6d8c05f4..d045b760 100644 --- a/src/Haskoin/Wallet/Signing.hs +++ b/src/Haskoin/Wallet/Signing.hs @@ -167,7 +167,7 @@ signTxWithKeys :: signTxWithKeys net ctx tsd@(TxSignData tx _ _ _ signed) publicKey secKeys = do when signed $ Left "The transaction is already signed" when (null secKeys) $ Left "There are no private keys to sign" - txInfoU <- parseTxSignData net ctx publicKey tsd + txInfoU <- parseTxSignData net ctx publicKey tsd -- signing let myInputs = unsignedTxInfoMyInputs txInfoU othInputs = unsignedTxInfoOtherInputs txInfoU diff --git a/src/Haskoin/Wallet/TxInfo.hs b/src/Haskoin/Wallet/TxInfo.hs index 33a57089..d0f0f058 100644 --- a/src/Haskoin/Wallet/TxInfo.hs +++ b/src/Haskoin/Wallet/TxInfo.hs @@ -166,16 +166,17 @@ instance MarshalJSON (Network, Ctx) TxInfo where <*> o .: "confirmations" data NoSigTxInfo - = NoSigSigned !TxHash !TxInfo + = NoSigSigned !TxHash !TxInfo !Bool | NoSigUnsigned !TxHash !UnsignedTxInfo deriving (Eq, Show) instance MarshalJSON (Network, Ctx) NoSigTxInfo where - marshalValue (net, ctx) (NoSigSigned h t) = + marshalValue (net, ctx) (NoSigSigned h t o) = object [ "nosighash" .= h, "txinfo" .= marshalValue (net, ctx) t, - "signed" .= True + "signed" .= True, + "online" .= o ] marshalValue (net, ctx) (NoSigUnsigned h t) = object @@ -190,7 +191,7 @@ instance MarshalJSON (Network, Ctx) NoSigTxInfo where tV <- o .: "txinfo" h <- o .: "nosighash" if s - then NoSigSigned h <$> unmarshalValue (net, ctx) tV + then NoSigSigned h <$> unmarshalValue (net, ctx) tV <*> o .: "online" else NoSigUnsigned h <$> unmarshalValue (net, ctx) tV marshalMap :: diff --git a/stack.yaml b/stack.yaml index 82c4babc..37bec674 100644 --- a/stack.yaml +++ b/stack.yaml @@ -8,7 +8,11 @@ extra-deps: - git: https://github.com/haskoin/haskoin-core.git commit: 88ff19c35ad2f3d8afdf2a2970342990b40b6f40 - git: https://github.com/haskoin/haskoin-store.git - commit: bc8b99428fedbe88b3dcf80dd4ae298b682112fb + commit: 8466ea2f54d7728aff9417b4d52e829450952fd0 subdirs: - data + - git: https://github.com/haskoin/secp256k1-haskell.git + commit: 17cfeb3ed3c610704fddbb86c276c728d3613521 + subdirs: + - secp256k1-haskell - scotty-0.20@sha256:f9b3f21773f813e92e41a6b5eee1fbefbbe0c695ac5259413500270ec0847151,5292 diff --git a/stack.yaml.lock b/stack.yaml.lock index 8f1a7b9b..09ea4f98 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -16,18 +16,31 @@ packages: commit: 88ff19c35ad2f3d8afdf2a2970342990b40b6f40 git: https://github.com/haskoin/haskoin-core.git - completed: - commit: bc8b99428fedbe88b3dcf80dd4ae298b682112fb + commit: 8466ea2f54d7728aff9417b4d52e829450952fd0 git: https://github.com/haskoin/haskoin-store.git name: haskoin-store-data pantry-tree: - sha256: fa2a2cdca5a3d0531d81269eee4fa1e8e318e86e519a64673da9915579c37f59 + sha256: c3c237abefa741367596811b321765313d4d5f2bebf7c8ea28e0941c76c2182c size: 624 subdir: data - version: 1.2.0 + version: 1.2.2 original: - commit: bc8b99428fedbe88b3dcf80dd4ae298b682112fb + commit: 8466ea2f54d7728aff9417b4d52e829450952fd0 git: https://github.com/haskoin/haskoin-store.git subdir: data +- completed: + commit: 17cfeb3ed3c610704fddbb86c276c728d3613521 + git: https://github.com/haskoin/secp256k1-haskell.git + name: secp256k1-haskell + pantry-tree: + sha256: f6770a7467efee1d23594cd166b2fe53bb67a9efadfe40c23d91026906f4e02f + size: 935 + subdir: secp256k1-haskell + version: 1.1.0 + original: + commit: 17cfeb3ed3c610704fddbb86c276c728d3613521 + git: https://github.com/haskoin/secp256k1-haskell.git + subdir: secp256k1-haskell - completed: hackage: scotty-0.20@sha256:f9b3f21773f813e92e41a6b5eee1fbefbbe0c695ac5259413500270ec0847151,5292 pantry-tree: diff --git a/test/Haskoin/Wallet/CommandsSpec.hs b/test/Haskoin/Wallet/CommandsSpec.hs index ba7d10ed..246bd792 100644 --- a/test/Haskoin/Wallet/CommandsSpec.hs +++ b/test/Haskoin/Wallet/CommandsSpec.hs @@ -575,7 +575,7 @@ pendingTxsSpec ctx cfg = `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = False}, jsonCoin1 {jsonCoinLocked = True} ] - pendingTxPage accId (Page 5 0) `dbShouldBeE` [(h1, tsd)] + pendingTxPage accId (Page 5 0) `dbShouldBeE` [(h1, tsd, False)] -- Check address free status checkFree 0 False checkFree 1 False @@ -610,7 +610,7 @@ pendingTxsSpec ctx cfg = jsonCoin1 {jsonCoinLocked = True} ] pendingTxPage accId (Page 5 0) - `dbShouldBeE` [(h2, tsd2), (h1, tsd)] + `dbShouldBeE` [(h2, tsd2, False), (h1, tsd, False)] checkFree 0 False checkFree 1 False checkFree 2 False @@ -619,12 +619,12 @@ pendingTxsSpec ctx cfg = shouldBeLeft' "chooseCoins: No solution found" $ buildTxSignData btc ctx cfg gen accId [(oAddr' 2, 1)] 0 0 False -- Delete first transaction - deletePendingTx btc ctx accId h1 `dbShouldBeE` (1, 1) + deletePendingTx ctx h1 `dbShouldBeE` (1, 1) coinPage btc accId (Page 5 0) `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True}, jsonCoin1 {jsonCoinLocked = False} ] - pendingTxPage accId (Page 5 0) `dbShouldBeE` [(h2, tsd2)] + pendingTxPage accId (Page 5 0) `dbShouldBeE` [(h2, tsd2, False)] checkFree 0 False checkFree 1 True checkFree 2 False @@ -651,7 +651,7 @@ pendingTxsSpec ctx cfg = jsonCoin1 {jsonCoinLocked = True} ] pendingTxPage accId (Page 5 0) - `dbShouldBeE` [(h3, tsd3), (h2, tsd2)] + `dbShouldBeE` [(h3, tsd3, False), (h2, tsd2, False)] checkFree 0 False checkFree 1 True checkFree 2 False @@ -667,7 +667,7 @@ pendingTxsSpec ctx cfg = jsonCoin1 {jsonCoinLocked = True} ] pendingTxPage accId (Page 5 0) - `dbShouldBeE` [(h3, tsd3), (h2, tsd2')] + `dbShouldBeE` [(h3, tsd3, False), (h2, tsd2', False)] checkFree 0 False checkFree 1 True checkFree 2 False @@ -682,13 +682,13 @@ pendingTxsSpec ctx cfg = shouldBeLeft' "The transaction is already online" $ importPendingTx btc ctx accId tsd2' -- Can not delete an online transaction - shouldBeLeft $ deletePendingTx btc ctx accId h2 + shouldBeLeft $ deletePendingTx ctx h2 lift $ deletePendingTxOnline $ DBPendingTxKey $ txHashToHex h2 coinPage btc accId (Page 5 0) `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True}, jsonCoin1 {jsonCoinLocked = True} ] - pendingTxPage accId (Page 5 0) `dbShouldBeE` [(h3, tsd3)] + pendingTxPage accId (Page 5 0) `dbShouldBeE` [(h3, tsd3, False)] checkFree 0 False checkFree 1 True checkFree 2 False diff --git a/test/Haskoin/Wallet/TestUtils.hs b/test/Haskoin/Wallet/TestUtils.hs index 26fc8ca8..1d34040e 100644 --- a/test/Haskoin/Wallet/TestUtils.hs +++ b/test/Haskoin/Wallet/TestUtils.hs @@ -251,7 +251,7 @@ arbitraryUnsignedTxInfo net ctx = arbitraryNoSigTxInfo :: Network -> Ctx -> Gen NoSigTxInfo arbitraryNoSigTxInfo net ctx = oneof - [ NoSigSigned <$> arbitraryTxHash <*> arbitraryTxInfo net ctx, + [ NoSigSigned <$> arbitraryTxHash <*> arbitraryTxInfo net ctx <*> arbitrary, NoSigUnsigned <$> arbitraryTxHash <*> arbitraryUnsignedTxInfo net ctx ] @@ -263,80 +263,44 @@ arbitraryResponse net ctx = <$> arbitraryText <*> resize 12 (listOf arbitraryText) <*> resize 12 (listOf $ resize 12 $ listOf arbitraryText), - ResponseCreateAcc <$> arbitraryDBAccount net ctx, + ResponseAccount <$> arbitraryDBAccount net ctx, ResponseTestAcc <$> arbitraryDBAccount net ctx <*> arbitrary <*> arbitraryText, - ResponseImportAcc - <$> arbitraryDBAccount net ctx, - ResponseExportAcc - <$> arbitraryDBAccount net ctx - <*> (cs <$> arbitraryText), - ResponseRenameAcc - <$> arbitraryDBAccount net ctx - <*> arbitraryText - <*> arbitraryText, + ResponseFile + <$> (cs <$> arbitraryText), ResponseAccounts <$> resize 20 (listOf $ arbitraryDBAccount net ctx), - ResponseReceive + ResponseAddress <$> arbitraryDBAccount net ctx <*> arbitraryDBAddress net, ResponseAddresses <$> arbitraryDBAccount net ctx <*> resize 20 (listOf $ arbitraryDBAddress net), - ResponseLabel - <$> arbitraryDBAccount net ctx - <*> arbitraryDBAddress net, ResponseTxs <$> arbitraryDBAccount net ctx <*> resize 20 (listOf $ arbitraryTxInfo net ctx), - ResponsePrepareTx + ResponseTxInfo <$> arbitraryDBAccount net ctx <*> arbitraryNoSigTxInfo net ctx, - ResponsePendingTxs + ResponseTxInfos <$> arbitraryDBAccount net ctx <*> resize 20 (listOf $ arbitraryNoSigTxInfo net ctx), - ResponseReviewTx - <$> arbitraryDBAccount net ctx - <*> arbitraryNoSigTxInfo net ctx, - ResponseExportTx - <$> (cs <$> arbitraryText), - ResponseImportTx - <$> arbitraryDBAccount net ctx - <*> arbitraryNoSigTxInfo net ctx, ResponseDeleteTx - <$> arbitraryNatural + <$> arbitraryTxHash + <*> arbitraryNatural <*> arbitraryNatural, - ResponseSignTx - <$> arbitraryDBAccount net ctx - <*> arbitraryNoSigTxInfo net ctx, ResponseCoins <$> arbitraryDBAccount net ctx <*> resize 20 (listOf arbitraryJsonCoin), - ResponseSendTx - <$> arbitraryDBAccount net ctx - <*> arbitraryTxInfo net ctx - <*> arbitraryTxHash, - ResponseSyncAcc - <$> arbitraryDBAccount net ctx - <*> arbitraryBlockHash - <*> arbitraryNatural - <*> arbitraryNatural - <*> arbitraryNatural, - ResponseDiscoverAcc + ResponseSync <$> arbitraryDBAccount net ctx <*> arbitraryBlockHash <*> arbitraryNatural <*> arbitraryNatural <*> arbitraryNatural, ResponseVersion <$> arbitraryText, - ResponsePrepareSweep - <$> arbitraryDBAccount net ctx - <*> arbitraryNoSigTxInfo net ctx, - ResponseSignSweep - <$> arbitraryDBAccount net ctx - <*> arbitraryNoSigTxInfo net ctx, ResponseRollDice <$> resize 20 (listOf arbitraryNatural) <*> arbitraryText