-
Notifications
You must be signed in to change notification settings - Fork 214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ADP-3344] Implement createPayment
using balanceTx
#4814
Changes from all commits
6b13278
303c4a9
905e18e
4710c06
eb891da
80aeecd
842e571
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
{-# LANGUAGE DuplicateRecordFields #-} | ||
{-# LANGUAGE FlexibleContexts #-} | ||
{-# LANGUAGE GADTs #-} | ||
{-# LANGUAGE ScopedTypeVariables #-} | ||
{-# LANGUAGE TypeOperators #-} | ||
|
||
module Cardano.Wallet.Deposit.Pure | ||
|
@@ -38,11 +39,13 @@ module Cardano.Wallet.Deposit.Pure | |
, getTxHistoryByTime | ||
|
||
-- ** Writing to the blockchain | ||
, ErrCreatePayment (..) | ||
, createPayment | ||
|
||
, BIP32Path (..) | ||
, DerivationType (..) | ||
, getBIP32PathsForOwnedInputs | ||
, signTxBody | ||
, signTx | ||
, addTxSubmission | ||
, listTxsInSubmission | ||
|
||
|
@@ -93,6 +96,15 @@ import Cardano.Wallet.Deposit.Read | |
, TxId | ||
, WithOrigin | ||
) | ||
import Control.Monad.Trans.Except | ||
( runExceptT | ||
) | ||
import Data.Bifunctor | ||
( first | ||
) | ||
import Data.Digest.CRC32 | ||
( crc32 | ||
) | ||
import Data.Foldable | ||
( fold | ||
, foldl' | ||
|
@@ -127,6 +139,8 @@ import qualified Cardano.Wallet.Deposit.Pure.UTxO as UTxO | |
import qualified Cardano.Wallet.Deposit.Pure.UTxO.UTxOHistory as UTxOHistory | ||
import qualified Cardano.Wallet.Deposit.Read as Read | ||
import qualified Cardano.Wallet.Deposit.Write as Write | ||
import qualified Cardano.Wallet.Read.Hash as Hash | ||
import qualified Control.Monad.Random.Strict as Random | ||
import qualified Data.Delta as Delta | ||
import qualified Data.List as L | ||
import qualified Data.Map.Strict as Map | ||
|
@@ -139,11 +153,18 @@ type Customer = Address.Customer | |
|
||
data WalletState = WalletState | ||
{ walletTip :: Read.ChainPoint | ||
-- ^ The wallet includes information from all blocks until | ||
-- and including this one. | ||
, addresses :: !Address.AddressState | ||
-- ^ Addresses and public keys known to this wallet. | ||
, utxoHistory :: !UTxOHistory.UTxOHistory | ||
-- ^ UTxO of this wallet, with support for rollbacks. | ||
, txHistory :: !TxHistory | ||
-- ^ (Summarized) transaction history of this wallet. | ||
, submissions :: Sbm.TxSubmissions | ||
-- ^ Queue of pending transactions. | ||
, rootXSignKey :: Maybe XPrv | ||
-- ^ Maybe a private key for signing transactions. | ||
-- , info :: !WalletInfo | ||
} | ||
|
||
|
@@ -217,7 +238,7 @@ fromXPubAndGenesis xpub knownCustomerCount genesisData = | |
{ walletTip = Read.GenesisPoint | ||
, addresses = | ||
Address.fromXPubAndCount network xpub knownCustomerCount | ||
, utxoHistory = UTxOHistory.empty initialUTxO | ||
, utxoHistory = UTxOHistory.fromOrigin initialUTxO | ||
, txHistory = mempty | ||
, submissions = Sbm.empty | ||
, rootXSignKey = Nothing | ||
|
@@ -342,23 +363,109 @@ wonders interval = | |
|
||
{----------------------------------------------------------------------------- | ||
Operations | ||
Writing to blockchain | ||
Constructing transactions | ||
------------------------------------------------------------------------------} | ||
data ErrCreatePayment | ||
= ErrCreatePaymentNotRecentEra (Read.EraValue Read.Era) | ||
| ErrCreatePaymentBalanceTx (Write.ErrBalanceTx Write.Conway) | ||
deriving (Eq, Show) | ||
|
||
-- | Create a payment to a list of destinations. | ||
createPayment | ||
:: [(Address, Write.Value)] -> WalletState -> Maybe Write.TxBody | ||
createPayment = undefined | ||
:: Read.EraValue Read.PParams | ||
-> Write.TimeTranslation | ||
-> [(Address, Write.Value)] | ||
-> WalletState | ||
-> Either ErrCreatePayment Write.Tx | ||
createPayment (Read.EraValue (Read.PParams pparams :: Read.PParams era)) a b w = | ||
case Read.theEra :: Read.Era era of | ||
Read.Conway -> | ||
first ErrCreatePaymentBalanceTx | ||
$ createPaymentConway pparams a b w | ||
era' -> Left $ ErrCreatePaymentNotRecentEra (Read.EraValue era') | ||
|
||
-- | In the Conway era: Create a payment to a list of destinations. | ||
createPaymentConway | ||
:: Write.PParams Write.Conway | ||
-> Write.TimeTranslation | ||
-> [(Address, Write.Value)] | ||
-> WalletState | ||
-> Either (Write.ErrBalanceTx Write.Conway) Write.Tx | ||
createPaymentConway pparams timeTranslation destinations w = | ||
fmap (Read.Tx . fst) | ||
. flip Random.evalRand (pilferRandomGen w) | ||
. runExceptT | ||
. balance | ||
(availableUTxO w) | ||
(addresses w) | ||
. mkPartialTx | ||
$ paymentTxBody | ||
where | ||
paymentTxBody :: Write.TxBody | ||
paymentTxBody = Write.TxBody | ||
{ spendInputs = mempty | ||
, collInputs = mempty | ||
, txouts = | ||
Map.fromList $ zip [(toEnum 0)..] $ map (uncurry Write.mkTxOut) destinations | ||
, collRet = Nothing | ||
} | ||
|
||
mkPartialTx :: Write.TxBody -> Write.PartialTx Write.Conway | ||
mkPartialTx txbody = Write.PartialTx | ||
{ tx = Read.unTx $ Write.mkTx txbody | ||
, extraUTxO = mempty :: Write.UTxO Write.Conway | ||
, redeemers = mempty | ||
, stakeKeyDeposits = Write.StakeKeyDepositMap mempty | ||
, timelockKeyWitnessCounts = Write.TimelockKeyWitnessCounts mempty | ||
} | ||
|
||
balance utxo addressState = | ||
Write.balanceTx | ||
pparams | ||
timeTranslation | ||
Write.AllKeyPaymentCredentials | ||
(Write.constructUTxOIndex $ Write.toConwayUTxO utxo) | ||
(changeAddressGen addressState) | ||
() | ||
|
||
changeAddressGen s = Write.ChangeAddressGen | ||
{ Write.genChangeAddress = | ||
first Read.decompactAddr . Address.newChangeAddress s | ||
, Write.maxLengthChangeAddress = | ||
Read.decompactAddr $ Address.mockMaxLengthChangeAddress s | ||
} | ||
|
||
-- | Use entropy contained in the current 'WalletState' | ||
-- to construct a pseudorandom seed. | ||
-- (NOT a viable source of cryptographic randomness.) | ||
-- | ||
-- Possible downsides of this approach: | ||
-- | ||
-- 1. security/privacy | ||
-- 2. concurrency | ||
-- 3. retries for different coin selections | ||
pilferRandomGen :: WalletState -> Random.StdGen | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It not being E.g. possibly re 1) security/privacy or 2) concurrency or 3) retries for different selections There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 My thinking was that
Alternative designs:
The point 3 cannot be changed in a stateless manner — you'd need to know about previous attempts at calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was more thinking that: if
*the same inputs, yes. And I think there's a decent chance it's guaranteed. But... this may possibly not be terrible as you shouldn't rely on randomness to prevent selecting the same inputs anyway 🤷♂️
Not necessarily. There could be cases where the user as strong opinions on the selection and would like to discard it to try again. As for retries not being an intended solution for failing balanceTx results, I do agree. (Whether
If you provide a non-public initial seed, probably 🤔
What tx? The one that isn't constructed yet? Identical partial txs may be balanced and submitted repeatedly. We could use all arguments to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyway, happy to go with the approach you have, as it's interesting and I see no clear problem with it, but I did want to raise the points. |
||
pilferRandomGen = | ||
Random.mkStdGen . fromEnum . fromChainPoint . walletTip | ||
where | ||
fromChainPoint (Read.GenesisPoint) = 0 | ||
fromChainPoint (Read.BlockPoint _ headerHash) = | ||
crc32 $ Hash.hashToBytes headerHash | ||
|
||
-- needs balanceTx | ||
-- needs to sign the transaction | ||
{----------------------------------------------------------------------------- | ||
Operations | ||
Signing transactions | ||
------------------------------------------------------------------------------} | ||
|
||
getBIP32PathsForOwnedInputs | ||
:: Write.TxBody -> WalletState -> [BIP32Path] | ||
getBIP32PathsForOwnedInputs txbody w = | ||
getBIP32PathsForOwnedInputs :: Write.Tx -> WalletState -> [BIP32Path] | ||
getBIP32PathsForOwnedInputs tx w = | ||
getBIP32Paths w | ||
. resolveInputAddresses | ||
$ Write.spendInputs txbody <> Write.collInputs txbody | ||
$ Write.spendInputs txBody <> Write.collInputs txBody | ||
where | ||
txBody :: Write.TxBody | ||
txBody = undefined tx | ||
|
||
resolveInputAddresses :: Set Read.TxIn -> [Read.Address] | ||
resolveInputAddresses ins = | ||
map (Read.address . snd) | ||
|
@@ -369,8 +476,13 @@ getBIP32Paths :: WalletState -> [Read.Address] -> [BIP32Path] | |
getBIP32Paths w = | ||
mapMaybe $ Address.getBIP32Path (addresses w) | ||
|
||
signTxBody :: Write.TxBody -> WalletState -> Maybe Write.Tx | ||
signTxBody _txbody _w = undefined | ||
signTx :: Write.Tx -> WalletState -> Maybe Write.Tx | ||
signTx _tx _w = undefined | ||
|
||
{----------------------------------------------------------------------------- | ||
Operations | ||
Pending transactions | ||
------------------------------------------------------------------------------} | ||
|
||
addTxSubmission :: Write.Tx -> WalletState -> WalletState | ||
addTxSubmission _tx _w = undefined | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Frankly — I don't think eventually duplicating
createPaymentConway
andcreatePaymentNextEra
seem that appealing. Why not have the implementation for anyIsRecentEra era
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On this note, I actually still suspect it would be better to consistently keep two eras as "recent" so that we don't forget details which matter only when supporting an era which isn't the latest one, for when we have to before the HF. For instance, that the
utxo
in theory could be newer than theera
wheretoConwayUTxO
might fail (although in practice, this might require both switching nodes and a race condition... 🤔)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally agree on the
IsRecentEra era
polymorphism.However, for the Deposit Wallet MVP, I figured that I would limit the scope to
Conway
— that's whyCardano.Wallet.Deposit.Write.Tx
is pinned to a single era for now.As for the actual implementation of era-polymorphic functions: It's a matter of code reuse — the function
createPaymentConway
can be reused in an era-polymorphic implementation, or it can be cheaper to writecreatePaymentPolymorphic
directly.Compare
vs
The second style tends to use lines of code more efficiently, but not always due to case distinctions and type inference. The first style can always be wrapped into the second style by adding code rather than changing it.