store: move badge service app to store service

This commit is contained in:
shum committed 2026-10-21 12:27:02 +00:00
1 parent 4aaeadd591
commit a454ba63ea
152 files changed
+464 -462

No files matched your search

+4 -4
View File
@@ -6,11 +6,11 @@ on:
- master
- stable
paths:
- "apps/simplex-badge-service/web/**"
- "apps/simplex-store-service/web/**"
- ".github/workflows/badge-web.yml"
pull_request:
paths:
- "apps/simplex-badge-service/web/**"
- "apps/simplex-store-service/web/**"
- ".github/workflows/badge-web.yml"
jobs:
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/simplex-badge-service/web
working-directory: apps/simplex-store-service/web
steps:
- uses: actions/checkout@v4
@@ -26,7 +26,7 @@ jobs:
with:
node-version: 24
cache: npm
cache-dependency-path: apps/simplex-badge-service/web/package-lock.json
cache-dependency-path: apps/simplex-store-service/web/package-lock.json
- run: npm ci
+1 -1
View File
@@ -15,7 +15,7 @@ on:
- "apps/simplex-chat/**"
- "apps/simplex-bot/**"
- "apps/simplex-bot-advanced/**"
- "apps/simplex-badge-service/**"
- "apps/simplex-store-service/**"
- "apps/simplex-broadcast-bot/**"
- "apps/simplex-directory-service/**"
- "tests/**"
+6 -4
View File
@@ -88,10 +88,12 @@ website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release
apps/multiplatform/.kotlin/sessions
# anywhere in the tree: this file holds a real BTCPay api_key and webhook_secret
store_service.ini
# a local ini under its name before the store service rename holds the same secrets
badge_service.ini
# Badge service Docker deployment: the Dockerfile, compose, and example config are tracked; the
# Store service Docker deployment: the Dockerfile, compose, and example config are tracked; the
# live config (a secret) and its data volume are not.
scripts/badge-service/badge_service.ini
scripts/badge-service/data/
scripts/badge-service/web/
scripts/store-service/store_service.ini
scripts/store-service/data/
scripts/store-service/web/
@@ -2,8 +2,8 @@
module Main where
import BadgeService.Options (BadgeServiceOpts (..))
import BadgeService.Service
import StoreService.Options (StoreServiceOpts (..))
import StoreService.Service
import Control.Logger.Simple (LogConfig (..), LogLevel (..), setLogLevel, withGlobalLogging)
import GHC.IO.Encoding (setLocaleEncoding)
import Simplex.Chat.Terminal (terminalChatConfig)
@@ -17,7 +17,7 @@ main = do
mapM_ (`hSetEncoding` utf8) [stdout, stderr]
withGlobalLogging LogConfig {lc_file = Nothing, lc_stderr = True} $ do
setLogLevel LogWarn
opts@BadgeServiceOpts {runCLI} <- welcomeGetOpts
opts@StoreServiceOpts {runCLI} <- welcomeGetOpts
if runCLI
then badgeServiceCLI opts
else newServiceState >>= badgeService opts terminalChatConfig
then storeServiceCLI opts
else newServiceState >>= storeService opts terminalChatConfig
@@ -1,8 +1,8 @@
# SimpleX badge service
# SimpleX store service
`simplex-badge-service` runs the whole supporter-badge service as one process, two
`simplex-store-service` runs the whole store service as one process, two
lanes: a SimpleX chat bot that answers service RPC over a double-ratchet contact
address, and — only when `--service-config` names a `badge_service.ini` — the badge-codes
address, and — only when `--service-config` names a `store_service.ini` — the badge-codes
web checkout (BTCPay and Stripe payment, code issuance, the poller). **Without
`--service-config` the web listener does not start at all**; the process still runs the
chat side and nothing else.
@@ -11,7 +11,7 @@ The wire protocol for the RPC side is
[`docs/protocol/badges-rpc.md`](../../docs/protocol/badges-rpc.md). The web checkout is
specified in
[`plans/badges-codes/2026-08-27-badge-codes.md`](../../plans/badges-codes/2026-08-27-badge-codes.md),
whose §9 is the `badge_service.ini` reference, with the implementation plan in
whose §9 is the `store_service.ini` reference, with the implementation plan in
[`plans/badges-codes/2026-08-31-service-btcpay.md`](../../plans/badges-codes/2026-08-31-service-btcpay.md).
Earlier plans (`2026-07-30-supporter-badges-v3-ux.md`, `2026-07-31-badges-core-implementation.md`,
`2026-08-04-badges-mvp-scope.md`) predate the web checkout and describe the RPC-only
@@ -37,13 +37,13 @@ purchase that already exists.
Build prerequisites and the general contribution flow are in [`docs/CONTRIBUTING.md`](../../docs/CONTRIBUTING.md).
```
cabal build exe:simplex-badge-service
cabal build exe:simplex-store-service
```
## Run
```
simplex-badge-service --help
simplex-store-service --help
```
- default (no `--run-cli`): background service mode, no interactive terminal.
@@ -62,7 +62,7 @@ The service checks the secret against the configured public key at that index an
if they disagree: credentials signed with the wrong key cannot be verified by any client, and the
codes redeemed against them would be spent for nothing.
The key can come from `badge_service.ini` instead:
The key can come from `store_service.ini` instead:
```ini
[issuer]
@@ -76,40 +76,40 @@ Rotating is a change to both and a restart, once clients trust the new index.
The command line wins over the file when both `--issuer-key-idx` and `--issuer-secret` are given.
Note that a secret passed as a flag is visible to every user on the machine through `ps`, where one
in the ini is only as readable as the file: `badge_service.ini` is gitignored and already holds the
in the ini is only as readable as the file: `store_service.ini` is gitignored and already holds the
provider API keys and webhook secrets.
Other options:
- `--service-config INI_FILE`: path to `badge_service.ini`. Omit it to run the chat/RPC side
- `--service-config INI_FILE`: path to `store_service.ini`. Omit it to run the chat/RPC side
only; the process never starts a web listener without it, and never starts one under
`--run-cli`, which parses and validates the whole file (`[listener] static_dir` included) but
uses only its `[issuer]` section. An issuer key is still required either way.
- `--service-name NAME`: the bot's display name, without `*`s or spaces (default `SimpleX Badges`).
- `--client-service`: use the client service certificate.
- also accepts the standard SimpleX Chat core options — database path, SMP/XFTP servers,
`--socks-proxy`, `--log-level`/`-l`, and the rest — run `simplex-badge-service --help`
`--socks-proxy`, `--log-level`/`-l`, and the rest — run `simplex-store-service --help`
for the complete list.
### Running the web checkout
`badge_service.ini` holds the listener bind address and `static_dir`, an optional
`store_service.ini` holds the listener bind address and `static_dir`, an optional
`[btcpay]` section (omitting it disables Bitcoin and Monero), an optional `[stripe]`
section (omitting it disables card payments), an optional `[group]` section (omitting it
turns off the group's commands, though a group created earlier still has its code messages updated)
and the poll cadence.
`badge_service.ini.example` is the committed template; `badge_service.ini` itself is
`store_service.ini.example` is the committed template; `store_service.ini` itself is
gitignored, since a real one holds API keys and webhook secrets.
The full walkthrough is in
[`web/README.md`](web/README.md#running-the-real-service-against-this-build). Short version:
```
cd apps/simplex-badge-service/web && npm install && npm run build && cd ../../..
cp apps/simplex-badge-service/badge_service.ini.example apps/simplex-badge-service/badge_service.ini
cabal run simplex-badge-service -- \
cd apps/simplex-store-service/web && npm install && npm run build && cd ../../..
cp apps/simplex-store-service/store_service.ini.example apps/simplex-store-service/store_service.ini
cabal run simplex-store-service -- \
--issuer-key-idx IDX --issuer-secret SECRET \
--service-config apps/simplex-badge-service/badge_service.ini
--service-config apps/simplex-store-service/store_service.ini
```
`IDX` and `SECRET` must be one of the issuer keys clients already ship, not a fresh
@@ -249,7 +249,7 @@ why issuing codes lives in the service rather than in core.
### From the group
With `[group]` in `badge_service.ini`, the service manages one group and serves three commands in
With `[group]` in `store_service.ini`, the service manages one group and serves three commands in
it: `/issue <type> [months <M>] [uses <N>]` and `/bulk <type> [months <M>] count <B>` for moderators
and above, `/revoke <code>` for admins and owners. `months` is 1 to 255, `uses` 1 to 1000 and
`count` 1 to 100; a value outside these gets the usage reply. A member's role is checked as the
@@ -3,7 +3,7 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeService.Catalog
module StoreService.Catalog
( OfferInvalid (..),
CatalogRefusal (..),
offerTotal,
@@ -1,7 +1,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module BadgeService.Codes
module StoreService.Codes
( issueOneCode,
issueFailedText,
revokeBadgeCode,
@@ -9,8 +9,8 @@ module BadgeService.Codes
)
where
import BadgeService.Store (RevokeResult, insertBadgeCode, revokeCode)
import BadgeService.Store.Invoices (truncateToSecond)
import StoreService.Store (RevokeResult, insertBadgeCode, revokeCode)
import StoreService.Store.Invoices (truncateToSecond)
import Data.Int (Int64)
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
@@ -3,7 +3,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Config
module StoreService.Config
( ListenerConfig (..),
BTCPayConfig (..),
StripeConfig (..),
@@ -21,7 +21,7 @@ module BadgeService.Config
where
import qualified Control.Exception as E
import BadgeService.Log (logWarn)
import StoreService.Log (logWarn)
import Control.Monad (mfilter)
import Data.Attoparsec.Text (Parser, endOfInput, isEndOfLine, parseOnly, satisfy, skipMany, skipSpace, skipWhile)
import qualified Data.ByteString.Char8 as B
@@ -5,7 +5,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module BadgeService.Group
module StoreService.Group
( ensureManagedGroup,
runGroupLane,
inertGroupConfig,
@@ -24,12 +24,12 @@ module BadgeService.Group
)
where
import BadgeService.Codes (issueFailedText, issueOneCode, revokeBadgeCode, singleUse)
import BadgeService.Config (GroupConfig (..))
import BadgeService.Group.Command (CmdAction (..), GroupCmd (..), groupCmdAction, groupCommands)
import BadgeService.Log (logError, logInfo, logWarn)
import BadgeService.Store (CodeTracker (..), ManagedGroup (..), RevokeResult (..), clearCodeGroupItems, getCodeTracker, getEditableTrackers, getManagedGroup, insertManagedGroup, markOwnerBootstrapped, setCodeGroupItem)
import BadgeService.Store.Invoices (truncateToSecond)
import StoreService.Codes (issueFailedText, issueOneCode, revokeBadgeCode, singleUse)
import StoreService.Config (GroupConfig (..))
import StoreService.Group.Command (CmdAction (..), GroupCmd (..), groupCmdAction, groupCommands)
import StoreService.Log (logError, logInfo, logWarn)
import StoreService.Store (CodeTracker (..), ManagedGroup (..), RevokeResult (..), clearCodeGroupItems, getCodeTracker, getEditableTrackers, getManagedGroup, insertManagedGroup, markOwnerBootstrapped, setCodeGroupItem)
import StoreService.Store.Invoices (truncateToSecond)
import Control.Concurrent.STM (TQueue, atomically, flushTQueue, readTQueue, readTVarIO, writeTQueue)
import Control.Monad (forM_, forever, mfilter, replicateM, unless, void)
import Control.Monad.Except (runExceptT)
@@ -2,7 +2,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module BadgeService.Group.Command
module StoreService.Group.Command
( GroupCmd (..),
CmdAction (..),
groupCmdAction,
@@ -1,7 +1,7 @@
{-# LANGUAGE OverloadedStrings #-}
-- | The SMP agent logs unconditionally at info on the global logger, so the service keeps its own info lines on this separate channel when that logger is raised to Warn.
module BadgeService.Log
module StoreService.Log
( logInfo,
logWarn,
logError,
@@ -2,7 +2,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Names.Chain
module StoreService.Names.Chain
( ChainClient (..),
ChainError (..),
TxHash (..),
@@ -11,7 +11,7 @@ module BadgeService.Names.Chain
)
where
import BadgeService.Names.Registration (RegisterIntent, commitCalldata, registerWithCreditCalldata)
import StoreService.Names.Registration (RegisterIntent, commitCalldata, registerWithCreditCalldata)
import Control.Concurrent.STM (TVar)
import Control.Exception (try)
import Control.Monad.Except (ExceptT, liftEither, runExceptT, throwError)
@@ -2,7 +2,7 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeService.Names.Registration
module StoreService.Names.Registration
( Registration (..),
RegisterIntent (..),
sponsorDomain,
@@ -3,15 +3,15 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeService.Options
( BadgeServiceOpts (..),
getBadgeServiceOpts,
badgeServiceOpts,
module StoreService.Options
( StoreServiceOpts (..),
getStoreServiceOpts,
storeServiceOpts,
mkChatOpts,
)
where
import BadgeService.Config (BadgeIssuerKey (..))
import StoreService.Config (BadgeIssuerKey (..))
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import Options.Applicative
@@ -19,7 +19,7 @@ import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP)
import Simplex.Messaging.Encoding.String (strDecode)
data BadgeServiceOpts = BadgeServiceOpts
data StoreServiceOpts = StoreServiceOpts
{ coreOptions :: CoreChatOpts,
serviceName :: T.Text,
clientService :: Bool,
@@ -30,14 +30,14 @@ data BadgeServiceOpts = BadgeServiceOpts
testing :: Bool
}
badgeServiceOpts :: FilePath -> FilePath -> Parser BadgeServiceOpts
badgeServiceOpts appDir defaultDbName = do
storeServiceOpts :: FilePath -> FilePath -> Parser StoreServiceOpts
storeServiceOpts appDir defaultDbName = do
coreOptions <- coreChatOptsP appDir defaultDbName
serviceName <-
strOption
( long "service-name"
<> metavar "SERVICE_NAME"
<> help "The display name of the badge service bot, without *'s and spaces (SimpleX Badges)"
<> help "The display name of the store service bot, without *'s and spaces (SimpleX Badges)"
<> value "SimpleX Badges"
)
clientService <-
@@ -53,14 +53,14 @@ badgeServiceOpts appDir defaultDbName = do
runCLI <-
switch
( long "run-cli"
<> help "Run badge service as CLI"
<> help "Run store service as CLI"
)
serviceConfigFile <-
optional
( strOption
( long "service-config"
<> metavar "INI_FILE"
<> help "Path to badge_service.ini: parsed in full in every mode, but --run-cli starts no web listener"
<> help "Path to store_service.ini: parsed in full in every mode, but --run-cli starts no web listener"
)
)
issuerKeyIdx <-
@@ -80,7 +80,7 @@ badgeServiceOpts appDir defaultDbName = do
<> help "Issuer secret from `simplex-chat badge keygen` (base64url)"
)
pure
BadgeServiceOpts
StoreServiceOpts
{ coreOptions,
serviceName = T.pack serviceName,
clientService,
@@ -94,19 +94,19 @@ badgeServiceOpts appDir defaultDbName = do
testing = False
}
getBadgeServiceOpts :: FilePath -> FilePath -> IO BadgeServiceOpts
getBadgeServiceOpts appDir defaultDbName =
getStoreServiceOpts :: FilePath -> FilePath -> IO StoreServiceOpts
getStoreServiceOpts appDir defaultDbName =
execParser $
info
(helper <*> versionOption <*> badgeServiceOpts appDir defaultDbName)
(header versionStr <> fullDesc <> progDesc "Start SimpleX Badge Service with DB_FILE options")
(helper <*> versionOption <*> storeServiceOpts appDir defaultDbName)
(header versionStr <> fullDesc <> progDesc "Start SimpleX Store Service with DB_FILE options")
where
versionStr = versionString versionNumber
versionOption = infoOption versionAndUpdate (long "version" <> short 'v' <> help "Show version")
versionAndUpdate = versionStr <> "\n" <> updateStr
mkChatOpts :: BadgeServiceOpts -> ChatOpts
mkChatOpts BadgeServiceOpts {coreOptions, serviceName, clientService} =
mkChatOpts :: StoreServiceOpts -> ChatOpts
mkChatOpts StoreServiceOpts {coreOptions, serviceName, clientService} =
ChatOpts
{ coreOptions,
chatCmd = "",
@@ -2,11 +2,11 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeService.Orders (settleOrder, decide, codeLifetime) where
module StoreService.Orders (settleOrder, decide, codeLifetime) where
import BadgeService.Providers (Funded (..), PaymentSignal (..), Received (..))
import BadgeService.Store.Invoices (InvoicePayment (..), InvoiceRow (..), markCodePaid, paymentStatusText, settlementCodeHash, settlementInvoice, truncateToSecond, updateInvoiceStatus, upsertPayment)
import BadgeService.Waiters (Waiters, publish, publishPayment)
import StoreService.Providers (Funded (..), PaymentSignal (..), Received (..))
import StoreService.Store.Invoices (InvoicePayment (..), InvoiceRow (..), markCodePaid, paymentStatusText, settlementCodeHash, settlementInvoice, truncateToSecond, updateInvoiceStatus, upsertPayment)
import StoreService.Waiters (Waiters, publish, publishPayment)
import Control.Concurrent.STM (atomically)
import Control.Monad (when)
import Data.ByteString (ByteString)
@@ -4,7 +4,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Poller
module StoreService.Poller
( PollerEnv (..),
ReadHints,
newReadHints,
@@ -27,14 +27,14 @@ module BadgeService.Poller
)
where
import BadgeService.Config (PollConfig (..))
import BadgeService.Orders (decide, settleOrder)
import BadgeService.Providers (ListPass (..), PaymentSignal (..), Provider (..), ProviderError (..), Received (..), expiresItself, settleWindow)
import BadgeService.Store.Invoices (InvoiceRow (..), OverdueInvoice (..), expireOverdue, getInvoiceByProviderRef, overdueInvoices, providerText, unpaidRefs)
import BadgeService.Waiters (Waiters, publish, waitingCount, waitingCountSTM)
import StoreService.Config (PollConfig (..))
import StoreService.Orders (decide, settleOrder)
import StoreService.Providers (ListPass (..), PaymentSignal (..), Provider (..), ProviderError (..), Received (..), expiresItself, settleWindow)
import StoreService.Store.Invoices (InvoiceRow (..), OverdueInvoice (..), expireOverdue, getInvoiceByProviderRef, overdueInvoices, providerText, unpaidRefs)
import StoreService.Waiters (Waiters, publish, waitingCount, waitingCountSTM)
import Control.Concurrent.STM
import Control.Exception (SomeAsyncException, SomeException, fromException, throwIO, try)
import BadgeService.Log (logError, logInfo, logWarn)
import StoreService.Log (logError, logInfo, logWarn)
import Control.Monad (filterM, forever, unless, void, when)
import Data.List (find, partition, sortOn)
import Data.Map.Strict (Map)
@@ -1,7 +1,7 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module BadgeService.Providers
module StoreService.Providers
( ProviderError (..),
WebhookError (..),
Received (..),
@@ -3,7 +3,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Providers.BTCPay
module StoreService.Providers.BTCPay
( btcpayProvider,
btcMethodId,
xmrMethodId,
@@ -16,8 +16,8 @@ module BadgeService.Providers.BTCPay
)
where
import BadgeService.Config (BTCPayConfig (..), speedPolicyName)
import BadgeService.Providers
import StoreService.Config (BTCPayConfig (..), speedPolicyName)
import StoreService.Providers
( ListPass (..),
OrderDraft (..),
PaymentSignal (..),
@@ -30,7 +30,7 @@ import BadgeService.Providers
WebhookError (..),
)
import Control.Exception (try)
import BadgeService.Log (logError, logInfo, logWarn)
import StoreService.Log (logError, logInfo, logWarn)
import Control.Monad (unless)
import Crypto.Hash (Digest, SHA256)
import Crypto.MAC.HMAC (HMAC, hmac, hmacGetDigest)
@@ -3,15 +3,15 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Providers.Stripe
module StoreService.Providers.Stripe
( stripeProvider,
signalOf,
IntentRead (..),
)
where
import BadgeService.Config (StripeConfig (..))
import BadgeService.Providers
import StoreService.Config (StripeConfig (..))
import StoreService.Providers
( ListPass (..),
OrderDraft (..),
PaymentSignal (..),
@@ -4,37 +4,37 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module BadgeService.Service
module StoreService.Service
( ServiceState (..),
newServiceState,
welcomeGetOpts,
checkIssuerKey,
badgeService,
badgeServiceCLI,
badgeServiceResponse,
storeService,
storeServiceCLI,
storeServiceResponse,
badgeErrorRetryAfter,
IssueCodeOpts (..),
issueBadgeCode,
)
where
import BadgeService.Catalog (defaultCatalog)
import BadgeService.Codes (issueFailedText, issueOneCode, singleUse)
import BadgeService.Config (BadgeIssuerKey (..), ServiceConfig (..), readServiceConfig)
import BadgeService.Group (GroupEvent, ensureManagedGroup, groupEvent, hasTracker, refreshTracker, revokeWithTracker, runGroupLane)
import BadgeService.Group.Command (badgeTypeP, codeP, maxMonths, textTokenP)
import BadgeService.Options
import BadgeService.Poller (newPollerEnv, newReadHints, runPoller)
import BadgeService.Providers.BTCPay (btcpayProvider)
import BadgeService.Providers.Stripe (stripeProvider)
import BadgeService.Store
import BadgeService.Store.Invoices (seedCatalog, truncateToSecond)
import BadgeService.Store.Migrate (runBadgeServiceMigrations)
import BadgeService.Waiters (Waiters, newWaiters)
import BadgeService.Web.Server (exportWebapp, newWebEnv, runWebListener)
import StoreService.Catalog (defaultCatalog)
import StoreService.Codes (issueFailedText, issueOneCode, singleUse)
import StoreService.Config (BadgeIssuerKey (..), ServiceConfig (..), readServiceConfig)
import StoreService.Group (GroupEvent, ensureManagedGroup, groupEvent, hasTracker, refreshTracker, revokeWithTracker, runGroupLane)
import StoreService.Group.Command (badgeTypeP, codeP, maxMonths, textTokenP)
import StoreService.Options
import StoreService.Poller (newPollerEnv, newReadHints, runPoller)
import StoreService.Providers.BTCPay (btcpayProvider)
import StoreService.Providers.Stripe (stripeProvider)
import StoreService.Store
import StoreService.Store.Invoices (seedCatalog, truncateToSecond)
import StoreService.Store.Migrate (runStoreServiceMigrations)
import StoreService.Waiters (Waiters, newWaiters)
import StoreService.Web.Server (exportWebapp, newWebEnv, runWebListener)
import Control.Applicative (optional)
import Control.Concurrent.STM
import BadgeService.Log (logError, logInfo)
import StoreService.Log (logError, logInfo)
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as J
@@ -83,22 +83,22 @@ newServiceState = do
groupEventQ <- newTQueueIO
pure ServiceState {serviceCC, serviceRequestQ, groupEventQ}
welcomeGetOpts :: IO BadgeServiceOpts
welcomeGetOpts :: IO StoreServiceOpts
welcomeGetOpts = do
appDir <- getAppUserDataDirectory "simplex"
opts@BadgeServiceOpts {coreOptions, testing, serviceName} <- getBadgeServiceOpts appDir "simplex_badge_service"
opts@StoreServiceOpts {coreOptions, testing, serviceName} <- getStoreServiceOpts appDir "simplex_badge_service"
unless testing $ do
putStrLn $ "SimpleX Badge Service v" ++ versionNumber
putStrLn $ "SimpleX Store Service v" ++ versionNumber
printDbOpts coreOptions
putStrLn $ "Service name: " ++ T.unpack serviceName
pure opts
checkIssuerKey :: BadgeServiceOpts -> Maybe ServiceConfig -> ChatConfig -> IO (Either String BadgeIssuerKey)
checkIssuerKey BadgeServiceOpts {issuerKey} serviceCfg cfg = case issuerKey of
checkIssuerKey :: StoreServiceOpts -> Maybe ServiceConfig -> ChatConfig -> IO (Either String BadgeIssuerKey)
checkIssuerKey StoreServiceOpts {issuerKey} serviceCfg cfg = case issuerKey of
Left e -> pure (Left e)
Right (Just k) -> checkOne cfg k
Right Nothing -> case serviceCfg >>= issuer of
Nothing -> pure $ Left "an issuer key is required - pass --issuer-key-idx and --issuer-secret, or add an [issuer] section to badge_service.ini (see `simplex-chat badge keygen`)"
Nothing -> pure $ Left "an issuer key is required - pass --issuer-key-idx and --issuer-secret, or add an [issuer] section to store_service.ini (see `simplex-chat badge keygen`)"
Just k -> checkOne cfg k
checkOne :: ChatConfig -> BadgeIssuerKey -> IO (Either String BadgeIssuerKey)
@@ -110,7 +110,7 @@ checkOne ChatConfig {badgePublicKeys} k@BadgeIssuerKey {keyIdx, secretKey} =
Just _ -> Left $ "issuer secret does not match the configured key at index " <> show keyIdx <> ", its public key is " <> T.unpack (safeDecodeUtf8 $ strEncode pk)
Nothing -> Left $ "no configured badge key at index " <> show keyIdx <> ", clients could not verify what this service signs"
requireIssuerKey :: BadgeServiceOpts -> Maybe ServiceConfig -> ChatConfig -> IO BadgeIssuerKey
requireIssuerKey :: StoreServiceOpts -> Maybe ServiceConfig -> ChatConfig -> IO BadgeIssuerKey
requireIssuerKey opts serviceCfg cfg =
checkIssuerKey opts serviceCfg cfg >>= either (\e -> putStrLn ("Error: " <> e) >> exitFailure) pure
@@ -120,8 +120,8 @@ readConfigOrExit path =
Left e -> putStrLn (path <> ": " <> e) >> exitFailure
Right sc -> pure sc
badgeService :: BadgeServiceOpts -> ChatConfig -> ServiceState -> IO ()
badgeService opts@BadgeServiceOpts {serviceConfigFile} cfg env = do
storeService :: StoreServiceOpts -> ChatConfig -> ServiceState -> IO ()
storeService opts@StoreServiceOpts {serviceConfigFile} cfg env = do
serviceCfg <- traverse readConfigOrExit serviceConfigFile
key <- requireIssuerKey opts serviceCfg cfg
waiters <- newWaiters
@@ -171,8 +171,8 @@ seedServiceCatalog st = do
"badge catalog: " <> tshow (length prices) <> " prices and " <> tshow (length offers) <> " offers compiled in, "
<> tshow seededPrices <> " prices and " <> tshow seededOffers <> " offers inserted"
badgeServiceCLI :: BadgeServiceOpts -> IO ()
badgeServiceCLI opts@BadgeServiceOpts {serviceConfigFile} = do
storeServiceCLI :: StoreServiceOpts -> IO ()
storeServiceCLI opts@StoreServiceOpts {serviceConfigFile} = do
serviceCfg <- traverse readConfigOrExit serviceConfigFile
key <- requireIssuerKey opts serviceCfg terminalChatConfig
env <- newServiceState
@@ -248,12 +248,12 @@ processQueuedRequests key trackerQ_ env = do
(u, reqId, sigKey, reqData) <- atomically $ readTQueue $ serviceRequestQ env
handleServiceRequest key cc trackerQ_ u reqId sigKey reqData
badgePreStartHook :: BadgeServiceOpts -> ChatController -> IO ()
badgePreStartHook :: StoreServiceOpts -> ChatController -> IO ()
badgePreStartHook opts ChatController {config, chatStore} =
runBadgeServiceMigrations opts config chatStore
runStoreServiceMigrations opts config chatStore
badgePostStartHook :: BadgeServiceOpts -> ServiceState -> ChatController -> IO ()
badgePostStartHook BadgeServiceOpts {noAddress, testing} env cc = do
badgePostStartHook :: StoreServiceOpts -> ServiceState -> ChatController -> IO ()
badgePostStartHook StoreServiceOpts {noAddress, testing} env cc = do
-- Core starts this False and gates service request delivery on it, so the hook must set it.
atomically $ writeTVar (processServiceRequests cc) True
readTVarIO (currentUser cc) >>= \case
@@ -266,11 +266,11 @@ badgePostStartHook BadgeServiceOpts {noAddress, testing} env cc = do
handleServiceRequest :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> User -> AgentInvId -> Maybe C.PublicKeyEd25519 -> J.Object -> IO ()
handleServiceRequest key cc trackerQ_ User {userId} reqId sigKey reqData = do
let reqIdT = safeDecodeUtf8 (strEncode reqId)
logInfo $ "badge service request " <> reqIdT
resp <- badgeServiceResponse key cc trackerQ_ sigKey reqData
logInfo $ "store service request " <> reqIdT
resp <- storeServiceResponse key cc trackerQ_ sigKey reqData
sendChatCmd cc (APISendServiceResponse userId reqId (responseObject resp)) >>= \case
Right _ -> pure ()
Left e -> logError $ "badge service response failed for " <> reqIdT <> ": " <> tshow e
Left e -> logError $ "store service response failed for " <> reqIdT <> ": " <> tshow e
responseObject :: StoreServiceResponse -> J.Object
responseObject r = case J.toJSON r of
@@ -289,8 +289,8 @@ badgeErrorRetryAfter = \case
-- | The agent verified the signature, so sigKey is a key the sender holds; a differing purchaseKey would let a client claim a purchase it cannot sign for.
badgeServiceResponse :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Maybe C.PublicKeyEd25519 -> J.Object -> IO StoreServiceResponse
badgeServiceResponse key cc trackerQ_ sigKey reqData = case J.fromJSON (J.Object reqData) of
storeServiceResponse :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Maybe C.PublicKeyEd25519 -> J.Object -> IO StoreServiceResponse
storeServiceResponse key cc trackerQ_ sigKey reqData = case J.fromJSON (J.Object reqData) of
J.Error _ -> pure $ errorResponse STEBadRequest
J.Success StoreServiceRequest {version, purchaseKey, request}
| not (version `isCompatible` supportedStoreServiceVRange) -> pure $ errorResponse STEUnsupportedVersion
@@ -348,7 +348,7 @@ redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode
case issueEntry now issueUuid granted of
Nothing -> pure $ errorResponse STEInternal
Just issued -> credentialForEntry key masterKey issued >>= \case
Left e -> logError ("badge service signing failed: " <> T.pack e) $> errorResponse STEInternal
Left e -> logError ("store service signing failed: " <> T.pack e) $> errorResponse STEInternal
Right signed -> do
-- If the code was revoked or used up while signing, the claim fails. Read the code again to tell the client why.
r <- withDB "writeCodeRedemption" cc $ \db ->
@@ -356,7 +356,7 @@ redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode
Nothing ->
readCode now code db >>= \case
Left resp -> pure (resp, False)
Right _ -> logError "badge service: redeeming a code failed, but the code has uses left and is not revoked" $> (errorResponse STEInternal, False)
Right _ -> logError "store service: redeeming a code failed, but the code has uses left and is not revoked" $> (errorResponse STEInternal, False)
Just purchaseId -> liftIO $ do
appendLedgerPlan db purchaseId [granted] $ Just $ issuanceAfter granted signed
entries_ <- getLedgerEntries db purchaseId 0
@@ -377,7 +377,7 @@ redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode
<$> getLedgerEntries db badgePurchaseId 0
-- code_used would make the client drop its keys, so the holder could never get the badge back.
KeyRedeemedUnreadable ->
logError "badge service: a redeemed code's credential is missing or unreadable" $> Left (errorResponse STEInternal)
logError "store service: a redeemed code's credential is missing or unreadable" $> Left (errorResponse STEInternal)
KeyUnredeemed
-- Revoked is checked first, so it answers as if the code never existed.
| Just _ <- revokedAt -> pure $ Left $ errorResponse STECodeInvalid
@@ -406,7 +406,7 @@ issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do
Nothing -> writeIssued badgePurchaseId tip (maybeToList lapsed) now Nothing
Just e ->
credentialForEntry key masterKey e >>= \case
Left err -> logError ("badge service signing failed: " <> T.pack err) $> errorResponse STEInternal
Left err -> logError ("store service signing failed: " <> T.pack err) $> errorResponse STEInternal
Right signed ->
writeIssued badgePurchaseId tip (maybeToList lapsed) now $ Just $ issuanceAfter current signed
where
@@ -5,7 +5,7 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Store
module StoreService.Store
( IssuedCode (..),
KeyRedemption (..),
KeyPurchase (..),
@@ -36,7 +36,7 @@ module BadgeService.Store
)
where
import BadgeService.Store.Invoices (executeChanging)
import StoreService.Store.Invoices (executeChanging)
import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
@@ -5,7 +5,7 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeOperators #-}
module BadgeService.Store.Invoices
module StoreService.Store.Invoices
( InvoiceRow (..),
InvoicePayment (..),
paymentHolds,
@@ -63,12 +63,12 @@ import Simplex.Messaging.Encoding.String (textEncode)
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
#if defined(dbPostgres)
import BadgeService.Store.Postgres.Migrations (servicePrefix, withPrefix)
import StoreService.Store.Postgres.Migrations (servicePrefix, withPrefix)
import Database.PostgreSQL.Simple (Only (..), Query, ToRow, (:.) (..))
import qualified Database.PostgreSQL.Simple as PSQL
import Database.PostgreSQL.Simple.Errors (ConstraintViolation (..), constraintViolation)
#else
import BadgeService.Store.SQLite.Migrations (servicePrefix, withPrefix)
import StoreService.Store.SQLite.Migrations (servicePrefix, withPrefix)
import Database.SQLite.Simple (Only (..), Query, ToRow, (:.) (..))
import qualified Database.SQLite.Simple as SQL
#endif
@@ -3,12 +3,12 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeService.Store.Migrate
( runBadgeServiceMigrations,
module StoreService.Store.Migrate
( runStoreServiceMigrations,
)
where
import BadgeService.Options
import StoreService.Options
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options (CoreChatOpts (..))
import Simplex.Chat.Options.DB
@@ -18,22 +18,22 @@ import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConf
import System.Exit (exitFailure)
#if defined(dbPostgres)
import BadgeService.Store.Postgres.Migrations
import StoreService.Store.Postgres.Migrations
#else
import BadgeService.Store.SQLite.Migrations
import StoreService.Store.SQLite.Migrations
#endif
runBadgeServiceMigrations :: BadgeServiceOpts -> ChatConfig -> DBStore -> IO ()
runBadgeServiceMigrations opts ChatConfig {confirmMigrations} chatStore =
runStoreServiceMigrations :: StoreServiceOpts -> ChatConfig -> DBStore -> IO ()
runStoreServiceMigrations opts ChatConfig {confirmMigrations} chatStore =
migrateDBSchema
chatStore
(toDBOpts dbOptions chatSuffix False [])
(Just "sx_badge_service_migrations")
badgeServiceSchemaMigrations
storeServiceSchemaMigrations
MigrationConfig {confirm, backupPath = Nothing}
>>= either (exit . ("badge service migrations " <>) . show) pure
>>= either (exit . ("store service migrations " <>) . show) pure
where
BadgeServiceOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations}} = opts
StoreServiceOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations}} = opts
confirm = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
exit :: String -> IO a
@@ -2,7 +2,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module BadgeService.Store.Postgres.Migrations (badgeServiceSchemaMigrations, servicePrefix, withPrefix) where
module StoreService.Store.Postgres.Migrations (storeServiceSchemaMigrations, servicePrefix, withPrefix) where
import Data.List (sortOn)
import Data.Text (Text)
@@ -10,8 +10,8 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges (badgeSchema
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
import Text.RawString.QQ (r)
badgeServiceSchemaMigrations :: [Migration]
badgeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
storeServiceSchemaMigrations :: [Migration]
storeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
where
migration (name, up, down) = Migration {name, up, down}
@@ -2,7 +2,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module BadgeService.Store.SQLite.Migrations (badgeServiceSchemaMigrations, servicePrefix, withPrefix) where
module StoreService.Store.SQLite.Migrations (storeServiceSchemaMigrations, servicePrefix, withPrefix) where
import Data.List (sortOn)
import Data.Text (Text)
@@ -11,8 +11,8 @@ import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges (badgeSchema, badgeSchemaDown, withPrefix)
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
badgeServiceSchemaMigrations :: [Migration]
badgeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
storeServiceSchemaMigrations :: [Migration]
storeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
where
migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down}
@@ -1,6 +1,6 @@
{-# LANGUAGE NamedFieldPuns #-}
module BadgeService.Waiters (Waiters, Seen, newWaiters, publish, publishPayment, awaitStatus, waitingCount, waitingCountSTM) where
module StoreService.Waiters (Waiters, Seen, newWaiters, publish, publishPayment, awaitStatus, waitingCount, waitingCountSTM) where
import Control.Concurrent.STM
import Control.Exception (bracket)
@@ -3,7 +3,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module BadgeService.Web.Server
module StoreService.Web.Server
( WebEnv (..),
Limit (..),
newWebEnv,
@@ -19,15 +19,15 @@ module BadgeService.Web.Server
)
where
import BadgeService.Catalog (PricedOffer (..), priceOffer)
import BadgeService.Config (BTCPayConfig (..), ListenerConfig (..), ServiceConfig (..), SpeedPolicy (..), StripeConfig (..), defaultExpiryMinutes, defaultSessionMinutes)
import BadgeService.Poller (ReadHints, queueReadHint)
import BadgeService.Providers (OrderDraft (..), Provider (..), ProviderError (..), ProviderInvoice (..), WebhookError (..))
import BadgeService.Store.Invoices (CreateError (..), InvoicePayment (..), InvoiceRow (..), NewInvoice (..), cancelOpenInvoice, codeHashExists, createInvoiceRows, cryptoCurrencyText, getInvoice, getInvoiceByProviderRef, invoiceStatusText, newInvoiceId, paymentHolds, readCatalogRows, textToInvoiceStatus, truncateToSecond)
import BadgeService.Waiters (Seen, Waiters, awaitStatus, publish)
import StoreService.Catalog (PricedOffer (..), priceOffer)
import StoreService.Config (BTCPayConfig (..), ListenerConfig (..), ServiceConfig (..), SpeedPolicy (..), StripeConfig (..), defaultExpiryMinutes, defaultSessionMinutes)
import StoreService.Poller (ReadHints, queueReadHint)
import StoreService.Providers (OrderDraft (..), Provider (..), ProviderError (..), ProviderInvoice (..), WebhookError (..))
import StoreService.Store.Invoices (CreateError (..), InvoicePayment (..), InvoiceRow (..), NewInvoice (..), cancelOpenInvoice, codeHashExists, createInvoiceRows, cryptoCurrencyText, getInvoice, getInvoiceByProviderRef, invoiceStatusText, newInvoiceId, paymentHolds, readCatalogRows, textToInvoiceStatus, truncateToSecond)
import StoreService.Waiters (Seen, Waiters, awaitStatus, publish)
import Control.Concurrent.STM
import qualified Control.Exception as E
import BadgeService.Log (logError, logInfo, logWarn)
import StoreService.Log (logError, logInfo, logWarn)
import Control.Monad (forM_, when)
import Data.Aeson ((.=))
import qualified Data.Aeson as J
@@ -336,7 +336,7 @@ exportWebapp lc@ListenerConfig {lStaticDir, lWebappExportDir} stripeCfg =
emptyDir out
copyTree lStaticDir out
prepareShell lc stripeCfg >>= mapM_ (LB.writeFile (out </> "index.html"))
logInfo ("badge service: exported the webapp to " <> T.pack out)
logInfo ("store service: exported the webapp to " <> T.pack out)
-- | Contents only: the directory itself may be a bind or volume mount point that cannot be removed.
emptyDir :: FilePath -> IO ()
@@ -735,7 +735,7 @@ runWebListener env = do
warnIfHeaderTrustIsExposed :: ListenerConfig -> IO ()
warnIfHeaderTrustIsExposed ListenerConfig {lHost, lTrustForwardedFor} =
when (lTrustForwardedFor && not (isLoopbackHost lHost)) $
logWarn ("badge service: trust_forwarded_for is on and the listener binds " <> lHost <> "; every caller can then choose its own rate limit bucket unless a proxy sets the header")
logWarn ("store service: trust_forwarded_for is on and the listener binds " <> lHost <> "; every caller can then choose its own rate limit bucket unless a proxy sets the header")
isLoopbackHost :: Text -> Bool
isLoopbackHost h = h `elem` ["127.0.0.1", "::1", "localhost"]
@@ -3,8 +3,8 @@
; the long poll's `wait`, `seenPaid` and `seenFull` all travel in it
host = 127.0.0.1
port = 8080
; required: the directory `npm run build` writes in apps/simplex-badge-service/web
static_dir = ./apps/simplex-badge-service/web/dist
; required: the directory `npm run build` writes in apps/simplex-store-service/web
static_dir = ./apps/simplex-store-service/web/dist
; serve the webapp over HTTP. On (default) is the all-in-one deployment. Off is a split deployment
; where a reverse proxy serves the exported folder and this service is only the API and webhooks.
serve_webapp = on
@@ -68,23 +68,23 @@ is plain TypeScript, tested directly in Node.
## Running the real service against this build
**`mock/server.py` is not the service.** The service is the Haskell
executable `simplex-badge-service`, in `apps/simplex-badge-service/`, and
executable `simplex-store-service`, in `apps/simplex-store-service/`, and
`dist/` is the directory its listener serves. Nothing about the page changes
between the two — the service matches this build, never the reverse.
```
# 1. build the page. dist/ is the deploy tree: index.html, sw.js, assets/<hash>/
cd apps/simplex-badge-service/web
cd apps/simplex-store-service/web
npm install && npm run build
cd ../../..
# 2. configuration, from the committed example. Copy it IN
# PLACE, beside the example: that path is the one .gitignore covers, and this
# file is about to hold a real api_key and webhook_secret.
cd apps/simplex-badge-service
cp badge_service.ini.example badge_service.ini
cd apps/simplex-store-service
cp store_service.ini.example store_service.ini
cd ../..
# static_dir = ./apps/simplex-badge-service/web/dist <- the build above
# static_dir = ./apps/simplex-store-service/web/dist <- the build above
# [btcpay] = a real store's host, api_key, store_id and webhook_secret,
# or delete the whole section to disable BTC and XMR (the provider-unavailable screen)
# [issuer] = uncomment the section and put a real issuer secret in private_key,
@@ -98,28 +98,28 @@ cd ../..
# without a key it could sign a credential with. Put it in the ini's [issuer]
# section, or pass --issuer-key-idx and --issuer-secret, which override it.
# `simplex-chat badge keygen` prints a pair.
cabal run simplex-badge-service -- \
--service-config apps/simplex-badge-service/badge_service.ini
cabal run simplex-store-service -- \
--service-config apps/simplex-store-service/store_service.ini
```
Then open the `[listener]` host and port — `http://127.0.0.1:8080/` as the
example ships. `static_dir` is relative to the working directory, so run from
the repository root or make it absolute.
**The executable is the whole badge service, not just this listener.**
`badgeService` starts `simplexChatCore` unconditionally and the web listener
**The executable is the whole store service, not just this listener.**
`storeService` starts `simplexChatCore` unconditionally and the web listener
is one lane beside the chat one, so a local run opens or creates a chat
database (`printDbOpts` names the path at startup) and does agent network
work. `--no-address` skips creating the service's contact address on first
start, which a run of the web listener alone does not need.
*These three steps are read out of `Options.hs`, `Service.hs`, `Config.hs` and
`badge_service.ini.example` rather than from a run — driving them end to end
`store_service.ini.example` rather than from a run — driving them end to end
needs a chat database and a real BTCPay store.*
What the mock cannot stand in for:
| | `mock/server.py` | `simplex-badge-service` |
| | `mock/server.py` | `simplex-store-service` |
|---|---|---|
| Invoices | invented in memory | created at BTCPay over Greenfield |
| Payment detected by | `POST /control/settle/:id`, an endpoint that exists nowhere else | a poller reading the provider, which is the only thing that carries authority |
@@ -129,10 +129,10 @@ What the mock cannot stand in for:
The Haskell side's own end-to-end coverage of that lane — checkout, payment,
polling, partial payment, expiry, late settlement and replay, all against a
fake Greenfield — is `tests/Bots/BadgeService/WebTests.hs`:
fake Greenfield — is `tests/Bots/StoreService/WebTests.hs`:
```
cabal test --test-options='-m "/SimpleX badge service/"'
cabal test --test-options='-m "/SimpleX store service/"'
```
## Running the mock
@@ -311,7 +311,7 @@ def main():
# so only a publishable key (pk_) is allowed.
sys.exit("mock: STRIPE_PUBLISHABLE_KEY must be a publishable key (pk_...)")
server = Server(("127.0.0.1", port), Handler)
print(f"mock badge service on http://localhost:{port}", flush=True)
print(f"mock store service on http://localhost:{port}", flush=True)
print("stripe: " + (f"publishable key {STRIPE_PUBLISHABLE_KEY[:11]}… — the real card form"
if STRIPE_PUBLISHABLE_KEY
else "no STRIPE_PUBLISHABLE_KEY — the card path renders the development stand-in"),
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more