mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 12:57:45 +00:00
store: check owner signatures and messages
This commit is contained in:
1 parent
27a99dc19a
commit
606818ba8b
4 files changed
+314
No files matched your search
@@ -0,0 +1,120 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module StoreService.Names.Signatures
|
||||
( SignatureError (..),
|
||||
AddressError (..),
|
||||
OrderFields (..),
|
||||
parseSignature,
|
||||
personalDigest,
|
||||
orderMessage,
|
||||
findMessage,
|
||||
retargetMessage,
|
||||
takenCodeMessage,
|
||||
parseChecksumAddress,
|
||||
parseAnyCaseAddress,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (unless, when)
|
||||
import Crypto.Number.Serialize (os2ip)
|
||||
import qualified Crypto.PubKey.ECC.Types as ECC
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import Data.Char (isAsciiLower, isDigit, isHexDigit)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word64)
|
||||
import Simplex.Messaging.Crypto (keccak256, sha256Hash)
|
||||
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature (..))
|
||||
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
|
||||
import Simplex.Messaging.Eth.Address (Address, unAddress)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import StoreService.Names.Abi (hex)
|
||||
|
||||
data SignatureError
|
||||
= SEFormat
|
||||
| SERecoveryId
|
||||
| SEHighS
|
||||
deriving (Eq, Show)
|
||||
|
||||
data AddressError
|
||||
= ADFormat
|
||||
| ADChecksum
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | The fields an owner signs in an order and in another name for it.
|
||||
data OrderFields = OrderFields
|
||||
{ ofLabelHash :: ByteString,
|
||||
ofLength :: Int,
|
||||
ofDuration :: Word64,
|
||||
ofCommitment :: ByteString,
|
||||
ofSealed :: Text
|
||||
}
|
||||
|
||||
signatureHexLength :: Int
|
||||
signatureHexLength = 130
|
||||
|
||||
scalarSize :: Int
|
||||
scalarSize = 32
|
||||
|
||||
addressHexLength :: Int
|
||||
addressHexLength = 40
|
||||
|
||||
-- the contracts' SignatureChecker refuses a higher s, though recovery accepts it
|
||||
maxS :: Integer
|
||||
maxS = ECC.ecc_n (ECC.common_curve $ ECC.getCurveByName ECC.SEC_p256k1) `div` 2
|
||||
|
||||
parseSignature :: Text -> Either SignatureError RecoverableSignature
|
||||
parseSignature t = do
|
||||
h <- maybe (Left SEFormat) Right $ T.stripPrefix "0x" t
|
||||
unless (T.length h == signatureHexLength && T.all isLowerHex h) $ Left SEFormat
|
||||
sig <- first (const SEFormat) $ convertFromBase Base16 (encodeUtf8 h)
|
||||
let (rsR, (rsS, v)) = B.splitAt scalarSize <$> B.splitAt scalarSize sig
|
||||
rsRecId <- case B.unpack v of
|
||||
[27] -> Right 0
|
||||
[28] -> Right 1
|
||||
_ -> Left SERecoveryId
|
||||
when (os2ip rsS > maxS) $ Left SEHighS
|
||||
pure RecoverableSignature {rsR, rsS, rsRecId}
|
||||
where
|
||||
isLowerHex c = isDigit c || (isAsciiLower c && isHexDigit c)
|
||||
|
||||
-- | EIP-191 @personal_sign@ digest.
|
||||
personalDigest :: ByteString -> ByteString
|
||||
personalDigest msg = keccak256 $ "\x19\&Ethereum Signed Message:\n" <> BC.pack (show $ B.length msg) <> msg
|
||||
|
||||
orderMessage :: OrderFields -> Word64 -> ByteString
|
||||
orderMessage order timestamp = message $ ["order"] <> orderWords order <> [tshow timestamp]
|
||||
|
||||
findMessage :: Address -> Word64 -> ByteString
|
||||
findMessage owner timestamp = message ["find", hex $ unAddress owner, tshow timestamp]
|
||||
|
||||
retargetMessage :: Text -> OrderFields -> Word64 -> ByteString
|
||||
retargetMessage registrationId order timestamp = message $ ["name", registrationId] <> orderWords order <> [tshow timestamp]
|
||||
|
||||
takenCodeMessage :: Text -> Text -> Int -> Word64 -> ByteString
|
||||
takenCodeMessage registrationId codeHash codeLength timestamp = message ["code", registrationId, codeHash, tshow codeLength, tshow timestamp]
|
||||
|
||||
message :: [Text] -> ByteString
|
||||
message = encodeUtf8 . T.unwords . ("simplex.domains" :)
|
||||
|
||||
orderWords :: OrderFields -> [Text]
|
||||
orderWords OrderFields {ofLabelHash, ofLength, ofDuration, ofCommitment, ofSealed} =
|
||||
[hex ofLabelHash, tshow ofLength, tshow ofDuration, hex ofCommitment, hex $ sha256Hash $ encodeUtf8 ofSealed]
|
||||
|
||||
-- the address parser checks EIP-55 only on mixed-case hex, so one-case text is refused here by comparing the encoding
|
||||
parseChecksumAddress :: Text -> Either AddressError Address
|
||||
parseChecksumAddress t = do
|
||||
a <- parseAnyCaseAddress t
|
||||
unless (strEncode a == encodeUtf8 t) $ Left ADChecksum
|
||||
pure a
|
||||
|
||||
parseAnyCaseAddress :: Text -> Either AddressError Address
|
||||
parseAnyCaseAddress t = case T.stripPrefix "0x" t of
|
||||
Just h | T.length h == addressHexLength && T.all isHexDigit h -> first (const ADFormat) $ strDecode $ encodeUtf8 $ T.toLower h
|
||||
_ -> Left ADFormat
|
||||
@@ -443,6 +443,7 @@ executable simplex-store-service
|
||||
StoreService.Group.Command
|
||||
StoreService.Log
|
||||
StoreService.Names.Abi
|
||||
StoreService.Names.Signatures
|
||||
StoreService.Options
|
||||
StoreService.Orders
|
||||
StoreService.Poller
|
||||
@@ -739,6 +740,7 @@ test-suite simplex-chat-test
|
||||
Bots.StoreService.GroupIntegrationTests
|
||||
Bots.StoreService.GroupTests
|
||||
Bots.StoreService.NameRegistrationTests
|
||||
Bots.StoreService.NamesSignatureTests
|
||||
Bots.StoreService.StripeTests
|
||||
Bots.StoreService.WaitersTests
|
||||
Bots.StoreService.WebTests
|
||||
@@ -763,6 +765,7 @@ test-suite simplex-chat-test
|
||||
StoreService.Names.Abi
|
||||
StoreService.Names.Chain
|
||||
StoreService.Names.Registration
|
||||
StoreService.Names.Signatures
|
||||
StoreService.Options
|
||||
StoreService.Orders
|
||||
StoreService.Poller
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Bots.StoreService.NamesSignatureTests (namesSignatureTests) where
|
||||
|
||||
import Control.Monad (forM_)
|
||||
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word64)
|
||||
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature (..))
|
||||
import Simplex.Messaging.Encoding.String (strDecode)
|
||||
import Simplex.Messaging.Eth.Address (Address, recoverAddress)
|
||||
import Simplex.Messaging.SimplexName (SimplexTLD (..))
|
||||
import StoreService.Names.Abi
|
||||
import StoreService.Names.Registration
|
||||
import StoreService.Names.Signatures
|
||||
import Test.Hspec
|
||||
|
||||
namesSignatureTests :: Spec
|
||||
namesSignatureTests = describe "StoreService names: signatures" $ do
|
||||
-- expected values are viem's, for the webapp's fixture wallet and the SimplexController, SimplexResolver and SimplexPriceOracle ABIs
|
||||
describe "EIP-712" $ do
|
||||
it "commits to a registration with one contact link" $
|
||||
makeCommitment dynamis `shouldBe` commitment
|
||||
it "hashes RegisterName" $
|
||||
registerIntentDigest (sponsorDomain chainId sponsor) dynamis 0 1800000000 `shouldBe` hx "0x4b0bcc282db581446f6c25e0e3fbe2839409acbc011bb3e082523dd9eab85a6a"
|
||||
it "hashes SetText" $
|
||||
setTextDigest (resolverDomain chainId resolver) node "simplex.contact" "https://x" 3 99 `shouldBe` hx "0xa62d4320889eed3713462d725dc298cd96885572b8e6b424e93a205a96b96fb5"
|
||||
it "hashes ClearRecords" $
|
||||
clearRecordsDigest (resolverDomain chainId resolver) node 4 99 `shouldBe` hx "0xca4bd243288f27f38e3b45384ba36ce685ff155352cf8125f031e6fb189a46dd"
|
||||
describe "EIP-191 messages" $ do
|
||||
it "signs an order" $
|
||||
testMessage
|
||||
(orderMessage dynamisOrder timestamp)
|
||||
"simplex.domains order 0x78b602f03f164366531da963764c1f2fdfd588d0487daf2aa261ca799e5ffe33 7 69259500 0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1 0xb1971beb567c8dc1b2f01f0d823754c6168d8d6830db2221702a7678e1804674 1792574100"
|
||||
"0x5deee1b3589df9e53665e34aa65a491a4eabfd76a39207b0e9303457f7c44b7a"
|
||||
"0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b"
|
||||
it "signs find with the owner in lowercase" $
|
||||
testMessage
|
||||
(findMessage owner timestamp)
|
||||
"simplex.domains find 0x014f9c09cceedef1b713110d04d3a07d5b5d9b73 1792574100"
|
||||
"0x08a989fbf0457984c062b44d7237be9c7948f2cf23d48410609777a09b515646"
|
||||
"0xfa0908260c3f21c8e47bd37e2c3cf3bd6bc62966591ab1a2c4e31695646548f47d2e79b34413be27b4b07330682de642f48a8f5e641449d7d79728eeb4ab14da1c"
|
||||
it "signs another name for an order, with its duration" $
|
||||
testMessage
|
||||
(retargetMessage registrationId dynamisOrder {ofLabelHash = hx "0x5bfbbf75311ec002834031d73fa30bfe8bdf675f4a3df72443841490c4b49d02"} timestamp)
|
||||
"simplex.domains name t2Jk8RmQ4vXw9NcP3yLb6g 0x5bfbbf75311ec002834031d73fa30bfe8bdf675f4a3df72443841490c4b49d02 7 69259500 0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1 0xb1971beb567c8dc1b2f01f0d823754c6168d8d6830db2221702a7678e1804674 1792574100"
|
||||
"0x8bc79aa98fbd53013e8961db27554ecd17139e14ec168b86afbaf2fc910bfc52"
|
||||
"0xe3d680bcc62255179d5260beeb920319c86bd919f6b01927bee01509cf6c1fe3121151947fb9e3a310e6652c0c9a9bf7ceb48e8d903774a38a3162d916e8b4591b"
|
||||
it "signs a taken order's code, with its length" $
|
||||
testMessage
|
||||
(takenCodeMessage registrationId "7hg6ED6EotRVm21uS1KOZ-CKEzkJK5hvG4W5atfcn_Q" 7 timestamp)
|
||||
"simplex.domains code t2Jk8RmQ4vXw9NcP3yLb6g 7hg6ED6EotRVm21uS1KOZ-CKEzkJK5hvG4W5atfcn_Q 7 1792574100"
|
||||
"0x1f61297cd585c7c94218f8f87f30ea20e9b0631d36890460f24ee9bca83a2a19"
|
||||
"0x897173292eb63ac3587d3c900af08dee71f296af8718ec045dd49ce953bb0f666eefe546bea0d3623022c7cc7f03cc572a498bb0ceac26bb46ed37c9e99ecdb51b"
|
||||
describe "signature format" $ do
|
||||
it "refuses a high s, though recovery accepts it" testHighS
|
||||
it "refuses v other than 27 or 28" $
|
||||
forM_ ["00", "01", "1d"] $ \v -> parseSignature (T.dropEnd 2 orderSignature <> v) `shouldBe` Left SERecoveryId
|
||||
it "refuses uppercase hex, no 0x and a wrong length" $ do
|
||||
parseSignature ("0x" <> T.toUpper (T.drop 2 orderSignature)) `shouldBe` Left SEFormat
|
||||
parseSignature (T.drop 2 orderSignature) `shouldBe` Left SEFormat
|
||||
parseSignature (T.dropEnd 2 orderSignature) `shouldBe` Left SEFormat
|
||||
parseSignature (orderSignature <> "00") `shouldBe` Left SEFormat
|
||||
describe "addresses" $ do
|
||||
it "accepts the EIP-55 form in orders" $
|
||||
parseChecksumAddress ownerText `shouldBe` Right owner
|
||||
it "refuses a letter in the wrong case and one-case hex in orders" $ do
|
||||
parseChecksumAddress "0x014F9c09CcEeDEF1B713110d04D3a07d5B5D9b73" `shouldBe` Left ADChecksum
|
||||
parseChecksumAddress ownerLower `shouldBe` Left ADChecksum
|
||||
parseChecksumAddress ownerUpper `shouldBe` Left ADChecksum
|
||||
it "accepts any case in find" $
|
||||
forM_ [ownerText, ownerLower, ownerUpper, "0x014F9c09CcEeDEF1B713110d04D3a07d5B5D9b73"] $ \a ->
|
||||
parseAnyCaseAddress a `shouldBe` Right owner
|
||||
it "refuses what is not an address" $
|
||||
forM_ ["014f9c09cceedef1b713110d04d3a07d5b5d9b73", "0X014f9c09cceedef1b713110d04d3a07d5b5d9b73", T.dropEnd 1 ownerLower, ownerLower <> "0", "0x014f9c09cceedef1b713110d04d3a07d5b5d9b7g"] $ \a -> do
|
||||
parseAnyCaseAddress a `shouldBe` Left ADFormat
|
||||
parseChecksumAddress a `shouldBe` Left ADFormat
|
||||
describe "ABI return data" $ do
|
||||
it "decodes each type as viem encodes it" $ do
|
||||
decodeUint256 (hx "00000000000000000000000000000000000000018ee90ff6c373e0ee4e3f0ad2") `shouldBe` Right 123456789012345678901234567890
|
||||
decodeAddress (hx "000000000000000000000000014f9c09cceedef1b713110d04d3a07d5b5d9b73") `shouldBe` Right owner
|
||||
decodeBool (hx "0000000000000000000000000000000000000000000000000000000000000001") `shouldBe` Right True
|
||||
decodeBool (B.replicate 32 0) `shouldBe` Right False
|
||||
decodeBytes32 (hx "833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb") `shouldBe` Right node
|
||||
decodeString retString `shouldBe` Right "https://smp8.simplex.im/a#contact"
|
||||
decodeString (hx "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") `shouldBe` Right ""
|
||||
decodeBytes (hx "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010410f13a8c833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e636f6e746163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e746163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") `shouldBe` Right (hx "10f13a8c833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e636f6e746163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e7461637400000000000000000000000000000000000000000000000000000000000000")
|
||||
it "decodes the oracle's prices" $ do
|
||||
decodePrices retPrices `shouldBe` Right (5000, [(7, 10000), (6, 20000)])
|
||||
decodePrices (hx "000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000") `shouldBe` Right (5000, [])
|
||||
it "refuses data that does not hold the type" $ do
|
||||
decodeUint256 (B.replicate 31 0) `shouldBe` Left AEShort
|
||||
decodeAddress (B.singleton 1 <> B.drop 1 (hx "000000000000000000000000014f9c09cceedef1b713110d04d3a07d5b5d9b73")) `shouldBe` Left AENotAddress
|
||||
decodeBool (uint256 (2 :: Int)) `shouldBe` Left AENotBool
|
||||
decodeString (uint256 (4096 :: Int) <> B.drop 32 retString) `shouldBe` Left AEOffset
|
||||
decodeString (B.take 80 retString) `shouldBe` Left AEShort
|
||||
decodeString (abiTuple [Dynamic $ abiBytes "\xff"]) `shouldBe` Left AENotUtf8
|
||||
decodePrices (B.dropEnd 32 retPrices) `shouldBe` Left AEShort
|
||||
|
||||
testMessage :: ByteString -> ByteString -> Text -> Text -> IO ()
|
||||
testMessage msg expected digest signature = do
|
||||
msg `shouldBe` expected
|
||||
personalDigest msg `shouldBe` hx digest
|
||||
sig <- either (fail . show) pure $ parseSignature signature
|
||||
recoverAddress sig (personalDigest msg) `shouldReturn` Right owner
|
||||
|
||||
testHighS :: IO ()
|
||||
testHighS = do
|
||||
parseSignature highS `shouldBe` Left SEHighS
|
||||
let (rsR, (rsS, v)) = B.splitAt 32 <$> B.splitAt 32 (hx highS)
|
||||
recoverAddress RecoverableSignature {rsR, rsS, rsRecId = B.head v - 27} (personalDigest $ orderMessage dynamisOrder timestamp)
|
||||
`shouldReturn` Right owner
|
||||
where
|
||||
highS = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a8b206002333c63a347d8321b27b93899bc51d768793c21589fe7ae62862f6fff31c"
|
||||
|
||||
-- fixtures
|
||||
|
||||
dynamis :: Registration
|
||||
dynamis =
|
||||
Registration
|
||||
{ rLabel = "dynamis",
|
||||
rTLD = TLDSimplex,
|
||||
rOwner = owner,
|
||||
rDuration = duration,
|
||||
rSecret = B.replicate 32 0x11,
|
||||
rResolver = resolver,
|
||||
rContactLink = Just "https://smp8.simplex.im/a#contact",
|
||||
rChannelLink = Nothing
|
||||
}
|
||||
|
||||
dynamisOrder :: OrderFields
|
||||
dynamisOrder =
|
||||
OrderFields
|
||||
{ ofLabelHash = hx "0x78b602f03f164366531da963764c1f2fdfd588d0487daf2aa261ca799e5ffe33",
|
||||
ofLength = 7,
|
||||
ofDuration = duration,
|
||||
ofCommitment = commitment,
|
||||
ofSealed = "q3Zk0Yx7c2VhbGVkLW9yZGVyLWZpeHR1cmU"
|
||||
}
|
||||
|
||||
commitment :: ByteString
|
||||
commitment = hx "0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1"
|
||||
|
||||
node :: ByteString
|
||||
node = hx "0x833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb"
|
||||
|
||||
-- 21 October 2026 09:15 UTC
|
||||
timestamp :: Word64
|
||||
timestamp = 1792574100
|
||||
|
||||
-- 2 years of 365 days and the free days until 1 January 2027
|
||||
duration :: Word64
|
||||
duration = 69259500
|
||||
|
||||
registrationId :: Text
|
||||
registrationId = "t2Jk8RmQ4vXw9NcP3yLb6g"
|
||||
|
||||
orderSignature :: Text
|
||||
orderSignature = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b"
|
||||
|
||||
retString :: ByteString
|
||||
retString = hx "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e7461637400000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
retPrices :: ByteString
|
||||
retPrices = hx "0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000004e20"
|
||||
|
||||
owner :: Address
|
||||
owner = either error id $ strDecode $ encodeUtf8 ownerText
|
||||
|
||||
ownerText, ownerLower, ownerUpper :: Text
|
||||
ownerText = "0x014f9c09CcEeDEF1B713110d04D3a07d5B5D9b73"
|
||||
ownerLower = T.toLower ownerText
|
||||
ownerUpper = "0x" <> T.toUpper (T.drop 2 ownerText)
|
||||
|
||||
sponsor, resolver :: Address
|
||||
sponsor = either error id $ strDecode "0x9a676e781a523b5d0c0e43731313a708cb607508"
|
||||
resolver = either error id $ strDecode "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6"
|
||||
|
||||
chainId :: Word64
|
||||
chainId = 31337
|
||||
|
||||
hx :: Text -> ByteString
|
||||
hx t = either error id $ convertFromBase Base16 $ encodeUtf8 $ fromMaybe t $ T.stripPrefix "0x" t
|
||||
@@ -10,6 +10,7 @@ import Bots.StoreService.ConfigTests
|
||||
import Bots.StoreService.GroupIntegrationTests
|
||||
import Bots.StoreService.GroupTests
|
||||
import Bots.StoreService.NameRegistrationTests
|
||||
import Bots.StoreService.NamesSignatureTests
|
||||
import Bots.StoreService.StripeTests
|
||||
import Bots.StoreService.WaitersTests
|
||||
import Bots.StoreService.WebTests
|
||||
@@ -86,6 +87,7 @@ main = do
|
||||
badgeBTCPayTests
|
||||
badgeStripeTests
|
||||
badgeNameRegistrationTests
|
||||
namesSignatureTests
|
||||
describe "SimpleX chat markdown" markdownTests
|
||||
describe "JSON Tests" jsonTests
|
||||
describe "Member relations" memberRelationsTests
|
||||
|
||||
Reference in new issue
Block a user