-
Notifications
You must be signed in to change notification settings - Fork 721
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
Implement Multi-Asset JSON instances #2092
Conversation
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 can't help thinking there's a simpler approach: do the parse/render on a nested Map representation, and then have two functions for the nested Maps <-> single Map conversions.
The reason the code is complicated is because we're mixing in the flattening/unflattening into the rendering/parsing.
Just a note that this needs to be |
c3883f5
to
5ff3a8f
Compare
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 see we've made your life harder here with the change to get rid of the ada policy id and have a distinct ada asset id.
If it were not for that, the following scheme would work:
Scheme 1 (doesn't quite work now with ada asset id)
The starting structure matches the Aeson structure 1:1. There are no clashes any more, so no merges needed. We can also do it all with "bulk" operations rather than folds that insert one by one.
Indeed we could do most of it using the existing ToJSON class instances for Map:
(ToJSON v, ToJSONKey k) => ToJSON (Map k v)
All we need is for PolicyId and AssetId to be in ToJSONKey and Quantity to be in ToJSON.
It'd just be
instance ToJSON Value where
toJSON = toJSON . unflatten
Scheme 2
So then the question is given ada is represented specially now, how do still get as much as possible for free?
I think the answer is to apply the same principle as before: use an intermedite representation that matches the external JSON representation 1:1
Before we merged the change to the multi-asset Value representation, that intermediate representation was
Map PolicyId (Map AssetName Quantity)
but now that we abolished the ada policy id, it'll have to look a little different, and not a simple type alias for nested maps.
How about this as the intermediate representation:
newtype AssetsBundle = AssetsBundle [AssetIdBundle]
data AssetIdBundle = AssetIdBundle PolicyId (Map AssetName Quantity)
| AdaAsset Quantity
and then we make these instances of ToJSON
/FromJSON
and adapt the flatten
/unflatten
that converts to/from the multi-asset Value
type.
instance ToJSON AssetsBundle where
toJSON (AssetsBundle xss) = Aeson.object (map toPair xss)
where
toPair (AdaAsset q) = ("lovelace", toJSON q)
toPair (AssetIdBundle pid xs) = (renderPolicyId pid, toJSON xs)
42d3872
to
3ee99fa
Compare
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.
Yes much nicer.
One can tell we've got something structurally better from the fact that we now have just
instance ToJSON Value where
toJSON = toJSON . unflatten
instance FromJSON Value where
parseJSON jv = flatten <$> parseJSON jv
Name bikeshedding: yes I suggested the asset bundle names. That doesn't mean I'm happy with my own suggestion 😁. If we are forced to export it for testing purposes, then a name is more important. Perhaps NestedValue
, to indicate it's equivalent to Value
but has a nested representation. And then for the inner one, what? NestedValueAssets
? 😬
cardano-api/src/Cardano/Api/Value.hs
Outdated
@@ -115,6 +131,98 @@ instance Semigroup Value where | |||
instance Monoid Value where | |||
mempty = Value Map.empty | |||
|
|||
instance ToJSON Value where | |||
toJSON = toJSON . unflatten |
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.
👍
f9e9545
to
1c07404
Compare
Sorry, I think we both did the last bit of name changing at the same time. |
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.
Ok, lets squash into a single patch and merge.
cda4ba0
to
9a6d588
Compare
bors merge |
2092: Implement Multi-Asset JSON instances r=dcoutts a=Jimbo4350 Multi-Asset JSON is of the following format: ```json { "fooPolicyId": { "fooAssetName": 42 }, "barPolicyId": { "barAssetName": 84 }, "lovelace": 3 } ``` Co-authored-by: Jordan Millar <[email protected]>
Timed out. |
https://hydra.iohk.io/build/4907903/nixlog/1
Ok, this is a (minor) problem. The generator is producing nested rep values with zeros for the ada case. That's not a canonical representation. Either we should canonicalise the nested rep, or we should generate only canonical reps. I think the better solution is to canonicalise only the original rep and then we just use
The canonicalise would eliminate zero quantities, merge (sum) duplicates, and put the entries in order. |
cardano-api/src/Cardano/Api/Value.hs
Outdated
@@ -211,7 +212,7 @@ data ValueNestedBundle = ValueNestedBundle PolicyId (Map AssetName Quantity) | |||
valueToNestedRep :: Value -> ValueNestedRep | |||
valueToNestedRep v = | |||
-- unflatten all the non-ada assets, and add ada separately | |||
ValueNestedRep $ | |||
ValueNestedRep . sort $ |
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.
Ah! This is why in the other branch I have swapped the order of the AssetId constructors, so that this sort would not be necessary.
Perhaps we should include that swap here in this PR, then we don't need the sort.
removeZeroAda :: ValueNestedBundle -> Bool | ||
removeZeroAda (ValueNestedBundleAda 0) = False | ||
removeZeroAda (ValueNestedBundle _ aNameQmap) = | ||
0 `notElem` Map.elems aNameQmap | ||
removeZeroAda _ = True |
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.
This is a filter predicate not a filter itself, so call it something like filter (not . isZeroAda)
But also, filtering at this level cannot work. We need to filter out the zero quantities in the nested bundles, and omit the ada if it itself is zero.
If the test did not catch this, it shows we don't have well-tuned generators. I think we should adjust the Value
and ValueNestedRep
so 1 out of 10 times it samples from the full range, and 9 out of 10 times from a very small (e.g. 5) range of policy ids (A..E), asset names (A..E) and quantities (-2..2). That will mean we frequently get asset names with the same policy id, and quantities that sum to 0.
flattenedMinted = map (uncurry ValueNestedBundle) $ Map.toList summedMinted | ||
flattenedAda = ValueNestedBundleAda summedAda | ||
|
||
in sort $ flattenedAda : flattenedMinted |
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.
We'll be able to get rid of this sort for the same reasons above.
ValueNestedRep . filter removeZeroAda $ mergeDuplicates bundles | ||
where | ||
mergeDuplicates :: [ValueNestedBundle] -> [ValueNestedBundle] | ||
mergeDuplicates l = | ||
let allAssets :: [(PolicyId, Map AssetName Quantity)] | ||
allAssets = [(pId, qMap) | (ValueNestedBundle pId qMap) <- l] | ||
|
||
allAda :: [Quantity] | ||
allAda = [ada | ValueNestedBundleAda ada <- l] | ||
|
||
summedMinted :: Map PolicyId (Map AssetName Quantity) | ||
summedMinted = Map.fromListWith (Map.unionWith (<>)) allAssets | ||
|
||
summedAda :: Quantity | ||
summedAda = sum allAda | ||
|
||
flattenedMinted = map (uncurry ValueNestedBundle) $ Map.toList summedMinted | ||
flattenedAda = ValueNestedBundleAda summedAda |
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 think we can probably write this more concisely, but lets solve the sort & generator issues first.
3c891f7
to
743e4e2
Compare
743e4e2
to
c1d3beb
Compare
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've rebased on master and adjusted the second patch slightly with a (hopefully) clearer and simpler version of the canonicalise function. This required adjusting the order of the ValueNestedBundle
constructors to match the AssetId
sort order.
@@ -149,6 +153,74 @@ genMultiSigScriptsMary = | |||
|
|||
] | |||
|
|||
genAssetName :: Gen AssetName | |||
genAssetName = AssetName <$> Gen.utf8 (Range.constant 1 15) Gen.alphaNum |
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.
genAssetName = AssetName <$> Gen.utf8 (Range.constant 1 15) Gen.alphaNum | |
genAssetName = AssetName <$> Gen.utf8 (Range.constant 1 32) Gen.alphaNum |
c1d3beb
to
e7334bd
Compare
bors merge |
2092: Implement Multi-Asset JSON instances r=dcoutts a=Jimbo4350 Multi-Asset JSON is of the following format: ```json { "fooPolicyId": { "fooAssetName": 42 }, "barPolicyId": { "barAssetName": 84 }, "lovelace": 3 } ``` 2126: Updated help text for query ledger/protocol-state r=intricate a=kevinhammond Clarified that these commands are not intended for general use, and pointed to the underlying data structures for more information about content Co-authored-by: Jordan Millar <[email protected]> Co-authored-by: Kevin Hammond <[email protected]>
Build failed (retrying...): |
2092: Implement Multi-Asset JSON instances r=dcoutts a=Jimbo4350 Multi-Asset JSON is of the following format: ```json { "fooPolicyId": { "fooAssetName": 42 }, "barPolicyId": { "barAssetName": 84 }, "lovelace": 3 } ``` Co-authored-by: Jordan Millar <[email protected]>
e7334bd
to
142d139
Compare
Canceled. |
bors merge |
2092: Implement Multi-Asset JSON instances r=dcoutts a=Jimbo4350 Multi-Asset JSON is of the following format: ```json { "fooPolicyId": { "fooAssetName": 42 }, "barPolicyId": { "barAssetName": 84 }, "lovelace": 3 } ``` 2126: Updated help text for query ledger/protocol-state r=intricate a=kevinhammond Clarified that these commands are not intended for general use, and pointed to the underlying data structures for more information about content Co-authored-by: Jordan Millar <[email protected]> Co-authored-by: Kevin Hammond <[email protected]>
Build failed (retrying...): |
142d139
to
8390bf7
Compare
Canceled. |
Add round-trip tests. Improve the generators for Value and related types: sample the PolicyId and AssetName from a smaller range of values so that for things like Value we get more "interesting" values with duplicate policy ids or asset names. Add a canonicalise function for ValueNestedRep which should be the equivalent of converting from ValueNestedRep to Value and back.
8390bf7
to
89f8374
Compare
bors merge |
Build succeeded: |
Multi-Asset JSON is of the following format: