Skip to content

Commit

Permalink
Add externally-signed transaction payment support in deposit UI (#4970)
Browse files Browse the repository at this point in the history
### Changes

- Added a new state transition in payment page to go from unsigned to
signed
- Added a form to copy the unsigned tx and paste the externally signed
one
- Added a script to sign txs based on the unsigned tx offered in the
payments page

### Issue

fix #4969
fix #4974
  • Loading branch information
paolino authored Feb 6, 2025
2 parents 6342471 + 48779b6 commit 3acbb2a
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 30 deletions.
49 changes: 41 additions & 8 deletions lib/ui/src/Cardano/Wallet/UI/Deposit/API/Payments.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Data.Aeson
, withObject
, withText
, (.:)
, (.:?)
)
import Data.Aeson.Types
( Parser
Expand Down Expand Up @@ -72,6 +73,9 @@ import Web.FormUrlEncoded

import qualified Data.Aeson as Aeson
import qualified Data.Map.Monoidal.Strict as MonoidalMap
import Data.Maybe
( fromMaybe
)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
Expand Down Expand Up @@ -132,8 +136,14 @@ instance FromJSON Transaction where
dataType <- o .: "type"
description <- o .: "description"
cborHex <- o .: "cborHex"
bip32Paths <- o .: "bip32Paths"
pure Transaction{dataType, description, cborHex, bip32Paths}
bip32Paths <- o .:? "bip32Paths"
pure
Transaction
{ dataType
, description
, cborHex
, bip32Paths = fromMaybe [] bip32Paths
}

-- Orphan instances for BIP32Path
-- TODO: move where they belong, in the module defining BIP32Path
Expand Down Expand Up @@ -179,16 +189,36 @@ instance FromJSON BIP32Path where

newtype Password = Password Text

data SignatureForm = SignatureForm
{ signatureFormState :: State
, signaturePassword :: Password
}
data SignatureForm
= SignatureForm
{ signatureFormState :: State
, signaturePassword :: Password
}
| ExternalSignatureForm
{ signatureFormState :: State
, signatureSignedTransaction :: Transaction
}

instance FromForm SignatureForm where
fromForm form = do
signatureFormState <- fromForm form
signaturePassword <- Password <$> parseUnique "passphrase" form
pure SignatureForm{signatureFormState, signaturePassword}
let
signature = do
signaturePassword <- Password <$> parseUnique "passphrase" form
pure SignatureForm{signatureFormState, signaturePassword}
externalSignature = do
signatureSignedTransaction <- parseUnique "signed-transaction" form
pure
ExternalSignatureForm{signatureFormState, signatureSignedTransaction}
case signature of
Left _ -> externalSignature
Right s -> pure s

instance FromHttpApiData Transaction where
parseQueryParam :: Text -> Either Text Transaction
parseQueryParam t = case Aeson.decode $ TL.encodeUtf8 $ TL.fromStrict t of
Nothing -> Left "Invalid JSON for a Transaction"
Just tx -> pure tx

data StateA t
= NoState
Expand Down Expand Up @@ -219,6 +249,7 @@ data Signal
= AddReceiver Receiver
| DeleteReceiver Address
| Sign Password
| ExternallySign Transaction
| Unsign
| Submit
| Reset
Expand Down Expand Up @@ -274,6 +305,8 @@ step c (Unsigned utx) (DeleteReceiver addr) = do
step c (Unsigned utx) (Sign pwd) = do
stx <- sign c utx pwd
pure $ Just $ Signed utx stx
step _ (Unsigned utx) (ExternallySign stx) = do
pure $ Just $ Signed utx stx
step c (Signed utx _) (AddReceiver receiver) = do
Just <$> addReceiver c utx receiver
step c (Signed utx _) (DeleteReceiver addr) = do
Expand Down
51 changes: 34 additions & 17 deletions lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Payments/Page.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
module Cardano.Wallet.UI.Deposit.Html.Pages.Payments.Page
( paymentsH
, paymentsElementH
-- , receiversH
-- , updateReceiversH
, availableBalanceElementH
, receiverAddressValidationH
, receiverAmountValidationH
, paymentsChangeH
-- , submitH
)
where

Expand Down Expand Up @@ -89,6 +86,9 @@ import Control.Monad
( forM_
, when
)
import Data.Foldable
( Foldable (..)
)
import Data.Maybe
( fromMaybe
)
Expand Down Expand Up @@ -153,7 +153,7 @@ paymentsChangeH balance transaction = do
setInspection Nothing
setBalance balance Nothing
Unsigned (utx, inspect) -> do
setReceivers $ Just (signatureFormH canSign, inspect)
setReceivers $ Just (signatureFormH utx canSign, inspect)
setInspection $ Just (inspect, utx, Nothing)
setBalance balance $ Just inspect
Signed utx (stx, inspect) -> do
Expand Down Expand Up @@ -183,7 +183,8 @@ setReceivers mInspect =
$ case mInspect of
Nothing -> receiversH Nothing
Just (canSign, inspect) -> do
receiversH $ Just (canSign, extractReceivers inspect)
receiversH $ Just (extractReceivers inspect)
canSign

setInspection
:: Maybe (InspectTx, Transaction, Maybe Transaction)
Expand Down Expand Up @@ -253,7 +254,7 @@ newReceiverH = do
]
mempty

receiversH :: Maybe (Html (), Receivers) -> Html ()
receiversH :: Maybe Receivers -> Html ()
receiversH m = do
div_ [class_ "d-flex justify-content-end"] $ do
table_
Expand All @@ -266,7 +267,7 @@ receiversH m = do
thEnd (Just 9) "Amount"
thEnd (Just 5) "Actions"
tbody_ [id_ "payment-state"]
$ forM_ (MonoidalMap.assocs $ foldMap snd m)
$ forM_ (MonoidalMap.assocs $ fold m)
$ \(address, Sum amount) -> do
tr_ $ do
tdEnd $ do
Expand All @@ -285,9 +286,6 @@ receiversH m = do
]
$ i_ [class_ "bi bi-trash"] mempty
newReceiverH
case m of
Just (h, _) -> div_ [class_ "px-2"] h
_ -> pure ()

ifNotEmpty :: (Foldable t, Monoid b) => t a -> b -> b
ifNotEmpty xs b = if null xs then mempty else b
Expand Down Expand Up @@ -345,7 +343,9 @@ transactionInspectionH (InspectTx{..}, utx, mstx) = do
$ do
thead_ $ do
tr_ $ do
thEnd Nothing $ toHtml $ truncatableText WithoutCopy "" "Change Address"
thEnd Nothing
$ toHtml
$ truncatableText WithoutCopy "" "Change Address"
thEnd (Just 7) "Amount"
tbody_
$ forM_ change
Expand Down Expand Up @@ -386,16 +386,15 @@ transactionInspectionH (InspectTx{..}, utx, mstx) = do

transactionCBORH :: Text -> Transaction -> Html ()
transactionCBORH copyName cbor =
truncatableText WithCopy copyName -- "unsigned-transaction-copy"
truncatableText WithCopy copyName
$ toHtml
$ Aeson.encode cbor

signatureFormH :: CanSign -> Html ()
signatureFormH = \case
signatureFormH :: Transaction -> CanSign -> Html ()
signatureFormH utx = \case
CanSign -> do
div_ [class_ "d-flex justify-content-end"] $ do
div_ [class_ "input-group", style_ "max-width:35em"] $ do
-- span_ [class_ "input-group-text"] "Sign"
input_
[ id_ "signature-password"
, class_ "form-control text-end"
Expand All @@ -410,7 +409,25 @@ signatureFormH = \case
, hxInclude_ "#signature-password, #payment-state"
]
"Sign"
CannotSign -> "paste signed tx not implemented"
CannotSign -> do
record (Just 15) Full Striped $ do
field [] "unsigned transaction"
$ transactionCBORH "unsigned-transaction-signature-copy" utx
div_ [class_ "d-flex justify-content-end"] $ do
div_ [class_ "input-group", style_ "max-width:35em"] $ do
input_
[ id_ "signed-transaction"
, class_ "form-control text-end"
, name_ "signed-transaction"
, placeholder_ "signed transaction"
]
button_
[ class_ "btn btn-secondary"
, hxPost_ $ linkText paymentsSignLink
, hxInclude_ "#payment-state, #signed-transaction"
, hxTarget_ "#none"
]
"Accept"

submitH :: Html ()
submitH = do
Expand Down Expand Up @@ -492,7 +509,7 @@ paymentsElementH =
box "New" mempty
$ do
setState [] NoState
box "Payment Receivers" mempty
box "Transaction Creation" mempty
$ do
div_
[ id_ "receivers"
Expand Down
11 changes: 6 additions & 5 deletions lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Payments/Page.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{-# LANGUAGE NamedFieldPuns #-}

module Cardano.Wallet.UI.Deposit.Server.Payments.Page
( servePaymentsPage
, servePaymentsNewReceiver
Expand Down Expand Up @@ -154,15 +152,18 @@ servePaymentsSign
-> SignatureForm
-> Maybe RequestCookies
-> Handler (CookieResponse RawHtml)
servePaymentsSign ul SignatureForm{signatureFormState, signaturePassword} =
servePaymentsSign ul r = -- SignatureForm{signatureFormState, signaturePassword} =
withSessionLayer ul $ \layer -> do
renderHtml
<$> signalHandler
layer
alertH
paymentsChangeH
signatureFormState
(Sign signaturePassword)
(signatureFormState r)
(case r of
SignatureForm _ s -> Sign s
ExternalSignatureForm _ s -> ExternallySign s
)

servePaymentsSubmit
:: UILayer WalletResource
Expand Down
59 changes: 59 additions & 0 deletions scripts/sign-tx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env -S nix shell nixpkgs#jq .#cardano-cli .#cardano-address -c bash
# shellcheck shell=bash

set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "Usage: $0 <mnemonic> <unsigned-tx-json>" >&2
exit 1
fi

temp_dir=$(mktemp -d)

# cleanup function
cleanup() {
rm -rf "$temp_dir"
}

trap cleanup EXIT
trap cleanup ERR
trap cleanup SIGINT

# collect arguments
mnemonic=$1
unsigned=$2

# create the root key
cardano-address key from-recovery-phrase Shelley <<<"$mnemonic" >"$temp_dir/root.xsk"

# extract bip32 paths from the unsigned tx json
paths=$(jq -r '.bip32Paths[]' <<<"$unsigned")

# derive keys, convert to cardano-cli format and collect --signing-key-file arguments
index=0
signing_key_files=""
for path in $paths; do
key_file="$temp_dir/key${index}.xsk"
cardano-address key child "$path" <"$temp_dir/root.xsk" >"$key_file"
cli_key_file="$temp_dir/key${index}.skey"
cardano-cli key convert-cardano-address-key \
--shelley-payment-key \
--signing-key-file "$key_file" \
--out-file "$cli_key_file"
signing_key_files="$signing_key_files --signing-key-file $temp_dir/key${index}.skey"
index=$((index + 1))
done

# dump unsigned tx to a file
echo "$unsigned" >"$temp_dir/tx.unsigned"

# sign the transaction
# shellcheck disable=SC2086
cardano-cli conway transaction sign \
$signing_key_files \
--tx-body-file "$temp_dir/tx.unsigned" \
--out-file "$temp_dir/tx.signed" \
--testnet-magic 1

# print the signed transaction
cat "$temp_dir/tx.signed"

0 comments on commit 3acbb2a

Please sign in to comment.