Skip to content
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

CLI: Add option "cardano-wallet transaction create --metadata=JSON" #2125

Merged
merged 9 commits into from Sep 22, 2020
1 change: 1 addition & 0 deletions lib/cli/cardano-wallet-cli.cabal
Expand Up @@ -69,6 +69,7 @@ test-suite unit
base
, cardano-wallet-cli
, cardano-wallet-core
, containers
, filepath
, hspec
, network-uri
Expand Down
30 changes: 27 additions & 3 deletions lib/cli/src/Cardano/CLI.hs
Expand Up @@ -54,6 +54,7 @@ module Cardano.CLI
, syncToleranceOption
, tlsOption
, smashURLOption
, metadataOption

-- * Option parsers for configuring tracing
, LoggingOptions (..)
Expand Down Expand Up @@ -128,6 +129,7 @@ import Cardano.Wallet.Api.Types
, ApiPostRandomAddressData (..)
, ApiT (..)
, ApiTxId (ApiTxId)
, ApiTxMetadata (..)
, ApiWallet
, ByronWalletPostData (..)
, ByronWalletStyle (..)
Expand Down Expand Up @@ -687,6 +689,7 @@ data TransactionCreateArgs t = TransactionCreateArgs
{ _port :: Port "Wallet"
, _id :: WalletId
, _payments :: NonEmpty Text
, _metadata :: ApiTxMetadata
}

cmdTransactionCreate
Expand All @@ -702,7 +705,8 @@ cmdTransactionCreate mkTxClient mkWalletClient =
<$> portOption
<*> walletIdArgument
<*> fmap NE.fromList (some paymentOption)
exec (TransactionCreateArgs wPort wId wAddressAmounts) = do
<*> metadataOption
exec (TransactionCreateArgs wPort wId wAddressAmounts md) = do
wPayments <- either (fail . getTextDecodingError) pure $
traverse (fromText @(AddressAmount Text)) wAddressAmounts
res <- sendRequest wPort $ getWallet mkWalletClient $ ApiT wId
Expand All @@ -715,6 +719,7 @@ cmdTransactionCreate mkTxClient mkWalletClient =
(Aeson.object
[ "payments" .= wPayments
, "passphrase" .= ApiT wPwd
, "metadata" .= md
]
)
Left _ ->
Expand All @@ -733,7 +738,8 @@ cmdTransactionFees mkTxClient mkWalletClient =
<$> portOption
<*> walletIdArgument
<*> fmap NE.fromList (some paymentOption)
exec (TransactionCreateArgs wPort wId wAddressAmounts) = do
<*> metadataOption
exec (TransactionCreateArgs wPort wId wAddressAmounts md) = do
wPayments <- either (fail . getTextDecodingError) pure $
traverse (fromText @(AddressAmount Text)) wAddressAmounts
res <- sendRequest wPort $ getWallet mkWalletClient $ ApiT wId
Expand All @@ -742,7 +748,10 @@ cmdTransactionFees mkTxClient mkWalletClient =
runClient wPort Aeson.encodePretty $ postTransactionFee
mkTxClient
(ApiT wId)
(Aeson.object [ "payments" .= wPayments ])
(Aeson.object
[ "payments" .= wPayments
, "metadata" .= md
])
Left _ ->
handleResponse Aeson.encodePretty res

Expand Down Expand Up @@ -1348,6 +1357,21 @@ transactionSubmitPayloadArgument = argumentT $ mempty
<> metavar "BINARY_BLOB"
<> help "hex-encoded binary blob of externally-signed transaction."

-- | <metadata=JSON>
--
-- Note: we decode the JSON just so that we can validate more client-side.
metadataOption :: Parser ApiTxMetadata
metadataOption = option txMetadataReader $ mempty
<> long "metadata"
<> metavar "JSON"
<> value (ApiTxMetadata Nothing)
<> help ("Application-specific transaction metadata as a JSON object. "
<> "The value must match the schema defined in the "
<> "cardano-wallet OpenAPI specification.")

txMetadataReader :: ReadM ApiTxMetadata
txMetadataReader = eitherReader (Aeson.eitherDecode' . BL8.pack)

-- | <address=ADDRESS>
addressIdArgument :: Parser Text
addressIdArgument = argumentT $ mempty
Expand Down
36 changes: 35 additions & 1 deletion lib/cli/test/unit/Cardano/CLISpec.hs
Expand Up @@ -28,6 +28,7 @@ import Cardano.CLI
, cmdWalletCreate
, hGetLine
, hGetSensitiveLine
, metadataOption
, smashURLOption
)
import Cardano.Wallet.Api.Client
Expand All @@ -37,6 +38,10 @@ import Cardano.Wallet.Api.Client
, transactionClient
, walletClient
)
import Cardano.Wallet.Api.Types
( ApiT (..), ApiTxMetadata (..) )
import Cardano.Wallet.Primitive.Types
( TxMetadata (..), TxMetadataValue (..) )
import Control.Concurrent
( forkFinally )
import Control.Concurrent.MVar
Expand Down Expand Up @@ -91,6 +96,7 @@ import Test.QuickCheck
import Test.Text.Roundtrip
( textRoundtrip )

import qualified Data.Map as Map
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

Expand Down Expand Up @@ -262,7 +268,7 @@ spec = do

["transaction", "create", "--help"] `shouldShowUsage`
[ "Usage: transaction create [--port INT] WALLET_ID"
, " --payment PAYMENT"
, " --payment PAYMENT [--metadata JSON]"
, " Create and submit a new transaction."
, ""
, "Available options:"
Expand All @@ -272,10 +278,15 @@ spec = do
, " --payment PAYMENT address to send to and amount to send"
, " separated by @, e.g."
, " '<amount>@<address>'"
, " --metadata JSON Application-specific transaction"
, " metadata as a JSON object. The value"
, " must match the schema defined in the"
, " cardano-wallet OpenAPI specification."
]

["transaction", "fees", "--help"] `shouldShowUsage`
[ "Usage: transaction fees [--port INT] WALLET_ID --payment PAYMENT"
, " [--metadata JSON]"
, " Estimate fees for a transaction."
, ""
, "Available options:"
Expand All @@ -285,6 +296,10 @@ spec = do
, " --payment PAYMENT address to send to and amount to send"
, " separated by @, e.g."
, " '<amount>@<address>'"
, " --metadata JSON Application-specific transaction"
, " metadata as a JSON object. The value"
, " must match the schema defined in the"
, " cardano-wallet OpenAPI specification."
]

["transaction", "list", "--help"] `shouldShowUsage`
Expand Down Expand Up @@ -650,6 +665,25 @@ spec = do
, ( "relative", "/home/user", err )
]

describe "Tx Metadata JSON option" $ do
let parse arg = execParserPure defaultPrefs
(info metadataOption mempty) ["--metadata", arg]
let md = ApiT (TxMetadata (Map.singleton 42 (TxMetaText "hi")))
let ok ex (Success res) = ex == getApiTxMetadata res
ok _ _ = False
let err (Failure _) = True
err _ = False
mapM_
(\(desc, arg, tst) -> it desc (parse arg `shouldSatisfy` tst))
[ ("valid", "{ \"42\": { \"string\": \"hi\" } }", ok (Just md))
, ("malformed", "testing", err)
, ("malformed trailling", "{ \"0\": { \"string\": \"\" } } arstneio", err)
, ("invalid", "{ \"json\": true }", err)
, ("null 1", "{ \"0\": null }", err)
, ("null 2", "null", ok Nothing)
, ("null 3", "{ }", ok (Just (ApiT mempty)))
]

where
backspace :: Text
backspace = T.singleton (toEnum 127)
Expand Down
@@ -1,5 +1,6 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
Expand All @@ -15,6 +16,7 @@ import Cardano.CLI
( Port )
import Cardano.Wallet.Api.Types
( ApiFee (..)
, ApiT (..)
, ApiTransaction
, ApiWallet
, DecodeAddress
Expand All @@ -23,7 +25,12 @@ import Cardano.Wallet.Api.Types
, getApiT
)
import Cardano.Wallet.Primitive.Types
( Direction (..), SortOrder (..), TxStatus (..) )
( Direction (..)
, SortOrder (..)
, TxMetadata (..)
, TxMetadataValue (..)
, TxStatus (..)
)
import Control.Monad
( forM_, join )
import Data.Generics.Internal.VL.Lens
Expand All @@ -36,6 +43,8 @@ import Data.Proxy
( Proxy (..) )
import Data.Quantity
( Quantity (..) )
import Data.Text
( Text )
import Data.Text.Class
( showT )
import Data.Time.Utils
Expand Down Expand Up @@ -96,6 +105,7 @@ import Test.Integration.Framework.TestData
, wildcardsWalletName
)

import qualified Data.Map as Map
import qualified Data.Text as T

spec :: forall n t.
Expand All @@ -110,16 +120,17 @@ spec = describe "SHELLEY_CLI_TRANSACTIONS" $ do
wDest <- emptyWallet ctx

let amt = fromIntegral minUTxOValue
args <- postTxArgs ctx wSrc wDest amt
args <- postTxArgs ctx wSrc wDest amt Nothing
Stdout feeOut <- postTransactionFeeViaCLI @t ctx args
ApiFee (Quantity feeMin) (Quantity feeMax) <- expectValidJSON Proxy feeOut

txJson <- postTxViaCLI ctx wSrc wDest amt
txJson <- postTxViaCLI ctx wSrc wDest amt Nothing
verify txJson
[ expectCliField (#amount . #getQuantity)
(between (feeMin + amt, feeMax + amt))
, expectCliField (#direction . #getApiT) (`shouldBe` Outgoing)
, expectCliField (#status . #getApiT) (`shouldBe` Pending)
, expectCliField (#metadata . #getApiTxMetadata) (`shouldBe` Nothing)
]

-- verify balance on src wallet
Expand Down Expand Up @@ -295,6 +306,37 @@ spec = describe "SHELLEY_CLI_TRANSACTIONS" $ do
out `shouldBe` ""
c `shouldBe` ExitFailure 1

it "TRANSMETA_CREATE_01 - Transaction with metadata via CLI" $ \ctx -> do
(wSrc, wDest) <- (,) <$> fixtureWallet ctx <*> emptyWallet ctx
let amt = 10_000_000
let md = Just "{ \"1\": { \"string\": \"hello\" } }"
let expected = Just $ ApiT $ TxMetadata $
Map.singleton 1 (TxMetaText "hello")

args <- postTxArgs ctx wSrc wDest amt md
Stdout feeOut <- postTransactionFeeViaCLI @t ctx args
ApiFee (Quantity feeMin) (Quantity feeMax) <- expectValidJSON Proxy feeOut

txJson <- postTxViaCLI ctx wSrc wDest amt md
verify txJson
[ expectCliField (#amount . #getQuantity)
(between (feeMin + amt, feeMax + amt))
, expectCliField (#direction . #getApiT) (`shouldBe` Outgoing)
, expectCliField (#status . #getApiT) (`shouldBe` Pending)
, expectCliField (#metadata . #getApiTxMetadata) (`shouldBe` expected)
]

eventually "metadata is confirmed in transaction list" $ do
(Exit code, Stdout out, Stderr err) <-
listTransactionsViaCLI @t ctx [T.unpack $ wSrc ^. walletId]
err `shouldBe` "Ok.\n"
code `shouldBe` ExitSuccess
outJson <- expectValidJSON (Proxy @([ApiTransaction n])) out
verify outJson
[ expectCliListField 0 (#metadata . #getApiTxMetadata) (`shouldBe` expected)
, expectCliListField 0 (#status . #getApiT) (`shouldBe` InLedger)
]

describe "TRANS_ESTIMATE_08 - Invalid addresses" $ do
forM_ matrixInvalidAddrs $ \(title, addr, errMsg) -> it title $ \ctx -> do
wSrc <- emptyWallet ctx
Expand Down Expand Up @@ -679,7 +721,7 @@ spec = describe "SHELLEY_CLI_TRANSACTIONS" $ do
let wSrcId = T.unpack (wSrc ^. walletId)

-- post transaction
txJson <- postTxViaCLI ctx wSrc wDest minUTxOValue
txJson <- postTxViaCLI ctx wSrc wDest minUTxOValue Nothing
verify txJson
[ expectCliField (#direction . #getApiT) (`shouldBe` Outgoing)
, expectCliField (#status . #getApiT) (`shouldBe` Pending)
Expand Down Expand Up @@ -734,7 +776,7 @@ spec = describe "SHELLEY_CLI_TRANSACTIONS" $ do
-- post tx
wSrc <- fixtureWallet ctx
wDest <- emptyWallet ctx
txJson <- postTxViaCLI ctx wSrc wDest minUTxOValue
txJson <- postTxViaCLI ctx wSrc wDest minUTxOValue Nothing

-- try to forget from different wallet
widDiff <- emptyWallet' ctx
Expand Down Expand Up @@ -801,9 +843,10 @@ spec = describe "SHELLEY_CLI_TRANSACTIONS" $ do
-> ApiWallet
-> ApiWallet
-> Natural
-> Maybe Text
-> IO (ApiTransaction n)
postTxViaCLI ctx wSrc wDest amt = do
args <- postTxArgs ctx wSrc wDest amt
postTxViaCLI ctx wSrc wDest amt md = do
args <- postTxArgs ctx wSrc wDest amt md

-- post transaction
(c, out, err) <- postTransactionViaCLI @t ctx "cardano-wallet" args
Expand All @@ -816,14 +859,15 @@ spec = describe "SHELLEY_CLI_TRANSACTIONS" $ do
-> ApiWallet
-> ApiWallet
-> Natural
-> Maybe Text
-> IO [String]
postTxArgs ctx wSrc wDest amt = do
postTxArgs ctx wSrc wDest amt md = do
addr:_ <- listAddresses @n ctx wDest
let addrStr = encodeAddress @n (getApiT $ fst $ addr ^. #id)
return $ T.unpack <$>
[ wSrc ^. walletId
, "--payment", T.pack (show amt) <> "@" <> addrStr
]
] ++ maybe [] (\json -> ["--metadata", json]) md

fixtureWallet' :: Context t -> IO String
fixtureWallet' = fmap (T.unpack . view walletId) . fixtureWallet
Expand Down
1 change: 1 addition & 0 deletions nix/.stack.nix/cardano-wallet-cli.nix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.