Skip to content

Commit c0e8d3b

Browse files
committed
bolt11 wip
1 parent 4223845 commit c0e8d3b

File tree

5 files changed

+658
-1
lines changed

5 files changed

+658
-1
lines changed

pub/functora/functora.cabal

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,18 @@ common pkg-elm2miso
242242
, base
243243
, regex-compat
244244

245+
common pkg-bolt11
246+
import: pkg
247+
hs-source-dirs: src/bolt11
248+
build-depends:
249+
, aeson
250+
, array
251+
, attoparsec
252+
, base
253+
, base16-bytestring
254+
, bytestring
255+
, text
256+
245257
library
246258
import: pkg-prelude
247259
exposed-modules:
@@ -331,6 +343,15 @@ library elm2miso-lib
331343
visibility: public
332344
exposed-modules: Functora.Elm2Miso
333345

346+
library bolt11
347+
import: pkg-bolt11
348+
exposed: True
349+
visibility: public
350+
exposed-modules:
351+
Functora.Bech32
352+
Functora.Bolt11
353+
Functora.Denomination
354+
334355
executable elm2miso
335356
import: pkg-elm2miso, exe
336357
main-is: Main.hs
@@ -359,6 +380,7 @@ test-suite functora-test
359380

360381
other-modules:
361382
Functora.AesSpec
383+
Functora.Bolt11Spec
362384
Functora.CfgSpec
363385
Functora.Elm2MisoSpec
364386
Functora.PreludeSpec
@@ -373,14 +395,17 @@ test-suite functora-test
373395
pkg-prelude, pkg-qr, pkg-aes,
374396
pkg-cfg, pkg-web, pkg-sql,
375397
pkg-money, pkg-rates, pkg-tags,
376-
pkg-soplate, pkg-elm2miso
398+
pkg-soplate, pkg-elm2miso, pkg-bolt11
377399

378400
ghc-options: -Wno-unused-packages
379401
other-modules:
380402
Functora.Aes
381403
Functora.AesOrphan
404+
Functora.Bech32
405+
Functora.Bolt11
382406
Functora.Cfg
383407
Functora.CfgOrphan
408+
Functora.Denomination
384409
Functora.Elm2Miso
385410
Functora.Money
386411
Functora.MoneySing
@@ -401,6 +426,7 @@ test-suite functora-test
401426
else
402427
build-depends:
403428
, aes
429+
, bolt11
404430
, cfg
405431
, elm2miso-lib
406432
, functora
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
{- Copied from Network.Haskoin.Address.Bech32 -}
4+
{- Copied from reference implementation contributed by Marko Bencun -}
5+
6+
module Functora.Bech32
7+
( HRP,
8+
Bech32,
9+
Data,
10+
bech32Encode,
11+
bech32Decode,
12+
toBase32,
13+
toBase256,
14+
toBase256',
15+
segwitEncode,
16+
segwitDecode,
17+
Word5 (..),
18+
word5,
19+
fromWord5,
20+
)
21+
where
22+
23+
import Control.Monad (guard)
24+
import Data.Array
25+
( Array,
26+
assocs,
27+
bounds,
28+
listArray,
29+
(!),
30+
(//),
31+
)
32+
import Data.Bits
33+
( Bits,
34+
testBit,
35+
unsafeShiftL,
36+
unsafeShiftR,
37+
xor,
38+
(.&.),
39+
(.|.),
40+
)
41+
import qualified Data.ByteString as B
42+
import Data.Char (toUpper)
43+
import Data.Foldable (foldl')
44+
import Data.Functor.Identity (Identity, runIdentity)
45+
import Data.Ix (Ix (..))
46+
import Data.Text (Text)
47+
import qualified Data.Text as T
48+
import qualified Data.Text.Encoding as E
49+
import Data.Word (Word8)
50+
import Prelude
51+
52+
-- | Bech32 human-readable string.
53+
type Bech32 = Text
54+
55+
-- | Human-readable part of 'Bech32' address.
56+
type HRP = Text
57+
58+
-- | Data part of 'Bech32' address.
59+
type Data = [Word8]
60+
61+
(.>>.), (.<<.) :: (Bits a) => a -> Int -> a
62+
(.>>.) = unsafeShiftR
63+
(.<<.) = unsafeShiftL
64+
65+
-- | Five-bit word for Bech32.
66+
newtype Word5
67+
= UnsafeWord5 Word8
68+
deriving newtype (Eq, Ord, Num)
69+
70+
instance Show Word5 where
71+
show (UnsafeWord5 w8) = show w8
72+
73+
instance Ix Word5 where
74+
range (UnsafeWord5 m, UnsafeWord5 n) = map UnsafeWord5 $ range (m, n)
75+
index (UnsafeWord5 m, UnsafeWord5 n) (UnsafeWord5 i) = index (m, n) i
76+
inRange (m, n) i = m <= i && i <= n
77+
78+
-- | Convert an integer number into a five-bit word.
79+
word5 :: (Integral a) => a -> Word5
80+
word5 x = UnsafeWord5 (fromIntegral x .&. 31)
81+
{-# INLINE word5 #-}
82+
{-# SPECIALIZE INLINE word5 :: Word8 -> Word5 #-}
83+
84+
-- TODO: FIX ME: https://github.com/input-output-hk/cardano-wallet/pull/312
85+
86+
-- | Convert a five-bit word into a number.
87+
fromWord5 :: (Num a) => Word5 -> a
88+
fromWord5 (UnsafeWord5 x) = fromIntegral x
89+
{-# INLINE fromWord5 #-}
90+
{-# SPECIALIZE INLINE fromWord5 :: Word5 -> Word8 #-}
91+
92+
-- | 'Bech32' character map as array of five-bit integers to character.
93+
charset :: Array Word5 Char
94+
charset =
95+
listArray (UnsafeWord5 0, UnsafeWord5 31) "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
96+
97+
-- | Convert a character to its five-bit value from 'Bech32' 'charset'.
98+
charsetMap :: Char -> Maybe Word5
99+
charsetMap c
100+
| inRange (bounds inv) upperC = inv ! upperC
101+
| otherwise = Nothing
102+
where
103+
upperC = toUpper c
104+
inv = listArray ('0', 'Z') (repeat Nothing) // map swap (assocs charset)
105+
swap (a, b) = (toUpper b, Just a)
106+
107+
-- | Calculate or validate 'Bech32' checksum.
108+
bech32Polymod :: [Word5] -> Word
109+
bech32Polymod values = foldl' go 1 values .&. 0x3fffffff
110+
where
111+
go chk value =
112+
foldl' xor chk' [g | (g, i) <- zip generator [25 ..], testBit chk i]
113+
where
114+
generator :: [Word]
115+
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
116+
chk' = chk .<<. 5 `xor` fromWord5 value
117+
118+
-- | Convert human-readable part of 'Bech32' string into a list of five-bit
119+
-- words.
120+
bech32HRPExpand :: HRP -> [Word5]
121+
bech32HRPExpand hrp =
122+
map (UnsafeWord5 . (.>>. 5)) hrpBytes
123+
++ [UnsafeWord5 0]
124+
++ map word5 hrpBytes
125+
where
126+
hrpBytes = B.unpack $ E.encodeUtf8 hrp
127+
128+
-- | Calculate checksum for a string of five-bit words.
129+
bech32CreateChecksum :: HRP -> [Word5] -> [Word5]
130+
bech32CreateChecksum hrp dat = [word5 (polymod .>>. i) | i <- [25, 20 .. 0]]
131+
where
132+
values = bech32HRPExpand hrp ++ dat
133+
polymod =
134+
bech32Polymod (values ++ map UnsafeWord5 [0, 0, 0, 0, 0, 0]) `xor` 1
135+
136+
-- | Verify checksum for a human-readable part and string of five-bit words.
137+
bech32VerifyChecksum :: HRP -> [Word5] -> Bool
138+
bech32VerifyChecksum hrp dat = bech32Polymod (bech32HRPExpand hrp ++ dat) == 1
139+
140+
-- | Maximum length of a Bech32 result.
141+
maxBech32Length :: Int
142+
maxBech32Length = 90
143+
144+
-- | Encode string of five-bit words into 'Bech32' using a provided
145+
-- human-readable part. Can fail if 'HRP' is invalid or result would be longer
146+
-- than 90 characters.
147+
bech32Encode :: HRP -> [Word5] -> Maybe Bech32
148+
bech32Encode hrp dat = do
149+
guard $ checkHRP hrp
150+
let dat' = dat ++ bech32CreateChecksum hrp dat
151+
rest = map (charset !) dat'
152+
result = T.concat [T.toLower hrp, T.pack "1", T.pack rest]
153+
guard $ T.length result <= maxBech32Length
154+
return result
155+
156+
-- | Check that human-readable part is valid for a 'Bech32' string.
157+
checkHRP :: HRP -> Bool
158+
checkHRP hrp = not (T.null hrp) && T.all (\char -> char >= '\x21' && char <= '\x7e') hrp
159+
160+
-- | Decode human-readable 'Bech32' string into a human-readable part and a
161+
-- string of five-bit words.
162+
bech32Decode :: Bech32 -> Maybe (HRP, [Word5])
163+
bech32Decode bech32 = do
164+
guard $ T.toUpper bech32 == bech32 || lowerBech32 == bech32
165+
let (hrp, dat) = T.breakOnEnd "1" lowerBech32
166+
guard $ T.length dat >= 6
167+
hrp' <- T.stripSuffix "1" hrp
168+
guard $ checkHRP hrp'
169+
dat' <- mapM charsetMap $ T.unpack dat
170+
guard $ bech32VerifyChecksum hrp' dat'
171+
return (hrp', take (T.length dat - 6) dat')
172+
where
173+
lowerBech32 = T.toLower bech32
174+
175+
type Pad f = Int -> Int -> Word -> [[Word]] -> f [[Word]]
176+
177+
yesPadding :: Pad Identity
178+
yesPadding _ 0 _ result = return result
179+
yesPadding _ _ padValue result = return $ [padValue] : result
180+
{-# INLINE yesPadding #-}
181+
182+
noPadding :: Pad Maybe
183+
noPadding frombits bits padValue result = do
184+
guard $ bits < frombits && padValue == 0
185+
return result
186+
{-# INLINE noPadding #-}
187+
188+
-- | Big endian conversion of a bytestring from base \(2^{frombits}\) to base
189+
-- \(2^{tobits}\). {frombits} and {twobits} must be positive and
190+
-- \(2^{frombits}\) and \(2^{tobits}\) must be smaller than the size of Word.
191+
-- Every value in 'dat' must be strictly smaller than \(2^{frombits}\).
192+
convertBits :: (Functor f) => [Word] -> Int -> Int -> Pad f -> f [Word]
193+
convertBits dat frombits tobits pad = concat . reverse <$> go dat 0 0 []
194+
where
195+
go [] acc bits result =
196+
let padValue = (acc .<<. (tobits - bits)) .&. maxv
197+
in pad frombits bits padValue result
198+
go (value : dat') acc bits result =
199+
go dat' acc' (bits' `rem` tobits) (result' : result)
200+
where
201+
acc' = (acc .<<. frombits) .|. value
202+
bits' = bits + frombits
203+
result' =
204+
[ (acc' .>>. b) .&. maxv
205+
| b <- [bits' - tobits, bits' - 2 * tobits .. 0]
206+
]
207+
maxv = (1 .<<. tobits) - 1
208+
{-# INLINE convertBits #-}
209+
210+
-- | Convert from eight-bit to five-bit word string, adding padding as required.
211+
toBase32 :: [Word8] -> [Word5]
212+
toBase32 dat =
213+
map word5 $ runIdentity $ convertBits (map fromIntegral dat) 8 5 yesPadding
214+
215+
-- | Convert from five-bit word string to eight-bit word string, ignoring padding.
216+
toBase256 :: [Word5] -> Maybe [Word8]
217+
toBase256 dat =
218+
map fromIntegral <$> convertBits (map fromWord5 dat) 5 8 noPadding
219+
220+
toBase256' :: [Word5] -> [Word8]
221+
toBase256' dat =
222+
map fromIntegral $ runIdentity $ convertBits (map fromWord5 dat) 5 8 yesPadding
223+
224+
-- | Check if witness version and program are valid.
225+
segwitCheck :: Word8 -> Data -> Bool
226+
segwitCheck witver witprog =
227+
witver <= 16
228+
&& if witver == 0
229+
then length witprog == 20 || length witprog == 32
230+
else length witprog >= 2 && length witprog <= 40
231+
232+
-- | Decode SegWit 'Bech32' address from a string and expected human-readable part.
233+
segwitDecode :: HRP -> Bech32 -> Maybe (Word8, Data)
234+
segwitDecode hrp addr = do
235+
(hrp', dat) <- bech32Decode addr
236+
guard $ (hrp == hrp') && not (null dat)
237+
case dat of
238+
[] -> error "empty UnsafeWord5"
239+
UnsafeWord5 witver : datBase32 -> do
240+
decoded <- toBase256 datBase32
241+
guard $ segwitCheck witver decoded
242+
return (witver, decoded)
243+
244+
-- | Encode 'Data' as a SegWit 'Bech32' address. Needs human-readable part and
245+
-- witness program version.
246+
segwitEncode :: HRP -> Word8 -> Data -> Maybe Text
247+
segwitEncode hrp witver witprog = do
248+
guard $ segwitCheck witver witprog
249+
bech32Encode hrp $ UnsafeWord5 witver : toBase32 witprog

0 commit comments

Comments
 (0)