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

Commit 81d0932

Browse files
authored
Merge pull request #3466 from input-output-hk/feature/cbr-349-redeem-ada
[CBR-349] Implement redemption
2 parents d6ac5e0 + 417c94d commit 81d0932

File tree

10 files changed

+265
-77
lines changed

10 files changed

+265
-77
lines changed

wallet-new/cardano-sl-wallet-new.cabal

-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ library
146146
Cardano.Wallet.WalletLayer.Kernel.Transactions
147147
Cardano.Wallet.WalletLayer.Kernel.Wallets
148148
Cardano.Wallet.WalletLayer.Kernel
149-
Cardano.Wallet.WalletLayer.Error
150149

151150
other-modules: Paths_cardano_sl_wallet_new
152151
ghc-options: -Wall

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

+8-27
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import Data.Coerce (coerce)
1717
import Pos.Client.Txp.Util (InputSelectionPolicy (..),
1818
defaultInputSelectionPolicy)
1919
import Pos.Core (Address, Timestamp)
20-
import Pos.Core.Txp (Tx (..), TxId, TxOut (..))
21-
import Pos.Crypto (hash)
20+
import Pos.Core.Txp (TxId)
2221

2322
import Cardano.Wallet.API.Request
2423
import Cardano.Wallet.API.Response
@@ -28,8 +27,7 @@ import Cardano.Wallet.Kernel.CoinSelection.FromGeneric
2827
(ExpenseRegulation (..), InputGrouping (..))
2928
import Cardano.Wallet.Kernel.DB.HdWallet (UnknownHdAccount)
3029
import Cardano.Wallet.Kernel.DB.TxMeta (TxMeta)
31-
import Cardano.Wallet.Kernel.Util.Core (getCurrentTimestamp,
32-
paymentAmount)
30+
import qualified Cardano.Wallet.Kernel.Transactions as Kernel
3331
import Cardano.Wallet.WalletLayer (ActiveWalletLayer,
3432
NewPaymentError (..), PassiveWalletLayer)
3533
import qualified Cardano.Wallet.WalletLayer as WalletLayer
@@ -109,28 +107,11 @@ estimateFees aw payment@Payment{..} = do
109107
redeemAda :: ActiveWalletLayer IO
110108
-> Redemption
111109
-> Handler (WalletResponse Transaction)
112-
redeemAda layer redemption = do
113-
res <- liftIO $ WalletLayer.redeemAda layer redemption
110+
redeemAda aw redemption = liftIO $ do
111+
res <- WalletLayer.redeemAda aw redemption
114112
case res of
115-
Left e -> throwM e
116-
Right tx -> do
117-
-- TODO: This is a straight copy and paste from 'newTransaction' in
118-
-- "Cardano.Wallet.API.V1.Handlers.Transactions". Once [CBR-239]
119-
-- is fixed, we should fix both instances.
120-
now <- liftIO getCurrentTimestamp
121-
-- NOTE(adn) As part of [CBR-239], we could simply fetch the
122-
-- entire 'Transaction' as part of the TxMeta.
123-
return $ single Transaction {
124-
txId = V1 (hash tx)
125-
, txConfirmations = 0
126-
, txAmount = V1 (paymentAmount $ _txOutputs tx)
127-
, txInputs = error "TODO, see [CBR-324]"
128-
, txOutputs = fmap outputsToDistribution (_txOutputs tx)
129-
, txType = error "TODO, see [CBR-324]"
130-
, txDirection = OutgoingTransaction
131-
, txCreationTime = V1 now
132-
, txStatus = Creating
133-
}
113+
Left err -> throwM err
114+
Right (_, meta) -> txFromMeta aw embedErr meta
134115
where
135-
outputsToDistribution :: TxOut -> PaymentDistribution
136-
outputsToDistribution (TxOut addr amount) = PaymentDistribution (V1 addr) (V1 amount)
116+
embedErr :: UnknownHdAccount -> WalletLayer.RedeemAdaError
117+
embedErr = WalletLayer.RedeemAdaError . Kernel.RedeemAdaUnknownAccountId

wallet-new/src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import Data.Typeable (TypeRep, typeRep)
3737
import Pos.Binary.Class (LengthOf, Range (..), SizeOverride (..),
3838
encode, szSimplify, szWithCtx, toLazyByteString)
3939
import qualified Pos.Chain.Txp as Core
40-
import qualified Pos.Client.Txp.Util as Core
40+
import qualified Pos.Client.Txp.Util as CTxp
4141
import Pos.Core (AddrAttributes, Coin (..), TxSizeLinear,
4242
calculateTxSizeLinear)
4343
import qualified Pos.Core as Core
@@ -198,7 +198,7 @@ mkStdTx :: Monad m
198198
-> m (Either e Core.TxAux)
199199
mkStdTx pm shuffle hdwSigners inps outs change = do
200200
allOuts <- shuffle $ foldl' (flip NE.cons) outs change
201-
return $ Core.makeMPubKeyTxAddrs pm hdwSigners (fmap repack inps) allOuts
201+
return $ CTxp.makeMPubKeyTxAddrs pm hdwSigners (fmap repack inps) allOuts
202202
where
203203
-- Repack a utxo-derived tuple into a format suitable for
204204
-- 'TxOwnedInputs'.

wallet-new/src/Cardano/Wallet/Kernel/DB/AcidState.hs

+6
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,12 @@ instance Buildable NewPendingError where
535535
build (NewPendingFailed npf) =
536536
bprint ("NewPendingFailed " % build) npf
537537

538+
instance Buildable NewForeignError where
539+
build (NewForeignUnknown unknownAccount) =
540+
bprint ("NewForeignUnknown " % build) unknownAccount
541+
build (NewForeignFailed npf) =
542+
bprint ("NewForeignFailed " % build) npf
543+
538544
{-------------------------------------------------------------------------------
539545
Arbitrary
540546
-------------------------------------------------------------------------------}

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

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module Cardano.Wallet.Kernel.NodeStateAdaptor (
2727
, compileInfo
2828
, getNtpStatus
2929
-- * Non-mockable
30+
, filterUtxo
3031
, mostRecentMainBlock
3132
, triggerShutdown
3233
, waitForUpdate
@@ -43,6 +44,8 @@ import Universum
4344
import Control.Lens (lens)
4445
import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (UnliftIO),
4546
askUnliftIO, unliftIO, withUnliftIO)
47+
import Data.Conduit (mapOutputMaybe, runConduitRes, (.|))
48+
import qualified Data.Conduit.List as Conduit
4649
import Data.SafeCopy (base, deriveSafeCopy)
4750
import Data.Time.Units (Millisecond)
4851
import Formatting (bprint, build, sformat, shown, (%))
@@ -63,12 +66,14 @@ import Pos.Core.Configuration (HasConfiguration, genesisHash,
6366
protocolConstants)
6467
import Pos.Core.Slotting (EpochIndex (..), HasSlottingVar (..),
6568
LocalSlotIndex (..), MonadSlots (..), SlotId (..))
69+
import Pos.Core.Txp (TxIn, TxOutAux)
6670
import qualified Pos.DB.Block as DB
6771
import Pos.DB.BlockIndex (getTipHeader)
6872
import Pos.DB.Class (MonadDBRead (..), getBlock)
6973
import Pos.DB.GState.Lock (StateLock, withStateLockNoMetrics)
7074
import Pos.DB.Rocks.Functions (dbGetDefault, dbIterSourceDefault)
7175
import Pos.DB.Rocks.Types (NodeDBs)
76+
import Pos.DB.Txp.Utxo (utxoSource)
7277
import Pos.DB.Update (UpdateContext, getAdoptedBVData,
7378
ucDownloadedUpdate)
7479
import Pos.Infra.Shutdown.Class (HasShutdownContext (..))
@@ -401,6 +406,11 @@ defaultGetNextEpochSlotDuration = Slotting.getNextEpochSlotDuration
401406
Non-mockable functinos
402407
-------------------------------------------------------------------------------}
403408

409+
filterUtxo :: (NodeConstraints, MonadCatch m, MonadUnliftIO m)
410+
=> ((TxIn, TxOutAux) -> Maybe a) -> WithNodeState m [a]
411+
filterUtxo p = runConduitRes $ mapOutputMaybe p utxoSource
412+
.| Conduit.fold (flip (:)) []
413+
404414
triggerShutdown :: MonadIO m => WithNodeState m ()
405415
triggerShutdown = Shutdown.triggerShutdown
406416

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

+121-6
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
module Cardano.Wallet.Kernel.Transactions (
33
pay
44
, estimateFees
5+
, redeemAda
56
-- * Errors
67
, NewTransactionError(..)
78
, SignTransactionError(..)
89
, PaymentError(..)
910
, EstimateFeesError(..)
11+
, RedeemAdaError(..)
1012
, cardanoFee
1113
-- * Internal & testing use only
1214
, newTransaction
@@ -32,12 +34,14 @@ import Formatting (bprint, build, sformat, (%))
3234
import qualified Formatting.Buildable
3335

3436
import Pos.Chain.Txp (Utxo)
37+
import qualified Pos.Client.Txp.Util as CTxp
3538
import Pos.Core (Address, Coin, unsafeIntegerToCoin, unsafeSubCoin)
3639
import qualified Pos.Core as Core
3740
import Pos.Core.Txp (Tx (..), TxAux (..), TxId, TxIn (..), TxOut (..),
3841
TxOutAux (..))
39-
import Pos.Crypto (EncryptedSecretKey, PassPhrase, SafeSigner (..),
40-
ShouldCheckPassphrase (..), hash)
42+
import Pos.Crypto (EncryptedSecretKey, PassPhrase, RedeemSecretKey,
43+
SafeSigner (..), ShouldCheckPassphrase (..), hash,
44+
redeemToPublic)
4145

4246
import qualified Cardano.Wallet.Kernel.Addresses as Kernel
4347
import Cardano.Wallet.Kernel.CoinSelection.FromGeneric
@@ -47,7 +51,8 @@ import Cardano.Wallet.Kernel.CoinSelection.FromGeneric
4751
import qualified Cardano.Wallet.Kernel.CoinSelection.FromGeneric as CoinSelection
4852
import Cardano.Wallet.Kernel.CoinSelection.Generic
4953
(CoinSelHardErr (..))
50-
import Cardano.Wallet.Kernel.DB.AcidState (DB, NewPendingError)
54+
import Cardano.Wallet.Kernel.DB.AcidState (DB, NewForeignError,
55+
NewPendingError)
5156
import Cardano.Wallet.Kernel.DB.HdWallet
5257
import qualified Cardano.Wallet.Kernel.DB.HdWallet as HD
5358
import Cardano.Wallet.Kernel.DB.InDb
@@ -56,8 +61,8 @@ import Cardano.Wallet.Kernel.DB.TxMeta.Types
5661
import Cardano.Wallet.Kernel.Internal (ActiveWallet (..),
5762
walletKeystore, walletNode)
5863
import qualified Cardano.Wallet.Kernel.Keystore as Keystore
59-
import Cardano.Wallet.Kernel.NodeStateAdaptor (getMaxTxSize)
60-
import Cardano.Wallet.Kernel.Pending (newPending)
64+
import qualified Cardano.Wallet.Kernel.NodeStateAdaptor as Node
65+
import Cardano.Wallet.Kernel.Pending (newForeign, newPending)
6166
import Cardano.Wallet.Kernel.Read (getWalletSnapshot)
6267
import Cardano.Wallet.Kernel.Types (AccountId (..),
6368
RawResolvedTx (..), WalletId (..))
@@ -173,7 +178,7 @@ newTransaction :: ActiveWallet
173178
-> IO (Either NewTransactionError (TxAux, TxMeta, Utxo))
174179
newTransaction ActiveWallet{..} spendingPassword options accountId payees = runExceptT $ do
175180
initialEnv <- liftIO $ newEnvironment
176-
maxTxSize <- liftIO $ getMaxTxSize (walletPassive ^. walletNode)
181+
maxTxSize <- liftIO $ Node.getMaxTxSize (walletPassive ^. walletNode)
177182
let maxInputs = estimateMaxTxInputs dummyAddrAttrSize dummyTxAttrSize maxTxSize
178183

179184
-- STEP 0: Get available UTxO
@@ -413,3 +418,113 @@ cardanoFee inputs outputs = Core.mkCoin $
413418
estimateCardanoFee linearFeePolicy inputs (toList $ fmap Core.getCoin outputs)
414419
where
415420
linearFeePolicy = Core.TxSizeLinear (Core.Coeff 155381) (Core.Coeff 43.946)
421+
422+
{-------------------------------------------------------------------------------
423+
Ada redemption
424+
425+
Old wallet layer implemention lives in "Pos.Wallet.Web.Methods.Redeem".
426+
-------------------------------------------------------------------------------}
427+
428+
data RedeemAdaError =
429+
-- | Unknown account
430+
--
431+
-- NOTE: The Daedalus frontend requires users to create an account before
432+
-- they can redeem Ada.
433+
RedeemAdaUnknownAccountId UnknownHdAccount
434+
435+
-- | We failed to translate the redeem key to a valid Cardano address
436+
| RedeemAdaErrorCreateAddressFailed Kernel.CreateAddressError
437+
438+
-- | There are no UTxO available at the redeem address
439+
--
440+
-- This probably means that the voucher has already been redeemed, but it
441+
-- /could/ also mean that there was never any voucher at this address.
442+
| RedeemAdaNotAvailable Address
443+
444+
-- | There are multiple outputs available at this address
445+
--
446+
-- This really should not happen at all. If this happens, we are running
447+
-- in a test setup with an invalid genesis block.
448+
| RedeemAdaMultipleOutputs Address
449+
450+
-- | We were unable to submit the transaction
451+
--
452+
-- If this error happens, it almost certainly indicates a bug.
453+
| RedeemAdaNewForeignFailed NewForeignError
454+
455+
instance Buildable RedeemAdaError where
456+
build (RedeemAdaUnknownAccountId err) =
457+
bprint ("RedeemAdaUnknownAccountId " % build) err
458+
build (RedeemAdaErrorCreateAddressFailed err) =
459+
bprint ("RedeemAdaErrorCreateAddressFailed " % build) err
460+
build (RedeemAdaNotAvailable addr) =
461+
bprint ("RedeemAdaNotAvailable " % build) addr
462+
build (RedeemAdaMultipleOutputs addr) =
463+
bprint ("RedeemAdaMultipleOutputs " % build) addr
464+
build (RedeemAdaNewForeignFailed err) =
465+
bprint ("RedeemAdaNewForeignFailed " % build) err
466+
467+
-- | Redeem Ada voucher
468+
--
469+
-- NOTE: The account must already exist, it is /not/ created implicitly if it
470+
-- does not yet exist.
471+
redeemAda :: ActiveWallet
472+
-> HdAccountId -- ^ Account ID
473+
-> PassPhrase -- ^ Spending password
474+
-> RedeemSecretKey -- ^ Redemption key
475+
-> IO (Either RedeemAdaError (Tx, TxMeta))
476+
redeemAda w@ActiveWallet{..} accId pw rsk = runExceptT $ do
477+
snapshot <- liftIO $ getWalletSnapshot walletPassive
478+
_accExists <- withExceptT RedeemAdaUnknownAccountId $ exceptT $
479+
lookupHdAccountId snapshot accId
480+
changeAddr <- withExceptT RedeemAdaErrorCreateAddressFailed $ ExceptT $ liftIO $
481+
Kernel.createAddress
482+
pw
483+
(AccountIdHdRnd accId)
484+
walletPassive
485+
(tx, meta) <- mkTx changeAddr
486+
withExceptT RedeemAdaNewForeignFailed $ ExceptT $ liftIO $
487+
newForeign
488+
w
489+
accId
490+
tx
491+
meta
492+
return (taTx tx, meta)
493+
where
494+
redeemAddr :: Address
495+
redeemAddr = Core.makeRedeemAddress $ redeemToPublic rsk
496+
497+
mkTx :: Address -> ExceptT RedeemAdaError IO (TxAux, TxMeta)
498+
mkTx output = do
499+
now <- liftIO $ Core.getCurrentTimestamp
500+
utxo <- liftIO $
501+
Node.withNodeState (walletPassive ^. walletNode) $ \_lock ->
502+
Node.filterUtxo isOutput
503+
(inp@(TxInUtxo inHash inIx), coin) <-
504+
case utxo of
505+
[i] -> return i
506+
[] -> throwError $ RedeemAdaNotAvailable redeemAddr
507+
_:_:_ -> throwError $ RedeemAdaMultipleOutputs redeemAddr
508+
let out = TxOutAux $ TxOut output coin
509+
txAux = CTxp.makeRedemptionTx
510+
walletProtocolMagic
511+
rsk
512+
(inp :| [])
513+
(out :| [])
514+
txMeta = TxMeta {
515+
_txMetaId = hash (taTx txAux)
516+
, _txMetaAmount = coin
517+
, _txMetaInputs = (inHash, inIx, redeemAddr, coin) :| []
518+
, _txMetaOutputs = (output, coin) :| []
519+
, _txMetaCreationAt = now
520+
, _txMetaIsLocal = False -- input does not belong to wallet
521+
, _txMetaIsOutgoing = False -- increases wallet's balance
522+
, _txMetaWalletId = _fromDb $ getHdRootId (accId ^. hdAccountIdParent)
523+
, _txMetaAccountIx = getHdAccountIx (accId ^. hdAccountIdIx)
524+
}
525+
return (txAux, txMeta)
526+
where
527+
isOutput :: (TxIn, TxOutAux) -> Maybe (TxIn, Coin)
528+
isOutput (inp, TxOutAux (TxOut addr coin)) = do
529+
guard $ addr == redeemAddr
530+
return (inp, coin)

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import qualified Cardano.Wallet.Kernel.Transactions as Kernel
5454
import qualified Cardano.Wallet.Kernel.Wallets as Kernel
5555
import Cardano.Wallet.WalletLayer.ExecutionTimeLimit
5656
(TimeExecutionLimit)
57+
import Cardano.Wallet.WalletLayer.Kernel.Conv (InvalidRedemptionCode)
5758

5859
------------------------------------------------------------
5960
-- Errors when manipulating wallets
@@ -434,7 +435,7 @@ data ActiveWalletLayer m = ActiveWalletLayer {
434435
-> m (Either EstimateFeesError Coin)
435436

436437
-- | Redeem ada
437-
, redeemAda :: Redemption -> m (Either RedeemAdaError Tx)
438+
, redeemAda :: Redemption -> m (Either RedeemAdaError (Tx, TxMeta))
438439

439440
-- | Node info
440441
--
@@ -494,13 +495,20 @@ instance Arbitrary EstimateFeesError where
494495
, EstimateFeesTimeLimitReached <$> arbitrary
495496
]
496497

497-
-- | TODO: Will need to be extended
498-
data RedeemAdaError = RedeemAdaError
498+
data RedeemAdaError =
499+
RedeemAdaError Kernel.RedeemAdaError
500+
| RedeemAdaWalletIdDecodingFailed Text
501+
| RedeemAdaInvalidRedemptionCode InvalidRedemptionCode
499502

500503
instance Show RedeemAdaError where
501504
show = formatToString build
502505

503506
instance Exception RedeemAdaError
504507

505508
instance Buildable RedeemAdaError where
506-
build RedeemAdaError = "RedeemAdaError"
509+
build (RedeemAdaError err) =
510+
bprint ("RedeemAdaError " % build) err
511+
build (RedeemAdaWalletIdDecodingFailed txt) =
512+
bprint ("RedeemAdaWalletIdDecodingFailed " % build) txt
513+
build (RedeemAdaInvalidRedemptionCode txt) =
514+
bprint ("RedeemAdaInvalidRedemptionCode " % build) txt

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

-33
This file was deleted.

0 commit comments

Comments
 (0)