Skip to content

Commit

Permalink
Add unsignedChange field to UnsignedTx.
Browse files Browse the repository at this point in the history
This allows change outputs to be distinguished from ordinary outputs
within an `UnsignedTx`, which makes it easy to construct an
`ApiCoinSelection` where change outputs are distinguished from ordinary
outputs.
  • Loading branch information
jonathanknowles committed Oct 20, 2020
1 parent db97c0b commit 8888da9
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 27 deletions.
18 changes: 10 additions & 8 deletions lib/core/src/Cardano/Wallet.hs
Expand Up @@ -1619,7 +1619,7 @@ signPayment ctx wid argGenChange mkRewardAccount pwd md cs = db & \DBLayer{..} -

-- | Very much like 'signPayment', but doesn't not generate change addresses.
signTx
:: forall ctx s t k.
:: forall ctx s t k input output change.
( HasTransactionLayer t k ctx
, HasDBLayer s k ctx
, HasNetworkLayer t ctx
Expand All @@ -1628,14 +1628,16 @@ signTx
, HardDerivation k
, Bounded (Index (AddressIndexDerivationType k) 'AddressK)
, WalletKey k
, input ~ (TxIn, TxOut)
, output ~ TxOut
)
=> ctx
-> WalletId
-> Passphrase "raw"
-> Maybe TxMetadata
-> UnsignedTx (TxIn, TxOut)
-> UnsignedTx input output change
-> ExceptT ErrSignPayment IO (Tx, TxMeta, UTCTime, SealedTx)
signTx ctx wid pwd md (UnsignedTx inpsNE outs) = db & \DBLayer{..} -> do
signTx ctx wid pwd md (UnsignedTx inpsNE outs _change) = db & \DBLayer{..} ->
withRootKey @_ @s ctx wid pwd ErrSignPaymentWithRootKey $ \xprv scheme -> do
let pwdP = preparePassphrase scheme pwd
nodeTip <- withExceptT ErrSignPaymentNetwork $ currentNodeTip nl
Expand Down Expand Up @@ -1664,32 +1666,32 @@ signTx ctx wid pwd md (UnsignedTx inpsNE outs) = db & \DBLayer{..} -> do

-- | Makes a fully-resolved coin selection for the given set of payments.
selectCoinsExternal
:: forall ctx s k e resolvedInput.
:: forall ctx s k e input output change.
( GenChange s
, HasDBLayer s k ctx
, IsOurs s Address
, resolvedInput ~ (TxIn, TxOut, NonEmpty DerivationIndex)
, input ~ (TxIn, TxOut, NonEmpty DerivationIndex)
, output ~ TxOut
)
=> ctx
-> WalletId
-> ArgGenChange s
-> ExceptT (ErrSelectCoinsExternal e) IO CoinSelection
-> ExceptT (ErrSelectCoinsExternal e) IO (UnsignedTx resolvedInput)
-> ExceptT (ErrSelectCoinsExternal e) IO (UnsignedTx input output change)
selectCoinsExternal ctx wid argGenChange selectCoins = do
cs <- selectCoins

(cs', s') <- db & \DBLayer{..} ->
withExceptT ErrSelectCoinsExternalNoSuchWallet $
mapExceptT atomically $ do
cp <- withNoSuchWallet wid $ readCheckpoint $ PrimaryKey wid
(cs', s') <- assignChangeAddresses argGenChange cs (getState cp)
putCheckpoint (PrimaryKey wid) (updateState s' cp)
pure (cs', s')

UnsignedTx
<$> (fullyQualifiedInputs s' cs'
(ErrSelectCoinsExternalUnableToAssignInputs cs'))
<*> pure (outputs cs')
<*> pure []
where
db = ctx ^. dbLayer @s @k

Expand Down
26 changes: 16 additions & 10 deletions lib/core/src/Cardano/Wallet/Api/Server.hs
Expand Up @@ -1685,22 +1685,27 @@ migrateWallet ctx (ApiT wid) migrateData = do
-- the number of addresses in the specified address list, addresses will be
-- recycled in order of their appearance in the original list.
assignMigrationAddresses
:: [Address]
:: forall input output change .
( input ~ (TxIn, TxOut)
, output ~ TxOut
)
=> [Address]
-- ^ Target addresses
-> [CoinSelection]
-- ^ Migration data for the source wallet.
-> [UnsignedTx (TxIn, TxOut)]
-> [UnsignedTx input output change]
assignMigrationAddresses addrs selections =
fst $ foldr accumulate ([], cycle addrs) selections
where
accumulate sel (txs, addrsAvailable) = first
(\addrsSelected -> makeTx sel addrsSelected : txs)
(splitAt (length $ view #change sel) addrsAvailable)

makeTx :: CoinSelection -> [Address] -> UnsignedTx (TxIn, TxOut)
makeTx :: CoinSelection -> [Address] -> UnsignedTx input output change
makeTx sel addrsSelected = UnsignedTx
(NE.fromList (sel ^. #inputs))
(zipWith TxOut addrsSelected (sel ^. #change))
[]

{-------------------------------------------------------------------------------
Network
Expand Down Expand Up @@ -1887,11 +1892,14 @@ rndStateChange ctx (ApiT wid) pwd =

-- | Makes an 'ApiCoinSelection' from the given 'UnsignedTx'.
mkApiCoinSelection
:: forall n. ()
:: forall n input output change.
( input ~ (TxIn, TxOut, NonEmpty DerivationIndex)
, output ~ TxOut
)
=> Maybe (DelegationAction, NonEmpty DerivationIndex)
-> UnsignedTx (TxIn, TxOut, NonEmpty DerivationIndex)
-> UnsignedTx input output change
-> ApiCoinSelection n
mkApiCoinSelection mcerts (UnsignedTx inputs outputs) =
mkApiCoinSelection mcerts (UnsignedTx inputs outputs _) =
ApiCoinSelection
(mkApiCoinSelectionInput <$> inputs)
(mkAddressAmount <$> outputs)
Expand Down Expand Up @@ -1919,13 +1927,11 @@ mkApiCoinSelection mcerts (UnsignedTx inputs outputs) =
where
apiStakePath = ApiT <$> xs

mkAddressAmount :: TxOut -> AddressAmount (ApiT Address, Proxy n)
mkAddressAmount :: output -> AddressAmount (ApiT Address, Proxy n)
mkAddressAmount (TxOut addr (Coin c)) =
AddressAmount (ApiT addr, Proxy @n) (Quantity $ fromIntegral c)

mkApiCoinSelectionInput
:: (TxIn, TxOut, NonEmpty DerivationIndex)
-> ApiCoinSelectionInput n
mkApiCoinSelectionInput :: input -> ApiCoinSelectionInput n
mkApiCoinSelectionInput (TxIn txid index, TxOut addr (Coin c), path) =
ApiCoinSelectionInput
{ id = ApiT txid
Expand Down
4 changes: 2 additions & 2 deletions lib/core/src/Cardano/Wallet/DB/Sqlite.hs
Expand Up @@ -132,7 +132,7 @@ import Data.Coerce
import Data.Either
( isRight )
import Data.Generics.Internal.VL.Lens
( (^.) )
( view, (^.) )
import Data.IORef
( modifyIORef', newIORef, readIORef )
import Data.List
Expand Down Expand Up @@ -1176,7 +1176,7 @@ mkTxInputsOutputs tx =
mkTxOut tid (ix, txOut) = TxOut
{ txOutputTxId = TxId tid
, txOutputIndex = ix
, txOutputAddress = W.address txOut
, txOutputAddress = view #address txOut
, txOutputAmount = W.coin txOut
}
ordered f = fmap (zip [0..] . f)
Expand Down
16 changes: 14 additions & 2 deletions lib/core/src/Cardano/Wallet/Primitive/Types.hs
Expand Up @@ -41,6 +41,7 @@ module Cardano.Wallet.Primitive.Types

-- * Tx
, Tx (..)
, TxChange (..)
, TxIn(..)
, TxOut(..)
, TxMeta(..)
Expand Down Expand Up @@ -911,6 +912,15 @@ data TxOut = TxOut
:: !Coin
} deriving (Show, Generic, Eq, Ord)

data TxChange derivationPath = TxChange
{ address
:: !Address
, amount
:: !Coin
, derivationPath
:: derivationPath
} deriving (Show, Generic, Eq, Ord)

instance NFData TxOut

instance Buildable TxOut where
Expand All @@ -921,7 +931,7 @@ instance Buildable TxOut where
<> "..."
<> suffixF 8 addrF
where
addrF = build $ address txout
addrF = build $ view #address txout

instance Buildable (TxIn, TxOut) where
build (txin, txout) = build txin <> " ==> " <> build txout
Expand Down Expand Up @@ -966,7 +976,7 @@ instance ToText TxStatus where
--
-- See 'Tx' for a signed transaction.
--
data UnsignedTx input = UnsignedTx
data UnsignedTx input output change = UnsignedTx
{ unsignedInputs
:: NonEmpty input
-- Inputs are *necessarily* non-empty because Cardano requires at least
Expand All @@ -983,6 +993,8 @@ data UnsignedTx input = UnsignedTx
-- depending on which input(s) get selected to fuel the transaction, it
-- may or may not include a change output should its value be less than
-- the minimal UTxO value set by the network.
, unsignedChange
:: [change]
}
deriving (Eq, Show)

Expand Down
10 changes: 5 additions & 5 deletions lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelectionSpec.hs
Expand Up @@ -144,7 +144,7 @@ prop_coinValuesPreserved (CoinSelectionsSetup cs addrs) = do
sum . map (\(_, TxOut _ (Coin c)) -> c)
let selsCoinValue =
sum $ getCoinValueFromInp . inputs . getCS <$> cs
let getCoinValueFromTxOut (UnsignedTx _ txouts) =
let getCoinValueFromTxOut (UnsignedTx _ txouts _) =
sum $ map (\(TxOut _ (Coin c)) -> c) txouts
let txsCoinValue =
sum . map getCoinValueFromTxOut
Expand All @@ -163,7 +163,7 @@ prop_coinValuesPreservedPerTx f (CoinSelectionsSetup cs addrs) = do
let getCoinValueFromInp =
f . map (\(_, TxOut _ (Coin c)) -> c)
let selsCoinValue = getCoinValueFromInp . inputs . getCS <$> cs
let getCoinValueFromTxOut (UnsignedTx _ txouts) =
let getCoinValueFromTxOut (UnsignedTx _ txouts _) =
f $ map (\(TxOut _ (Coin c)) -> c) txouts
let txsCoinValue = map getCoinValueFromTxOut
txsCoinValue (assignMigrationAddresses addrs sels) === selsCoinValue
Expand All @@ -176,7 +176,7 @@ prop_allInputsAreUsed
prop_allInputsAreUsed (CoinSelectionsSetup cs addrs) = do
let sels = getCS <$> cs
let csInps = Set.fromList $ concatMap inputs sels
let getInpsFromTx (UnsignedTx inp _) = NE.toList inp
let getInpsFromTx (UnsignedTx inp _ _) = NE.toList inp
let txsCoinValue = Set.fromList . concatMap getInpsFromTx
txsCoinValue (assignMigrationAddresses addrs sels) === csInps

Expand All @@ -189,7 +189,7 @@ prop_allInputsAreUsedPerTx
prop_allInputsAreUsedPerTx (CoinSelectionsSetup cs addrs) = do
let sels = getCS <$> cs
let csInps = Set.fromList . inputs <$> sels
let getInpsFromTx (UnsignedTx inp _) = NE.toList inp
let getInpsFromTx (UnsignedTx inp _ _) = NE.toList inp
let txsCoinValue = map (Set.fromList . getInpsFromTx)
txsCoinValue (assignMigrationAddresses addrs sels) === csInps

Expand All @@ -201,7 +201,7 @@ prop_fairAddressesRecycled
-> Property
prop_fairAddressesRecycled (CoinSelectionsSetup cs addrs) = do
let sels = getCS <$> cs
let getAllAddrPerTx (UnsignedTx _ txouts) =
let getAllAddrPerTx (UnsignedTx _ txouts _) =
map (\(TxOut addr _) -> addr) txouts
let getAllAddrCounts =
Map.elems .
Expand Down

0 comments on commit 8888da9

Please sign in to comment.