{-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Bots.StoreService.NamesStoreTests (namesStoreTests, issue, order, ownerA, ownerB, placeCode, placePaid, reveal, t0, usable) where import Bots.StoreService.WebTests (columnsOf, digestFixture, runMigrations, withServiceStore) import Control.Arrow ((&&&)) import Control.Monad (forM_, void) import Data.List (sort) import Data.Maybe (fromJust, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..), addUTCTime) import Data.Word (Word8) import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..)) import Simplex.Chat.PaymentService.Types (CardProvider (..), CryptoCurrency (..), InvoiceId (..), ServicePaymentMethod (..)) import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationsToRun (..), toDownMigration) import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature) import Simplex.Messaging.Eth.Address (Address) import Simplex.Messaging.SimplexName (labelHash) import StoreService.Names.Chain (TxHash (..)) import StoreService.Names.Phases (RegistrationId (..)) import StoreService.Names.Signatures (OrderFields (..), parseAnyCaseAddress, parseSignature) import StoreService.Names.Store import StoreService.Store (RevokeResult (..)) import Test.Hspec #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..), (:.) (..)) import StoreService.Store.Postgres.Migrations (storeServiceSchemaMigrations) #else import Database.SQLite.Simple (Only (..), (:.) (..)) import StoreService.Store.SQLite.Migrations (storeServiceSchemaMigrations) #endif namesStoreTests :: Spec namesStoreTests = describe "StoreService names: store" $ do describe "migration" $ do it "creates the names tables with the planned columns" testNamesColumns it "rolls back and re-applies only the names migration, with rows in it, leaving the badge tables and paid invoices as they were" testNamesDownUp describe "order hash" $ do it "is equal for the same order fields, and differs when any one field differs" testOrderHashFields it "keeps field boundaries, so moving a byte between two fields changes it" testOrderHashBoundaries describe "registrations" $ do it "creates a paid order unpaid and live at version 1, linked to its invoice" testCreatePaid it "creates a code order queued with its code reserved" testCreateCode it "refuses a second live order for one owner and label hash, or for one commitment" testLiveUnique it "leaves no registration when the code cannot be reserved" testCodeNotReservable it "finds an exact resend by its order hash, live or not" testOrderHashLookup it "tells an overdue live order apart, whatever the poller marked, unless paid" testLiveOrdersOverdue it "closes an overdue order so a new one from the same wallet fits in the same transaction" testCloseOverdue it "closes nothing from a stale version" testCloseOverdueStale it "bumps the version on every write and changes nothing for a stale one" testVersionBumps it "writes and reads back every changeable field, and clears the reveal" testRoundTrip it "keeps live in step with the phase" testLiveFollowsPhase it "is live while an order can still progress to a registration for its name" testOrderLive it "lets a wallet order a name again once its registration is done" testReorderAfterRegistered it "refuses to queue a closed order, also when a newer live order holds its name" testLatePayment it "refuses a write that would meet another live order, writing nothing" testWriteMeetsLiveOrder it "retargets to the new fields, clearing the reveal, transactions, reveal times and refusal" testRetarget it "finds an owner's orders newest first, at most 50" testFind it "orders registrations made in the same second by their id" testSameSecondOrder it "tells a label being registered here" testLabelRegistering it "lists the registrations a worker moves on" testActive it "lists unpaid orders whose invoice expired before the cutoff without a payment, and deletes one with its invoice" testUnpaidCleanup it "lists code orders failed before the cutoff that still hold their code" testFailedCodeOrders describe "prepaid codes" $ do it "issues one code per hash" testIssueCode it "reserves only an unused code, releases and spends it only for its holder" testReserveReleaseSpend it "refuses an unpaid, revoked or expired code" testReserveRefused it "lets a registration hold one code" testOneCodePerRegistration it "revokes an unspent code, reserved or not, never a spent one" testRevoke describe "transactions" $ do it "keeps a signed transaction pending until its outcome, and lists them by owner" testTransactions it "moves a transaction only from the status its caller read" testTransactionFromStatus describe "record edits" $ do it "holds one pending or mined edit per owner and nonce, freed by a revert" testEditNonceUnique it "settles a pending edit once, with its mined hash" testSettleEdit it "round-trips a clear edit" testClearEdit it "gives the times of a name's relayed edits in the window" testRelayedEditTimes t0 :: UTCTime t0 = UTCTime (fromGregorian 2026 10 21) 0 at :: Int -> UTCTime at minutes = addUTCTime (fromIntegral $ minutes * 60) t0 ownerA :: Address ownerA = address "0x014f9c09CcEeDEF1B713110d04D3a07d5B5D9b73" ownerB :: Address ownerB = address "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" address :: Text -> Address address = either (error . show) id . parseAnyCaseAddress -- r all 0x11, s all 0x22 (a low s), v 27 signature :: RecoverableSignature signature = either (error . show) id $ parseSignature $ "0x" <> T.replicate 32 "11" <> T.replicate 32 "22" <> "1b" twoYears :: Int twoYears = 2 order :: Text -> Word8 -> OrderFields order label n = OrderFields { ofLabelHash = labelHash label, ofLength = T.length label, ofDuration = 69292800, ofCommitment = digestFixture n, ofSealed = "sealed-" <> label <> "-" <> T.pack (show n) } newReg :: Text -> Address -> OrderFields -> NewRegistration newReg rid nrgOwner nrgOrder = NewRegistration {nrgId = RegistrationId rid, nrgOwner, nrgOrder, nrgYears = twoYears} btc :: ServicePaymentMethod btc = SPMCrypto CCBtc price :: Int price = 20000 paidOrder :: Text -> PaidOrder paidOrder invId = PaidOrder {poMethod = btc, poPrice = price, poInvoiceId = InvoiceId invId, poProviderRef = "ref-" <> invId} insertInvoiceRow :: DB.Connection -> Text -> UTCTime -> Text -> IO () insertInvoiceRow db invId expiresAt status = DB.execute db "INSERT INTO sx_badge_service_invoices (invoice_id, provider, price, amount, currency, payment_address, payment_crypto_currency, payment_crypto_amount, expires_at, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" ((invId, "crypto" :: Text, price, price, "usd" :: Text, "bc1qexampleaddress" :: Text, "btc" :: Text, "0.00312" :: Text) :. (expiresAt, status, t0, t0)) -- | A paid order whose invoice, named after the registration, expires at the given time. placePaid :: DBStore -> Text -> Address -> OrderFields -> UTCTime -> IO (Either RegistrationRefused ()) placePaid st rid owner o expiresAt = withTransaction st $ \db -> do insertInvoiceRow db ("inv-" <> rid) expiresAt "open" createPaidRegistration db (newReg rid owner o) (paidOrder $ "inv-" <> rid) t0 issue :: DBStore -> Word8 -> NewPrepaidCode -> IO PrepaidCode issue st n c = withTransaction st $ \db -> do Right codeId <- insertPrepaidCode db c {npcCodeHash = digestFixture n} t0 fromJust <$> getPrepaidCodeById db codeId usable :: NewPrepaidCode usable = NewPrepaidCode {npcCodeHash = "", npcMinLength = 8, npcYears = twoYears, npcPaymentStatus = CPSPaid, npcExpiresAt = Nothing} placeCode :: DBStore -> Text -> Address -> OrderFields -> PrepaidCode -> IO (Either RegistrationRefused ()) placeCode st rid owner o code = withTransaction st $ \db -> createCodeRegistration db (newReg rid owner o) code t0 reg :: DBStore -> Text -> IO NameRegistration reg st rid = withTransaction st $ \db -> fromJust <$> getRegistration db (RegistrationId rid) write :: DBStore -> NameRegistration -> IO VersionWrite write st r = withTransaction st $ \db -> updateRegistration db r (at 1) namesTables :: [Text] namesTables = map ("sx_badge_service_name_" <>) ["prepaid_codes", "registrations", "invoices", "record_edits", "transactions"] badgeTables :: [Text] badgeTables = map ("sx_badge_service_" <>) ["invoices", "payments", "badge_prices", "badge_offers", "badge_purchases", "badge_ledger", "badge_issuances", "badge_codes", "badge_code_invoices", "group"] testNamesColumns :: IO () testNamesColumns = withServiceStore $ \st -> do let has table cols = columnsOf st table >>= \cs -> filter (`notElem` cs) cols `shouldBe` [] has "sx_badge_service_name_prepaid_codes" ["name_prepaid_code_id", "code_hash", "min_length", "years", "code_payment_status", "reserved_by", "reserved_at", "spent_at", "revoked_at", "expires_at", "created_at"] has "sx_badge_service_name_registrations" [ "registration_id", "label_hash", "length", "label", "secret", "contact_link", "channel_link", "intent_nonce", "intent_deadline", "intent_signature", "sealed", "owner", "commitment", "phase", "commit_tx", "commit_block", "register_tx", "register_block", "reveal_after", "reveal_before", "years", "duration", "expires_at", "price", "name_prepaid_code_id", "issued_code_id", "registered_at", "failed_at", "error", "created_at", "updated_at", "version", "live", "order_hash", "method", "moves", "links_refused_at" ] has "sx_badge_service_name_invoices" ["invoice_id", "registration_id", "provider_ref"] has "sx_badge_service_name_record_edits" ["label_hash", "owner", "nonce", "record_key", "record_value", "deadline", "signature", "body_hash", "state", "revert", "mined_tx", "created_at"] has "sx_badge_service_name_transactions" ["registration_id", "name_record_edit_id", "kind", "raw", "tx_hash", "registrar_nonce", "sent_at", "mined_block", "status"] testNamesDownUp :: IO () testNamesDownUp = withServiceStore $ \st -> do let names = [m | m@Migration {name = "20261010_names"} <- storeServiceSchemaMigrations] badgeColumns = mapM (fmap sort . columnsOf st) badgeTables length names `shouldBe` 1 map name storeServiceSchemaMigrations `shouldSatisfy` ((== "20261010_names") . last) badgeBefore <- badgeColumns code <- issue st 1 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () placePaid st "r2" ownerB (order "bobsbakery" 2) (at 60) `shouldReturn` Right () placePaid st "r3" ownerB (order "carolsbakery" 3) (at 60) `shouldReturn` Right () withTransaction st $ \db -> insertPayment db "inv-r3" runMigrations st $ MTRDown (mapMaybe toDownMigration names) mapM (columnsOf st) namesTables `shouldReturn` map (const []) namesTables mapM (invoiceCount st) ["inv-r2", "inv-r3"] `shouldReturn` [0, 1] badgeColumns `shouldReturn` badgeBefore runMigrations st $ MTRUp names mapM (fmap null . columnsOf st) namesTables `shouldReturn` map (const False) namesTables badgeColumns `shouldReturn` badgeBefore testOrderHashFields :: IO () testOrderHashFields = do let o = order "alicebakery" 1 paid = OTInvoice btc price base = orderHash ownerA o paid variants = [ ("labelHash" :: Text, orderHash ownerA o {ofLabelHash = labelHash "alicebakers"} paid), ("length", orderHash ownerA o {ofLength = 12} paid), ("duration", orderHash ownerA o {ofDuration = 69292801} paid), ("owner", orderHash ownerB o paid), ("commitment", orderHash ownerA o {ofCommitment = digestFixture 2} paid), ("sealed", orderHash ownerA o {ofSealed = ofSealed o <> "x"} paid), ("method", orderHash ownerA o (OTInvoice (SPMCard CPStripe) price)), ("price", orderHash ownerA o (OTInvoice btc (price + 1))), ("code instead of invoice", orderHash ownerA o (OTCode $ digestFixture 3)), ("another code", orderHash ownerA o (OTCode $ digestFixture 4)) ] orderHash ownerA (order "alicebakery" 1) paid `shouldBe` base forM_ variants $ \(field, h) -> (field, h == base) `shouldBe` (field, False) orderHash ownerA o (OTCode $ digestFixture 3) `shouldNotBe` orderHash ownerA o (OTCode $ digestFixture 4) testOrderHashBoundaries :: IO () testOrderHashBoundaries = do let o = order "alicebakery" 1 orderHash ownerA o {ofSealed = "ab"} (OTCode "c") `shouldNotBe` orderHash ownerA o {ofSealed = "a"} (OTCode "bc") testCreatePaid :: IO () testCreatePaid = withServiceStore $ \st -> do let o = order "alicebakery" 1 placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () r <- reg st "r1" r `shouldBe` NameRegistration { nrId = RegistrationId "r1", nrVersion = 1, nrLabelHash = ofLabelHash o, nrLength = 11, nrDuration = 69292800, nrOwner = ownerA, nrCommitment = ofCommitment o, nrSealed = ofSealed o, nrPhase = PUnpaid, nrYears = twoYears, nrPrice = price, nrPayment = RPInvoice btc (InvoiceId "inv-r1"), nrReveal = Nothing, nrCommitTx = Nothing, nrCommitBlock = Nothing, nrRegisterTx = Nothing, nrRegisterBlock = Nothing, nrRevealAfter = Nothing, nrRevealBefore = Nothing, nrExpiresAt = Nothing, nrIssuedCodeId = Nothing, nrRegisteredAt = Nothing, nrFailedAt = Nothing, nrError = Nothing, nrLive = True, nrOrderHash = orderHash ownerA o (OTInvoice btc price), nrMoves = 0, nrLinksRefusedAt = Nothing, nrCreatedAt = t0, nrUpdatedAt = t0 } testCreateCode :: IO () testCreateCode = withServiceStore $ \st -> do let o = order "alicebakery" 1 code@PrepaidCode {pcId, pcCodeHash} <- issue st 1 usable placeCode st "r1" ownerA o code `shouldReturn` Right () r <- reg st "r1" (nrPhase r, nrLive r, nrPrice r, nrPayment r, nrVersion r) `shouldBe` (PQueued, True, 0, RPCode pcId, 1) nrOrderHash r `shouldBe` orderHash ownerA o (OTCode pcCodeHash) held <- withTransaction st $ \db -> getPrepaidCode db pcCodeHash (pcReservedBy =<< held, pcReservedAt =<< held) `shouldBe` (Just (RegistrationId "r1"), Just t0) testLiveUnique :: IO () testLiveUnique = withServiceStore $ \st -> do let o = order "alicebakery" 1 placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () placePaid st "r2" ownerA o {ofCommitment = digestFixture 2} (at 60) `shouldReturn` Left RRLiveOrder placePaid st "r3" ownerB o (at 60) `shouldReturn` Left RRLiveOrder code <- issue st 9 usable placeCode st "r4" ownerA (order "alicebakery" 3) code `shouldReturn` Left RRLiveOrder placePaid st "r5" ownerB (order "alicebakery" 5) (at 60) `shouldReturn` Right () existing <- withTransaction st $ \db -> mapM (getRegistration db . RegistrationId) ["r2", "r3", "r4"] existing `shouldBe` [Nothing, Nothing, Nothing] free <- withTransaction st $ \db -> getPrepaidCodeById db (pcId code) (pcReservedBy =<< free) `shouldBe` Nothing testCodeNotReservable :: IO () testCodeNotReservable = withServiceStore $ \st -> do code <- issue st 1 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () placeCode st "r2" ownerB (order "bobsbakery" 2) code `shouldReturn` Left RRCodeNotReservable withTransaction st (\db -> getRegistration db (RegistrationId "r2")) `shouldReturn` Nothing held <- withTransaction st $ \db -> getPrepaidCodeById db (pcId code) (pcReservedBy =<< held) `shouldBe` Just (RegistrationId "r1") testOrderHashLookup :: IO () testOrderHashLookup = withServiceStore $ \st -> do let o = order "alicebakery" 1 placePaid st "r1" ownerA o (at 1) `shouldReturn` Right () r <- reg st "r1" closed <- withTransaction st $ \db -> closeOverdue db r (at 2) closed `shouldBe` Just 2 found <- withTransaction st $ \db -> getRegistrationByOrderHash db (orderHash ownerA o (OTInvoice btc price)) (nrId <$> found, nrLive <$> found) `shouldBe` (Just (RegistrationId "r1"), Just False) withTransaction st (\db -> getRegistrationByOrderHash db (orderHash ownerA o (OTInvoice btc (price + 1)))) `shouldReturn` Nothing testLiveOrdersOverdue :: IO () testLiveOrdersOverdue = withServiceStore $ \st -> do let o = order "alicebakery" 1 live now = withTransaction st $ \db -> map (\LiveOrder {loRegistration, loOverdue} -> (nrId loRegistration, loOverdue)) <$> getLiveOrders db (ofCommitment o) ownerA (ofLabelHash o) now setStatus status = withTransaction st $ \db -> DB.execute db "UPDATE sx_badge_service_invoices SET status = ? WHERE invoice_id = ?" (status :: Text, "inv-r1" :: Text) placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () placePaid st "r2" ownerB (order "alicebakery" 2) (at 60) `shouldReturn` Right () live (at 30) `shouldReturn` [(RegistrationId "r1", False)] live (at 61) `shouldReturn` [(RegistrationId "r1", True)] withTransaction st (\db -> map (nrId . loRegistration) <$> getLiveOrders db (ofCommitment o) ownerB (labelHash "zzzzzzzz") (at 30)) `shouldReturn` [RegistrationId "r1"] setStatus "expired" live (at 61) `shouldReturn` [(RegistrationId "r1", True)] setStatus "paid" live (at 61) `shouldReturn` [(RegistrationId "r1", False)] testCloseOverdue :: IO () testCloseOverdue = withServiceStore $ \st -> do let o = order "alicebakery" 1 o' = o {ofCommitment = digestFixture 2, ofSealed = "new"} placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () placed <- withTransaction st $ \db -> do [LiveOrder {loRegistration, loOverdue = True}] <- getLiveOrders db (ofCommitment o') ownerA (ofLabelHash o') (at 61) void $ closeOverdue db loRegistration (at 61) insertInvoiceRow db "inv-r2" (at 120) "open" createPaidRegistration db (newReg "r2" ownerA o') (paidOrder "inv-r2") (at 61) placed `shouldBe` Right () old <- reg st "r1" (nrLive old, nrPhase old, nrVersion old) `shouldBe` (False, PUnpaid, 2) invoiceStatus st "inv-r1" `shouldReturn` "expired" invoiceStatus st "inv-r2" `shouldReturn` "open" testCloseOverdueStale :: IO () testCloseOverdueStale = withServiceStore $ \st -> do placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () r <- reg st "r1" write st r {nrPhase = PQueued} `shouldReturn` Written 2 withTransaction st (\db -> closeOverdue db r (at 61)) `shouldReturn` Nothing invoiceStatus st "inv-r1" `shouldReturn` "open" (nrLive &&& nrPhase) <$> reg st "r1" `shouldReturn` (True, PQueued) invoiceStatus :: DBStore -> Text -> IO Text invoiceStatus st invId = withTransaction st $ \db -> DB.query db "SELECT status FROM sx_badge_service_invoices WHERE invoice_id = ?" (Only invId) >>= \case [Only status] -> pure status rows -> fail $ "invoice " <> show invId <> ": " <> show (length rows) <> " rows" reveal :: NameReveal reveal = NameReveal { rvLabel = "alicebakery", rvSecret = digestFixture 7, rvContactLink = Just "https://smp8.simplex.im/a#abc", rvChannelLink = Nothing, rvNonce = 3, rvDeadline = 1792915140, rvSignature = signature } testVersionBumps :: IO () testVersionBumps = withServiceStore $ \st -> do code <- issue st 1 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () r1 <- reg st "r1" write st r1 {nrPhase = PCommitting, nrCommitTx = Just (TxHash $ digestFixture 8)} `shouldReturn` Written 2 r2 <- reg st "r1" write st r2 {nrPhase = PCommitted, nrRevealAfter = Just 1792828800, nrRevealBefore = Just 1792915140} `shouldReturn` Written 3 r3 <- reg st "r1" write st r3 {nrReveal = Just reveal} `shouldReturn` Written 4 write st r3 {nrLinksRefusedAt = Just (at 5)} `shouldReturn` VersionChanged write st r1 {nrPhase = PFailed} `shouldReturn` VersionChanged r4 <- reg st "r1" (nrVersion r4, nrPhase r4, nrReveal r4, nrLinksRefusedAt r4) `shouldBe` (4, PCommitted, Just reveal, Nothing) testRoundTrip :: IO () testRoundTrip = withServiceStore $ \st -> do code <- issue st 1 usable issued <- issue st 2 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () r <- reg st "r1" let changed = r { nrPhase = PRegistered, nrReveal = Just reveal {rvChannelLink = Just "simplex:/c#xyz"}, nrCommitTx = Just (TxHash $ digestFixture 8), nrCommitBlock = Just 100, nrRegisterTx = Just (TxHash $ digestFixture 9), nrRegisterBlock = Just 120, nrRevealAfter = Just 1792828800, nrRevealBefore = Just 1792915140, nrExpiresAt = Just 1861833600, nrIssuedCodeId = Just (pcId issued), nrRegisteredAt = Just (at 3), nrFailedAt = Just (at 2), nrError = Just "The name was not signed in time", nrLinksRefusedAt = Just (at 4), nrMoves = 2 } write st changed `shouldReturn` Written 2 reg st "r1" `shouldReturn` changed {nrVersion = 2, nrLive = False, nrUpdatedAt = at 1} write st changed {nrVersion = 2, nrReveal = Nothing} `shouldReturn` Written 3 nrReveal <$> reg st "r1" `shouldReturn` Nothing testLiveFollowsPhase :: IO () testLiveFollowsPhase = withServiceStore $ \st -> do placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () code <- issue st 1 usable placeCode st "r2" ownerB (order "bobsbakery" 2) code `shouldReturn` Right () let step rid phase expected = do r <- reg st rid write st r {nrPhase = phase} `shouldReturn` Written (nrVersion r + 1) (nrPhase &&& nrLive) <$> reg st rid `shouldReturn` (phase, expected) step "r1" PQueued True step "r1" PTaken True step "r1" PFailed True step "r1" PTaken True step "r1" PCodeIssued False step "r2" PRegistering True step "r2" PTaken False placePaid st "r3" ownerA (order "carolsbakery" 3) (at 60) `shouldReturn` Right () r3 <- reg st "r3" write st r3 {nrLive = False} `shouldReturn` Written 2 nrLive <$> reg st "r3" `shouldReturn` False testOrderLive :: IO () testOrderLive = withServiceStore $ \st -> do placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () paid <- reg st "r1" let code = paid {nrPayment = RPCode 1} liveIn r phase = (phase, orderLive r {nrPhase = phase}) map (liveIn paid) [minBound .. maxBound] `shouldBe` [(PUnpaid, True), (PQueued, True), (PCommitting, True), (PCommitted, True), (PRegistering, True), (PRegistered, False), (PTaken, True), (PFailed, True), (PCodeIssued, False)] map (liveIn code) [PTaken, PFailed, PRegistered] `shouldBe` [(PTaken, False), (PFailed, True), (PRegistered, False)] orderLive paid {nrLive = False} `shouldBe` False orderLive paid {nrLive = False, nrPhase = PQueued} `shouldBe` True testReorderAfterRegistered :: IO () testReorderAfterRegistered = withServiceStore $ \st -> do let o = order "alicebakery" 1 code <- issue st 1 usable placeCode st "r1" ownerA o code `shouldReturn` Right () placePaid st "r2" ownerA o {ofCommitment = digestFixture 2} (at 60) `shouldReturn` Left RRLiveOrder r <- reg st "r1" write st r {nrPhase = PRegistered, nrRegisteredAt = Just (at 1)} `shouldReturn` Written 2 nrLive <$> reg st "r1" `shouldReturn` False placePaid st "r3" ownerA o {ofCommitment = digestFixture 2} (at 60) `shouldReturn` Right () testLatePayment :: IO () testLatePayment = withServiceStore $ \st -> do let o = order "alicebakery" 1 o' = o {ofCommitment = digestFixture 2, ofSealed = "new"} placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () closed <- reg st "r1" withTransaction st (\db -> closeOverdue db closed (at 61)) `shouldReturn` Just 2 r1 <- reg st "r1" write st r1 {nrPhase = PQueued} `shouldReturn` NotLive reg st "r1" `shouldReturn` r1 placePaid st "r2" ownerA o' (at 120) `shouldReturn` Right () write st r1 {nrPhase = PQueued} `shouldReturn` NotLive reg st "r1" `shouldReturn` r1 (nrPhase &&& nrLive) <$> reg st "r2" `shouldReturn` (PUnpaid, True) write st r1 {nrVersion = 1, nrPhase = PQueued} `shouldReturn` VersionChanged testWriteMeetsLiveOrder :: IO () testWriteMeetsLiveOrder = withServiceStore $ \st -> do let held = order "bobsbakery" 2 placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () placePaid st "r2" ownerA held (at 60) `shouldReturn` Right () placePaid st "r3" ownerB (order "carolsbakery" 3) (at 60) `shouldReturn` Right () r1 <- reg st "r1" write st r1 {nrPhase = PTaken} `shouldReturn` Written 2 taken <- reg st "r1" let retarget o = withTransaction st $ \db -> retargetRegistration db taken o 10000 (at 6) retarget held {ofCommitment = digestFixture 4} `shouldReturn` LiveOrderExists r3 <- reg st "r3" retarget (order "carolsbakery" 5) {ofCommitment = nrCommitment r3} `shouldReturn` LiveOrderExists reg st "r1" `shouldReturn` taken retarget (order "davesbakery" 6) `shouldReturn` Written 3 testRetarget :: IO () testRetarget = withServiceStore $ \st -> do let o = order "alicebakery" 1 o' = order "alicebakers" 2 placePaid st "r1" ownerA o (at 60) `shouldReturn` Right () r <- reg st "r1" let taken = r { nrPhase = PTaken, nrReveal = Just reveal, nrCommitTx = Just (TxHash $ digestFixture 8), nrCommitBlock = Just 100, nrRegisterTx = Just (TxHash $ digestFixture 9), nrRegisterBlock = Just 120, nrRevealAfter = Just 1, nrRevealBefore = Just 2, nrLinksRefusedAt = Just (at 4), nrFailedAt = Just (at 5), nrError = Just "failed" } write st taken `shouldReturn` Written 2 withTransaction st (\db -> retargetRegistration db taken {nrVersion = 2} o' 10000 (at 6)) `shouldReturn` Written 3 r' <- reg st "r1" r' `shouldBe` r { nrVersion = 3, nrLabelHash = ofLabelHash o', nrCommitment = ofCommitment o', nrSealed = ofSealed o', nrPrice = 10000, nrPhase = PQueued, nrMoves = 1, nrUpdatedAt = at 6 } withTransaction st (\db -> retargetRegistration db taken {nrVersion = 2} o 10000 (at 7)) `shouldReturn` VersionChanged testFind :: IO () testFind = withServiceStore $ \st -> do forM_ [1 .. 52 :: Int] $ \i -> withTransaction st $ \db -> do let rid = "r" <> T.pack (show i) o = order ("alicebakery" <> T.pack (show i)) (fromIntegral i) insertInvoiceRow db ("inv-" <> rid) (at 60) "open" createPaidRegistration db (newReg rid ownerA o) (paidOrder $ "inv-" <> rid) (at i) `shouldReturn` Right () placePaid st "other" ownerB (order "bobsbakery" 99) (at 60) `shouldReturn` Right () found <- withTransaction st $ \db -> findRegistrations db ownerA map nrId found `shouldBe` [RegistrationId $ "r" <> T.pack (show i) | i <- [52, 51 .. 3 :: Int]] withTransaction st (\db -> findRegistrations db (address "0x0000000000000000000000000000000000000001")) `shouldReturn` [] testSameSecondOrder :: IO () testSameSecondOrder = withServiceStore $ \st -> do let o = order "alicebakery" 1 ids = ["r2", "r3", "r1"] forM_ (zip ids [10 ..]) $ \(rid, n) -> do placePaid st rid ownerA (order ("alicebakery" <> rid) n) (at 60) `shouldReturn` Right () r <- reg st rid write st r {nrPhase = PQueued} `shouldReturn` Written 2 map nrId <$> withTransaction st (`findRegistrations` ownerA) `shouldReturn` map RegistrationId ["r3", "r2", "r1"] map nrId <$> withTransaction st activeRegistrations `shouldReturn` map RegistrationId ["r1", "r2", "r3"] forM_ ["r5", "r4"] $ \rid -> do placePaid st rid ownerB o (at 60) `shouldReturn` Right () r <- reg st rid withTransaction st (\db -> closeOverdue db r (at 61)) `shouldReturn` Just 2 found <- withTransaction st $ \db -> getRegistrationByOrderHash db (orderHash ownerB o (OTInvoice btc price)) nrId <$> found `shouldBe` Just (RegistrationId "r5") testLabelRegistering :: IO () testLabelRegistering = withServiceStore $ \st -> do let o = order "alicebakery" 1 registering = withTransaction st $ \db -> isLabelRegistering db (ofLabelHash o) code <- issue st 1 usable placeCode st "r1" ownerA o code `shouldReturn` Right () registering `shouldReturn` False r <- reg st "r1" void $ write st r {nrPhase = PRegistering} registering `shouldReturn` True r' <- reg st "r1" void $ write st r' {nrPhase = PRegistered} registering `shouldReturn` False testActive :: IO () testActive = withServiceStore $ \st -> do let phases = [PUnpaid ..] forM_ (zip [1 ..] phases) $ \(i, phase) -> do let rid = "r" <> T.pack (show i) placePaid st rid ownerA (order ("alicebakery" <> T.pack (show i)) i) (at 60) `shouldReturn` Right () r <- reg st rid void $ write st r {nrPhase = phase} active <- withTransaction st activeRegistrations sort (map (fromEnum . nrPhase) active) `shouldBe` map fromEnum [PQueued, PCommitting, PCommitted, PRegistering] testUnpaidCleanup :: IO () testUnpaidCleanup = withServiceStore $ \st -> do placePaid st "old" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () placePaid st "funded" ownerA (order "bobsbakery" 2) (at 60) `shouldReturn` Right () placePaid st "queued" ownerA (order "carolsbakery" 3) (at 60) `shouldReturn` Right () placePaid st "open" ownerA (order "davesbakery" 4) (at 120) `shouldReturn` Right () withTransaction st $ \db -> insertPayment db "inv-funded" r <- reg st "queued" void $ write st r {nrPhase = PQueued} candidates <- withTransaction st $ \db -> unpaidCleanupCandidates db (at 61) map (\(RegistrationId rid, InvoiceId invId) -> (rid, invId)) candidates `shouldBe` [("old", "inv-old")] withTransaction st (\db -> mapM (deleteUnpaidRegistration db . RegistrationId) ["old", "funded", "queued"]) `shouldReturn` [True, False, False] withTransaction st (\db -> getRegistration db (RegistrationId "old")) `shouldReturn` Nothing invoiceCount st "inv-old" `shouldReturn` 0 invoiceCount st "inv-funded" `shouldReturn` 1 nrPhase <$> reg st "funded" `shouldReturn` PUnpaid insertPayment :: DB.Connection -> Text -> IO () insertPayment db invId = DB.execute db "INSERT INTO sx_badge_service_payments (payment_id, invoice_id, provider, status, created_at, updated_at) VALUES (?,?,?,?,?,?)" (invId, invId, "crypto" :: Text, "pending" :: Text, t0, t0) invoiceCount :: DBStore -> Text -> IO Int invoiceCount st invId = withTransaction st $ \db -> DB.query db "SELECT COUNT(*) FROM sx_badge_service_invoices WHERE invoice_id = ?" (Only invId) >>= \case [Only n] -> pure n rows -> fail $ "count: " <> show (length rows) <> " rows" testFailedCodeOrders :: IO () testFailedCodeOrders = withServiceStore $ \st -> do codes@[oldCode, _, releasedCode, _] <- mapM (\n -> issue st n usable) [1, 2, 3, 4] forM_ (zip3 ["old", "recent", "released", "queued"] [1 ..] codes) $ \(rid, n, code) -> placeCode st rid ownerA (order ("alicebakery" <> rid) n) code `shouldReturn` Right () let failAt rid t = reg st rid >>= \r -> void (write st r {nrPhase = PFailed, nrFailedAt = Just t}) failAt "old" (at 1) failAt "recent" (at 30) failAt "released" (at 1) withTransaction st (\db -> releasePrepaidCode db (pcId releasedCode) (RegistrationId "released")) `shouldReturn` True withTransaction st (\db -> failedCodeOrdersBefore db (at 10)) `shouldReturn` [(RegistrationId "old", pcId oldCode)] testIssueCode :: IO () testIssueCode = withServiceStore $ \st -> do code <- issue st 1 usable {npcMinLength = 6, npcYears = 5, npcPaymentStatus = CPSFree, npcExpiresAt = Just (at 100)} (pcMinLength code, pcYears code, pcPaymentStatus code, pcExpiresAt code, pcCreatedAt code) `shouldBe` (6, 5, CPSFree, Just (at 100), t0) withTransaction st (\db -> insertPrepaidCode db usable {npcCodeHash = digestFixture 1} t0) `shouldReturn` Left CodeHashTaken withTransaction st (\db -> getPrepaidCode db (digestFixture 1)) `shouldReturn` Just code testReserveReleaseSpend :: IO () testReserveReleaseSpend = withServiceStore $ \st -> do PrepaidCode {pcId} <- issue st 1 usable placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () placePaid st "r2" ownerB (order "bobsbakery" 2) (at 60) `shouldReturn` Right () let r1 = RegistrationId "r1" r2 = RegistrationId "r2" run :: (DB.Connection -> IO a) -> IO a run = withTransaction st run (\db -> releasePrepaidCode db pcId r1) `shouldReturn` False run (\db -> spendPrepaidCode db pcId r1 t0) `shouldReturn` False run (\db -> reservePrepaidCode db pcId r1 (at 1)) `shouldReturn` True run (\db -> reservePrepaidCode db pcId r2 (at 1)) `shouldReturn` False run (\db -> releasePrepaidCode db pcId r2) `shouldReturn` False run (\db -> spendPrepaidCode db pcId r2 (at 2)) `shouldReturn` False run (\db -> releasePrepaidCode db pcId r1) `shouldReturn` True run (\db -> reservePrepaidCode db pcId r2 (at 2)) `shouldReturn` True run (\db -> spendPrepaidCode db pcId r2 (at 3)) `shouldReturn` True run (\db -> releasePrepaidCode db pcId r2) `shouldReturn` False run (\db -> reservePrepaidCode db pcId r1 (at 4)) `shouldReturn` False run (\db -> spendPrepaidCode db pcId r2 (at 4)) `shouldReturn` False spent <- run $ \db -> getPrepaidCodeById db pcId (pcReservedBy =<< spent, pcSpentAt =<< spent) `shouldBe` (Just r2, Just (at 3)) testReserveRefused :: IO () testReserveRefused = withServiceStore $ \st -> do placePaid st "r1" ownerA (order "alicebakery" 1) (at 60) `shouldReturn` Right () unpaid <- issue st 1 usable {npcPaymentStatus = CPSUnpaid} expired <- issue st 2 usable {npcExpiresAt = Just (at 10)} revoked <- issue st 3 usable free <- issue st 4 usable {npcPaymentStatus = CPSFree, npcExpiresAt = Just (at 30)} withTransaction st (\db -> revokePrepaidCode db (pcCodeHash revoked) t0) `shouldReturn` Revoked (pcId revoked) let reserve code = withTransaction st $ \db -> reservePrepaidCode db (pcId code) (RegistrationId "r1") (at 20) mapM reserve [unpaid, expired, revoked] `shouldReturn` [False, False, False] reserve free `shouldReturn` True testOneCodePerRegistration :: IO () testOneCodePerRegistration = withServiceStore $ \st -> do first <- issue st 1 usable second <- issue st 2 usable placeCode st "r1" ownerA (order "alicebakery" 1) first `shouldReturn` Right () withTransaction st (\db -> reservePrepaidCode db (pcId second) (RegistrationId "r1") t0) `shouldThrow` anyException free <- withTransaction st $ \db -> getPrepaidCodeById db (pcId second) (pcReservedBy =<< free) `shouldBe` Nothing testRevoke :: IO () testRevoke = withServiceStore $ \st -> do reserved <- issue st 1 usable spent <- issue st 2 usable unused <- issue st 3 usable placeCode st "r1" ownerA (order "alicebakery" 1) reserved `shouldReturn` Right () placeCode st "r2" ownerA (order "bobsbakery" 2) spent `shouldReturn` Right () withTransaction st (\db -> spendPrepaidCode db (pcId spent) (RegistrationId "r2") t0) `shouldReturn` True let revoke code = withTransaction st $ \db -> revokePrepaidCode db (pcCodeHash code) (at 1) revoke reserved `shouldReturn` Revoked (pcId reserved) revoke unused `shouldReturn` Revoked (pcId unused) revoke unused `shouldReturn` AlreadyRevoked (pcId unused) revoke spent `shouldReturn` AlreadyRedeemed withTransaction st (\db -> revokePrepaidCode db (digestFixture 9) t0) `shouldReturn` NoSuchCode held <- withTransaction st $ \db -> getPrepaidCodeById db (pcId reserved) (pcReservedBy =<< held, pcRevokedAt =<< held) `shouldBe` (Just (RegistrationId "r1"), Just (at 1)) newEdit :: Address -> Word8 -> RecordChange -> NewRecordEdit newEdit owner n change = NewRecordEdit { nreLabelHash = labelHash "alicebakery", nreOwner = owner, nreNonce = 4, nreChange = change, nreDeadline = 1792832400, nreSignature = signature, nreBodyHash = digestFixture n } setChannel :: RecordChange setChannel = RCSetText "simplex.channel" "https://smp9.simplex.im/c#abc" testTransactions :: IO () testTransactions = withServiceStore $ \st -> do code <- issue st 1 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () Just editId <- withTransaction st $ \db -> insertRecordEdit db (newEdit ownerA 20 setChannel) t0 let r1 = RegistrationId "r1" tx :: Word8 -> NewTransaction tx n = NewTransaction {ntxRaw = digestFixture (n + 100), ntxHash = TxHash $ digestFixture n, ntxNonce = fromIntegral n} [commitId, _, registerId] <- withTransaction st $ \db -> sequence [insertTransaction db (TxCommit r1) (tx 2) (at 1), insertTransaction db (TxEdit editId) (tx 1) (at 2), insertTransaction db (TxRegister r1) (tx 3) (at 3)] unsettled <- withTransaction st pendingTransactions map (\NameTransaction {ntFor, ntNonce, ntStatus} -> (ntFor, ntNonce, ntStatus)) unsettled `shouldBe` [(TxEdit editId, 1, TSPending), (TxCommit r1, 2, TSPending), (TxRegister r1, 3, TSPending)] map ntRaw unsettled `shouldBe` map (\n -> digestFixture (n + 100)) [1, 2, 3] let outcome n from status block = withTransaction st $ \db -> recordTransactionOutcome db (TxHash $ digestFixture n) from status block outcome 2 TSPending TSMined (Just 77) `shouldReturn` True outcome 1 TSPending TSReverted (Just 78) `shouldReturn` True outcome 50 TSPending TSMined (Just 79) `shouldReturn` False outcome 3 TSMined TSPending Nothing `shouldReturn` False map ntFor <$> withTransaction st pendingTransactions `shouldReturn` [TxRegister r1] ofRegistration <- withTransaction st $ \db -> registrationTransactions db r1 map (\NameTransaction {ntId, ntFor, ntMinedBlock, ntStatus, ntSentAt} -> (ntId, ntFor, ntMinedBlock, ntStatus, ntSentAt)) ofRegistration `shouldBe` [(commitId, TxCommit r1, Just 77, TSMined, at 1), (registerId, TxRegister r1, Nothing, TSPending, at 3)] map ntStatus <$> withTransaction st (\db -> editTransactions db editId) `shouldReturn` [TSReverted] testTransactionFromStatus :: IO () testTransactionFromStatus = withServiceStore $ \st -> do code <- issue st 1 usable placeCode st "r1" ownerA (order "alicebakery" 1) code `shouldReturn` Right () let outcome from status block = withTransaction st $ \db -> recordTransactionOutcome db (TxHash $ digestFixture 2) from status block current = withTransaction st $ \db -> map (\NameTransaction {ntStatus, ntMinedBlock} -> (ntStatus, ntMinedBlock)) <$> registrationTransactions db (RegistrationId "r1") void $ withTransaction st $ \db -> insertTransaction db (TxCommit $ RegistrationId "r1") NewTransaction {ntxRaw = "raw", ntxHash = TxHash $ digestFixture 2, ntxNonce = 0} t0 outcome TSPending TSMined (Just 77) `shouldReturn` True outcome TSPending TSDropped Nothing `shouldReturn` False current `shouldReturn` [(TSMined, Just 77)] outcome TSMined TSPending Nothing `shouldReturn` True current `shouldReturn` [(TSPending, Nothing)] testEditNonceUnique :: IO () testEditNonceUnique = withServiceStore $ \st -> do let run action = withTransaction st action Just first <- run $ \db -> insertRecordEdit db (newEdit ownerA 1 setChannel) t0 run (\db -> insertRecordEdit db (newEdit ownerA 2 RCClear) t0) `shouldReturn` Nothing Just other <- run $ \db -> insertRecordEdit db (newEdit ownerB 3 setChannel) t0 other `shouldNotBe` first run (\db -> settleRecordEdit db first (ESReverted (Just "SignatureExpired"))) `shouldReturn` True Just second <- run $ \db -> insertRecordEdit db (newEdit ownerA 2 RCClear) (at 1) fmap reId <$> run (\db -> getRecordEditByNonce db ownerA 4) `shouldReturn` Just second fmap reState <$> run (\db -> getRecordEditByBody db (digestFixture 1)) `shouldReturn` Just (ESReverted (Just "SignatureExpired")) run (\db -> settleRecordEdit db second (ESMined Nothing)) `shouldReturn` True run (\db -> insertRecordEdit db (newEdit ownerA 5 setChannel) (at 2)) `shouldReturn` Nothing testSettleEdit :: IO () testSettleEdit = withServiceStore $ \st -> do Just editId <- withTransaction st $ \db -> insertRecordEdit db (newEdit ownerA 1 setChannel) t0 withTransaction st (\db -> settleRecordEdit db editId (ESMined (Just $ TxHash $ digestFixture 9))) `shouldReturn` True withTransaction st (\db -> settleRecordEdit db editId (ESReverted Nothing)) `shouldReturn` False edit <- withTransaction st $ \db -> getRecordEdit db editId edit `shouldBe` Just RecordEdit { reId = editId, reLabelHash = labelHash "alicebakery", reOwner = ownerA, reNonce = 4, reChange = setChannel, reDeadline = 1792832400, reSignature = signature, reBodyHash = digestFixture 1, reState = ESMined (Just $ TxHash $ digestFixture 9), reCreatedAt = t0 } testClearEdit :: IO () testClearEdit = withServiceStore $ \st -> do Just editId <- withTransaction st $ \db -> insertRecordEdit db (newEdit ownerA 1 RCClear) t0 fmap (\RecordEdit {reChange, reState} -> (reChange, reState)) <$> withTransaction st (\db -> getRecordEdit db editId) `shouldReturn` Just (RCClear, ESPending) testRelayedEditTimes :: IO () testRelayedEditTimes = withServiceStore $ \st -> do forM_ (zip [1 ..] [0, 10, 20]) $ \(n, m) -> withTransaction st $ \db -> void $ insertRecordEdit db (newEdit ownerA n setChannel) {nreNonce = fromIntegral n} (at m) withTransaction st (\db -> void $ insertRecordEdit db (newEdit ownerA 9 setChannel) {nreLabelHash = labelHash "bobsbakery", nreNonce = 9} (at 15)) withTransaction st (\db -> relayedEditTimes db (labelHash "alicebakery") (at 5)) `shouldReturn` [at 10, at 20]