|
1 |
| -{-# LANGUAGE DeriveGeneric #-} |
2 |
| -{-# LANGUAGE LambdaCase #-} |
3 |
| -{-# LANGUAGE OverloadedStrings #-} |
4 |
| -{-# LANGUAGE TemplateHaskell #-} |
5 |
| - |
6 | 1 | module Cardano.Wallet.API.V1.Errors where
|
7 | 2 |
|
8 | 3 | import Universum
|
9 | 4 |
|
10 |
| -import Data.Aeson |
11 |
| -import Generics.SOP.TH (deriveGeneric) |
12 |
| -import Servant |
13 |
| -import Test.QuickCheck (Arbitrary (arbitrary)) |
14 |
| -import Test.QuickCheck.Gen (oneof) |
15 |
| - |
16 |
| -import Cardano.Wallet.API.Response.JSend (ResponseStatus (ErrorStatus)) |
17 |
| -import Cardano.Wallet.API.V1.Generic (gparseJsend, gtoJsend) |
18 | 5 | import Cardano.Wallet.API.V1.Headers (applicationJson)
|
| 6 | +import Data.Aeson (ToJSON, encode) |
| 7 | +import Formatting (build, sformat) |
| 8 | +import Servant (ServantErr (..)) |
19 | 9 |
|
20 | 10 | import qualified Network.HTTP.Types as HTTP
|
21 | 11 |
|
22 | 12 |
|
23 |
| --- |
24 |
| --- Error handling |
25 |
| --- |
26 |
| - |
27 |
| --- | Type representing any error which might be thrown by wallet. |
28 |
| --- |
29 |
| --- Errors are represented in JSON in the JSend format (<https://labs.omniti.com/labs/jsend>): |
30 |
| --- ``` |
31 |
| --- { |
32 |
| --- "status": "error" |
33 |
| --- "message" : <constr_name>, |
34 |
| --- "diagnostic" : <data> |
35 |
| --- } |
36 |
| --- ``` |
37 |
| --- where `<constr_name>` is a string containing name of error's constructor (e. g. `NotEnoughMoney`), |
38 |
| --- and `<data>` is an object containing additional error data. |
39 |
| --- Additional data contains constructor fields, field names are record field names without |
40 |
| --- a `we` prefix, e. g. for `OutputIsRedeem` error "diagnostic" field will be the following: |
41 |
| --- ``` |
42 |
| --- { |
43 |
| --- "address" : <address> |
44 |
| --- } |
45 |
| --- ``` |
46 |
| --- |
47 |
| --- Additional data in constructor should be represented as record fields. |
48 |
| --- Otherwise TemplateHaskell will raise an error. |
49 |
| --- |
50 |
| --- If constructor does not have additional data (like in case of `WalletNotFound` error), |
51 |
| --- then "diagnostic" field will be empty object. |
52 |
| --- |
53 |
| --- TODO: change fields' types to actual Cardano core types, like `Coin` and `Address` |
54 |
| -data WalletError address syncProgress syncPercentage = |
55 |
| - NotEnoughMoney { weNeedMore :: !Int } |
56 |
| - | OutputIsRedeem { weAddress :: !address } |
57 |
| - | MigrationFailed { weDescription :: !Text } |
58 |
| - | JSONValidationFailed { weValidationError :: !Text } |
59 |
| - | UnknownError { weMsg :: !Text } |
60 |
| - | InvalidAddressFormat { weMsg :: !Text } |
61 |
| - | WalletNotFound |
62 |
| - -- FIXME(akegalj): https://iohk.myjetbrains.com/youtrack/issue/CSL-2496 |
63 |
| - | WalletAlreadyExists |
64 |
| - | AddressNotFound |
65 |
| - | TxFailedToStabilize |
66 |
| - | TxRedemptionDepleted |
67 |
| - | TxSafeSignerNotFound { weAddress :: address } |
68 |
| - | MissingRequiredParams { requiredParams :: NonEmpty (Text, Text) } |
69 |
| - | WalletIsNotReadyToProcessPayments { weStillRestoring :: syncProgress } |
70 |
| - -- ^ The @Wallet@ where a @Payment@ is being originated is not fully |
71 |
| - -- synced (its 'WalletSyncState' indicates it's either syncing or |
72 |
| - -- restoring) and thus cannot accept new @Payment@ requests. |
73 |
| - | NodeIsStillSyncing { wenssStillSyncing :: syncPercentage } |
74 |
| - -- ^ The backend couldn't process the incoming request as the underlying |
75 |
| - -- node is still syncing with the blockchain. |
76 |
| - deriving (Show, Eq) |
77 |
| - |
78 |
| - |
79 |
| --- |
80 |
| --- Instances for `WalletError` |
81 |
| - |
82 |
| --- deriveWalletErrorJSON ''WalletError |
83 |
| -deriveGeneric ''WalletError |
84 |
| - |
85 |
| -instance (ToJSON a, ToJSON b, ToJSON c) => ToJSON (WalletError a b c) where |
86 |
| - toJSON = gtoJsend ErrorStatus |
87 |
| - |
88 |
| -instance (FromJSON a, FromJSON b, FromJSON c) => FromJSON (WalletError a b c) where |
89 |
| - parseJSON = gparseJsend |
90 |
| - |
91 |
| -instance (Typeable a, Show a, Typeable b, Show b, Typeable c, Show c) => |
92 |
| - Exception (WalletError a b c) |
93 |
| - |
94 |
| -instance (Arbitrary a, Arbitrary b, Arbitrary c) => Arbitrary (WalletError a b c) where |
95 |
| - arbitrary = oneof |
96 |
| - [ NotEnoughMoney <$> arbitrary |
97 |
| - , OutputIsRedeem <$> arbitrary |
98 |
| - , pure (MigrationFailed "Migration failed.") |
99 |
| - , pure (JSONValidationFailed "Expected String, found Null.") |
100 |
| - , pure (UnknownError "Unknown error.") |
101 |
| - , pure (InvalidAddressFormat "Invalid Base58 representation.") |
102 |
| - , pure WalletNotFound |
103 |
| - , pure WalletAlreadyExists |
104 |
| - , pure AddressNotFound |
105 |
| - , pure TxFailedToStabilize |
106 |
| - , pure TxRedemptionDepleted |
107 |
| - , TxSafeSignerNotFound <$> arbitrary |
108 |
| - , pure (MissingRequiredParams (("wallet_id", "walletId") :| [])) |
109 |
| - , WalletIsNotReadyToProcessPayments <$> arbitrary |
110 |
| - , NodeIsStillSyncing <$> arbitrary |
111 |
| - ] |
112 |
| - |
113 |
| - |
114 |
| --- |
115 |
| --- Helpers |
116 |
| --- |
117 |
| - |
118 |
| --- | Give a short description of an error |
119 |
| -describe :: forall a b c. WalletError a b c -> String |
120 |
| -describe = \case |
121 |
| - NotEnoughMoney _ -> |
122 |
| - "Not enough available coins to proceed." |
123 |
| - OutputIsRedeem _ -> |
124 |
| - "One of the TX outputs is a redemption address." |
125 |
| - MigrationFailed _ -> |
126 |
| - "Error while migrating a legacy type into the current version." |
127 |
| - JSONValidationFailed _ -> |
128 |
| - "Couldn't decode a JSON input." |
129 |
| - UnknownError _ -> |
130 |
| - "Unexpected internal error." |
131 |
| - InvalidAddressFormat _ -> |
132 |
| - "Provided address format is not valid." |
133 |
| - WalletNotFound -> |
134 |
| - "Reference to an unexisting wallet was given." |
135 |
| - WalletAlreadyExists -> |
136 |
| - "Can't create or restore a wallet. The wallet already exists." |
137 |
| - AddressNotFound -> |
138 |
| - "Reference to an unexisting address was given." |
139 |
| - MissingRequiredParams _ -> |
140 |
| - "Missing required parameters in the request payload." |
141 |
| - WalletIsNotReadyToProcessPayments _ -> |
142 |
| - "This wallet is restoring, and it cannot send new transactions until restoration completes." |
143 |
| - NodeIsStillSyncing _ -> |
144 |
| - "The node is still syncing with the blockchain, and cannot process the request yet." |
145 |
| - TxRedemptionDepleted -> |
146 |
| - "The redemption address was already used." |
147 |
| - TxSafeSignerNotFound _ -> |
148 |
| - "The safe signer at the specified address was not found." |
149 |
| - TxFailedToStabilize -> |
150 |
| - "We were unable to find a set of inputs to satisfy this transaction." |
151 |
| - |
152 |
| - |
153 |
| --- | Convert wallet errors to Servant errors |
154 |
| -toServantError |
155 |
| - :: forall a b c. (ToJSON a, ToJSON b, ToJSON c) |
156 |
| - => WalletError a b c |
157 |
| - -> ServantErr |
158 |
| -toServantError err = |
159 |
| - mkServantErr $ case err of |
160 |
| - NotEnoughMoney{} -> |
161 |
| - err403 |
162 |
| - OutputIsRedeem{} -> |
163 |
| - err403 |
164 |
| - MigrationFailed{} -> |
165 |
| - err422 |
166 |
| - JSONValidationFailed{} -> |
167 |
| - err400 |
168 |
| - UnknownError{} -> |
169 |
| - err500 |
170 |
| - WalletNotFound{} -> |
171 |
| - err404 |
172 |
| - WalletAlreadyExists{} -> |
173 |
| - err403 |
174 |
| - InvalidAddressFormat{} -> |
175 |
| - err401 |
176 |
| - AddressNotFound{} -> |
177 |
| - err404 |
178 |
| - MissingRequiredParams{} -> |
179 |
| - err400 |
180 |
| - WalletIsNotReadyToProcessPayments{} -> |
181 |
| - err403 |
182 |
| - NodeIsStillSyncing{} -> |
183 |
| - err412 -- Precondition failed |
184 |
| - TxFailedToStabilize{} -> |
185 |
| - err500 |
186 |
| - TxRedemptionDepleted{} -> |
187 |
| - err400 |
188 |
| - TxSafeSignerNotFound{} -> |
189 |
| - err400 |
190 |
| - where |
191 |
| - mkServantErr serr@ServantErr{..} = serr |
192 |
| - { errBody = encode err |
193 |
| - , errHeaders = applicationJson : errHeaders |
194 |
| - } |
195 |
| - |
196 |
| --- | |
197 |
| -toHttpStatus |
198 |
| - :: forall a b c. (ToJSON a, ToJSON b, ToJSON c) |
199 |
| - => WalletError a b c |
200 |
| - -> HTTP.Status |
201 |
| -toHttpStatus err = HTTP.Status (errHTTPCode $ toServantError err) |
202 |
| - (encodeUtf8 $ describe err) |
| 13 | +class (ToJSON e) => ToServantError e where |
| 14 | + declareServantError :: e -> ServantErr |
| 15 | + toServantError :: e -> ServantErr |
| 16 | + toServantError err = |
| 17 | + mkServantErr (declareServantError err) |
| 18 | + where |
| 19 | + mkServantErr serr@ServantErr{..} = serr |
| 20 | + { errBody = encode err |
| 21 | + , errHeaders = applicationJson : errHeaders |
| 22 | + } |
| 23 | + |
| 24 | +class (ToServantError e, Buildable e) => ToHttpErrorStatus e where |
| 25 | + toHttpErrorStatus :: e -> HTTP.Status |
| 26 | + toHttpErrorStatus err = |
| 27 | + HTTP.Status (errHTTPCode $ toServantError err) (encodeUtf8 $ sformat build err) |
0 commit comments