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

feat(stickers): Add content hash and EDN decoding #304

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,6 @@
[submodule "vendor/combparser"]
path = vendor/combparser
url = https://github.com/PMunch/combparser.git
[submodule "vendor/edn.nim"]
path = vendor/edn.nim
url = https://github.com/status-im/edn.nim
171 changes: 171 additions & 0 deletions status/private/stickers.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
{.push raises: [Defect].}

import # std libs
std/strutils

import # vendor libs
chronicles, edn, libp2p/[multihash, multicodec, cid], nimcrypto, stint

import
./common, ./util

export cid

type
StickersError* = enum
CidV0InitFailure = "stickers: failed to init CIDv0 from codec and multihash"
EdnNodeTypeUnknown = "stickers: couldn't decode EDN, node type unknown"
EdnReadError = "stickers: error reading EDN string"
HashValueError = "stickers: provided hash must not be an empty string"
HexIntParseError = "stickers: error parsing string to hex int"
InvalidMultiCodec = "stickers: content hash contains an invalid codec"
MultiHashInitFailure = "stickers: failed to init MultiHash given the hash"

StickersResult*[T] = Result[T, StickersError]

Sticker* = object
hash*: string
packId*: int

StickerPack* = object
author*: string
id*: int
name*: string
price*: Stuint[256]
preview*: string
stickers*: seq[Sticker]
thumbnail*: string

# forward declaration:
proc parseNode[T](node: EdnNode, searchName: string): StickersResult[T]
proc parseMap[T](map: HMap, searchName: string,): StickersResult[T]

proc getValueFromNode[T](node: EdnNode): StickersResult[T] {.raises: [].} =
if node.kind == EdnSymbol:
when T is string:
return ok node.symbol.name
elif node.kind == EdnKeyword:
when T is string:
return ok node.keyword.name
elif node.kind == EdnString:
when T is string:
return ok node.str
elif node.kind == EdnCharacter:
when T is string:
return ok node.character
elif node.kind == EdnBool:
when T is bool:
return ok node.boolVal
elif node.kind == EdnInt:
when T is int:
return ok node.num.int
else:
return err EdnNodeTypeUnknown

proc parseVector[T: seq[Sticker]](node: EdnNode, searchName: string):
StickersResult[T] =

var vector: T = @[]

for i in 0..<node.vec.len:
var sticker: Sticker
let child = node.vec[i]
if child.kind == EdnMap:
for k, v in sticker.fieldPairs:
let parseRes = parseMap[v.type](child.map, k)
v = parseRes.get(default v.type)
vector.add(sticker)

return ok vector

proc parseMap[T](map: HMap, searchName: string): StickersResult[T] =
var res: StickersResult[T]

for iBucket in 0..<map.buckets.len:
let bucket = map.buckets[iBucket]
if bucket.len > 0:
for iChild in 0..<bucket.len:
let child = bucket[iChild]
let isRoot = child.key.kind == EdnSymbol and child.key.symbol.name == "meta"
if child.key.kind != EdnKeyword and not isRoot:
continue
if isRoot or child.key.keyword.name == searchName:
if child.value.kind == EdnMap:
res = parseMap[T](child.value.map, searchName)
break
elif child.value.kind == EdnVector:
when T is seq[Sticker]:
res = parseVector[T](child.value, searchName)
break
res = getValueFromNode[T](child.value)
break

return res

proc parseNode[T](node: EdnNode, searchName: string): StickersResult[T] =
if node.kind == EdnMap:
return parseMap[T](node.map, searchName)
else:
return getValueFromNode[T](node)

proc decode*[T](node: EdnNode): StickersResult[T] =
var res = T()
for k, v in res.fieldPairs:
let parseRes = parseNode[v.type](node, k)
v = parseRes.get(default v.type)
return ok res

proc decode*[T](edn: string): StickersResult[T] {.raises: [].} =
try:
return decode[T](edn.read)
except IOError, OSError, Exception: # list exception last on purpose
return err EdnReadError

proc decodeContentHash*(value: string): StickersResult[Cid] =
if value == "":
return err HashValueError

# eg encoded sticker multihash cid:
# e30101701220eab9a8ef4eac6c3e5836a3768d8e04935c10c67d9a700436a0e53199e9b64d29
# e3017012205c531b83da9dd91529a4cf8ecd01cb62c399139e6f767e397d2f038b820c139f (testnet)
# e3011220c04c617170b1f5725070428c01280b4c19ae9083b7e6d71b7a0d2a1b5ae3ce30 (testnet)
#
# The first 4 bytes (in hex) represent:
# e3 = codec identifier "ipfs-ns" for content-hash
# 01 = unused - sometimes this is NOT included (ie ropsten)
# 01 = CID version (effectively unused, as we will decode with CIDv0 regardless)
# 70 = codec identifier "dag-pb"

# ipfs-ns
if value[0..1] != "e3":
return err InvalidMultiCodec

# dag-pb
let defaultCodec = ? catch(parseHexInt("70")).mapErrTo(HexIntParseError)
var
codec = defaultCodec # no codec specified
codecStartIdx = 2 # idx of where codec would start if it was specified
# handle the case when starts with 0xe30170 instead of 0xe3010170
if value[2..5] == "0101":
codecStartIdx = 6
codec = ? catch(parseHexInt(value[6..7])).mapErrTo(HexIntParseError)
elif value[2..3] == "01" and value[4..5] != "12":
codecStartIdx = 4
codec = ? catch(parseHexInt(value[4..5])).mapErrTo(HexIntParseError)

# strip the info we no longer need
var multiHashStr = value[codecStartIdx + 2..<value.len]

# The rest of the hash identifies the multihash algo, length, and digest
# More info: https://multiformats.io/multihash/
# 12 = identifies sha2-256 hash
# 20 = multihash length = 32
# ...rest = multihash digest
let
multiHash = ? MultiHash.init(nimcrypto.fromHex(multiHashStr))
.mapErrTo(MultiHashInitFailure)
cid = Cid.init(CIDv0, MultiCodec.codec(codec), multiHash)
.mapErrTo(CidV0InitFailure)

trace "Decoded sticker hash", cid = $cid.get
return cid
72 changes: 72 additions & 0 deletions test/stickers.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import # std libs
std/unittest

import # status lib
chronos, status/private/stickers

import # test modules
./test_helpers

procSuite "stickers":
asyncTest "decodeContentHash":
var
hash = "e30101701220eab9a8ef4eac6c3e5836a3768d8e04935c10c67d9a700436a0e53199e9b64d29"
cid = hash.decodeContentHash

check:
cid.isOk
$cid.get == "Qme8vJtyrEHxABcSVGPF95PtozDgUyfr1xGjePmFdZgk9v"

# testnet form #1
hash = "e3017012205c531b83da9dd91529a4cf8ecd01cb62c399139e6f767e397d2f038b820c139f"
cid = hash.decodeContentHash

check:
cid.isOk
$cid.get == "QmUZ3icyt1wQpaPXk1RRvtioMu5LtyuHvjwvtYo4iu58nr"

# testnet form #2
hash = "e3011220c04c617170b1f5725070428c01280b4c19ae9083b7e6d71b7a0d2a1b5ae3ce30"
cid = hash.decodeContentHash

check:
cid.isOk
$cid.get == "QmbHJYDDW5CRdUfpLFRdJNCEDbZNYvAi7DAAfXrgheM1C3"

# blank
hash = ""
cid = hash.decodeContentHash

check:
cid.isErr
cid.error == HashValueError

# wrong codec
hash = "e4"
cid = hash.decodeContentHash

check:
cid.isErr
cid.error == InvalidMultiCodec

asyncTest "decode edn":
var
edn = """{meta {:name "Dogeth"
:author "Mugen Flen"
:thumbnail "e3010170122094d3d16b41d6882dbc93e1690d945d0c00a2869cea94d0939f3ba3c0399685e6"
:preview "e301017012204d6059ec0fab356add176ad2b2dd95656e50cb1e822232c00a30df04efa50378"
:stickers [{:hash "e3010170122094d3d16b41d6882dbc93e1690d945d0c00a2869cea94d0939f3ba3c0399685e6"}{:hash "e30101701220fcd853d7633d411a7002f286c8316336823c71114f52d672361300deb4760596"}{:hash "e3010170122058c8e08765a5d8a103f036ae65e6a3ba3f110fefb859965b213f92c293972bf5"}{:hash "e30101701220f7d8386783bf8691428389379bedaf94c79011a1fb1f17677c3d1582ddc8ab97"}{:hash "e3010170122040f0a7a6cfa8c281eeda40f6ba65667f07ab861e1c78f1374c738f1ca2161a31"}{:hash "e30101701220ac7cf793ac2baa36bd69dd10462b45831d85bbdf375f19a29a62dec78a9f8d3c"}{:hash "e3010170122070acac3d71c6243c5d100c2e6a53de7812fb42528236a0c3a4e2f4b2e61c6662"}{:hash "e301017012205aac43b08a22ddf3f40cacba2fa2f72251eb2c6bfb21fb46f8c0586a4655dc50"}{:hash "e3010170122092e871abca1681bbb46c9869fa6f449e85591accb16fac8130819f7ebbcf4305"}{:hash "e301017012204005f3279924ac12b1a396c20c7313f2356d1920d8ef3a2c60072e1223b8d4c8"}{:hash "e30101701220d655e130a998c54f3f68b23b52fe5a08b8f25f98b52270f9b438cd000bb2c719"}{:hash "e301017012207de9418dcb3a12feeb7c1d54e71ca45da8344c61a828ffab5285138da5a9b3a9"}{:hash "e3010170122073d50f10d4cebc4a816e4c6c86cd2ad28e03ba8b64279750113a2dcc893ba773"}{:hash "e30101701220adb1b6799d69d994a46eaedd2ba63a00b086355ae4ed0182c28173cd9df17797"}{:hash "e301017012205b7764a62aa6d7999ea1698d901c3048f42cafee61625584cb6f67ae14b36f70"}]}}"""
decoded = edn.decode[:StickerPack]()

check decoded.isOk
let pack = decoded.get
check:
pack.author == "Mugen Flen"
pack.name == "Dogeth"
pack.preview == "e301017012204d6059ec0fab356add176ad2b2dd95656e50cb1e822232c00a30df04efa50378"
pack.thumbnail == "e3010170122094d3d16b41d6882dbc93e1690d945d0c00a2869cea94d0939f3ba3c0399685e6"
pack.stickers.len == 15
pack.stickers[0].hash == "e3010170122094d3d16b41d6882dbc93e1690d945d0c00a2869cea94d0939f3ba3c0399685e6"
pack.stickers[14].hash == "e301017012205b7764a62aa6d7999ea1698d901c3048f42cafee61625584cb6f67ae14b36f70"


1 change: 1 addition & 0 deletions test/test_all.nim
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import # test modules
./pendingtxs,
./permissions,
./settings,
./stickers,
./tokens,
./tx_history
# ./waku_smoke
1 change: 1 addition & 0 deletions vendor/edn.nim
Submodule edn.nim added at 3305e4