haskoin-wallet-0.4.0: tests/Network/Haskoin/Wallet/Units.hs
{-# LANGUAGE RecordWildCards #-}
module Network.Haskoin.Wallet.Units (tests) where
import Control.Concurrent.STM
import Control.Concurrent.STM.TBMChan
import Control.Exception (Exception, handleJust)
import Control.Monad (guard)
import Control.Monad.Logger (NoLoggingT)
import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Resource (ResourceT)
import qualified Data.ByteString as BS (ByteString, pack)
import Data.List (sort)
import Data.Maybe (fromJust, isJust)
import Data.String.Conversions (cs)
import Data.Word (Word32, Word64)
import Database.Persist (Entity (..), entityVal,
getBy)
import Database.Persist.Sqlite (SqlPersistT,
runMigrationSilent,
runSqlite)
import Network.Haskoin.Block
import Network.Haskoin.Crypto
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Script
import Network.Haskoin.Transaction
import Network.Haskoin.Wallet.Internals
import Test.Framework (Test, testGroup)
import Test.Framework.Providers.HUnit (testCase)
import Test.HUnit (Assertion, assertBool,
assertEqual, assertFailure)
type App = SqlPersistT (NoLoggingT (ResourceT IO))
-- TODO: Add tests for accounts with no private key
tests :: [Test]
tests =
[ testGroup "Account tests"
[ testCase "Fail create account with wrong keys" $
assertException
(WalletException "Invalid account keys")
( newAccount NewAccount
{ newAccountName = "fail-this"
, newAccountType = AccountRegular
-- This key does not correspond to the one below
, newAccountMaster = Just
"xprv9s21ZrQH143K33Ezpb81k5upGyhrVcwgqNzHRHnQ2kGBPHkJ3sLPjGwj4LML1kr1bLfguJiY21XrYfVrL1CGurfVoMKSPwRdmzt1LwBtVyR"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
)
, testCase "Creating two accounts with the same data should fail" $
assertException
(WalletException "Account already exists") $ do
_ <- newAccount NewAccount
{ newAccountName = "main"
, newAccountType = AccountRegular
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
newAccount NewAccount
{ newAccountName = "main"
, newAccountType = AccountRegular
, newAccountMaster = Nothing
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
, testCase "Invalid multisig parameters (0 of 1)" $
assertException (WalletException "Invalid account type") $
newAccount NewAccount
{ newAccountName = "multisig-0-of-1"
, newAccountType = AccountMultisig 0 1
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
, testCase "Invalid multisig parameters (2 of 1)" $
assertException (WalletException "Invalid account type") $
newAccount NewAccount
{ newAccountName = "multisig-2-of-1"
, newAccountType = AccountMultisig 2 1
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
, testCase "Invalid multisig parameters (15 of 16)" $
assertException (WalletException "Invalid account type") $
newAccount NewAccount
{ newAccountName = "multisig-15-of-16"
, newAccountType = AccountMultisig 15 16
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
, testCase "To many multisig keys (3 keys for 1 of 2)" $
assertException (WalletException "Invalid account keys") $
newAccount NewAccount
{ newAccountName = "multisig-1-of-2-with-3"
, newAccountType = AccountMultisig 1 2
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
[ "xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"
, "xpub661MyMwAqRbcFEPH5Aon6F7edspeu1v6a1Nw5qJgk1aX5XYg1ktBL9Azra2CKaAJ2bHXEXkeKHE3eFaCJktFiA5tSMDQDs6bi83maQtdYby"
, "xpub661MyMwAqRbcFtDszBWpawpg4KbNWL9qD4VdRwjd1L5cmcS8nXHWXpg9WL1Xc9Yh7HbQBwWDw37YJfc4AF3YEpvAHEBPBFQPFkUcFHnopw8"
]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
, testCase "Calling addAccountKeys with an empty key list should fail" $
assertException
(WalletException "Invalid account keys") $ do
res <- newAccount NewAccount
{ newAccountName = "multisig-1-of-2-plus-empty"
, newAccountType = AccountMultisig 1 2
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
addAccountKeys (fst res) []
, testCase "Calling addAccountKeys on a non-multisig account should fail" $
assertException
(WalletException "The account is already complete") $ do
res <- newAccount NewAccount
{ newAccountName = "regular-plus-more"
, newAccountType = AccountRegular
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
addAccountKeys (fst res)
["xpub661MyMwAqRbcFEPH5Aon6F7edspeu1v6a1Nw5qJgk1aX5XYg1ktBL9Azra2CKaAJ2bHXEXkeKHE3eFaCJktFiA5tSMDQDs6bi83maQtdYby"]
, testCase "Adding keys to a complete multisig account should fail" $
assertException
(WalletException "The account is already complete") $ do
res <- newAccount NewAccount
{ newAccountName = "regular-plus-more"
, newAccountType = AccountMultisig 1 2
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
[ "xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"
, "xpub661MyMwAqRbcFEPH5Aon6F7edspeu1v6a1Nw5qJgk1aX5XYg1ktBL9Azra2CKaAJ2bHXEXkeKHE3eFaCJktFiA5tSMDQDs6bi83maQtdYby"
]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
addAccountKeys (fst res)
["xpub661MyMwAqRbcFtDszBWpawpg4KbNWL9qD4VdRwjd1L5cmcS8nXHWXpg9WL1Xc9Yh7HbQBwWDw37YJfc4AF3YEpvAHEBPBFQPFkUcFHnopw8"]
, testCase "Getting a non-existing account should fail" $
assertException
(WalletException "Account inexistent does not exist") $
getAccount "inexistent"
]
, testGroup "Address tests"
[ testCase "Decreasing the address gap should fail" $
assertException (WalletException "The gap of an account can only be increased") $ do
res <- newAccount NewAccount
{ newAccountName = "reduce-gap"
, newAccountType = AccountRegular
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
accE' <- setAccountGap (fst res) 15
setAccountGap accE' 14
, testCase "Setting a label on a hidden address key should fail" $
assertException (WalletException "Invalid address index 10") $ do
res <- newAccount NewAccount
{ newAccountName = "label-hidden"
, newAccountType = AccountRegular
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
setAddrLabel (fst res) 10 AddressExternal "Gym membership"
, testCase "Setting a label on an invalid address key should fail" $
assertException (WalletException "Invalid address index 20") $ do
res <- newAccount NewAccount
{ newAccountName = "label-invalid"
, newAccountType = AccountRegular
, newAccountMaster = Just
"xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
setAddrLabel (fst res) 20 AddressExternal "Gym membership"
, testCase "Requesting an address prvkey on a read-only account should fail" $
assertException
(WalletException "Could not get private key") $ do
res <- newAccount NewAccount
{ newAccountName = "label-invalid"
, newAccountType = AccountRegular
, newAccountMaster = Nothing
, newAccountDeriv = Nothing
, newAccountKeys =
["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]
, newAccountMnemonic = Nothing
, newAccountReadOnly = False
}
addressPrvKey (fst res) Nothing 2 AddressExternal
]
, testGroup "Wallet tests"
[ testCase "Verify address derivations" $ runUnit testDerivations
, testCase "Verify balances" $ runUnit testBalances
, testCase "Verify balances in conflict" $ runUnit testConflictBalances
, testCase "Offline transactions" $ runUnit testOffline
, testCase "Kill an offline tx by spending its coins" $ runUnit testKillOffline
, testCase "Offline transaction exceptions" testOfflineExceptions
, testCase "Multisig test 1" $ runUnit testImportMultisig
, testCase "Kill Tx" $ runUnit testKillTx
, testCase "Delete Tx" $ runUnit testDeleteTx
, testCase "Delete Unsigned Tx" $ runUnit testDeleteUnsignedTx
, testCase "Notifications" $ runUnit testNotification
]
]
assertException :: (Exception e, Eq e) => e -> App a -> Assertion
assertException ex action =
handleJust matchEx (const $ return ()) $ do
runUnit action
assertFailure $ "Expecting exception: " ++ show ex
where
matchEx = guard . (== ex)
runUnit :: App a -> Assertion
runUnit action = do
_ <- runSqlite ":memory:" $ do
_ <- runMigrationSilent migrateWallet
initWallet 0.0001
action
return ()
ms :: Mnemonic
ms = "mass coast dance birth online various renew alert crunch middle absurd health"
tid1 :: TxHash
tid1 = "0000000000000000000000000000000000000000000000000000000000000001"
z :: Hash256
z = "0000000000000000000000000000000000000000000000000000000000000000"
fakeNode :: NodeBlock -- ^ Parent
-> [TxHash] -- ^ Transactions
-> Word32 -- ^ Chain
-> Word32 -- ^ Nonce
-> NodeBlock
fakeNode parent tids chain nonce =
nodeBlock parent chain header
where
header = createBlockHeader
(blockVersion $ nodeHeader parent)
(nodeHash parent)
(if null tids then z else buildMerkleRoot tids)
(nodeTimestamp parent + 600)
(blockBits $ nodeHeader parent)
nonce
-- -- Creates fake testing blocks
-- fakeNode :: Word32 -> BlockHash -> NodeBlock
-- fakeNode i h = BlockHeaderNode
-- { blockHeaderNodeHash = headerHash header
-- , blockHeaderNodeHeader = BlockHeader 1 z1 z2 0 0 0
-- , blockHeaderNodeHeight = i
-- , blockHeaderNodeWork = 0
-- , blockHeaderNodeMedianTimes = []
-- , blockHeaderNodeMinWork = 0
-- }
-- where
-- z1 = "0000000000000000000000000000000000000000000000000000000000000000"
-- z2 = "0000000000000000000000000000000000000000000000000000000000000000"
fakeTx :: [(TxHash, Word32)] -> [(BS.ByteString, Word64)] -> Tx
fakeTx xs ys =
createTx 1 txi txo 0
where
txi = map (\(h,p) -> TxIn (OutPoint h p) (BS.pack [1]) maxBound) xs
f = encodeOutputBS . PayPKHash . fromJust . base58ToAddr
txo = map (\(a,v) -> TxOut v $ f a ) ys
testDerivations :: App ()
testDerivations = do
accE <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
unusedAddresses accE AddressExternal (ListRequest 0 0 False)
>>= liftIO . assertEqual "Generated external addresses do not match"
[ "1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe"
, "1NrcKe9UNtxjjgMSLdhBYaEgrQWQFnvJXX"
, "123XgHRNxkprjec72EpxBFPytPim21u9Kc"
, "16AuD3mAQMzsUHkMGfkQQapG6jhHmV3Rys"
, "1AjF1GhsxyXCN5doPGLjSztDcuLbfYMkqw"
, "1LPUYEjUd1u9dgjM3RqnoYj7Zt4j4dmZYA"
, "1Kzyb5Fpj2VmMoCNWxLNMYeMSu5ocS7u7e"
, "131L3UXV6WakXpyXvmqzqNkHwWJzWExR9i"
, "19FWGTZERHzMePTqKoR8nB9y6w7S5u9yGr"
, "135dwGc8JG2dmhy79onerHKdqoqibHShRJ"
] . map (addrToBase58 . walletAddrAddress) . fst
unusedAddresses accE AddressInternal (ListRequest 0 0 False)
>>= liftIO . assertEqual "Generated internal addresses do not match"
[ "1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm"
, "1JVocGcqZvQFbfpeQUY92iL9ARLsAP4DjW"
, "15yaPowqF2g9B3xbdyXaPygV9bfXFjYj2X"
, "187xp9nQTsCa7QJLHYSMyaM1Mf5u2knJYY"
, "1QDQwk4d4zVWH3TnUHMCvDvvrTqFSaA3hr"
, "17ATGqWhkXYLxPdynWrFjnJBvD3EYxqC5A"
, "1EHgPV5DEs3GwUSaRQD7hd2m5MkR4hdByb"
, "1EvUYGoC7p7GC8BtYTz4RX2ERxjUsT3zLz"
, "15857PJZQxUH8Jgomv83mYqJqDsVEUZwz1"
, "18L5fQjr5yXdqqnK98v2UQi1WHAXfYHr7L"
] . map (addrToBase58 . walletAddrAddress) . fst
assertBalance :: AccountId -> Word32 -> Word64 -> App ()
assertBalance ai conf b = do
bal <- accountBalance ai conf False
liftIO $ assertEqual ("Balance is not " ++ show b) b bal
assertBalanceOffline :: AccountId -> Word32 -> Word64 -> App ()
assertBalanceOffline ai conf b = do
bal <- accountBalance ai conf True
liftIO $ assertEqual ("Balance is not " ++ show b) b bal
assertAddress :: Entity Account
-> Word32 -- Confirmations
-> Word32 -- Address Index
-> AddressType
-> [(KeyIndex, BalanceInfo)]
-> App ()
assertAddress acc conf addr addrtype b = do
b' <- addressBalances acc addr addr addrtype conf False
liftIO $ assertEqual "Address Balance incorrect" b b'
assertAddressOffline :: Entity Account
-> Word32 -- Confirmations
-> Word32 -- Address Index
-> AddressType
-> [(KeyIndex, BalanceInfo)]
-> App ()
assertAddressOffline acc conf addr addrtype b = do
b' <- addressBalances acc addr addr addrtype conf True
liftIO $ assertEqual "Address Balance incorrect" b b'
assertImportTx :: AccountId -> Int -> TxConfidence -> Tx -> App ()
assertImportTx ai as conf tx = do
tx' <- testTx <$> importNetTx tx Nothing
liftIO $ assertEqual "Transaction import failed" ([(ai, conf)], as) tx'
assertImportTxOffline :: AccountId -> Int -> TxConfidence -> Tx -> App ()
assertImportTxOffline ai as conf tx = do
tx' <- testTx <$> importTx tx Nothing ai
liftIO $ assertEqual "Transaction import failed" ([(ai, conf)], as) tx'
assertTxConfidence :: AccountId -> TxHash -> TxConfidence -> App ()
assertTxConfidence ai txh conf = do
txM <- getBy $ UniqueAccTx ai txh
case txM of
Just tx -> do
let conf' = walletTxConfidence $ entityVal tx
liftIO $ assertEqual "Transaction confidence wrong" conf' conf
Nothing -> liftIO $ assertFailure "Transaction not found"
testBalances :: App ()
testBalances = do
accE@(Entity ai _) <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let fundingTx = fakeTx
[ (tid1, 0) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000)
, ("1NrcKe9UNtxjjgMSLdhBYaEgrQWQFnvJXX", 20000000)
]
let tx1 = fakeTx
[ (txHash fundingTx, 0)
, (txHash fundingTx, 1)
]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 30000000) ] -- external
tx2 = fakeTx
[ (txHash fundingTx, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 5000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 5000000) -- change
]
assertBalance ai 0 0
-- Import funding transaction twice. This operation should be idempotent
assertImportTx ai 2 TxPending fundingTx
assertImportTx ai 0 TxPending fundingTx
spendableCoins ai 0 (const . const []) >>=
liftIO . assertEqual "0-conf spendable coins is not 2" 2 . length
spendableCoins ai 1 (const . const []) >>=
liftIO . assertEqual "1-conf spendable coins is not 0" 0 . length
assertBalance ai 0 30000000
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal [(0, BalanceInfo 10000000 0 1 0)]
assertAddress accE 0 1 AddressExternal [(1, BalanceInfo 20000000 0 1 0)]
assertAddress accE 1 0 AddressExternal [(0, BalanceInfo 0 0 0 0)]
assertAddress accE 1 1 AddressExternal [(1, BalanceInfo 0 0 0 0)]
assertImportTx ai 0 TxPending tx1
assertBalance ai 0 0
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
assertAddress accE 1 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
assertAddress accE 1 1 AddressExternal
[(1, BalanceInfo 0 0 0 0)]
-- We re-import tx1. This operation has to be idempotent with respect to
-- balances.
assertImportTx ai 0 TxPending tx1
assertBalance ai 1 0
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
assertAddress accE 1 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
assertAddress accE 1 1 AddressExternal
[(1, BalanceInfo 0 0 0 0)]
-- Importing tx2 twice. This operation has to be idempotent.
assertImportTx ai 1 TxDead tx2
assertImportTx ai 0 TxDead tx2
assertBalance ai 0 0
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
-- Confirm the funding transaction at height 1
let block1 = fakeNode genesisBlock [txHash fundingTx] 0 1
importMerkles (BestChain [block1]) [[txHash fundingTx]] Nothing
assertBalance ai 0 0
assertBalance ai 1 0
assertBalance ai 2 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
assertAddress accE 1 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 1 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
-- Confirm tx1 at height 2
let block2 = fakeNode block1 [txHash tx1] 0 2
importMerkles (BestChain [block2]) [[txHash tx1]] Nothing
assertBalance ai 0 0
assertBalance ai 1 0
assertBalance ai 2 0
assertBalance ai 3 0
assertAddress accE 2 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 2 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
assertAddress accE 3 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
assertAddress accE 3 1 AddressExternal
[(1, BalanceInfo 0 0 0 0)]
-- Reorg on tx2
let block2' = fakeNode block1 [] 1 22
block3' = fakeNode block2 [txHash tx2] 1 33
importMerkles (ChainReorg block1 [block2] [block2', block3']) [[], [txHash tx2]] Nothing
getBy (UniqueAccTx ai (txHash tx1))
>>= liftIO
. assertEqual "Confidence is not dead" TxDead
. walletTxConfidence . entityVal . fromJust
getBy (UniqueAccTx ai (txHash tx2))
>>= liftIO
. assertEqual "Confidence is not building" TxBuilding
. walletTxConfidence . entityVal . fromJust
assertBalance ai 0 25000000
assertBalance ai 1 25000000
assertBalance ai 2 20000000
assertBalance ai 3 20000000
assertBalance ai 4 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 0 1 0)]
assertAddress accE 3 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 3 1 AddressExternal
[(1, BalanceInfo 20000000 0 1 0)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 5000000 0 1 0)]
assertAddress accE 1 0 AddressInternal
[(0, BalanceInfo 5000000 0 1 0)]
assertAddress accE 2 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- Reimporting tx2 should be idempotent and return TxBuilding
assertImportTx ai 0 TxBuilding tx2
accountBalance ai 0 False >>=
liftIO . assertEqual "Balance is not 25000000" 25000000
accountBalance ai 1 False >>=
liftIO . assertEqual "Balance is not 25000000" 25000000
accountBalance ai 2 False >>=
liftIO . assertEqual "Balance is not 20000000" 20000000
accountBalance ai 3 False >>=
liftIO . assertEqual "Balance is not 20000000" 20000000
accountBalance ai 4 False >>=
liftIO . assertEqual "Balance is not 0" 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 0 1 0)]
assertAddress accE 3 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 3 1 AddressExternal
[(1, BalanceInfo 20000000 0 1 0)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 5000000 0 1 0)]
assertAddress accE 1 0 AddressInternal
[(0, BalanceInfo 5000000 0 1 0)]
assertAddress accE 2 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- Reorg back onto tx1
let block3 = fakeNode block2 [] 0 3
block4 = fakeNode block3 [] 0 4
importMerkles (ChainReorg block1 [block2', block3'] [block2, block3, block4])
[[txHash tx1], [], []] Nothing
assertBalance ai 0 0
assertBalance ai 1 0
assertBalance ai 2 0
assertBalance ai 3 0
assertBalance ai 4 0
assertBalance ai 5 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
assertAddress accE 4 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 4 1 AddressExternal
[(1, BalanceInfo 20000000 20000000 1 1)]
assertAddress accE 5 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
assertAddress accE 5 1 AddressExternal
[(1, BalanceInfo 0 0 0 0)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- tx1, tx2 and tx3 form a chain, and tx4 is in conflict with tx1
testConflictBalances :: App ()
testConflictBalances = do
accE@(Entity ai _) <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
tx4 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 20000000) ]
-- Import first transaction
assertImportTx ai 1 TxPending tx1
assertBalance ai 0 10000000
assertBalance ai 0 10000000
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
-- Import second transaction
assertImportTx ai 1 TxPending tx2
assertBalance ai 0 4000000
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
-- Let's confirm these two transactions
let block1 = fakeNode genesisBlock [txHash tx1] 0 1
block2 = fakeNode block1 [txHash tx2] 0 2
importMerkles (BestChain [block1, block2])
[[txHash tx1], [txHash tx2]] Nothing
assertBalance ai 0 4000000
assertBalance ai 1 4000000
assertBalance ai 2 0
assertBalance ai 3 0
assertAddress accE 1 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 1 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
assertAddress accE 2 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 2 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- Import third transaction
assertImportTx ai 0 TxPending tx3
assertBalance ai 0 0
assertBalance ai 1 0
assertBalance ai 2 0
assertBalance ai 3 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
assertAddress accE 1 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 1 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
assertAddress accE 2 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 2 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- Now let's add tx4 which is in conflict with tx1
assertImportTx ai 0 TxDead tx4
assertBalance ai 0 0
assertBalance ai 1 0
assertBalance ai 2 0
assertBalance ai 3 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
assertAddress accE 1 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 1 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
assertAddress accE 2 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 2 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- Now we trigger a reorg that validates tx4. tx1, tx2 and tx3 should be dead
let block1' = fakeNode genesisBlock [] 1 11
block2' = fakeNode block1' [txHash tx4] 1 22
block3' = fakeNode block2' [] 1 33
importMerkles
(ChainReorg genesisBlock [block1, block2] [block1', block2', block3'])
[[], [txHash tx4], []] Nothing
assertTxConfidence ai (txHash tx1) TxDead
assertTxConfidence ai (txHash tx2) TxDead
assertTxConfidence ai (txHash tx3) TxDead
assertBalance ai 0 20000000
assertBalance ai 1 20000000
assertBalance ai 2 20000000
assertBalance ai 3 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 20000000 0 1 0)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
assertAddress accE 2 0 AddressExternal
[(0, BalanceInfo 20000000 0 1 0)]
assertAddress accE 3 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
-- Reorg back to tx1, tx2 and tx3
let block3 = fakeNode block2 [] 0 3
block4 = fakeNode block3 [] 0 4
importMerkles
(ChainReorg genesisBlock [block1', block2', block3']
[block1, block2, block3, block4])
[[txHash tx1], [txHash tx2], [], []] Nothing
assertTxConfidence ai (txHash tx1) TxBuilding
assertTxConfidence ai (txHash tx2) TxBuilding
-- Tx3 remains dead until it is included into a block. Dead transaction are
-- only revived upon confirmations. They are not revived if they are not
-- confirmed even if they have no conflicts anymore.
assertTxConfidence ai (txHash tx3) TxDead
assertTxConfidence ai (txHash tx4) TxDead
assertBalance ai 0 4000000
assertBalance ai 1 4000000
assertBalance ai 2 4000000
assertBalance ai 3 4000000
assertBalance ai 4 0
assertBalance ai 5 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
assertAddress accE 3 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 3 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
assertAddress accE 4 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddress accE 4 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
assertAddress accE 5 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
testOffline :: App ()
testOffline = do
accE@(Entity ai _) <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
tx4 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 20000000) ]
-- Import first transaction
assertImportTxOffline ai 1 TxOffline tx1
assertBalance ai 0 0
assertBalanceOffline ai 0 10000000
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
-- Reimporting a transaction should me idempotent
assertImportTxOffline ai 0 TxOffline tx1
assertBalance ai 0 0
assertBalanceOffline ai 0 10000000
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 0 0 0 0)]
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
-- Import tx2
assertImportTxOffline ai 1 TxOffline tx2
assertBalanceOffline ai 0 4000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
-- Import tx3
assertImportTxOffline ai 0 TxOffline tx3
assertBalanceOffline ai 0 0
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
-- Import tx4
assertImportTxOffline ai 0 TxOffline tx4
assertTxConfidence ai (txHash tx1) TxDead
assertTxConfidence ai (txHash tx2) TxDead
assertTxConfidence ai (txHash tx3) TxDead
assertBalanceOffline ai 0 20000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 20000000 0 1 0)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- importTx should be idempotent
assertImportTxOffline ai 0 TxOffline tx4
assertBalanceOffline ai 0 20000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 20000000 0 1 0)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
testKillOffline :: App ()
testKillOffline = do
accE@(Entity ai _) <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
tx4 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 2000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 3000000) -- change
, ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 5000000) -- more change
]
-- Import tx1 as a network transaction
assertImportTx ai 1 TxPending tx1
assertBalance ai 0 10000000
assertBalanceOffline ai 0 10000000
assertBalance ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
-- Import tx2 as offline
assertImportTxOffline ai 1 TxOffline tx2
assertBalance ai 0 10000000
assertBalanceOffline ai 0 4000000
assertBalance ai 1 0
assertBalanceOffline ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
-- Import tx3 as offline
assertImportTxOffline ai 0 TxOffline tx3
assertBalance ai 0 10000000
assertBalanceOffline ai 0 0
assertBalance ai 1 0
assertBalanceOffline ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
-- Import tx4 as a network transaction. It should override tx2 and tx3.
assertImportTx ai 0 TxPending tx4
assertTxConfidence ai (txHash tx2) TxDead
assertTxConfidence ai (txHash tx3) TxDead
assertBalance ai 0 8000000
assertBalanceOffline ai 0 8000000
assertBalance ai 1 0
assertBalanceOffline ai 1 0
assertAddress accE 0 0 AddressExternal
[(0, BalanceInfo 15000000 10000000 2 1)]
assertAddress accE 0 0 AddressInternal
[(0, BalanceInfo 3000000 0 1 0)]
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 15000000 10000000 2 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 3000000 0 1 0)]
testOfflineExceptions :: Assertion
testOfflineExceptions = do
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
tx4 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 20000000) ]
assertException (WalletException "Could not import offline transaction") $ do
_ <- newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
Entity ai _ <- getAccount "acc1"
assertImportTx ai 1 TxPending tx1
importTx tx4 Nothing ai
assertException (WalletException "Could not import offline transaction") $ do
_ <- newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
Entity ai _ <- getAccount "acc1"
assertImportTx ai 1 TxPending tx4
assertImportTx ai 0 TxDead tx1
assertImportTx ai 1 TxDead tx2
importTx tx3 Nothing ai
assertException (WalletException "Could not import offline transaction") $ do
_ <- newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
Entity ai _ <- getAccount "acc1"
assertImportTx ai 1 TxPending tx1
importTx tx1 Nothing ai
-- This test create a multisig account with the key of testImportMultisig2
testImportMultisig :: App ()
testImportMultisig = do
accE1@(Entity ai1 _) <- fst <$> newAccount NewAccount
{ newAccountName = "ms1"
, newAccountType = AccountMultisig 2 2
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = ["xpub68kRFKHWxUt3oS8X5kVogwH5rvuAd4jrLkxVfHeudFC4MfwQ8oYV59F91uFnsLXANRB1MkN4Wa1PwymE4cRsU8PE755HNCb1EoBbSoAKXpW"]
, newAccountReadOnly = False
}
accE2@(Entity ai2 _) <- fst <$> newAccount NewAccount
{ newAccountName = "ms2"
, newAccountType = AccountMultisig 2 2
, newAccountDeriv = Just (Deriv :| 1)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = ["xpub68kRFKHWxUt3mfJjcXdLeuDjCHnByqKSBVfMktJRXM6LSNNDR4ae6Nw1Kh621fzyKiBf6ssyZWPPTDUTQp1BhuZQuoVdtb8j2TRzqDLHmY7"]
, newAccountReadOnly = False
}
let fundingTx = createTx
1
[ TxIn (OutPoint tid1 0) (BS.pack [1]) maxBound ] -- dummy input
[ TxOut 10000000 $
encodeOutputBS $ PayScriptHash $ fromJust $
base58ToAddr "32RexHZdsMoV8yzL1pQyFhYY6XeUNcWP78"
]
0
importNetTx fundingTx Nothing
>>= liftIO
. assertEqual "Transaction import failed"
([(ai1, TxPending), (ai2, TxPending)], 2)
. testTx
-- Create a transaction which has 0 signatures in ms1
tx1 <- fst <$> createWalletTx accE1 Nothing Nothing
[ ( fromJust $ base58ToAddr "3BYWaQHz6AVXx7wXmCka4846tRfa1ccWvh"
, 5000000
)
] 10000 0 False True
liftIO $ assertEqual "Confidence is not offline" TxOffline $
walletTxConfidence tx1
spendableCoins ai1 0 (const . const [])
>>= liftIO
. assertEqual "Wrong txhash in coins" []
. map (walletCoinHash . entityVal . inCoinDataCoin)
txs Nothing ai1 (ListRequest 0 10 False)
>>= liftIO
. assertEqual "Wrong txhash in tx list"
(sort [txHash fundingTx, walletTxHash tx1])
. sort . map walletTxHash . fst
assertBalance ai1 0 10000000
assertBalance ai1 1 0
assertBalanceOffline ai1 0 9990000
-- Import the empty transaction in ms2
tx2 <- head . fst <$> importTx (walletTxTx tx1) Nothing ai2
-- This second import should be idempotent
_ <- importTx (walletTxTx tx1) Nothing ai2
liftIO $ assertEqual "Txid do not match"
(walletTxHash tx1) (walletTxHash tx2)
liftIO $ assertEqual "Confidence is not offline" TxOffline $
walletTxConfidence tx2
spendableCoins ai2 0 (const . const [])
>>= liftIO
. assertEqual "Wrong txhash in coins" []
. map (walletCoinHash . entityVal . inCoinDataCoin)
txs Nothing ai2 (ListRequest 0 10 False)
>>= liftIO
. assertEqual "Wrong txhash in tx list"
(sort [txHash fundingTx, walletTxHash tx2])
. sort . map walletTxHash . fst
assertBalance ai2 0 10000000
assertBalance ai2 1 0
assertBalanceOffline ai2 0 9990000
-- Sign the transaction in ms2
tx3:_ <- fst <$> signAccountTx accE2 Nothing Nothing (walletTxHash tx2)
liftIO $ assertEqual "Confidence is not pending" TxPending $
walletTxConfidence tx3
spendableCoins ai2 0 (const . const [])
>>= liftIO
. assertEqual "Wrong txhash in coins"
[walletTxHash tx3, walletTxHash tx3]
. map (walletCoinHash . entityVal . inCoinDataCoin)
txs Nothing ai2 (ListRequest 0 10 False)
>>= liftIO
. assertEqual "Wrong txhash in tx list"
(sort [txHash fundingTx, walletTxHash tx3])
. sort . map walletTxHash . fst
assertBalance ai2 0 9990000
assertBalance ai2 1 0
assertBalanceOffline ai2 0 9990000
tx4 <- fmap (entityVal . fromJust) $
getBy $ UniqueAccTx ai1 $ walletTxHash tx3
liftIO $ assertEqual "Confidence is not pending" TxPending $
walletTxConfidence tx4
spendableCoins ai1 0 (const . const [])
>>= liftIO
. assertEqual "Wrong txhash in coins"
[walletTxHash tx3, walletTxHash tx3]
. map (walletCoinHash . entityVal . inCoinDataCoin)
txs Nothing ai1 (ListRequest 0 10 False)
>>= liftIO
. assertEqual "Wrong txhash in tx list"
(sort [txHash fundingTx, walletTxHash tx3])
. sort . map walletTxHash . fst
assertBalance ai1 0 9990000
assertBalance ai1 1 0
assertBalanceOffline ai1 0 9990000
-- Importing the transaction should have no effect as it was globally
-- imported already in the previous step.
tx5 <- head . fst <$> importTx (walletTxTx tx3) Nothing ai1
liftIO $ assertEqual "Confidence is not pending" TxPending $
walletTxConfidence tx5
spendableCoins ai1 0 (const . const [])
>>= liftIO
. assertEqual "Wrong txhash in coins"
[walletTxHash tx5, walletTxHash tx5]
. map (walletCoinHash . entityVal . inCoinDataCoin)
txs Nothing ai1 (ListRequest 0 10 False)
>>= liftIO
. assertEqual "Wrong txhash in tx list"
(sort [txHash fundingTx, walletTxHash tx5])
. sort . map walletTxHash . fst
assertBalance ai1 0 9990000
assertBalance ai1 1 0
assertBalanceOffline ai1 0 9990000
testDeleteTx :: App ()
testDeleteTx = do
accE@(Entity ai _) <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
assertImportTx ai 1 TxPending tx1
assertImportTx ai 1 TxPending tx2
assertImportTx ai 0 TxPending tx3
assertBalance ai 0 0
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
tx2M' <- getTx $ txHash tx2
liftIO $ assertBool "Transaction 2 not found" $ isJust tx2M'
deleteTx $ txHash tx2
tx1M <- getTx $ txHash tx1
tx2M'' <- getTx $ txHash tx2
tx3M <- getTx $ txHash tx3
liftIO $ assertEqual "Transaction 1 removed" (Just tx1) tx1M
liftIO $ assertEqual "Transaction 2 not removed" Nothing tx2M''
liftIO $ assertEqual "Transaction 3 not removed" Nothing tx3M
assertBalance ai 0 10000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
testDeleteUnsignedTx :: App ()
testDeleteUnsignedTx = do
accE1@(Entity ai1 _) <- fst <$> newAccount NewAccount
{ newAccountName = "ms1"
, newAccountType = AccountMultisig 2 2
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = ["xpub68kRFKHWxUt3oS8X5kVogwH5rvuAd4jrLkxVfHeudFC4MfwQ8oYV59F91uFnsLXANRB1MkN4Wa1PwymE4cRsU8PE755HNCb1EoBbSoAKXpW"]
, newAccountReadOnly = False
}
Entity ai2 _ <- fst <$> newAccount NewAccount
{ newAccountName = "ms2"
, newAccountType = AccountMultisig 2 2
, newAccountDeriv = Just (Deriv :| 1)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = ["xpub68kRFKHWxUt3mfJjcXdLeuDjCHnByqKSBVfMktJRXM6LSNNDR4ae6Nw1Kh621fzyKiBf6ssyZWPPTDUTQp1BhuZQuoVdtb8j2TRzqDLHmY7"]
, newAccountReadOnly = False
}
let fundingTx = createTx
1
[ TxIn (OutPoint tid1 0) (BS.pack [1]) maxBound ] -- dummy input
[ TxOut 10000000 $
encodeOutputBS $ PayScriptHash $ fromJust $
base58ToAddr "32RexHZdsMoV8yzL1pQyFhYY6XeUNcWP78"
]
0
importNetTx fundingTx Nothing
>>= liftIO
. assertEqual "Confidence is not pending"
([(ai1, TxPending), (ai2, TxPending)], 2)
. testTx
-- Create a transaction which has 0 signatures in ms1
tx1 <- fst <$> createWalletTx accE1 Nothing Nothing
[ ( fromJust $ base58ToAddr "3BYWaQHz6AVXx7wXmCka4846tRfa1ccWvh"
, 5000000
)
] 10000 0 False True
liftIO $ assertEqual "Confidence is not offline" TxOffline $
walletTxConfidence tx1
spendableCoins ai1 0 (const . const [])
>>= liftIO
. assertEqual "Wrong txhash in coins" []
. map (walletCoinHash . entityVal . inCoinDataCoin)
txs Nothing ai1 (ListRequest 0 10 False)
>>= liftIO
. assertEqual "Wrong txhash in tx list"
(sort [txHash fundingTx, walletTxHash tx1])
. sort . map walletTxHash . fst
assertBalance ai1 0 10000000
assertBalance ai1 1 0
assertBalanceOffline ai1 0 9990000
tx1EM <- getTx $ walletTxHash tx1
liftIO $ assertBool "Transaction 1 not found" $ isJust tx1EM
deleteTx $ walletTxHash tx1
tx1M <- getTx $ walletTxHash tx1
liftIO $ assertEqual "Transaction not removed" Nothing tx1M
testNotification :: App ()
testNotification = do
_ <- newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
notifChan <- liftIO $ atomically $ newTBMChan 1000
_ <- importNetTx tx1 (Just notifChan)
tx1NM <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case tx1NM of
Just (NotifTx JsonTx{..}) ->
assertEqual "Notif hash does not match" (txHash tx1) jsonTxHash
_ -> assertFailure "Transaction notification is not the right type"
_ <- importNetTx tx2 (Just notifChan)
tx2NM <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case tx2NM of
Just (NotifTx JsonTx{..}) ->
assertEqual "Notif hash does not match" (txHash tx2) jsonTxHash
_ -> assertFailure "Transaction notification is not the right type"
_ <- importNetTx tx3 Nothing
let block1 = fakeNode genesisBlock [txHash tx1] 0 1
block2 = fakeNode block1 [txHash tx2] 0 2
best = BestChain [block1, block2]
txs1 = [[txHash tx1], [txHash tx2]]
importMerkles best txs1 (Just notifChan)
b1NM <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case b1NM of
Just (NotifBlock JsonBlock{..}) ->
assertEqual "Block hash does not match"
(nodeHash block1) jsonBlockHash
_ -> assertFailure "Block notification not the right type"
tx1NM' <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case tx1NM' of
Just (NotifTx JsonTx{..}) ->
assertEqual "Transaction list does not match" (txHash tx1)
jsonTxHash
_ -> assertFailure "Transaction notification not the right type"
b2NM <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case b2NM of
Just (NotifBlock JsonBlock{..}) ->
assertEqual "Block hash does not match"
(nodeHash block2) jsonBlockHash
_ -> assertFailure "Block notification not the right type"
tx2NM' <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case tx2NM' of
Just (NotifTx JsonTx{..}) ->
assertEqual "Transaction list does not match" (txHash tx2)
jsonTxHash
_ -> assertFailure "Transaction notification not the right type"
let block2' = fakeNode block1 [txHash tx2, txHash tx3] 1 22
txs2 = [[txHash tx2, txHash tx3]]
reorg = ChainReorg block1 [block2] [block2']
importMerkles reorg txs2 (Just notifChan)
b3NM <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case b3NM of
Just (NotifBlock JsonBlock{..}) ->
assertEqual "Block hash does not match"
(nodeHash block2') jsonBlockHash
_ -> assertFailure "Block notification not the right type"
tx3NM2 <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case tx3NM2 of
Just (NotifTx JsonTx{..}) ->
assertEqual "Transaction does not match"
(txHash tx2)
jsonTxHash
_ -> assertFailure "Transaction notification not the right type"
tx3NM3 <- liftIO $ atomically $ readTBMChan notifChan
liftIO $ case tx3NM3 of
Just (NotifTx JsonTx{..}) ->
assertEqual "Transaction does not match"
(txHash tx3)
jsonTxHash
_ -> assertFailure "Transaction notification not the right type"
testKillTx :: App ()
testKillTx = do
accE@(Entity ai _) <- fst <$> newAccount NewAccount
{ newAccountName = "acc1"
, newAccountType = AccountRegular
, newAccountDeriv = Just (Deriv :| 0)
, newAccountMaster = Nothing
, newAccountMnemonic = Just (cs ms)
, newAccountKeys = []
, newAccountReadOnly = False
}
let tx1 = fakeTx
[ (tid1, 4) ]
[ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]
tx2 = fakeTx
[ (txHash tx1, 0) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external
, ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change
]
tx3 = fakeTx
[ (txHash tx2, 1) ]
[ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external
assertImportTx ai 1 TxPending tx1
assertImportTx ai 1 TxPending tx2
assertImportTx ai 0 TxPending tx3
assertBalanceOffline ai 0 0
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 4000000 1 1)]
killTxs Nothing [txHash tx2]
assertBalanceOffline ai 0 10000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
-- Killing a transaction should be idempotent
killTxs Nothing [txHash tx2]
assertBalanceOffline ai 0 10000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
killTxs Nothing [txHash tx3]
assertBalanceOffline ai 0 10000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 0 1 0)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 0 0 0 0)]
reviveTx Nothing tx2
assertBalanceOffline ai 0 4000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
-- Reviving a transaction should be idempotent
reviveTx Nothing tx2
assertBalanceOffline ai 0 4000000
assertAddressOffline accE 0 0 AddressExternal
[(0, BalanceInfo 10000000 10000000 1 1)]
assertAddressOffline accE 0 0 AddressInternal
[(0, BalanceInfo 4000000 0 1 0)]
testTx :: ([WalletTx], [WalletAddr])
-> ([(AccountId, TxConfidence)], Int)
testTx (txls, addrs) = (map f txls, length addrs)
where
f tx = (walletTxAccount tx, walletTxConfidence tx)