Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit 5de1f92

Browse files
committed
Foundation for batch address import
- Define top-level API - First (untested) logic implementation for batch imports
1 parent a385013 commit 5de1f92

File tree

9 files changed

+203
-12
lines changed

9 files changed

+203
-12
lines changed

wallet-new/src/Cardano/Wallet/API/V1/Addresses.hs

+4
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ type API = Tags '["Addresses"] :>
1919
:<|> "addresses" :> Capture "address" Text
2020
:> Summary "Returns interesting information about an address, if available and valid."
2121
:> Get '[ValidJSON] (WalletResponse WalletAddress)
22+
:<|> "wallets" :> CaptureWalletId :> "accounts" :> CaptureAccountId :> "addresses"
23+
:> Summary "Batch import existing addresses"
24+
:> ReqBody '[ValidJSON] [V1 Address]
25+
:> Post '[ValidJSON] (APIResponse (BatchImportResult (V1 Address)))
2226
)

wallet-new/src/Cardano/Wallet/API/V1/Handlers/Addresses.hs

+14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ handlers :: PassiveWalletLayer IO -> ServerT Addresses.API Handler
2020
handlers w = listAddresses w
2121
:<|> newAddress w
2222
:<|> getAddress w
23+
:<|> importAddresses w
2324

2425
listAddresses :: PassiveWalletLayer IO
2526
-> RequestParams -> Handler (WalletResponse [WalletAddress])
@@ -49,3 +50,16 @@ getAddress pwl addressRaw = do
4950
case res of
5051
Left err -> throwM err
5152
Right addr -> return $ single addr
53+
54+
55+
importAddresses
56+
:: PassiveWalletLayer IO
57+
-> WalletId
58+
-> AccountIndex
59+
-> [V1 Address]
60+
-> Handler (APIResponse (BatchImportResult (V1 Address)))
61+
importAddresses pwl walId accIx addrs = do
62+
res <- liftIO $ WalletLayer.importAddresses pwl walId accIx addrs
63+
case res of
64+
Left err -> throwM err
65+
Right res' -> return $ single res'

wallet-new/src/Cardano/Wallet/API/V1/Types.hs

+44-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ module Cardano.Wallet.API.V1.Types (
7272
, word32ToAddressLevel
7373
, IsChangeAddress (..)
7474
, mkAddressPathBIP44
75+
, BatchImportResult(..)
7576
-- * Payments
7677
, Payment (..)
7778
, PaymentSource (..)
@@ -395,9 +396,9 @@ instance Arbitrary (V1 Core.Address) where
395396

396397
instance ToSchema (V1 Core.Address) where
397398
declareNamedSchema _ =
398-
pure $ NamedSchema (Just "V1Address") $ mempty
399+
pure $ NamedSchema (Just "Address") $ mempty
399400
& type_ .~ SwaggerString
400-
-- TODO: any other constraints we can have here?
401+
& format ?~ "base58"
401402

402403
instance FromHttpApiData (V1 Core.Address) where
403404
parseQueryParam = fmap (fmap V1) Core.decodeTextAddress
@@ -1483,6 +1484,47 @@ instance BuildableSafeGen WalletAddress where
14831484
instance Buildable [WalletAddress] where
14841485
build = bprint listJson
14851486

1487+
instance Buildable [V1 Core.Address] where
1488+
build = bprint listJson
1489+
1490+
data BatchImportResult a = BatchImportResult
1491+
{ aimTotalSuccess :: !Natural
1492+
, aimFailures :: ![a]
1493+
} deriving (Show, Ord, Eq, Generic)
1494+
1495+
instance Buildable (BatchImportResult a) where
1496+
build res = bprint
1497+
("BatchImportResult (success:"%int%", failures:"%int%")")
1498+
(aimTotalSuccess res)
1499+
(length $ aimFailures res)
1500+
1501+
instance ToJSON a => ToJSON (BatchImportResult a) where
1502+
toJSON = genericToJSON Aeson.defaultOptions
1503+
1504+
instance FromJSON a => FromJSON (BatchImportResult a) where
1505+
parseJSON = genericParseJSON Aeson.defaultOptions
1506+
1507+
instance (ToJSON a, ToSchema a, Arbitrary a) => ToSchema (BatchImportResult a) where
1508+
declareNamedSchema =
1509+
genericSchemaDroppingPrefix "aim" (\(--^) props -> props
1510+
& ("totalSuccess" --^ "Total number of entities successfully imported")
1511+
& ("failures" --^ "Entities failed to be imported, if any")
1512+
)
1513+
1514+
instance Arbitrary a => Arbitrary (BatchImportResult a) where
1515+
arbitrary = BatchImportResult
1516+
<$> arbitrary
1517+
<*> scale (`mod` 3) arbitrary -- NOTE Small list
1518+
1519+
instance Arbitrary a => Example (BatchImportResult a)
1520+
1521+
instance Semigroup (BatchImportResult a) where
1522+
(BatchImportResult a0 b0) <> (BatchImportResult a1 b1) =
1523+
BatchImportResult (a0 + a1) (b0 <> b1)
1524+
1525+
instance Monoid (BatchImportResult a) where
1526+
mempty = BatchImportResult 0 mempty
1527+
14861528

14871529
-- | Create a new Address
14881530
data NewAddress = NewAddress

wallet-new/src/Cardano/Wallet/Client.hs

+7
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ data WalletClient m
7676
:: NewAddress -> Resp m WalletAddress
7777
, getAddress
7878
:: Text -> Resp m WalletAddress
79+
, importAddresses
80+
:: WalletId
81+
-> AccountIndex
82+
-> [V1 Address]
83+
-> Resp m (BatchImportResult (V1 Address))
7984
-- wallets endpoints
8085
, postWallet
8186
:: New Wallet -> Resp m Wallet
@@ -232,6 +237,8 @@ natMapClient phi f wc = WalletClient
232237
f . phi . postAddress wc
233238
, getAddress =
234239
f . phi . getAddress wc
240+
, importAddresses =
241+
\x y -> f . phi . importAddresses wc x y
235242
, postWallet =
236243
f . phi . postWallet wc
237244
, getWalletIndexFilterSorts =

wallet-new/src/Cardano/Wallet/Client/Http.hs

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ mkHttpClient baseUrl manager = WalletClient
9797
= run . postAddressR
9898
, getAddress
9999
= run . getAddressR
100+
, importAddresses
101+
= \x y -> run . importAddressesR x y
100102
-- wallets endpoints
101103
, postWallet
102104
= run . postWalletR
@@ -192,6 +194,7 @@ mkHttpClient baseUrl manager = WalletClient
192194
getAddressIndexR
193195
:<|> postAddressR
194196
:<|> getAddressR
197+
:<|> importAddressesR
195198
= addressesAPI
196199

197200
postWalletR

wallet-new/src/Cardano/Wallet/Kernel/Addresses.hs

+67-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
13
module Cardano.Wallet.Kernel.Addresses (
24
createAddress
35
, newHdAddress
6+
, importAddresses
47
-- * Errors
58
, CreateAddressError(..)
9+
, ImportAddressError(..)
610
) where
711

812
import qualified Prelude
913
import Universum
1014

1115
import Control.Lens (to)
16+
import Control.Monad.Except (throwError)
1217
import Formatting (bprint, build, formatToString, (%))
1318
import qualified Formatting as F
1419
import qualified Formatting.Buildable
@@ -24,8 +29,8 @@ import Pos.Crypto (EncryptedSecretKey, PassPhrase,
2429
import Cardano.Wallet.Kernel.DB.AcidState (CreateHdAddress (..))
2530
import Cardano.Wallet.Kernel.DB.HdWallet (HdAccountId,
2631
HdAccountIx (..), HdAddress, HdAddressId (..),
27-
HdAddressIx (..), hdAccountIdIx, hdAccountIdParent,
28-
hdAddressIdIx)
32+
HdAddressIx (..), HdRootId (..), IsOurs (..),
33+
hdAccountIdIx, hdAccountIdParent, hdAddressIdIx)
2934
import Cardano.Wallet.Kernel.DB.HdWallet.Create
3035
(CreateHdAddressError (..), initHdAddress)
3136
import Cardano.Wallet.Kernel.DB.HdWallet.Derivation
@@ -176,3 +181,63 @@ newHdAddress nm esk spendingPassword accId hdAddressId =
176181
in case mbAddr of
177182
Nothing -> Nothing
178183
Just (newAddress, _) -> Just $ initHdAddress hdAddressId newAddress
184+
185+
186+
data ImportAddressError
187+
= ImportAddressKeystoreNotFound HdAccountId
188+
-- ^ When trying to create the 'Address', the parent 'Account' was not there.
189+
deriving Eq
190+
191+
instance Arbitrary ImportAddressError where
192+
arbitrary = oneof
193+
[ ImportAddressKeystoreNotFound <$> arbitrary
194+
]
195+
196+
instance Buildable ImportAddressError where
197+
build = \case
198+
ImportAddressKeystoreNotFound uAccount ->
199+
bprint ("ImportAddressError" % F.build) uAccount
200+
201+
instance Show ImportAddressError where
202+
show = formatToString build
203+
204+
205+
-- | Import already existing addresses into the DB. A typical use-case for that
206+
-- is backend migration, where users (e.g. exchanges) want to import unused
207+
-- addresses they've generated in the past (and likely communicated to their
208+
-- users). Because Addresses in the old scheme are generated randomly, there's
209+
-- no guarantee that addresses would be generated in the same order on a new
210+
-- node (they better not actually!).
211+
importAddresses
212+
:: HdAccountId
213+
-- ^ An abstract notion of an 'Account' identifier
214+
-> [Address]
215+
-> PassiveWallet
216+
-> IO (Either ImportAddressError [Either Address ()])
217+
importAddresses accId addrs pw = runExceptT $ do
218+
let rootId = accId ^. hdAccountIdParent
219+
esk <- lookupSecretKey rootId
220+
lift $ forM addrs (flip importOneAddress [(rootId, esk)])
221+
where
222+
lookupSecretKey
223+
:: HdRootId
224+
-> ExceptT ImportAddressError IO EncryptedSecretKey
225+
lookupSecretKey rootId = do
226+
let nm = makeNetworkMagic (pw ^. walletProtocolMagic)
227+
let keystore = pw ^. walletKeystore
228+
lift (Keystore.lookup nm (WalletIdHdRnd rootId) keystore) >>= \case
229+
Nothing -> throwError (ImportAddressKeystoreNotFound accId)
230+
Just esk -> return esk
231+
232+
importOneAddress
233+
:: Address
234+
-> [(HdRootId, EncryptedSecretKey)]
235+
-> IO (Either Address ())
236+
importOneAddress addr = evalStateT $ do
237+
let updateLifted = fmap Just . lift . update (pw ^. wallets)
238+
res <- state (isOurs addr) >>= \case
239+
Nothing -> return Nothing
240+
Just hdAddr -> updateLifted $ CreateHdAddress hdAddr
241+
return $ case res of
242+
Just (Right _) -> Right ()
243+
_ -> Left addr

wallet-new/src/Cardano/Wallet/WalletLayer.hs

+36-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module Cardano.Wallet.WalletLayer
1515
, RedeemAdaError(..)
1616
, CreateAddressError(..)
1717
, ValidateAddressError(..)
18+
, ImportAddressError(..)
1819
, CreateAccountError(..)
1920
, GetAccountError(..)
2021
, GetAccountsError(..)
@@ -47,12 +48,12 @@ import Cardano.Wallet.API.Request.Filter (FilterOperations (..))
4748
import Cardano.Wallet.API.Request.Sort (SortOperations (..))
4849
import Cardano.Wallet.API.Response (SliceOf (..), WalletResponse)
4950
import Cardano.Wallet.API.V1.Types (Account, AccountBalance,
50-
AccountIndex, AccountUpdate, Address, ForceNtpCheck,
51-
NewAccount, NewAddress, NewWallet, NodeInfo, NodeSettings,
52-
PasswordUpdate, Payment, Redemption, SignedTransaction,
53-
SpendingPassword, Transaction, UnsignedTransaction,
54-
V1 (..), Wallet, WalletAddress, WalletId, WalletImport,
55-
WalletUpdate)
51+
AccountIndex, AccountUpdate, Address, BatchImportResult,
52+
ForceNtpCheck, NewAccount, NewAddress, NewWallet,
53+
NodeInfo, NodeSettings, PasswordUpdate, Payment,
54+
Redemption, SignedTransaction, SpendingPassword,
55+
Transaction, UnsignedTransaction, V1 (..), Wallet,
56+
WalletAddress, WalletId, WalletImport, WalletUpdate)
5657
import qualified Cardano.Wallet.Kernel.Accounts as Kernel
5758
import qualified Cardano.Wallet.Kernel.Addresses as Kernel
5859
import Cardano.Wallet.Kernel.CoinSelection.FromGeneric
@@ -231,6 +232,31 @@ instance Buildable ValidateAddressError where
231232
build (ValidateAddressDecodingFailed rawText) =
232233
bprint ("ValidateAddressDecodingFailed " % build) rawText
233234

235+
data ImportAddressError =
236+
ImportAddressError Kernel.ImportAddressError
237+
| ImportAddressAddressDecodingFailed Text
238+
-- ^ Decoding the input 'Text' as an 'Address' failed.
239+
deriving Eq
240+
241+
-- | Unsound show instance needed for the 'Exception' instance.
242+
instance Show ImportAddressError where
243+
show = formatToString build
244+
245+
instance Exception ImportAddressError
246+
247+
instance Arbitrary ImportAddressError where
248+
arbitrary = oneof [ ImportAddressError <$> arbitrary
249+
, pure (ImportAddressAddressDecodingFailed "Ae2tdPwUPEZ18ZjTLnLVr9CEvUEUX4eW1LBHbxxx")
250+
]
251+
252+
instance Buildable ImportAddressError where
253+
build (ImportAddressError kernelError) =
254+
bprint ("ImportAddressError " % build) kernelError
255+
build (ImportAddressAddressDecodingFailed txt) =
256+
bprint ("ImportAddressAddressDecodingFailed " % build) txt
257+
258+
259+
234260
------------------------------------------------------------
235261
-- Errors when dealing with Accounts
236262
------------------------------------------------------------
@@ -431,6 +457,10 @@ data PassiveWalletLayer m = PassiveWalletLayer
431457
, getAddresses :: RequestParams -> m (SliceOf WalletAddress)
432458
, validateAddress :: Text
433459
-> m (Either ValidateAddressError WalletAddress)
460+
, importAddresses :: WalletId
461+
-> AccountIndex
462+
-> [V1 Address]
463+
-> m (Either ImportAddressError (BatchImportResult (V1 Address)))
434464

435465
-- transactions
436466
, getTransactions :: Maybe WalletId

wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ bracketPassiveWallet pm mode logFunction keystore node fInjects f = do
120120
, updateAccount = Accounts.updateAccount w
121121
, deleteAccount = Accounts.deleteAccount w
122122
, createAddress = Addresses.createAddress w
123+
, importAddresses = Addresses.importAddresses w
123124
, addUpdate = Internal.addUpdate w
124125
, nextUpdate = Internal.nextUpdate w
125126
, applyUpdate = Internal.applyUpdate w

wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Addresses.hs

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
{-# LANGUAGE ViewPatterns #-}
1+
{-# LANGUAGE LambdaCase #-}
2+
23
module Cardano.Wallet.WalletLayer.Kernel.Addresses (
34
createAddress
45
, getAddresses
56
, validateAddress
7+
, importAddresses
68
) where
79

810
import Universum
@@ -28,7 +30,7 @@ import qualified Cardano.Wallet.Kernel.Internal as Kernel
2830
import qualified Cardano.Wallet.Kernel.Read as Kernel
2931
import Cardano.Wallet.Kernel.Types (AccountId (..))
3032
import Cardano.Wallet.WalletLayer (CreateAddressError (..),
31-
ValidateAddressError (..))
33+
ImportAddressError (..), ValidateAddressError (..))
3234
import Cardano.Wallet.WalletLayer.Kernel.Conv
3335

3436
createAddress :: MonadIO m
@@ -186,3 +188,26 @@ validateAddress rawText db = runExcept $ do
186188
, addrChangeAddress = False
187189
, addrOwnership = (V1 V1.AddressAmbiguousOwnership)
188190
}
191+
192+
193+
importAddresses
194+
:: (MonadIO m)
195+
=> Kernel.PassiveWallet
196+
-> V1.WalletId
197+
-> V1.AccountIndex
198+
-> [V1.V1 V1.Address]
199+
-> m (Either ImportAddressError (V1.BatchImportResult (V1.V1 V1.Address)))
200+
importAddresses wallet wId accIx addrs = runExceptT $ do
201+
accId <- withExceptT ImportAddressAddressDecodingFailed $
202+
fromAccountId wId accIx
203+
res <- withExceptT ImportAddressError $ ExceptT $ liftIO $
204+
Kernel.importAddresses accId (V1.unV1 <$> addrs) wallet
205+
return $ foldImportResults V1.V1 res
206+
where
207+
foldImportResults
208+
:: (a -> b)
209+
-> [Either a ()]
210+
-> V1.BatchImportResult b
211+
foldImportResults f = flip foldl' mempty $ \b -> \case
212+
Left a -> b <> V1.BatchImportResult 0 [f a]
213+
Right _ -> b <> V1.BatchImportResult 1 []

0 commit comments

Comments
 (0)