Skip to content

Commit 6f639a4

Browse files
authored
Merge pull request #197 from CardanoSolutions/asset-quantity-as-strings
Asset quantities as strings (adds feature #196)
2 parents 5b9f84d + be3adb3 commit 6f639a4

9 files changed

Lines changed: 350 additions & 25 deletions

File tree

docs/api/nightly.yaml

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,31 @@ components:
12601260
1220099e5e430475c219518179efc7e6c8289db028904834025d5b086: 231
12611261
289db028904834025d5b085d5b08661220099e5e430475c2195181796.08661220099e: 1
12621262

1263+
ValueAsString:
1264+
type: object
1265+
description: A (multi-asset) value of a transaction's output.
1266+
additionalProperties: false
1267+
required:
1268+
- coins
1269+
properties:
1270+
coins:
1271+
type: string
1272+
description: A quantity of Lovelace, encoded as string when 'asset-quantity=string' is specified as media-type parameter.
1273+
example: "42"
1274+
assets:
1275+
type: object
1276+
description: A _key:value_ map of asset identifier → quantity.
1277+
propertyNames:
1278+
type: string
1279+
pattern: ^[a-f0-9]{56}(.[a-f0-9]{2,64})?$
1280+
additionalProperties:
1281+
x-additionalPropertiesName: "{policy-id}.{asset-name}"
1282+
type: string
1283+
description: A quantity of some asset, encoded as string when 'asset-quantity=string' is specified as media-type parameter.
1284+
example:
1285+
1220099e5e430475c219518179efc7e6c8289db028904834025d5b086: "231"
1286+
289db028904834025d5b085d5b08661220099e5e430475c2195181796.08661220099e: "1"
1287+
12631288
Wildcard:
12641289
type: string
12651290
title: Wildcard
@@ -1317,6 +1342,58 @@ components:
13171342
- $ref: "#/components/schemas/SpentAt"
13181343
- type: "null"
13191344

1345+
MatchQuantityAsString:
1346+
type: object
1347+
additionalProperties: false
1348+
required:
1349+
- transaction_index
1350+
- transaction_id
1351+
- output_index
1352+
- address
1353+
- value
1354+
- datum_hash
1355+
- script_hash
1356+
- created_at
1357+
- spent_at
1358+
properties:
1359+
transaction_index:
1360+
$ref: "#/components/schemas/TransactionIndex"
1361+
transaction_id:
1362+
$ref: "#/components/schemas/TransactionId"
1363+
output_index:
1364+
$ref: "#/components/schemas/OutputIndex"
1365+
address:
1366+
$ref: "#/components/schemas/Address"
1367+
value:
1368+
$ref: "#/components/schemas/ValueAsString"
1369+
datum_hash:
1370+
$ref: "#/components/schemas/DatumHash"
1371+
datum:
1372+
description: The resolved datum, if available. The field is only and always present (yet may be `null`) if `?resolve_hashes` was set.
1373+
oneOf:
1374+
- <<: *BinaryData
1375+
description: A serialized Plutus' Data.
1376+
- type: "null"
1377+
description: None or unknown datum.
1378+
datum_type:
1379+
$ref: "#/components/schemas/DatumType"
1380+
script_hash:
1381+
$ref: "#/components/schemas/ScriptHash"
1382+
script:
1383+
description: The resolved script, if available. The field is only and always present (yet may be `null`) if `?resolve_hashes` was set.
1384+
oneOf:
1385+
- $ref: "#/components/schemas/Script"
1386+
- type: "null"
1387+
description: None or unknown script.
1388+
created_at:
1389+
<<: *Point
1390+
description: Block reference at which this transaction was included in the ledger.
1391+
spent_at:
1392+
description: Block reference at which this transaction input was spent, if any.
1393+
oneOf:
1394+
- $ref: "#/components/schemas/SpentAt"
1395+
- type: "null"
1396+
13201397
Health:
13211398
type: object
13221399
description: An overview of the server & connection status. Note that, when `most_recent_checkpoint` and `most_recent_node_tip` are equal, the index is fully synchronized.
@@ -1780,6 +1857,13 @@ paths:
17801857
Optionally, use `?resolve_hashes` to automatically resolve and include `datum` and `script` associated with hash references, if available. Datums and scripts can otherwise be fetched using the [_Get Datum by Hash_](#tag/Datums/paths/~1datums~1{datum-hash}/get) and [_Get Script by Hash_](#tag/Scripts/paths/~1scripts~1{script-hash}/get) endpoints respectively.
17811858
17821859
Note that it is generally a bad idea to fetch **ALL matches** for indexes built off permissive patterns (e.g. `*`), for the server will yield a large response.
1860+
1861+
> <sup><strong>TIP</strong></sup> <br/>
1862+
>
1863+
> You can customize coins and assets quantities encoding to always be strings instead of integers through the
1864+
> `Accept` header media-type as such:
1865+
>
1866+
> `Accept: application/json;asset-quantity=string`
17831867
parameters:
17841868
- $ref: "#/components/parameters/resolve-hashes"
17851869
- $ref: "#/components/parameters/spent"
@@ -1803,6 +1887,11 @@ paths:
18031887
type: array
18041888
items:
18051889
$ref: "#/components/schemas/Match"
1890+
"application/json;charset=utf-8;asset-quantity=string":
1891+
schema:
1892+
type: array
1893+
items:
1894+
$ref: "#/components/schemas/MatchQuantityAsString"
18061895
304:
18071896
$ref: "#/components/responses/304"
18081897

@@ -1814,6 +1903,13 @@ paths:
18141903
description: |
18151904
Retrieve matches from the database matching the given pattern, in descending `slot_no` order. Results are streamed to the client for more efficiency.
18161905
See [Patterns](#section/Patterns) for more information about constructing patterns.
1906+
1907+
> <sup><strong>TIP</strong></sup> <br/>
1908+
>
1909+
> You can customize coins and assets quantities encoding to always be strings instead of integers through the
1910+
> `Accept` header media-type as such:
1911+
>
1912+
> `Accept: application/json;asset-quantity=string`
18171913
parameters:
18181914
- $ref: "#/components/parameters/pattern"
18191915
- $ref: "#/components/parameters/resolve-hashes"
@@ -1837,7 +1933,12 @@ paths:
18371933
schema:
18381934
type: array
18391935
items:
1840-
$ref: "#/components/schemas/Match"
1936+
1937+
"application/json;charset=utf-8;asset-quantity=string":
1938+
schema:
1939+
type: array
1940+
items:
1941+
$ref: "#/components/schemas/MatchQuantityAsString"
18411942
304:
18421943
$ref: "#/components/responses/304"
18431944
400:

kupo.cabal

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ library:
8585
- filepath
8686
- generic-lens
8787
- http-client
88+
- http-media
8889
- http-types
8990
- io-classes
9091
- lens

src/Kupo/App/Http.hs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ import Kupo.Data.Http.GetCheckpointMode
118118
import Kupo.Data.Http.OrderMatchesBy
119119
( orderMatchesBy
120120
)
121+
import Kupo.Data.Http.QuantityEncoding
122+
( QuantityEncoding (..)
123+
)
121124
import Kupo.Data.Http.ReferenceFlag
122125
( referenceFlagFromQueryParams
123126
)
@@ -147,8 +150,16 @@ import Kupo.Data.Pattern
147150
, resultToJson
148151
, wildcard
149152
)
153+
import Network.HTTP.Media
154+
( mapAccept
155+
)
156+
import Network.HTTP.Media.MediaType
157+
( (//)
158+
, (/:)
159+
)
150160
import Network.HTTP.Types
151-
( hAccept
161+
( Header
162+
, hAccept
152163
, hContentType
153164
, status200
154165
, status202
@@ -181,6 +192,7 @@ import qualified Data.Set as Set
181192
import qualified GHC.Clock
182193
import qualified Kupo.Data.Http.Default as Default
183194
import qualified Kupo.Data.Http.Error as Errors
195+
import qualified Kupo.Data.Http.QuantityEncoding as QuantityEncoding
184196
import qualified Network.HTTP.Types.Header as Http
185197
import qualified Network.HTTP.Types.Status as Http
186198
import qualified Network.HTTP.Types.URI as Http
@@ -342,6 +354,7 @@ app networkParameters withDatabase forceRollback fetchBlock patternsVar readHeal
342354
withDatabase send ReadOnly $ \db -> do
343355
send $ handleGetMatches
344356
headers
357+
(requestHeaders req)
345358
(pathParametersToText args)
346359
(queryString req)
347360
db
@@ -481,7 +494,7 @@ handleGetHealth
481494
handleGetHealth reqHeaders forcedStatus resolveNetworkParameters health = do
482495
networkParameters <- resolveNetworkParameters
483496
now <- getCurrentTime
484-
case findContentType reqHeaders of
497+
case findAcceptHeader reqHeaders of
485498
Just ct | cTextPlain `BS.isInfixOf` ct -> do
486499
let resHeaders = addCacheHeaders [(hContentType, cTextPlain <> ";charset=utf-8")] health
487500
return $ responseBuilder status resHeaders (mkPrometheusMetrics now networkParameters health)
@@ -520,14 +533,6 @@ handleGetHealth reqHeaders forcedStatus resolveNetworkParameters health = do
520533
<$> mostRecentNodeTip
521534
<*> (getPointSlotNo <$> mostRecentCheckpoint)
522535

523-
findContentType = \case
524-
[] -> Nothing
525-
(headerName, headerValue):rest ->
526-
if headerName == hAccept then
527-
Just headerValue
528-
else
529-
findContentType rest
530-
531536
cTextPlain = "text/plain"
532537
cApplicationJson = "application/json"
533538
cAny = "*/*"
@@ -581,11 +586,12 @@ handleGetCheckpointBySlot headers mSlotNo query Database{..} =
581586

582587
handleGetMatches
583588
:: [Http.Header]
589+
-> [Http.Header]
584590
-> Maybe Text
585591
-> Http.Query
586592
-> Database IO
587593
-> Response
588-
handleGetMatches headers patternQuery queryParams Database{..} = handleRequest $ do
594+
handleGetMatches resHeaders reqHeaders patternQuery queryParams Database{..} = handleRequest $ do
589595
pattern_ <- (patternQuery >>= patternFromText)
590596
`orAbort` Errors.invalidPattern
591597

@@ -604,7 +610,24 @@ handleGetMatches headers patternQuery queryParams Database{..} = handleRequest $
604610
sortDirection <- mkSortDirection <$> orderMatchesBy queryParams
605611
`orAbort` Errors.invalidSortDirection
606612

607-
pure $ responseStreamJson headers (resultToJson referenceFlag) $ \yield done -> do
613+
let qualities =
614+
[ ("application" // "json" /: QuantityEncoding.mediaTypeParam
615+
, EncodeAsString
616+
)
617+
, ("application" // "json" /: QuantityEncoding.mediaTypeParam /: ("charset", "utf-8")
618+
, EncodeAsString
619+
)
620+
, ("application" // "json" /: ("charset", "utf-8") /: QuantityEncoding.mediaTypeParam
621+
, EncodeAsString
622+
)
623+
]
624+
625+
let quantityEncoding = (findAcceptHeader reqHeaders >>= mapAccept qualities)
626+
& fromMaybe EncodeAsInteger
627+
628+
let resHeaders' = (QuantityEncoding.adjustMediaType quantityEncoding resHeaders)
629+
630+
pure $ responseStreamJson resHeaders' (resultToJson referenceFlag quantityEncoding) $ \yield done -> do
608631
let assertPointExists :: Point -> DBTransaction IO ()
609632
assertPointExists requested = do
610633
let nextSlot = next (getPointSlotNo requested)
@@ -943,6 +966,16 @@ requestBodyJson parser req = do
943966
Just (Json.Success a) -> return (Just a)
944967
_failureOrMalformed -> return Nothing
945968

969+
findAcceptHeader :: [Header] -> Maybe ByteString
970+
findAcceptHeader = \case
971+
[] -> Nothing
972+
(headerName, headerValue):rest ->
973+
if headerName == hAccept then
974+
Just headerValue
975+
else
976+
findAcceptHeader rest
977+
978+
946979
--
947980
-- Tracer
948981
--

src/Kupo/Data/Cardano/Value.hs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import Kupo.Data.Cardano.PolicyId
1212
( PolicyId
1313
, unsafePolicyIdFromBytes
1414
)
15+
import Kupo.Data.Http.QuantityEncoding
16+
( QuantityEncoding (..)
17+
)
1518

1619
import qualified Cardano.Ledger.Coin as Ledger
1720
import qualified Cardano.Ledger.Hashes as Ledger
@@ -54,13 +57,17 @@ unsafeValueFromList ada assets =
5457
| (pid, name, q) <- assets
5558
]
5659

57-
valueToJson :: Value -> Json.Encoding
58-
valueToJson (Ledger.MaryValue (Ledger.Coin coins) (Ledger.MultiAsset assets)) =
60+
valueToJson :: QuantityEncoding -> Value -> Json.Encoding
61+
valueToJson quantityEncoding (Ledger.MaryValue (Ledger.Coin coins) (Ledger.MultiAsset assets)) =
5962
Json.pairs $
60-
Json.pair "coins" (Json.integer coins)
63+
Json.pair "coins" (encodeQuantity coins)
6164
<>
6265
Json.pair "assets" (assetsToJson assets)
6366
where
67+
encodeQuantity = case quantityEncoding of
68+
EncodeAsInteger -> Json.integer
69+
EncodeAsString -> Json.text . show
70+
6471
shortBytesToKey =
6572
Builder.fromText . encodeBase16 . fromShort
6673

@@ -82,7 +89,7 @@ valueToJson (Ledger.MaryValue (Ledger.Coin coins) (Ledger.MultiAsset assets)) =
8289
fieldName <> "." <> shortBytesToKey assetName
8390
) & Json.fromText . toStrict . Builder.toLazyText
8491

85-
v = Json.integer quantity
92+
v = encodeQuantity quantity
8693
in
8794
Json.pair k v <> json
8895
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
-- This Source Code Form is subject to the terms of the Mozilla Public
2+
-- License, v. 2.0. If a copy of the MPL was not distributed with this
3+
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
module Kupo.Data.Http.QuantityEncoding
6+
( QuantityEncoding(..)
7+
, adjustMediaType
8+
, mediaTypeParam
9+
) where
10+
11+
import Kupo.Prelude
12+
13+
import qualified Prelude as P
14+
( id
15+
)
16+
17+
import Network.HTTP.Media
18+
( MediaType
19+
, mapContentMedia
20+
, renderHeader
21+
, (//)
22+
, (/:)
23+
)
24+
import Network.HTTP.Types
25+
( Header
26+
, ResponseHeaders
27+
, hContentType
28+
)
29+
30+
data QuantityEncoding = EncodeAsInteger | EncodeAsString
31+
deriving Show
32+
33+
adjustMediaType :: QuantityEncoding -> ResponseHeaders -> ResponseHeaders
34+
adjustMediaType EncodeAsInteger = P.id
35+
adjustMediaType EncodeAsString = map insertParam
36+
37+
mediaTypeParam :: (ByteString, ByteString)
38+
mediaTypeParam = ("asset-quantity", "string")
39+
40+
insertParam :: Header -> Header
41+
insertParam (n,v)
42+
| n == hContentType = (n, maybe v P.id (mapContentMedia mmap v))
43+
| otherwise = (n, v)
44+
45+
mmap :: [(MediaType,ByteString)]
46+
mmap =
47+
[("application"//"json"/:("charset","utf-8"),
48+
renderHeader $ "application"//"json"/:("charset","utf-8")/:mediaTypeParam)
49+
,("application"//"json",
50+
renderHeader $ "application"//"json"/:mediaTypeParam)
51+
]

0 commit comments

Comments
 (0)