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

Commit cb5f826

Browse files
iohk-bors[bot]KtorZ
andcommitted
Merge #4041
4041: Batch Import Addresses to 1.4.2 r=disassembler a=KtorZ ## Description <!--- A brief description of this PR and the problem is trying to solve --> Backporting cardano-foundation/cardano-wallet#259 to 1.4.2 ## Linked issue <!--- Put here the relevant issue from YouTrack --> Co-authored-by: KtorZ <[email protected]>
2 parents c11c2ac + dc6aaeb commit cb5f826

File tree

16 files changed

+307
-36
lines changed

16 files changed

+307
-36
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## Cardano SL 2.0.2
4+
5+
### Features
6+
7+
- Support for (unused) addresses batch import ([CO-448](https://iohk.myjetbrains.com/youtrack/issue/CO-448) [#4041](https://github.com/input-output-hk/cardano-sl/pull/4041))
8+
39
## Cardano SL 2.0.1
410

511
### Fixes

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] (WalletResponse (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 (WalletResponse (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/LegacyHandlers/Addresses.hs

+9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ handlers
4646
handlers nm = listAddresses nm
4747
:<|> newAddress nm
4848
:<|> getAddress nm
49+
:<|> importAddresses nm
4950

5051
-- | This is quite slow. What happens when we have 50k addresses?
5152
-- TODO(ks): One idea I have is to persist the length of the
@@ -133,3 +134,11 @@ getAddress nm addrText = do
133134
accMod <- V0.txMempoolToModifier ws mps . keyToWalletDecrCredentials nm =<< V0.findKey nm accId
134135
let caddr = V0.getWAddress ws accMod adiWAddressMeta
135136
single <$> migrate caddr
137+
138+
importAddresses
139+
:: NetworkMagic
140+
-> WalletId
141+
-> AccountIndex
142+
-> [V1 Address]
143+
-> m (WalletResponse (BatchImportResult (V1 Address)))
144+
importAddresses _ _ _ _ = error "Not Implemented."

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

+46-1
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,7 @@ Getting Utxo statistics
964964

965965
You can get Utxo statistics of a given wallet using
966966
[`GET /api/v1/wallets/{{walletId}}/statistics/utxos`](#tag/Accounts%2Fpaths%2F~1api~1v1~1wallets~1{walletId}~1statistics~1utxos%2Fget)
967+
967968
```
968969
curl -X GET \
969970
https://127.0.0.1:8090/api/v1/wallets/Ae2tdPwUPE...8V3AVTnqGZ/statistics/utxos \
@@ -975,9 +976,53 @@ curl -X GET \
975976
```json
976977
$readUtxoStatistics
977978
```
978-
979979
Make sure to carefully read the section about [Pagination](#section/Pagination) to fully
980980
leverage the API capabilities.
981+
982+
983+
Importing (Unused) Addresses From a Previous Node (or Version)
984+
--------------------------------------------------------------
985+
986+
When restoring a wallet, only the information available on the blockchain can
987+
be retrieved. Some pieces of information aren't stored on
988+
the blockchain and are only defined as _Metadata_ of the wallet backend. This
989+
includes:
990+
991+
- The wallet's name
992+
- The wallet's assurance level
993+
- The wallet's spending password
994+
- The wallet's unused addresses
995+
996+
Unused addresses are not recorded on the blockchain and, in the case of random
997+
derivation, it is unlikely that the same addresses will be generated on two
998+
different node instances. However, some API users may wish to preserve unused
999+
addresses between different instances of the wallet backend.
1000+
1001+
To enable this, the wallet backend provides an endpoint ([`POST /api/v1/wallets/{{walletId}}/accounts/{{accountId}/addresses`](#tag/Addresses%2Fpaths%2F~1api~1v1~1wallets~1{walletId}~1accounts~1{accountId}~1addresses%2Fpost))
1002+
to import a list of addresses into a given account. Note that this endpoint is
1003+
quite lenient when it comes to errors: it tries to import all provided addresses
1004+
one by one, and ignores any that can't be imported for whatever reason. The
1005+
server will respond with the total number of successes and, if any, a list of
1006+
addresses that failed to be imported. Trying to import an address that is already
1007+
present will behave as a no-op.
1008+
1009+
For example:
1010+
1011+
```
1012+
curl -X POST \
1013+
https://127.0.0.1:8090/api/v1/wallets/Ae2tdPwUPE...8V3AVTnqGZ/accounts/2147483648/addresses \
1014+
-H 'Accept: application/json;charset=utf-8' \
1015+
--cacert ./scripts/tls-files/ca.crt \
1016+
--cert ./scripts/tls-files/client.pem \
1017+
-d '[
1018+
"Ae2tdPwUPE...8V3AVTnqGZ",
1019+
"Ae2odDwvbA...b6V104CTV8"
1020+
]'
1021+
```
1022+
1023+
> **IMPORTANT**: This feature is experimental and performance is
1024+
> not guaranteed. Users are advised to import small batches only.
1025+
9811026
|]
9821027
where
9831028
createAccount = decodeUtf8 $ encodePretty $ genExample @(WalletResponse Account)

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

+45-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,48 @@ 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 Serokell.defaultOptions
1503+
1504+
instance FromJSON a => FromJSON (BatchImportResult a) where
1505+
parseJSON = genericParseJSON Serokell.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+
mappend = (<>)
1528+
14861529

14871530
-- | Create a new Address
14881531
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

0 commit comments

Comments
 (0)