store: rename badge service to StoreService

This commit is contained in:
shum committed 2026-10-21 12:19:49 +00:00
1 parent 8990562850
commit 4aaeadd591
28 files changed
+386 -386

No files matched your search

@@ -19,7 +19,7 @@ import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Data.Word (Word32, Word64, Word8)
import Simplex.Chat.Badges (BadgeType (..))
import Simplex.Chat.Badges.Service (BadgeOffer (..), BadgePrice (..))
import Simplex.Chat.StoreService (BadgeOffer (..), BadgePrice (..))
import Simplex.Chat.Badges.Types (BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..))
import Simplex.Chat.PaymentService.Types (CurrencyAmount (..))
@@ -51,7 +51,7 @@ import Data.Word (Word32)
import Simplex.Chat.Badges
import Simplex.Chat.Badges.Code
import Simplex.Chat.Badges.Ledger
import Simplex.Chat.Badges.Service
import Simplex.Chat.StoreService
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..))
import Simplex.Chat.Bot (initializeBotAddress')
import Simplex.Chat.Bot.Store (withDB, withDB')
@@ -272,44 +272,44 @@ handleServiceRequest key cc trackerQ_ User {userId} reqId sigKey reqData = do
Right _ -> pure ()
Left e -> logError $ "badge service response failed for " <> reqIdT <> ": " <> tshow e
responseObject :: BadgeServiceResponse -> J.Object
responseObject :: StoreServiceResponse -> J.Object
responseObject r = case J.toJSON r of
J.Object o -> o
_ -> KM.fromList [("type", J.String "error"), ("code", J.toJSON BSEInternal)]
_ -> KM.fromList [("type", J.String "error"), ("code", J.toJSON STEInternal)]
errorResponse :: BadgeServiceErrorCode -> BadgeServiceResponse
errorResponse code = BSPError {code, message = Nothing, retryAfter = badgeErrorRetryAfter code}
errorResponse :: StoreServiceErrorCode -> StoreServiceResponse
errorResponse code = STPError {code, message = Nothing, retryAfter = badgeErrorRetryAfter code}
badgeErrorRetryAfter :: BadgeServiceErrorCode -> Maybe Word32
badgeErrorRetryAfter :: StoreServiceErrorCode -> Maybe Word32
badgeErrorRetryAfter = \case
BSEPaymentPending -> Just 300
BSEProviderUnavailable -> Just 300
BSERateLimited -> Just 60
STEPaymentPending -> Just 300
STEProviderUnavailable -> Just 300
STERateLimited -> Just 60
_ -> Nothing
-- | 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 BadgeServiceResponse
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
J.Error _ -> pure $ errorResponse BSEBadRequest
J.Success BadgeServiceRequest {version, purchaseKey, request}
| not (version `isCompatible` supportedBadgeServiceVRange) -> pure $ errorResponse BSEUnsupportedVersion
| purchaseKey /= sigKey -> pure $ errorResponse BSEBadRequest
J.Error _ -> pure $ errorResponse STEBadRequest
J.Success StoreServiceRequest {version, purchaseKey, request}
| not (version `isCompatible` supportedStoreServiceVRange) -> pure $ errorResponse STEUnsupportedVersion
| purchaseKey /= sigKey -> pure $ errorResponse STEBadRequest
| otherwise -> case request of
BSCRedeemBadgeCode {masterKey, code} -> case purchaseKey of
STCRedeemBadgeCode {masterKey, code} -> case purchaseKey of
Just k -> redeemCode key cc trackerQ_ k masterKey code
Nothing -> pure $ errorResponse BSEBadRequest
BSCIssueBadge {balance} -> case purchaseKey of
Nothing -> pure $ errorResponse STEBadRequest
STCIssueBadge {balance} -> case purchaseKey of
Just k -> issueBadgeCmd key cc k balance
Nothing -> pure $ errorResponse BSEBadRequest
Nothing -> pure $ errorResponse STEBadRequest
-- Every command but redeemBadgeCode needs a key the service already knows; that one creates the purchase, so its key is unknown on a first redemption.
_ -> case purchaseKey of
Nothing -> pure $ errorResponse BSEUnsupportedVersion
Nothing -> pure $ errorResponse STEUnsupportedVersion
Just k ->
withDB' "purchaseKeyExists" cc (`purchaseKeyExists` k) >>= \case
Right True -> pure $ errorResponse BSEUnsupportedVersion
Right False -> pure $ errorResponse BSEUnknownPurchaseKey
Left _ -> pure $ errorResponse BSEInternal
Right True -> pure $ errorResponse STEUnsupportedVersion
Right False -> pure $ errorResponse STEUnknownPurchaseKey
Left _ -> pure $ errorResponse STEInternal
badgeNow :: ChatController -> IO UTCTime
badgeNow ChatController {config = ChatConfig {badgeCurrentTime}} = badgeCurrentTime
@@ -326,18 +326,18 @@ credentialForEntry BadgeIssuerKey {keyIdx, secretKey} masterKey e@StatementEntry
issuanceAfter :: StatementEntry -> (StatementEntry, BadgeCredential) -> (StatementEntry, StatementEntry, BadgeCredential)
issuanceAfter previous (issued, credential) = (previous, issued, credential)
credentialResponse :: Maybe BadgeCredential -> Maybe T.Text -> [StatementEntry] -> BadgeServiceResponse
credentialResponse :: Maybe BadgeCredential -> Maybe T.Text -> [StatementEntry] -> StoreServiceResponse
credentialResponse credential previousEntryId entries =
BSPBadgeCredential {credential, receipt = Nothing, statement = BadgeStatement {entries, previousEntryId}}
STPBadgeCredential {credential, receipt = Nothing, statement = BadgeStatement {entries, previousEntryId}}
-- | Nothing is written until the credential is signed, so a signing failure leaves the code unspent.
redeemCode :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> BadgeMasterKey -> T.Text -> IO BadgeServiceResponse
redeemCode :: BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> BadgeMasterKey -> T.Text -> IO StoreServiceResponse
redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode codeText of
Nothing -> pure $ errorResponse BSECodeInvalid
Nothing -> pure $ errorResponse STECodeInvalid
Just code -> do
now <- badgeNow cc
withDB "getBadgeCode" cc (readCode now code) >>= \case
Left _ -> pure $ errorResponse BSEInternal
Left _ -> pure $ errorResponse STEInternal
Right (Left resp) -> pure resp
Right (Right IssuedCode {badgeCodeId, badgeType, months, redeemLimit}) -> do
(grantUuid, issueUuid) <- (,) <$> randomId cc <*> randomId cc
@@ -346,9 +346,9 @@ redeemCode key cc trackerQ_ purchaseKey masterKey codeText = case parseBadgeCode
let granted = grantEntry now grantUuid months SCCode $ emptyEntry now badgeType
-- A grant of at least one month starting now always has a month to issue.
case issueEntry now issueUuid granted of
Nothing -> pure $ errorResponse BSEInternal
Nothing -> pure $ errorResponse STEInternal
Just issued -> credentialForEntry key masterKey issued >>= \case
Left e -> logError ("badge service signing failed: " <> T.pack e) $> errorResponse BSEInternal
Left e -> logError ("badge 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,47 +356,47 @@ 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 BSEInternal, False)
Right _ -> logError "badge 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
pure (maybe (errorResponse BSEInternal) (credentialResponse (Just $ snd signed) Nothing) entries_, True)
let (resp, claimed) = fromRight (errorResponse BSEInternal, False) r
pure (maybe (errorResponse STEInternal) (credentialResponse (Just $ snd signed) Nothing) entries_, True)
let (resp, claimed) = fromRight (errorResponse STEInternal, False) r
when (claimed && hasTracker redeemLimit) $ refreshTracker cc trackerQ_ badgeCodeId code
pure resp
where
readCode now code db = liftIO $
getBadgeCode db (badgeCodeHash code) >>= \case
Nothing -> pure $ Left $ errorResponse BSECodeInvalid
Nothing -> pure $ Left $ errorResponse STECodeInvalid
Just c@IssuedCode {badgeCodeId, revokedAt, paymentStatus, expiresAt, redeemLimit, redeemCount} ->
getCodePurchaseForKey db badgeCodeId purchaseKey >>= \case
-- A key that already redeemed gets its credential back without a use, even if the code has since
-- expired or been revoked: a client whose reply was lost retries, and would otherwise lose the badge.
KeyRedeemed KeyPurchase {badgePurchaseId, credential} ->
maybe (Left $ errorResponse BSEInternal) (Left . credentialResponse (Just credential) Nothing)
maybe (Left $ errorResponse STEInternal) (Left . credentialResponse (Just credential) Nothing)
<$> 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 BSEInternal)
logError "badge 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 BSECodeInvalid
| Just _ <- revokedAt -> pure $ Left $ errorResponse STECodeInvalid
-- Redeeming an unpaid code would issue a free badge, so unpaid is refused.
| CPSUnpaid <- paymentStatus -> pure $ Left $ errorResponse BSEPaymentPending
| redeemCount >= redeemLimit -> pure $ Left $ errorResponse BSECodeUsed
| maybe False (now >=) expiresAt -> pure $ Left $ errorResponse BSECodeExpired
| CPSUnpaid <- paymentStatus -> pure $ Left $ errorResponse STEPaymentPending
| redeemCount >= redeemLimit -> pure $ Left $ errorResponse STECodeUsed
| maybe False (now >=) expiresAt -> pure $ Left $ errorResponse STECodeExpired
| otherwise -> pure $ Right c
-- | The purchase is reached through the verified signer key and no other way.
issueBadgeCmd :: BadgeIssuerKey -> ChatController -> C.PublicKeyEd25519 -> BadgeBalance -> IO BadgeServiceResponse
issueBadgeCmd :: BadgeIssuerKey -> ChatController -> C.PublicKeyEd25519 -> BadgeBalance -> IO StoreServiceResponse
issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do
now <- badgeNow cc
purchase_ <- withDB' "getBadgePurchase" cc $ \db -> do
p_ <- getPurchaseByKey db purchaseKey
forM p_ $ \p@ServicePurchase {badgePurchaseId} -> (p,) <$> getLedgerTip db badgePurchaseId
case purchase_ of
Left _ -> pure $ errorResponse BSEInternal
Right Nothing -> pure $ errorResponse BSEUnknownPurchaseKey
Left _ -> pure $ errorResponse STEInternal
Right Nothing -> pure $ errorResponse STEUnknownPurchaseKey
Right (Just (ServicePurchase {badgePurchaseId, masterKey, badgeType}, tip)) -> do
(lapseUuid, issueUuid) <- (,) <$> randomId cc <*> randomId cc
let tipEntry = fromMaybe (emptyEntry now badgeType) tip
@@ -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 BSEInternal
Left err -> logError ("badge service signing failed: " <> T.pack err) $> errorResponse STEInternal
Right signed ->
writeIssued badgePurchaseId tip (maybeToList lapsed) now $ Just $ issuanceAfter current signed
where
@@ -416,7 +416,7 @@ issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do
tip' <- getLedgerTip db purchaseId
when (fmap entryId tip' == fmap entryId tip) $ appendLedgerPlan db purchaseId rows issuance_
issueResponse db purchaseId t
pure $ fromRight (errorResponse BSEInternal) r
pure $ fromRight (errorResponse STEInternal) r
-- Only the asserted entry's identity is read, never the months it claims.
issueResponse db purchaseId t = do
let StatementEntry {entryId = assertedUuid} = lastEntry
@@ -425,4 +425,4 @@ issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do
-- single opening credit, rather than resending the whole history
entries_ <- getLedgerEntries db purchaseId (fromMaybe 0 assertedId)
credential_ <- getCurrentIssuance db purchaseId t
pure $ maybe (errorResponse BSEInternal) (credentialResponse credential_ (assertedUuid <$ assertedId)) entries_
pure $ maybe (errorResponse STEInternal) (credentialResponse credential_ (assertedUuid <$ assertedId)) entries_
@@ -46,7 +46,7 @@ import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Simplex.Chat.Badges (BadgeCredential, BadgeMasterKey (..), BadgeType)
import Simplex.Chat.Badges.Ledger
import Simplex.Chat.Badges.Service (StatementEntry (..))
import Simplex.Chat.StoreService (StatementEntry (..))
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus, BadgePurchaseStatus (..))
import Simplex.Chat.Store.Shared (insertedRowId)
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
@@ -53,7 +53,7 @@ import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
import Data.Word (Word8, Word32)
import Simplex.Chat.Badges (BadgeType (..))
import Simplex.Chat.Badges.Service (BadgePrice (..), BadgeOffer (..))
import Simplex.Chat.StoreService (BadgePrice (..), BadgeOffer (..))
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..), BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..))
import Simplex.Chat.Store.Shared (insertedRowId)
import Simplex.Chat.PaymentService.Types (CardProvider (..), CryptoCurrency (..), CurrencyAmount (..), InvoiceId (..), InvoiceStatus (..), PaymentProvider (..), PaymentStatus (..), ServicePaymentDestination (..))
+26 -26
View File
@@ -15,7 +15,6 @@ This file is generated automatically.
- [BadgeInfo](#badgeinfo)
- [BadgeProof](#badgeproof)
- [BadgeRedeemError](#badgeredeemerror)
- [BadgeServiceErrorCode](#badgeserviceerrorcode)
- [BadgeStatus](#badgestatus)
- [BadgeType](#badgetype)
- [BlockingInfo](#blockinginfo)
@@ -202,6 +201,7 @@ This file is generated automatically.
- [SndGroupEvent](#sndgroupevent)
- [SrvError](#srverror)
- [StoreError](#storeerror)
- [StoreServiceErrorCode](#storeserviceerrorcode)
- [SubscriptionStatus](#subscriptionstatus)
- [SupportGroupPreference](#supportgrouppreference)
- [SwitchPhase](#switchphase)
@@ -453,7 +453,7 @@ BadgeActive:
ServiceError:
- type: "serviceError"
- serviceError: [BadgeServiceErrorCode](#badgeserviceerrorcode)
- serviceError: [StoreServiceErrorCode](#storeserviceerrorcode)
InvalidResponse:
- type: "invalidResponse"
@@ -466,30 +466,6 @@ CredentialNotVerified:
- type: "credentialNotVerified"
---
## BadgeServiceErrorCode
**Enum type**:
- "bad_request"
- "unsupported_version"
- "unknown_purchase_key"
- "unknown_offer_id"
- "offer_disabled"
- "offer_mismatch"
- "product_unavailable"
- "payment_not_entitled"
- "payment_pending"
- "provider_unavailable"
- "rate_limited"
- "code_invalid"
- "code_used"
- "code_expired"
- "receipt_invalid"
- "receipt_used"
- "internal"
---
## BadgeStatus
@@ -4435,6 +4411,30 @@ WorkItemError:
- errContext: string
---
## StoreServiceErrorCode
**Enum type**:
- "bad_request"
- "unsupported_version"
- "unknown_purchase_key"
- "unknown_offer_id"
- "offer_disabled"
- "offer_mismatch"
- "product_unavailable"
- "payment_not_entitled"
- "payment_pending"
- "provider_unavailable"
- "rate_limited"
- "code_invalid"
- "code_used"
- "code_expired"
- "receipt_invalid"
- "receipt_used"
- "internal"
---
## SubscriptionStatus
+3 -3
View File
@@ -35,7 +35,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Operators
import Simplex.Messaging.Agent.Store.Entity (DBStored (..))
import Simplex.Chat.Badges
import Simplex.Chat.Badges.Service
import Simplex.Chat.StoreService
import Simplex.Chat.Names
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
@@ -221,7 +221,6 @@ chatTypesDocsData =
(sti @AutoAccept, STRecord, "", [], "", ""),
(sti @BadgeProof, STRecord, "", [], "", ""),
(sti @BadgeRedeemError, STUnion, "BRE", [], "", ""),
(sti @BadgeServiceErrorCode, STEnum' (consSep "BSE" '_'), "", ["BSEUnknown"], "", ""),
(sti @BlockingInfo, STRecord, "", [], "", ""),
(sti @BlockingReason, STEnum, "BR", [], "", ""),
(sti @BrokerErrorType, STUnion, "", [], "", ""),
@@ -392,6 +391,7 @@ chatTypesDocsData =
(sti @SndGroupEvent, STUnion, "SGE", [], "", ""),
(sti @SrvError, STUnion, "SrvErr", [], "", ""),
(sti @StoreError, STUnion, "SE", [], "", ""),
(sti @StoreServiceErrorCode, STEnum' (consSep "STE" '_'), "", ["STEUnknown"], "", ""),
(sti @SubscriptionStatus, STUnion, "SS", [], "", ""),
(sti @SupportGroupPreference, STRecord, "", [], "", ""),
(sti @SwitchPhase, STEnum, "SP", [], "", ""),
@@ -458,7 +458,6 @@ deriving instance Generic AgentServiceError
deriving instance Generic AutoAccept
deriving instance Generic BadgeProof
deriving instance Generic BadgeRedeemError
deriving instance Generic BadgeServiceErrorCode
deriving instance Generic BlockingInfo
deriving instance Generic BlockingReason
deriving instance Generic BrokerErrorType
@@ -634,6 +633,7 @@ deriving instance Generic SndFileTransfer
deriving instance Generic SndGroupEvent
deriving instance Generic SrvError
deriving instance Generic StoreError
deriving instance Generic StoreServiceErrorCode
deriving instance Generic SubscriptionStatus
deriving instance Generic SupportGroupPreference
deriving instance Generic SwitchPhase
+1 -1
View File
@@ -1,6 +1,6 @@
# Badge service RPC protocol
Schema: `badges-rpc.schema.json`, definitions `request` and `response`. Types: `Simplex.Chat.Badges.Service`. Model: `plans/2026-07-30-supporter-badges-v3-ux.md` §3 — cited below as "model".
Schema: `badges-rpc.schema.json`, definitions `request` and `response`. Types: `Simplex.Chat.StoreService`. Model: `plans/2026-07-30-supporter-badges-v3-ux.md` §3 — cited below as "model".
## Transport
+1 -1
View File
@@ -379,7 +379,7 @@
"properties": {
"code": {
"type": "string",
"metadata": {"comment": "BadgeServiceErrorCode"}
"metadata": {"comment": "StoreServiceErrorCode"}
}
},
"optionalProperties": {
@@ -284,7 +284,7 @@ export namespace BadgeRedeemError {
export interface ServiceError extends Interface {
type: "serviceError"
serviceError: BadgeServiceErrorCode
serviceError: StoreServiceErrorCode
}
export interface InvalidResponse extends Interface {
@@ -301,26 +301,6 @@ export namespace BadgeRedeemError {
}
}
export enum BadgeServiceErrorCode {
Bad_request = "bad_request",
Unsupported_version = "unsupported_version",
Unknown_purchase_key = "unknown_purchase_key",
Unknown_offer_id = "unknown_offer_id",
Offer_disabled = "offer_disabled",
Offer_mismatch = "offer_mismatch",
Product_unavailable = "product_unavailable",
Payment_not_entitled = "payment_not_entitled",
Payment_pending = "payment_pending",
Provider_unavailable = "provider_unavailable",
Rate_limited = "rate_limited",
Code_invalid = "code_invalid",
Code_used = "code_used",
Code_expired = "code_expired",
Receipt_invalid = "receipt_invalid",
Receipt_used = "receipt_used",
Internal = "internal",
}
export enum BadgeStatus {
Active = "active",
Expired = "expired",
@@ -5166,6 +5146,26 @@ export namespace StoreError {
}
}
export enum StoreServiceErrorCode {
Bad_request = "bad_request",
Unsupported_version = "unsupported_version",
Unknown_purchase_key = "unknown_purchase_key",
Unknown_offer_id = "unknown_offer_id",
Offer_disabled = "offer_disabled",
Offer_mismatch = "offer_mismatch",
Product_unavailable = "product_unavailable",
Payment_not_entitled = "payment_not_entitled",
Payment_pending = "payment_pending",
Provider_unavailable = "provider_unavailable",
Rate_limited = "rate_limited",
Code_invalid = "code_invalid",
Code_used = "code_used",
Code_expired = "code_expired",
Receipt_invalid = "receipt_invalid",
Receipt_used = "receipt_used",
Internal = "internal",
}
export type SubscriptionStatus =
| SubscriptionStatus.Active
| SubscriptionStatus.Pending
@@ -196,7 +196,7 @@ class BadgeRedeemError_badgeActive(TypedDict):
class BadgeRedeemError_serviceError(TypedDict):
type: Literal["serviceError"]
serviceError: "BadgeServiceErrorCode"
serviceError: "StoreServiceErrorCode"
class BadgeRedeemError_invalidResponse(TypedDict):
type: Literal["invalidResponse"]
@@ -220,8 +220,6 @@ BadgeRedeemError = (
BadgeRedeemError_Tag = Literal["invalidCode", "serviceNotConfigured", "badgeActive", "serviceError", "invalidResponse", "unknownKeyIndex", "credentialNotVerified"]
BadgeServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "internal"]
BadgeStatus = Literal["active", "expired", "expiredOld", "failed", "unknownKey"]
BadgeType = Literal["supporter", "legend", "investor"]
@@ -3602,6 +3600,8 @@ StoreError = (
StoreError_Tag = Literal["duplicateName", "userNotFound", "relayUserNotFound", "userNotFoundByName", "userNotFoundByContactId", "userNotFoundByGroupId", "userNotFoundByFileId", "userNotFoundByContactRequestId", "contactNotFound", "contactNotFoundByName", "contactNotFoundByMemberId", "contactNotReady", "duplicateContactLink", "userContactLinkNotFound", "contactRequestNotFound", "contactRequestNotFoundByName", "invalidContactRequestEntity", "invalidBusinessChatContactRequest", "groupNotFound", "groupNotFoundByName", "groupMemberNameNotFound", "groupMemberNotFound", "groupMemberNotFoundByIndex", "memberRelationsVectorNotFound", "groupHostMemberNotFound", "groupMemberNotFoundByMemberId", "memberContactGroupMemberNotFound", "invalidMemberRelationUpdate", "groupWithoutUser", "duplicateGroupMember", "duplicateMemberId", "groupAlreadyJoined", "groupInvitationNotFound", "noteFolderAlreadyExists", "noteFolderNotFound", "userNoteFolderNotFound", "sndFileNotFound", "sndFileInvalid", "rcvFileNotFound", "rcvFileDescrNotFound", "fileNotFound", "rcvFileInvalid", "rcvFileInvalidDescrPart", "localFileNoTransfer", "sharedMsgIdNotFoundByFileId", "fileIdNotFoundBySharedMsgId", "sndFileNotFoundXFTP", "rcvFileNotFoundXFTP", "connectionNotFound", "connectionNotFoundById", "connectionNotFoundByMemberId", "pendingConnectionNotFound", "uniqueID", "largeMsg", "internalError", "dBException", "dBBusyError", "badChatItem", "chatItemNotFound", "chatItemNotFoundByText", "chatItemSharedMsgIdNotFound", "chatItemNotFoundByFileId", "chatItemNotFoundByContactId", "chatItemNotFoundByGroupId", "profileNotFound", "duplicateGroupLink", "groupLinkNotFound", "hostMemberIdNotFound", "contactNotFoundByFileId", "noGroupSndStatus", "duplicateGroupMessage", "remoteHostNotFound", "remoteHostUnknown", "remoteHostDuplicateCA", "remoteCtrlNotFound", "remoteCtrlDuplicateCA", "prohibitedDeleteUser", "operatorNotFound", "usageConditionsNotFound", "userChatRelayNotFound", "groupRelayNotFound", "groupRelayNotFoundByMemberId", "invalidQuote", "invalidMention", "invalidDeliveryTask", "deliveryTaskNotFound", "invalidDeliveryJob", "deliveryJobNotFound", "walletNotFound", "duplicateWallet", "workItemError"]
StoreServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "internal"]
class SubscriptionStatus_active(TypedDict):
type: Literal["active"]
+1 -1
View File
@@ -42,7 +42,6 @@ library
Simplex.Chat.Badges.CLI
Simplex.Chat.Badges.Code
Simplex.Chat.Badges.Ledger
Simplex.Chat.Badges.Service
Simplex.Chat.Badges.Types
Simplex.Chat.Names
Simplex.Chat.Call
@@ -95,6 +94,7 @@ library
Simplex.Chat.Store.Remote
Simplex.Chat.Store.Shared
Simplex.Chat.Store.Wallets
Simplex.Chat.StoreService
Simplex.Chat.Styled
Simplex.Chat.Types
Simplex.Chat.Types.MemberRelations
+1 -1
View File
@@ -71,7 +71,7 @@ defaultChatConfig =
chatVRange = supportedChatVRange,
callVRange = supportedCallVRange,
badgePublicKeys = M.mapKeys fromIntegral entitlementIssuerKeys,
badgeServiceAddress = Just $ either error id $ strDecode "https://smp5.simplex.im/a#ooSNWlEZTO2RPE0Ff5ZoybAs5zEhWLMlQrXesnhaZHM",
storeServiceAddress = Just $ either error id $ strDecode "https://smp5.simplex.im/a#ooSNWlEZTO2RPE0Ff5ZoybAs5zEhWLMlQrXesnhaZHM",
badgeCurrentTime = getCurrentTime,
badgeRetryInterval = RetryInterval {initialInterval = 30_000000, increaseAfter = 0, maxInterval = 3600_000000},
confirmMigrations = MCConsole,
+1 -1
View File
@@ -188,7 +188,7 @@ mkBadgeStatus now verified BadgeInfo {badgeExpiry = e} = case verified of
-- A badge credential (own, secret) and a proof (a presentation) are independent records.
-- badgeKeyIdx is the issuer key index: it tells verifiers which configured key to use.
-- Only proofs ride the wire (in a profile); credentials come from the badge service. Neither is
-- Only proofs ride the wire (in a profile); credentials come from the store service. Neither is
-- ever serialized as a sum - each travels as its own record, so the JSON carries no credential/proof tag.
data BadgeCredential = BadgeCredential
+1 -1
View File
@@ -1,7 +1,7 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Badge redemption codes, shared by the client, the badge service and the checkout site.
-- | Badge redemption codes, shared by the client, the store service and the checkout site.
--
-- A code is "SB-" and 20 Crockford base32 characters in four groups of five:
-- 19 payload characters and a final check character.
+1 -1
View File
@@ -25,7 +25,7 @@ import Data.Time.Calendar (addDays, addGregorianMonthsClip, toGregorian)
import Data.Time.Calendar.WeekDate (toWeekDate)
import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime)
import Simplex.Chat.Badges (BadgeType)
import Simplex.Chat.Badges.Service (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..))
import Simplex.Chat.StoreService (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..))
-- The calendar difference overshoots by at most one month, so one comparison settles it.
monthsBetween :: UTCTime -> UTCTime -> Integer
+62 -62
View File
@@ -19,7 +19,7 @@ module Simplex.Chat.Badges.Types
LedgerCreditType (..),
LedgerDebitType (..),
BadgeAlertKind (..),
BadgeServiceErrorCode (..),
StoreServiceErrorCode (..),
BadgeIssueFailure (..),
BadgeIssueError (..),
BadgeFunding (..),
@@ -225,31 +225,31 @@ data BadgeAlertPrice = BadgeAlertPrice
}
deriving (Show)
data BadgeServiceErrorCode
= BSEBadRequest
| BSEUnsupportedVersion
| BSEUnknownPurchaseKey
| BSEUnknownOfferId
| BSEOfferDisabled
| BSEOfferMismatch
| BSEProductUnavailable
| BSEPaymentNotEntitled
| BSEPaymentPending
| BSEProviderUnavailable
| BSERateLimited
| BSECodeInvalid
| BSECodeUsed
| BSECodeExpired
| BSEReceiptInvalid
| BSEReceiptUsed
| BSEInternal
| BSEUnknown Text -- forwards-compatible: service is deployed ahead of clients
data StoreServiceErrorCode
= STEBadRequest
| STEUnsupportedVersion
| STEUnknownPurchaseKey
| STEUnknownOfferId
| STEOfferDisabled
| STEOfferMismatch
| STEProductUnavailable
| STEPaymentNotEntitled
| STEPaymentPending
| STEProviderUnavailable
| STERateLimited
| STECodeInvalid
| STECodeUsed
| STECodeExpired
| STEReceiptInvalid
| STEReceiptUsed
| STEInternal
| STEUnknown Text -- forwards-compatible: service is deployed ahead of clients
deriving (Eq, Show)
-- | Why a renewal request ended without a credential stored. retryable is the service's own view of
-- transience: it gave retryAfter, which holds for codes this version does not know.
data BadgeIssueFailure
= BIFServiceError {code :: BadgeServiceErrorCode, retryable :: Bool}
= BIFServiceError {code :: StoreServiceErrorCode, retryable :: Bool}
| BIFServiceTimeout
| BIFNetwork {agentError :: Text}
| BIFInvalidCredential
@@ -313,52 +313,52 @@ instance FromField BadgeCodePaymentStatus where fromField = fromTextField_ textD
instance ToField BadgeCodePaymentStatus where toField = toField . textEncode
instance TextEncoding BadgeServiceErrorCode where
instance TextEncoding StoreServiceErrorCode where
textEncode = \case
BSEBadRequest -> "bad_request"
BSEUnsupportedVersion -> "unsupported_version"
BSEUnknownPurchaseKey -> "unknown_purchase_key"
BSEUnknownOfferId -> "unknown_offer_id"
BSEOfferDisabled -> "offer_disabled"
BSEOfferMismatch -> "offer_mismatch"
BSEProductUnavailable -> "product_unavailable"
BSEPaymentNotEntitled -> "payment_not_entitled"
BSEPaymentPending -> "payment_pending"
BSEProviderUnavailable -> "provider_unavailable"
BSERateLimited -> "rate_limited"
BSECodeInvalid -> "code_invalid"
BSECodeUsed -> "code_used"
BSECodeExpired -> "code_expired"
BSEReceiptInvalid -> "receipt_invalid"
BSEReceiptUsed -> "receipt_used"
BSEInternal -> "internal"
BSEUnknown t -> t
STEBadRequest -> "bad_request"
STEUnsupportedVersion -> "unsupported_version"
STEUnknownPurchaseKey -> "unknown_purchase_key"
STEUnknownOfferId -> "unknown_offer_id"
STEOfferDisabled -> "offer_disabled"
STEOfferMismatch -> "offer_mismatch"
STEProductUnavailable -> "product_unavailable"
STEPaymentNotEntitled -> "payment_not_entitled"
STEPaymentPending -> "payment_pending"
STEProviderUnavailable -> "provider_unavailable"
STERateLimited -> "rate_limited"
STECodeInvalid -> "code_invalid"
STECodeUsed -> "code_used"
STECodeExpired -> "code_expired"
STEReceiptInvalid -> "receipt_invalid"
STEReceiptUsed -> "receipt_used"
STEInternal -> "internal"
STEUnknown t -> t
textDecode s = Just $ case s of
"bad_request" -> BSEBadRequest
"unsupported_version" -> BSEUnsupportedVersion
"unknown_purchase_key" -> BSEUnknownPurchaseKey
"unknown_offer_id" -> BSEUnknownOfferId
"offer_disabled" -> BSEOfferDisabled
"offer_mismatch" -> BSEOfferMismatch
"product_unavailable" -> BSEProductUnavailable
"payment_not_entitled" -> BSEPaymentNotEntitled
"payment_pending" -> BSEPaymentPending
"provider_unavailable" -> BSEProviderUnavailable
"rate_limited" -> BSERateLimited
"code_invalid" -> BSECodeInvalid
"code_used" -> BSECodeUsed
"code_expired" -> BSECodeExpired
"receipt_invalid" -> BSEReceiptInvalid
"receipt_used" -> BSEReceiptUsed
"internal" -> BSEInternal
t -> BSEUnknown t
"bad_request" -> STEBadRequest
"unsupported_version" -> STEUnsupportedVersion
"unknown_purchase_key" -> STEUnknownPurchaseKey
"unknown_offer_id" -> STEUnknownOfferId
"offer_disabled" -> STEOfferDisabled
"offer_mismatch" -> STEOfferMismatch
"product_unavailable" -> STEProductUnavailable
"payment_not_entitled" -> STEPaymentNotEntitled
"payment_pending" -> STEPaymentPending
"provider_unavailable" -> STEProviderUnavailable
"rate_limited" -> STERateLimited
"code_invalid" -> STECodeInvalid
"code_used" -> STECodeUsed
"code_expired" -> STECodeExpired
"receipt_invalid" -> STEReceiptInvalid
"receipt_used" -> STEReceiptUsed
"internal" -> STEInternal
t -> STEUnknown t
instance ToJSON BadgeServiceErrorCode where
instance ToJSON StoreServiceErrorCode where
toJSON = textToJSON
toEncoding = textToEncoding
instance FromJSON BadgeServiceErrorCode where
parseJSON = textParseJSON "BadgeServiceErrorCode"
instance FromJSON StoreServiceErrorCode where
parseJSON = textParseJSON "StoreServiceErrorCode"
instance StrEncoding BadgeIssueFailure where
strEncode = \case
@@ -388,7 +388,7 @@ instance StrEncoding BadgeIssueFailure where
"retry" -> pure True
"final" -> pure False
_ -> fail "bad BadgeIssueFailure retry flag"
codeP = A.takeByteString >>= maybe (fail "bad BadgeServiceErrorCode") pure . textDecode . safeDecodeUtf8
codeP = A.takeByteString >>= maybe (fail "bad StoreServiceErrorCode") pure . textDecode . safeDecodeUtf8
restP = safeDecodeUtf8 <$> (A.space *> A.takeByteString)
instance ToField BadgeIssueFailure where toField = toField . decodeLatin1 . strEncode
+5 -5
View File
@@ -85,7 +85,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Chat.Badges (BadgeCredential, FileSizeLimits, LocalBadge)
import Simplex.Chat.Badges.Service (BadgeServiceErrorCode, StatementEntry)
import Simplex.Chat.StoreService (StatementEntry, StoreServiceErrorCode)
import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind, BadgeState (..))
import Simplex.Messaging.Crypto.BBS (BBSPublicKey)
import Simplex.Messaging.Crypto.File (CryptoFile (..))
@@ -148,8 +148,8 @@ data ChatConfig = ChatConfig
callVRange :: VersionRangeCall,
-- issuer public keys by index: credentials and proofs name the key that signed them, for rotation
badgePublicKeys :: Map Int BBSPublicKey,
-- Nothing until the badge service is deployed
badgeServiceAddress :: Maybe (ConnectTarget 'CMContact),
-- Nothing until the store service is deployed
storeServiceAddress :: Maybe (ConnectTarget 'CMContact),
-- the only clock badge code reads, so tests can shift it; production arithmetic is unchanged
badgeCurrentTime :: IO UTCTime,
-- how long a badge worker waits before repeating a renewal that failed for a passing reason
@@ -667,7 +667,7 @@ data ChatCommand
| UpdateProfileImage (Maybe ImageData) -- UserId (not used in UI)
| UpdateProfileImageFromFile FilePath -- set profile image from a .png/.jpg/.jpeg file
| AddBadge BadgeCredential -- attach an issued badge credential (testing; credential from `simplex-chat badge sign`)
| APIRedeemBadgeCode {userId :: UserId, code :: Text} -- redeem a badge code with the configured badge service
| APIRedeemBadgeCode {userId :: UserId, code :: Text} -- redeem a badge code with the configured store service
| APIGetBadgeState {userId :: UserId} -- the user's badges, their balances and any current alert
| APIGetBadgeLedger {userId :: UserId, badgePurchaseId :: Int64} -- the purchase's ledger, oldest first
-- episode is last because it is free text: it is the value that makes one occurrence of an
@@ -1525,7 +1525,7 @@ data BadgeRedeemError
= BREInvalidCode -- format or check character
| BREServiceNotConfigured
| BREBadgeActive
| BREServiceError {serviceError :: BadgeServiceErrorCode}
| BREServiceError {serviceError :: StoreServiceErrorCode}
| BREInvalidResponse {message :: String}
| BREUnknownKeyIndex
| BRECredentialNotVerified
+24 -24
View File
@@ -63,7 +63,7 @@ import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), BadgeMasterKey
import qualified Simplex.Chat.Badges.Ledger as L
import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind (..), BadgeIssueError (..), BadgeIssueFailure (..), BadgeState (..))
import Simplex.Chat.Badges.Code (badgeCodeText, parseBadgeCode)
import Simplex.Chat.Badges.Service (BadgeBalance (..), BadgeServiceCommand (..), BadgeServiceErrorCode (..), BadgeServiceRequest (..), BadgeServiceResponse (..), BadgeStatement (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..), currentBadgeServiceVersion)
import Simplex.Chat.StoreService (BadgeBalance (..), BadgeStatement (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..), StoreServiceCommand (..), StoreServiceErrorCode (..), StoreServiceRequest (..), StoreServiceResponse (..), currentStoreServiceVersion)
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
import Simplex.Chat.Wallet (deriveAccount, masterMnemonic, newEntropy)
import Simplex.Chat.Call
@@ -5432,7 +5432,7 @@ presentUserBadgeToContacts user'@User {userId, profile = LocalProfile {localBadg
redeemBadgeCode :: NetworkRequestMode -> User -> Text -> CM ChatResponse
redeemBadgeCode nm user@User {userId} codeText = do
code <- maybe (throwRedeemError BREInvalidCode) pure $ parseBadgeCode codeText
sendTarget <- asks (badgeServiceAddress . config) >>= maybe (throwRedeemError BREServiceNotConfigured) pure
sendTarget <- asks (storeServiceAddress . config) >>= maybe (throwRedeemError BREServiceNotConfigured) pure
g <- asks random
now <- liftIO getCurrentTime
let codeSent = badgeCodeText code
@@ -5446,15 +5446,15 @@ redeemBadgeCode nm user@User {userId} codeText = do
unless replaying $ whenM (withStore' (`userHasBadge` user)) $ throwRedeemError BREBadgeActive
redemption@BadgeCodeRedemption {purchaseKey, purchasePrivKey, masterKey} <-
maybe (withStore' $ \db -> createBadgeCodeRedemption db g user codeSent now) pure redemption_
let req = BadgeServiceRequest {version = currentBadgeServiceVersion, purchaseKey = Just purchaseKey, request = BSCRedeemBadgeCode {masterKey, code = codeSent}}
let req = StoreServiceRequest {version = currentStoreServiceVersion, purchaseKey = Just purchaseKey, request = STCRedeemBadgeCode {masterKey, code = codeSent}}
respBytes <- sendServiceRequestBytes nm user sendTarget Nothing (Just purchasePrivKey) req
respData <- either (const $ throwRedeemError $ BREInvalidResponse "not JSON") pure $ J.eitherDecodeStrict' respBytes
case J.fromJSON (J.Object respData) of
J.Error _ -> throwRedeemError $ BREInvalidResponse "not a badge service response"
J.Success BSPError {code = errCode} -> do
J.Error _ -> throwRedeemError $ BREInvalidResponse "not a store service response"
J.Success STPError {code = errCode} -> do
when (terminalCodeError errCode) $ withStore' $ \db -> deleteBadgeCodeRedemption db (redemptionId redemption)
throwRedeemError $ BREServiceError errCode
J.Success BSPBadgeCredential {credential = Just cred, statement} -> storeRedeemedBadge user redemption cred statement
J.Success STPBadgeCredential {credential = Just cred, statement} -> storeRedeemedBadge user redemption cred statement
J.Success _ -> throwRedeemError $ BREInvalidResponse "unexpected response type"
-- outside the badge lock: the chat lock must not be taken under it
mapM_ presentUserBadgeToContacts present_
@@ -5462,23 +5462,23 @@ redeemBadgeCode nm user@User {userId} codeText = do
where
-- the code will never work, so the keys stashed for it are dead; a timeout keeps them
terminalCodeError = \case
BSECodeInvalid -> True
BSECodeUsed -> True
BSECodeExpired -> True
STECodeInvalid -> True
STECodeUsed -> True
STECodeExpired -> True
_ -> False
throwRedeemError :: BadgeRedeemError -> CM a
throwRedeemError = throwChatError . CEBadgeRedeemError
badgeServiceErrorText :: BadgeServiceErrorCode -> Text
badgeServiceErrorText = textEncode . boundedServiceErrorCode
storeServiceErrorText :: StoreServiceErrorCode -> Text
storeServiceErrorText = textEncode . boundedServiceErrorCode
-- | An unknown code is reported and recorded, since the service is deployed ahead of clients, but
-- its text is the service's - so it is bounded and stripped before it reaches a terminal that acts
-- on controls, or a sentence the app shows the user as its own.
boundedServiceErrorCode :: BadgeServiceErrorCode -> BadgeServiceErrorCode
boundedServiceErrorCode :: StoreServiceErrorCode -> StoreServiceErrorCode
boundedServiceErrorCode = \case
BSEUnknown t -> BSEUnknown $ case T.filter errorCodeChar (T.take 32 t) of
STEUnknown t -> STEUnknown $ case T.filter errorCodeChar (T.take 32 t) of
"" -> "unknown"
t' -> t'
code -> code
@@ -5492,7 +5492,7 @@ badgeNow = asks (badgeCurrentTime . config) >>= liftIO
-- | A signal carries nothing: each pass derives its work from stored state, so a signal lost or
-- duplicated changes no outcome.
startBadgeWork :: User -> CM' ()
startBadgeWork user = whenM (isJust <$> asks (badgeServiceAddress . config)) $ void $ getBadgeWorker user
startBadgeWork user = whenM (isJust <$> asks (storeServiceAddress . config)) $ void $ getBadgeWorker user
-- | Exactly one caller starts the thread and the rest wait for it: the lookup and the create cannot
-- be one transaction, because starting a thread is not STM.
@@ -5751,7 +5751,7 @@ badgeIssueFailure e = case e of
badgeFailureTransient :: BadgeIssueFailure -> Bool
badgeFailureTransient = \case
-- the service withholds retryAfter from internal to avoid being pressed while failing, not because it is final
BIFServiceError {code = BSEInternal} -> True
BIFServiceError {code = STEInternal} -> True
BIFServiceError {retryable} -> retryable
BIFServiceTimeout -> True
BIFNetwork {} -> True
@@ -5770,21 +5770,21 @@ badgeErrorRetry = badgeFailureTransient . badgeIssueFailure
-- nothing else records one.
requestBadgeIssue :: UserId -> UserBadgePurchase -> UTCTime -> CM (Either UTCTime StatementEntry)
requestBadgeIssue userId UserBadgePurchase {badgePurchaseId, badgeType, purchaseKey, purchasePrivKey, masterKey} now = do
sendTarget <- asks (badgeServiceAddress . config) >>= maybe (throwCmdError "badge service not configured") pure
sendTarget <- asks (storeServiceAddress . config) >>= maybe (throwCmdError "store service not configured") pure
withEntityLock "badgeIssue" (CLBadgeUser userId) $ do
user <- withStore $ \db -> getUser db userId
lastEntry <- withStore' (`getBadgeLedgerLastEntry` badgePurchaseId) >>= maybe (throwCmdError "badge ledger has no entry to assert") pure
let req =
BadgeServiceRequest
{ version = currentBadgeServiceVersion,
StoreServiceRequest
{ version = currentStoreServiceVersion,
purchaseKey = Just purchaseKey,
request = BSCIssueBadge {balance = BadgeBalance {lastEntry}}
request = STCIssueBadge {balance = BadgeBalance {lastEntry}}
}
respData <-
sendServiceRequestTo NRMBackground user sendTarget Nothing (Just purchasePrivKey) req
`catchAllErrors` \e -> recordFailure (badgeIssueFailure e) >> throwError e
case J.fromJSON (J.Object respData) of
J.Success BSPBadgeCredential {credential = sentCred_, statement} -> do
J.Success STPBadgeCredential {credential = sentCred_, statement} -> do
verifiedCred_ <- verifyIssuedCredential masterKey sentCred_
g <- asks random
-- read again: now was taken before a lock wait and an untimed request, and the check reads
@@ -5798,11 +5798,11 @@ requestBadgeIssue userId UserBadgePurchase {badgePurchaseId, badgeType, purchase
(Just _, Nothing, _) -> recordFailure BIFInvalidCredential
(_, _, False) -> recordUnexpected "issued badge credential has no ledger row to store it against"
-- no credential is the service saying the months ran out, which its statement then shows
(Nothing, _, _) | maybe False ((> 0) . balanceMonths) balance_ -> recordUnexpected "badge service issued no credential"
(Nothing, _, _) | maybe False ((> 0) . balanceMonths) balance_ -> recordUnexpected "store service issued no credential"
_ -> pure ()
maybe (throwCmdError "badge ledger has no balance") (pure . Right) balance_
J.Success BSPError {code, retryAfter} -> do
eToView $ ChatError $ CECommandError $ "badge service error: " <> T.unpack (badgeServiceErrorText code)
J.Success STPError {code, retryAfter} -> do
eToView $ ChatError $ CECommandError $ "store service error: " <> T.unpack (storeServiceErrorText code)
recordFailure BIFServiceError {code = boundedServiceErrorCode code, retryable = isJust retryAfter}
ri <- asks $ badgeRetryInterval . config
pure $ Left $ badgeRetryAfter ri retryAfter `addUTCTime` now
@@ -5810,7 +5810,7 @@ requestBadgeIssue userId UserBadgePurchase {badgePurchaseId, badgeType, purchase
recordFailure BIFUnexpected {message = unexpectedResponse}
throwCmdError $ T.unpack unexpectedResponse
where
unexpectedResponse = "unexpected badge service response"
unexpectedResponse = "unexpected store service response"
recordUnexpected message = do
eToView $ ChatError $ CEInternalError $ T.unpack message
recordFailure BIFUnexpected {message}
+1 -1
View File
@@ -40,7 +40,7 @@ import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Simplex.Chat.Badges
import Simplex.Chat.Badges.Ledger
import Simplex.Chat.Badges.Service (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..))
import Simplex.Chat.StoreService (StatementCreditType (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..))
import Simplex.Chat.Badges.Types (BadgeAlertKind, BadgeIssueError (..), BadgeIssueFailure, BadgePurchaseStatus (..))
import Simplex.Chat.Store.Shared (insertedRowId)
import Simplex.Chat.Types
@@ -7,7 +7,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
-- the same tables in the client and in the badge service; '@' is replaced with the table name prefix
-- the same tables in the client and in the store service; '@' is replaced with the table name prefix
badgeSchema :: Text -> Text
badgeSchema pfx = withPrefix pfx badgeSchemaTables
@@ -8,7 +8,7 @@ import qualified Data.Text as T
import Database.SQLite.Simple (Query (..))
import Database.SQLite.Simple.QQ (sql)
-- the same tables in the client and in the badge service; '@' is replaced with the table name prefix
-- the same tables in the client and in the store service; '@' is replaced with the table name prefix
badgeSchema :: Text -> Query
badgeSchema pfx = withPrefix pfx badgeSchemaTables
@@ -5,19 +5,19 @@
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Chat.Badges.Service
( BadgeServiceRequest (..),
BadgeServiceCommand (..),
BadgeServiceVersion,
VersionBadgeService,
VersionRangeBadgeService,
pattern VersionBadgeService,
initialBadgeServiceVersion,
currentBadgeServiceVersion,
supportedBadgeServiceVRange,
module Simplex.Chat.StoreService
( StoreServiceRequest (..),
StoreServiceCommand (..),
StoreServiceVersion,
VersionStoreService,
VersionRangeStoreService,
pattern VersionStoreService,
initialStoreServiceVersion,
currentStoreServiceVersion,
supportedStoreServiceVRange,
BadgeUpgrade (..),
BadgeServiceResponse (..),
BadgeServiceErrorCode (..),
StoreServiceResponse (..),
StoreServiceErrorCode (..),
BadgeCatalog (..),
BadgePrice (..),
BadgeOffer (..),
@@ -46,60 +46,60 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON)
import Simplex.Messaging.Version (VersionRange, VersionScope, mkVersionRange)
import Simplex.Messaging.Version.Internal (Version (..))
data BadgeServiceVersion
data StoreServiceVersion
instance VersionScope BadgeServiceVersion
instance VersionScope StoreServiceVersion
type VersionBadgeService = Version BadgeServiceVersion
type VersionStoreService = Version StoreServiceVersion
pattern VersionBadgeService :: Word16 -> VersionBadgeService
pattern VersionBadgeService v = Version v
pattern VersionStoreService :: Word16 -> VersionStoreService
pattern VersionStoreService v = Version v
type VersionRangeBadgeService = VersionRange BadgeServiceVersion
type VersionRangeStoreService = VersionRange StoreServiceVersion
initialBadgeServiceVersion :: VersionBadgeService
initialBadgeServiceVersion = VersionBadgeService 1
initialStoreServiceVersion :: VersionStoreService
initialStoreServiceVersion = VersionStoreService 1
currentBadgeServiceVersion :: VersionBadgeService
currentBadgeServiceVersion = VersionBadgeService 1
currentStoreServiceVersion :: VersionStoreService
currentStoreServiceVersion = VersionStoreService 1
-- the service is deployed ahead of app releases, so it answers within the client's version
supportedBadgeServiceVRange :: VersionRangeBadgeService
supportedBadgeServiceVRange = mkVersionRange initialBadgeServiceVersion currentBadgeServiceVersion
supportedStoreServiceVRange :: VersionRangeStoreService
supportedStoreServiceVRange = mkVersionRange initialStoreServiceVersion currentStoreServiceVersion
data BadgeServiceRequest = BadgeServiceRequest
{ version :: VersionBadgeService,
purchaseKey :: Maybe C.PublicKeyEd25519, -- optional for BSCGetBadgeCatalog, required for other commands
request :: BadgeServiceCommand
data StoreServiceRequest = StoreServiceRequest
{ version :: VersionStoreService,
purchaseKey :: Maybe C.PublicKeyEd25519, -- optional for STCGetBadgeCatalog, required for other commands
request :: StoreServiceCommand
}
data BadgeServiceCommand
= BSCGetBadgeCatalog
| BSCGetBadgeInvoice
data StoreServiceCommand
= STCGetBadgeCatalog
| STCGetBadgeInvoice
{ priceId :: BadgePriceId,
offerId :: Maybe BadgeOfferId, -- absent for 1 month at the badge price
badgeInfo :: BadgeInfo,
paymentVia :: ServicePaymentMethod,
upgrade :: Maybe BadgeUpgrade -- upgrade non-store badge
}
| BSCRedeemBadgeCode
| STCRedeemBadgeCode
{ masterKey :: BadgeMasterKey,
code :: Text -- no badgeRequest: a code carries no tier for the client to state
}
| BSCPurchaseBadge
| STCPurchaseBadge
{ badgeRequest :: BadgeRequest,
payment :: ServicePayment,
upgrade :: Maybe BadgeUpgrade -- upgrade store 1-time badge
}
| BSCUpgradeBadgeSubscription
| STCUpgradeBadgeSubscription
{ badgeRequest :: BadgeRequest,
payment :: ServicePayment, -- store payments only
balance :: BadgeBalance
}
| BSCIssueBadge
| STCIssueBadge
{ balance :: BadgeBalance -- no badgeRequest: the service holds the key, the tier and the expiry
}
| BSCPauseBadge
| STCPauseBadge
data BadgeUpgrade = BadgeUpgrade
{ fromPurchaseKey :: C.PublicKeyEd25519,
@@ -108,23 +108,23 @@ data BadgeUpgrade = BadgeUpgrade
balance :: BadgeBalance
}
data BadgeServiceResponse
= BSPBadgeCatalog
data StoreServiceResponse
= STPBadgeCatalog
{ catalog :: BadgeCatalog,
badgeStatement :: Maybe BadgeStatement -- for signed getBadgeCatalog
}
| BSPBadgeInvoice
| STPBadgeInvoice
{ invoice :: ServiceInvoice,
badgeType :: BadgeType,
months :: Word8
}
| BSPBadgeCredential
| STPBadgeCredential
{ credential :: Maybe BadgeCredential, -- Nothing when no balance to issueBadge or no current credential for pause
receipt :: Maybe Text, -- not provided for lifetime badges
statement :: BadgeStatement
}
| BSPError
{ code :: BadgeServiceErrorCode,
| STPError
{ code :: StoreServiceErrorCode,
message :: Maybe Text,
retryAfter :: Maybe Word32
}
@@ -246,9 +246,9 @@ $(JQ.deriveJSON defaultJSON ''BadgeBalance)
$(JQ.deriveJSON defaultJSON ''BadgeUpgrade)
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "BSC") ''BadgeServiceCommand)
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "STC") ''StoreServiceCommand)
$(JQ.deriveJSON defaultJSON ''BadgeServiceRequest)
$(JQ.deriveJSON defaultJSON ''StoreServiceRequest)
$(JQ.deriveJSON defaultJSON ''BadgePrice)
@@ -256,4 +256,4 @@ $(JQ.deriveJSON defaultJSON ''BadgeOffer)
$(JQ.deriveJSON defaultJSON ''BadgeCatalog)
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "BSP") ''BadgeServiceResponse)
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "STP") ''StoreServiceResponse)
+4 -4
View File
@@ -41,11 +41,11 @@ import Numeric (showFFloat)
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Help
import Simplex.Chat.Library.Commands (badgeServiceErrorText, maxImageSize)
import Simplex.Chat.Library.Commands (maxImageSize, storeServiceErrorText)
import Simplex.Chat.Markdown
import Simplex.Chat.Badges (BadgeInfo (..), BadgeStatus (..), BadgeType (..), LocalBadge, localBadgeInfo, localBadgeStatus)
import Simplex.Chat.Badges.Ledger (creditTypeTag, debitTypeTag)
import Simplex.Chat.Badges.Service (StatementEntry (..), StatementEntryType (..))
import Simplex.Chat.StoreService (StatementEntry (..), StatementEntryType (..))
import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeIssueError (..), BadgeState (..))
import Simplex.Chat.Messages hiding (NewChatItem (..))
import Simplex.Chat.Messages.CIContent
@@ -2921,9 +2921,9 @@ viewChatError isCmd logLevel testView = \case
CEBadgeRedeemError e ->
let reason = case e of
BREInvalidCode -> "invalid code"
BREServiceNotConfigured -> "badge service not configured"
BREServiceNotConfigured -> "store service not configured"
BREBadgeActive -> "badge already active"
BREServiceError code -> "badge service error: " <> T.unpack (badgeServiceErrorText code)
BREServiceError code -> "store service error: " <> T.unpack (storeServiceErrorText code)
BREInvalidResponse m -> "invalid service response: " <> m
BREUnknownKeyIndex -> "credential names an unknown badge key index"
BRECredentialNotVerified -> "credential does not verify against configured key"
+90 -90
View File
@@ -28,11 +28,11 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Chat.Badges
import Simplex.Chat.Badges.Code
import Simplex.Chat.Badges.Ledger
import Simplex.Chat.Badges.Service
import Simplex.Chat.StoreService
import Simplex.Chat.Badges.Types (BadgeIssueFailure (..), BadgeOfferId (..), BadgePriceId (..))
import Simplex.Chat (defaultChatConfig)
import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..), badgeRetryInterval, chatErrorAgent)
import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRetryAfter, badgeServiceErrorText, badgeStalledInterval)
import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRetryAfter, badgeStalledInterval, storeServiceErrorText)
import Simplex.Chat.PaymentService
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..), AgentServiceError (..), SMPAgentError (..))
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay)
@@ -704,7 +704,7 @@ testRetryClassification = do
-- terminal: the same request would fail the same way, and repeating it would spin
retryFor (AGENT (A_SERVICE ASEBadSignature)) `shouldBe` False
retryFor (AGENT (A_SERVICE (ASERejected "no"))) `shouldBe` False
badgeErrorRetry (ChatError (CECommandError "unexpected badge service response")) `shouldBe` False
badgeErrorRetry (ChatError (CECommandError "unexpected store service response")) `shouldBe` False
-- A failure that never clears is repeated for as long as the balance funds a month, so the wait
-- has to grow: at a fixed interval an annual code would ask hundreds of times a day, all year.
@@ -743,15 +743,15 @@ testServiceRetryFloor = do
-- schedule, so the set is the protocol's and not a judgement to make per call site.
testServiceRetryAfter :: IO ()
testServiceRetryAfter = do
badgeErrorRetryAfter BSEPaymentPending `shouldBe` Just 300
badgeErrorRetryAfter BSEProviderUnavailable `shouldBe` Just 300
badgeErrorRetryAfter BSERateLimited `shouldBe` Just 60
badgeErrorRetryAfter STEPaymentPending `shouldBe` Just 300
badgeErrorRetryAfter STEProviderUnavailable `shouldBe` Just 300
badgeErrorRetryAfter STERateLimited `shouldBe` Just 60
-- internal is the one most likely to clear on its own, and is still terminal: repeating it on
-- the service's cadence presses a service already failing, and the client has its own floor
badgeErrorRetryAfter BSEInternal `shouldBe` Nothing
badgeErrorRetryAfter STEInternal `shouldBe` Nothing
mapM_
(\code -> badgeErrorRetryAfter code `shouldBe` Nothing)
[BSEBadRequest, BSEUnsupportedVersion, BSEUnknownPurchaseKey, BSECodeInvalid, BSECodeUsed, BSECodeExpired, BSEUnknown "future_code"]
[STEBadRequest, STEUnsupportedVersion, STEUnknownPurchaseKey, STECodeInvalid, STECodeUsed, STECodeExpired, STEUnknown "future_code"]
-- The app shows the recorded failure in a sentence, so an agent error is stored as the agent
-- error and not as the chat error wrapping it, whether or not it can clear on its own.
@@ -763,18 +763,18 @@ testIssueFailureClassification = do
failureFor (AGENT (A_SERVICE ASETimeout)) `shouldBe` BIFServiceTimeout
failureFor timeout `shouldBe` BIFNetwork {agentError = tshow timeout}
failureFor auth `shouldBe` BIFUnexpected {message = tshow auth}
badgeIssueFailure (ChatError (CECommandError "unexpected badge service response")) `shouldBe` BIFUnexpected {message = "unexpected badge service response"}
badgeIssueFailure (ChatError (CECommandError "unexpected store service response")) `shouldBe` BIFUnexpected {message = "unexpected store service response"}
-- A refusal the service marks transient is not worth a word before the credential lapses. Internal
-- comes without retryAfter so that a failing service is not pressed, not because the fault is
-- final, so it waits the same way; any other refusal is told at once.
testServiceErrorTransience :: IO ()
testServiceErrorTransience = do
badgeFailureTransient BIFServiceError {code = BSERateLimited, retryable = True} `shouldBe` True
badgeFailureTransient BIFServiceError {code = BSEInternal, retryable = False} `shouldBe` True
badgeFailureTransient BIFServiceError {code = BSEUnknownPurchaseKey, retryable = False} `shouldBe` False
badgeFailureTransient BIFServiceError {code = BSEUnknown "future_code", retryable = False} `shouldBe` False
badgeFailureTransient BIFServiceError {code = BSEUnknown "future_code", retryable = True} `shouldBe` True
badgeFailureTransient BIFServiceError {code = STERateLimited, retryable = True} `shouldBe` True
badgeFailureTransient BIFServiceError {code = STEInternal, retryable = False} `shouldBe` True
badgeFailureTransient BIFServiceError {code = STEUnknownPurchaseKey, retryable = False} `shouldBe` False
badgeFailureTransient BIFServiceError {code = STEUnknown "future_code", retryable = False} `shouldBe` False
badgeFailureTransient BIFServiceError {code = STEUnknown "future_code", retryable = True} `shouldBe` True
-- The failure is stored as text and read back by getBadgePurchase, so every value this version
-- writes must read back as itself, and any other value must still read: a row it cannot parse
@@ -783,20 +783,20 @@ testIssueFailureEncoding :: IO ()
testIssueFailureEncoding = do
mapM_
(\f -> strDecode (strEncode f) `shouldBe` Right f)
[ BIFServiceError {code = BSEUnknownPurchaseKey, retryable = False},
BIFServiceError {code = BSERateLimited, retryable = True},
BIFServiceError {code = BSEUnknown "future_code", retryable = True},
BIFServiceError {code = BSEUnknown "code with spaces", retryable = False},
BIFServiceError {code = BSEUnknown "", retryable = False},
[ BIFServiceError {code = STEUnknownPurchaseKey, retryable = False},
BIFServiceError {code = STERateLimited, retryable = True},
BIFServiceError {code = STEUnknown "future_code", retryable = True},
BIFServiceError {code = STEUnknown "code with spaces", retryable = False},
BIFServiceError {code = STEUnknown "", retryable = False},
BIFServiceTimeout,
BIFNetwork {agentError = "BROKER \"smp://x\" NETWORK"},
BIFInvalidCredential,
BIFUnexpected {message = "unexpected badge service response"},
BIFUnexpected {message = "unexpected store service response"},
BIFUnexpected {message = "several words and : punctuation"}
]
-- the row the CLI prints, and what a reader of the database sees
strEncode BIFServiceError {code = BSECodeUsed, retryable = False} `shouldBe` "service_error final code_used"
strEncode BIFServiceError {code = BSERateLimited, retryable = True} `shouldBe` "service_error retry rate_limited"
strEncode BIFServiceError {code = STECodeUsed, retryable = False} `shouldBe` "service_error final code_used"
strEncode BIFServiceError {code = STERateLimited, retryable = True} `shouldBe` "service_error retry rate_limited"
strEncode BIFServiceTimeout `shouldBe` "service_timeout"
-- a row a later version wrote, or one edited by hand, still reads - as the text it holds
strDecode "future_failure with text" `shouldBe` Right BIFUnexpected {message = "future_failure with text"}
@@ -807,11 +807,11 @@ testIssueFailureEncoding = do
-- a sentence of its own - so it is bounded and stripped of anything but a code before it is stored.
testServiceErrorCodeBounded :: IO ()
testServiceErrorCodeBounded = do
badgeServiceErrorText BSECodeUsed `shouldBe` "code_used"
badgeServiceErrorText (BSEUnknown "future_code") `shouldBe` "future_code"
badgeServiceErrorText (BSEUnknown "two words") `shouldBe` "twowords"
badgeServiceErrorText (BSEUnknown (T.replicate 100 "a")) `shouldBe` T.replicate 32 "a"
badgeServiceErrorText (BSEUnknown "Visit evil.example.com!") `shouldBe` "isitevilexamplecom"
storeServiceErrorText STECodeUsed `shouldBe` "code_used"
storeServiceErrorText (STEUnknown "future_code") `shouldBe` "future_code"
storeServiceErrorText (STEUnknown "two words") `shouldBe` "twowords"
storeServiceErrorText (STEUnknown (T.replicate 100 "a")) `shouldBe` T.replicate 32 "a"
storeServiceErrorText (STEUnknown "Visit evil.example.com!") `shouldBe` "isitevilexamplecom"
-- The client replicates entry_credit_type / entry_debit_type verbatim, so a stored tag that
-- disagreed with the wire tag would put a different row on each side.
@@ -847,7 +847,7 @@ testRedeemRequestJSON = do
mk <- generateMasterKey drg
(k, _) <- atomically $ C.generateKeyPair drg :: IO (C.KeyPair 'C.Ed25519)
code <- randomBadgeCode drg
let req = BadgeServiceRequest {version = Version 1, purchaseKey = Just k, request = BSCRedeemBadgeCode {masterKey = mk, code = badgeCodeText code}}
let req = StoreServiceRequest {version = Version 1, purchaseKey = Just k, request = STCRedeemBadgeCode {masterKey = mk, code = badgeCodeText code}}
J.toJSON req
`shouldBe` J.object
[ "version" J..= (1 :: Int),
@@ -855,7 +855,7 @@ testRedeemRequestJSON = do
"request" J..= J.object ["type" J..= ("redeemBadgeCode" :: T.Text), "masterKey" J..= mk, "code" J..= badgeCodeText code]
]
-- purchaseKey is optional in the schema, and a nullary command is a bare tagged object
J.toJSON BadgeServiceRequest {version = Version 1, purchaseKey = Nothing, request = BSCGetBadgeCatalog}
J.toJSON StoreServiceRequest {version = Version 1, purchaseKey = Nothing, request = STCGetBadgeCatalog}
`shouldBe` J.object ["version" J..= (1 :: Int), "request" J..= J.object ["type" J..= ("getBadgeCatalog" :: T.Text)]]
roundTrips req
@@ -866,7 +866,7 @@ testCredentialResponseJSON = do
mk <- generateMasterKey drg
let info = BadgeInfo {badgeType = BTSupporter, badgeExpiry = futureTime, badgeExtra = ""}
Right cred <- issueBadge testKeyIdx sk (VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = info})
let resp = BSPBadgeCredential {credential = Just cred, receipt = Nothing, statement = BadgeStatement {entries = [], previousEntryId = Nothing}}
let resp = STPBadgeCredential {credential = Just cred, receipt = Nothing, statement = BadgeStatement {entries = [], previousEntryId = Nothing}}
J.toJSON resp
`shouldBe` J.object
[ "type" J..= ("badgeCredential" :: T.Text),
@@ -877,9 +877,9 @@ testCredentialResponseJSON = do
testErrorResponseJSON :: IO ()
testErrorResponseJSON = do
let resp = BSPError {code = BSECodeInvalid, message = Nothing, retryAfter = Nothing}
let resp = STPError {code = STECodeInvalid, message = Nothing, retryAfter = Nothing}
J.toJSON resp `shouldBe` J.object ["type" J..= ("error" :: T.Text), "code" J..= ("code_invalid" :: T.Text)]
J.toJSON BSPError {code = BSERateLimited, message = Just "slow down", retryAfter = Just 30}
J.toJSON STPError {code = STERateLimited, message = Just "slow down", retryAfter = Just 30}
`shouldBe` J.object ["type" J..= ("error" :: T.Text), "code" J..= ("rate_limited" :: T.Text), "message" J..= ("slow down" :: T.Text), "retryAfter" J..= (30 :: Int)]
testEntry :: StatementEntry
@@ -947,39 +947,39 @@ testServiceWireTags = do
paymentTo = SPDCard {provider = CPStripe, url = "https://checkout.example/inv-1"}
}
commands =
[ BSCGetBadgeCatalog,
BSCGetBadgeInvoice {priceId = BadgePriceId "price-1", offerId = Just (BadgeOfferId "offer-1"), badgeInfo, paymentVia = SPMCard {provider = CPStripe}, upgrade = Nothing},
BSCRedeemBadgeCode {masterKey, code = "SB-4K2P7-TQ9M1-ZX3RB-8HJ5W"},
BSCPurchaseBadge {badgeRequest, payment, upgrade = Nothing},
BSCUpgradeBadgeSubscription {badgeRequest, payment, balance},
BSCIssueBadge {balance},
BSCPauseBadge
[ STCGetBadgeCatalog,
STCGetBadgeInvoice {priceId = BadgePriceId "price-1", offerId = Just (BadgeOfferId "offer-1"), badgeInfo, paymentVia = SPMCard {provider = CPStripe}, upgrade = Nothing},
STCRedeemBadgeCode {masterKey, code = "SB-4K2P7-TQ9M1-ZX3RB-8HJ5W"},
STCPurchaseBadge {badgeRequest, payment, upgrade = Nothing},
STCUpgradeBadgeSubscription {badgeRequest, payment, balance},
STCIssueBadge {balance},
STCPauseBadge
]
responses =
[ BSPBadgeCatalog {catalog = BadgeCatalog {prices = [], offers = []}, badgeStatement = Just statement},
BSPBadgeInvoice {invoice, badgeType = BTSupporter, months = 3},
BSPBadgeCredential {credential = Nothing, receipt = Just "receipt", statement},
BSPError {code = BSEInternal, message = Nothing, retryAfter = Nothing}
[ STPBadgeCatalog {catalog = BadgeCatalog {prices = [], offers = []}, badgeStatement = Just statement},
STPBadgeInvoice {invoice, badgeType = BTSupporter, months = 3},
STPBadgeCredential {credential = Nothing, receipt = Just "receipt", statement},
STPError {code = STEInternal, message = Nothing, retryAfter = Nothing}
]
errors =
[ BSEBadRequest,
BSEUnsupportedVersion,
BSEUnknownPurchaseKey,
BSEUnknownOfferId,
BSEOfferDisabled,
BSEOfferMismatch,
BSEProductUnavailable,
BSEPaymentNotEntitled,
BSEPaymentPending,
BSEProviderUnavailable,
BSERateLimited,
BSECodeInvalid,
BSECodeUsed,
BSECodeExpired,
BSEReceiptInvalid,
BSEReceiptUsed,
BSEInternal,
BSEUnknown "future_code"
[ STEBadRequest,
STEUnsupportedVersion,
STEUnknownPurchaseKey,
STEUnknownOfferId,
STEOfferDisabled,
STEOfferMismatch,
STEProductUnavailable,
STEPaymentNotEntitled,
STEPaymentPending,
STEProviderUnavailable,
STERateLimited,
STECodeInvalid,
STECodeUsed,
STECodeExpired,
STEReceiptInvalid,
STEReceiptUsed,
STEInternal,
STEUnknown "future_code"
]
forM_ commands $ pinsTypeTag commandTag
forM_ responses $ pinsTypeTag responseTag
@@ -988,37 +988,37 @@ testServiceWireTags = do
J.fromJSON (J.String $ errorTag e) `shouldBe` J.Success e
where
commandTag = \case
BSCGetBadgeCatalog -> "getBadgeCatalog"
BSCGetBadgeInvoice {} -> "getBadgeInvoice"
BSCRedeemBadgeCode {} -> "redeemBadgeCode"
BSCPurchaseBadge {} -> "purchaseBadge"
BSCUpgradeBadgeSubscription {} -> "upgradeBadgeSubscription"
BSCIssueBadge {} -> "issueBadge"
BSCPauseBadge -> "pauseBadge"
STCGetBadgeCatalog -> "getBadgeCatalog"
STCGetBadgeInvoice {} -> "getBadgeInvoice"
STCRedeemBadgeCode {} -> "redeemBadgeCode"
STCPurchaseBadge {} -> "purchaseBadge"
STCUpgradeBadgeSubscription {} -> "upgradeBadgeSubscription"
STCIssueBadge {} -> "issueBadge"
STCPauseBadge -> "pauseBadge"
responseTag = \case
BSPBadgeCatalog {} -> "badgeCatalog"
BSPBadgeInvoice {} -> "badgeInvoice"
BSPBadgeCredential {} -> "badgeCredential"
BSPError {} -> "error"
STPBadgeCatalog {} -> "badgeCatalog"
STPBadgeInvoice {} -> "badgeInvoice"
STPBadgeCredential {} -> "badgeCredential"
STPError {} -> "error"
errorTag = \case
BSEBadRequest -> "bad_request"
BSEUnsupportedVersion -> "unsupported_version"
BSEUnknownPurchaseKey -> "unknown_purchase_key"
BSEUnknownOfferId -> "unknown_offer_id"
BSEOfferDisabled -> "offer_disabled"
BSEOfferMismatch -> "offer_mismatch"
BSEProductUnavailable -> "product_unavailable"
BSEPaymentNotEntitled -> "payment_not_entitled"
BSEPaymentPending -> "payment_pending"
BSEProviderUnavailable -> "provider_unavailable"
BSERateLimited -> "rate_limited"
BSECodeInvalid -> "code_invalid"
BSECodeUsed -> "code_used"
BSECodeExpired -> "code_expired"
BSEReceiptInvalid -> "receipt_invalid"
BSEReceiptUsed -> "receipt_used"
BSEInternal -> "internal"
BSEUnknown t -> t
STEBadRequest -> "bad_request"
STEUnsupportedVersion -> "unsupported_version"
STEUnknownPurchaseKey -> "unknown_purchase_key"
STEUnknownOfferId -> "unknown_offer_id"
STEOfferDisabled -> "offer_disabled"
STEOfferMismatch -> "offer_mismatch"
STEProductUnavailable -> "product_unavailable"
STEPaymentNotEntitled -> "payment_not_entitled"
STEPaymentPending -> "payment_pending"
STEProviderUnavailable -> "provider_unavailable"
STERateLimited -> "rate_limited"
STECodeInvalid -> "code_invalid"
STECodeUsed -> "code_used"
STECodeExpired -> "code_expired"
STEReceiptInvalid -> "receipt_invalid"
STEReceiptUsed -> "receipt_used"
STEInternal -> "internal"
STEUnknown t -> t
-- the tag is checked in both encodings, and decoding must land on the constructor the tag names
pinsTypeTag :: (HasCallStack, J.ToJSON a, J.FromJSON a) => (a -> T.Text) -> a -> IO ()
+36 -36
View File
@@ -46,7 +46,7 @@ import Data.Time.Format (defaultTimeLocale, formatTime)
import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), BadgeMasterKey, BadgeType (..), generateMasterKey)
import Simplex.Chat.Badges.Code (BadgeCode, badgeCodeHash, badgeCodeText, formatBadgeCode, parseBadgeCode, randomBadgeCode)
import Simplex.Chat.Badges.Ledger (addMonths, creditTypeTag, debitTypeTag, endOfMondayAfter)
import Simplex.Chat.Badges.Service
import Simplex.Chat.StoreService
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..))
import Simplex.Chat.Bot.Store (withDB')
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatError (..), ChatErrorType (..), ChatResponse (CRCustomChatResponse))
@@ -206,7 +206,7 @@ withBadgeServiceEnv ps test = do
bs <## "auto_accept off"
pure sLink
let clientCfg =
svcCfg {badgeServiceAddress = Just $ either (error . ("bad badge service address: " <>)) id $ strDecode (B.pack bsLink)}
svcCfg {storeServiceAddress = Just $ either (error . ("bad badge service address: " <>)) id $ strDecode (B.pack bsLink)}
-- Second start: badge service takes the ShowMyAddress branch, then serves the test body.
runBadgeService ps svcCfg opts $ \env -> do
cc <- atomically $ readTMVar $ serviceCC env
@@ -305,7 +305,7 @@ testRedeemUnknownCode ps =
g <- C.newRandom
unknown <- randomBadgeCode g
alice ##> ("/_redeem_badge_code 1 " <> codeArg unknown)
alice <## "cannot redeem badge code: badge service error: code_invalid"
alice <## "cannot redeem badge code: store service error: code_invalid"
-- a failed check character is refused before anything leaves the device
alice ##> "/_redeem_badge_code 1 SB-00000-00000-00000-00001"
alice <## "cannot redeem badge code: invalid code"
@@ -383,21 +383,21 @@ testRedeemSameCodeOtherProfile ps =
alice ##> "/create user alisa"
showActiveUser alice "alisa"
alice ##> ("/_redeem_badge_code 2 " <> codeArg code)
alice <## "cannot redeem badge code: badge service error: code_used"
alice <## "cannot redeem badge code: store service error: code_used"
alice ##> "/p"
showActiveUser alice "alisa"
alice ##> "/user alice"
showActiveUser alice "alice (Alice, * supporter)"
serviceCmd :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> BadgeServiceCommand -> IO BadgeServiceResponse
serviceCmd :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> StoreServiceCommand -> IO StoreServiceResponse
serviceCmd BadgeServiceEnv {bsIssuerKey, bsController} = serviceCmdWith bsIssuerKey bsController Nothing
serviceCmdWith :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> BadgeServiceCommand -> IO BadgeServiceResponse
serviceCmdWith :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> C.PublicKeyEd25519 -> StoreServiceCommand -> IO StoreServiceResponse
serviceCmdWith key cc trackerQ_ purchaseKey request =
badgeServiceResponse key cc trackerQ_ (Just purchaseKey) (requestObject purchaseKey request)
requestObject :: HasCallStack => C.PublicKeyEd25519 -> BadgeServiceCommand -> J.Object
requestObject purchaseKey request = case J.toJSON BadgeServiceRequest {version = currentBadgeServiceVersion, purchaseKey = Just purchaseKey, request} of
requestObject :: HasCallStack => C.PublicKeyEd25519 -> StoreServiceCommand -> J.Object
requestObject purchaseKey request = case J.toJSON StoreServiceRequest {version = currentStoreServiceVersion, purchaseKey = Just purchaseKey, request} of
J.Object o -> o
_ -> error "badge service request must encode as an object"
@@ -412,19 +412,19 @@ entryTag StatementEntry {entryType} = case entryType of
SECredit c -> creditTypeTag c
SEDebit d -> debitTypeTag d
statementOf :: HasCallStack => BadgeServiceResponse -> ([StatementEntry], Maybe Text)
statementOf :: HasCallStack => StoreServiceResponse -> ([StatementEntry], Maybe Text)
statementOf = \case
BSPBadgeCredential {statement = BadgeStatement {entries, previousEntryId}} -> (entries, previousEntryId)
STPBadgeCredential {statement = BadgeStatement {entries, previousEntryId}} -> (entries, previousEntryId)
r -> error $ "expected badgeCredential, got " <> show (J.toJSON r)
credentialOf :: HasCallStack => BadgeServiceResponse -> Maybe BadgeCredential
credentialOf :: HasCallStack => StoreServiceResponse -> Maybe BadgeCredential
credentialOf = \case
BSPBadgeCredential {credential} -> credential
STPBadgeCredential {credential} -> credential
r -> error $ "expected badgeCredential, got " <> show (J.toJSON r)
shouldAnswerError :: HasCallStack => BadgeServiceResponse -> BadgeServiceErrorCode -> IO ()
shouldAnswerError :: HasCallStack => StoreServiceResponse -> StoreServiceErrorCode -> IO ()
shouldAnswerError r expected = case r of
BSPError {code = ec} -> ec `shouldBe` expected
STPError {code = ec} -> ec `shouldBe` expected
_ -> expectationFailure $ "expected " <> show expected <> ", got: " <> show (J.toJSON r)
codeCounts :: DBStore -> ByteString -> IO (Maybe (Int, Int))
@@ -442,28 +442,28 @@ newPurchaseKeys = do
(purchaseKey, _) <- atomically $ C.generateKeyPair g :: IO (C.KeyPair 'C.Ed25519)
(purchaseKey,) <$> generateMasterKey g
redeemAsNewPurchase :: HasCallStack => BadgeServiceEnv -> BadgeCode -> IO BadgeServiceResponse
redeemAsNewPurchase :: HasCallStack => BadgeServiceEnv -> BadgeCode -> IO StoreServiceResponse
redeemAsNewPurchase BadgeServiceEnv {bsIssuerKey, bsController} = redeemWithQueue bsIssuerKey bsController Nothing . badgeCodeText
redeemWithKeys :: HasCallStack => BadgeServiceEnv -> (C.PublicKeyEd25519, BadgeMasterKey) -> BadgeCode -> IO BadgeServiceResponse
redeemWithKeys env (purchaseKey, masterKey) code = serviceCmd env purchaseKey BSCRedeemBadgeCode {masterKey, code = badgeCodeText code}
redeemWithKeys :: HasCallStack => BadgeServiceEnv -> (C.PublicKeyEd25519, BadgeMasterKey) -> BadgeCode -> IO StoreServiceResponse
redeemWithKeys env (purchaseKey, masterKey) code = serviceCmd env purchaseKey STCRedeemBadgeCode {masterKey, code = badgeCodeText code}
redeemWithQueue :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Text -> IO BadgeServiceResponse
redeemWithQueue :: HasCallStack => BadgeIssuerKey -> ChatController -> Maybe (TQueue GroupEvent) -> Text -> IO StoreServiceResponse
redeemWithQueue key cc trackerQ_ codeText = do
(purchaseKey, masterKey) <- newPurchaseKeys
serviceCmdWith key cc trackerQ_ purchaseKey BSCRedeemBadgeCode {masterKey, code = codeText}
serviceCmdWith key cc trackerQ_ purchaseKey STCRedeemBadgeCode {masterKey, code = codeText}
assertBalance :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> StatementEntry -> IO BadgeServiceResponse
assertBalance :: HasCallStack => BadgeServiceEnv -> C.PublicKeyEd25519 -> StatementEntry -> IO StoreServiceResponse
assertBalance env purchaseKey lastEntry =
serviceCmd env purchaseKey BSCIssueBadge {balance = BadgeBalance {lastEntry}}
serviceCmd env purchaseKey STCIssueBadge {balance = BadgeBalance {lastEntry}}
expiryOf :: HasCallStack => BadgeServiceResponse -> Maybe UTCTime
expiryOf :: HasCallStack => StoreServiceResponse -> Maybe UTCTime
expiryOf r = (\(BadgeCredential _ _ _ BadgeInfo {badgeExpiry}) -> badgeExpiry) <$> credentialOf r
badgeTypeOf :: HasCallStack => BadgeServiceResponse -> Maybe BadgeType
badgeTypeOf :: HasCallStack => StoreServiceResponse -> Maybe BadgeType
badgeTypeOf r = (\(BadgeCredential _ _ _ BadgeInfo {badgeType}) -> badgeType) <$> credentialOf r
masterKeyOf :: HasCallStack => BadgeServiceResponse -> Maybe BadgeMasterKey
masterKeyOf :: HasCallStack => StoreServiceResponse -> Maybe BadgeMasterKey
masterKeyOf r = (\(BadgeCredential _ mk _ _) -> mk) <$> credentialOf r
testCodeMonthsRenew :: HasCallStack => TestParams -> IO ()
@@ -525,7 +525,7 @@ testMultiUseRepeatAfterExpiry ps =
waitShownBadge (chatController alice) Nothing
codeUses cc code `shouldReturn` Just (2, 1)
withNewTestChatCfg ps bsClientCfg "bob" bobProfile $ \bob -> redeemFirstBadge bob code
redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeUsed)
redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeUsed)
testMultiUseWithoutGroup :: HasCallStack => TestParams -> IO ()
testMultiUseWithoutGroup ps =
@@ -540,7 +540,7 @@ testMultiUseWithoutGroup ps =
expiryOf redeemed `shouldBe` Just (endOfMondayAfter (nextDue entries))
masterKeyOf redeemed `shouldBe` Just masterKey
codeUses cc code `shouldReturn` Just (2, claimed)
redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeUsed)
redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeUsed)
testUnreadableCredentialIsInternal :: HasCallStack => TestParams -> IO ()
testUnreadableCredentialIsInternal ps =
@@ -551,11 +551,11 @@ testUnreadableCredentialIsInternal ps =
redeem holder >>= (`shouldSatisfy` isJust) . credentialOf
withTransaction (chatStore cc) $ \db ->
DB.execute db "UPDATE sx_badge_service_badge_issuances SET credential = ?" (Only (Binary ("not a credential" :: ByteString)))
redeem holder >>= (`shouldAnswerError` BSEInternal)
redeem holder >>= (`shouldAnswerError` STEInternal)
codeUses cc code `shouldReturn` Just (2, 1)
newPurchaseKeys >>= redeem >>= (`shouldSatisfy` isJust) . credentialOf
codeUses cc code `shouldReturn` Just (2, 2)
redeem holder >>= (`shouldAnswerError` BSEInternal)
redeem holder >>= (`shouldAnswerError` STEInternal)
codeUses cc code `shouldReturn` Just (2, 2)
-- Multi-use codes come from the group command, and this service has no group.
@@ -858,7 +858,7 @@ testRevokedMultiUseHolderRenews ps =
redeemFirstBadge alice code
redeemed <- ledgerRows (chatController alice) "badge_ledger"
revokeCodeAs cc code `shouldReturn` Right "Revoked."
redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeInvalid)
redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeInvalid)
codeUses cc code `shouldReturn` Just (2, 1)
-- A holder whose reply was lost retries with the same key and still gets its badge.
alice ##> ("/_redeem_badge_code 1 " <> codeArg code)
@@ -1234,13 +1234,13 @@ testNoCredentialMonthsLeft ps =
setClockAt bsClock $ snd $ renewalMoments rows
alice ##> "/_app activate"
alice <## "ok"
alice <## "internal chat error: badge service issued no credential"
alice <## "internal chat error: store service issued no credential"
alice <##. "badge alert: issue_failed "
alice <##. "1: supporter"
alice <##. "renewal failing since "
alice <##. "badge alert: issue_failed "
(_, reason) <- issueErrorRow (chatController alice)
reason `shouldBe` Just "unexpected badge service issued no credential"
reason `shouldBe` Just "unexpected store service issued no credential"
-- | A debit the service wrote on its own, taking back the months the purchase had: copied from
-- the tip so that it follows it in every column the client checks.
@@ -1262,7 +1262,7 @@ zeroServiceLedgerTip ChatController {chatStore} =
DB.execute_ db "UPDATE sx_badge_service_badge_ledger SET balance_months = 0 WHERE entry_id = (SELECT MAX(entry_id) FROM sx_badge_service_badge_ledger)"
badgeServiceRefused :: String
badgeServiceRefused = "bad chat command: badge service error: unknown_purchase_key"
badgeServiceRefused = "bad chat command: store service error: unknown_purchase_key"
-- | The alert's episode is the start of the run of failures, as the ack command spells it.
episodeOf :: HasCallStack => Maybe UTCTime -> Text
@@ -1498,7 +1498,7 @@ testRedeemUnpaidCode ps =
withNewTestChatCfg ps clientCfg "alice" aliceProfile $ \alice -> do
unpaid <- issueCodeAs cc BTSupporter 1 "unpaid"
alice ##> ("/_redeem_badge_code 1 " <> codeArg unpaid)
alice <## "cannot redeem badge code: badge service error: payment_pending"
alice <## "cannot redeem badge code: store service error: payment_pending"
paid <- issueCodeAs cc BTSupporter 1 "paid"
alice ##> ("/_redeem_badge_code 1 " <> codeArg paid)
alice <## "badge redeemed"
@@ -1515,7 +1515,7 @@ testExpiredCode ps =
withDB' "markCodePaid" cc (\db -> markCodePaid db (badgeCodeHash code) (addUTCTime (-60) now))
`shouldReturn` Right ()
alice ##> ("/_redeem_badge_code 1 " <> codeArg code)
alice <## "cannot redeem badge code: badge service error: code_expired"
alice <## "cannot redeem badge code: store service error: code_expired"
testRedeemedBeforeTheDeadline :: HasCallStack => TestParams -> IO ()
testRedeemedBeforeTheDeadline ps =
@@ -1569,7 +1569,7 @@ testRevokedCode ps =
paid <- issueCodeAs cc BTSupporter 1 "paid"
revokeCodeAs cc paid `shouldReturn` Right "Revoked."
alice ##> ("/_redeem_badge_code 1 " <> codeArg paid)
alice <## "cannot redeem badge code: badge service error: code_invalid"
alice <## "cannot redeem badge code: store service error: code_invalid"
revokeCodeAs cc paid `shouldReturn` Right "Already revoked."
testRevokedUnpaidCode :: HasCallStack => TestParams -> IO ()
@@ -1577,7 +1577,7 @@ testRevokedUnpaidCode ps =
withBadgeServiceEnv ps $ \env@BadgeServiceEnv {bsController = cc} -> do
code <- issueCodeAs cc BTSupporter 1 "unpaid"
revokeCodeAs cc code `shouldReturn` Right "Revoked."
redeemAsNewPurchase env code >>= (`shouldAnswerError` BSECodeInvalid)
redeemAsNewPurchase env code >>= (`shouldAnswerError` STECodeInvalid)
testRevokeRejectsTrailingInput :: HasCallStack => TestParams -> IO ()
testRevokeRejectsTrailingInput ps =
+1 -1
View File
@@ -15,7 +15,7 @@ import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Word (Word32, Word8)
import Simplex.Chat.Badges (BadgeType (..))
import Simplex.Chat.Badges.Service (BadgeOffer (..), BadgePrice (..))
import Simplex.Chat.StoreService (BadgeOffer (..), BadgePrice (..))
import Simplex.Chat.Badges.Types (BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..))
import Simplex.Chat.PaymentService.Types (CurrencyAmount (..))
import Test.Hspec
@@ -40,7 +40,7 @@ import Network.Socket (Family (..), SockAddr (..), SocketType (..), close, conne
import Network.Wai.Handler.Warp (openFreePort)
import Simplex.Chat.Badges (BadgeType (..))
import Simplex.Chat.Badges.Code (formatBadgeCode, parseBadgeCode, randomBadgeCode)
import Simplex.Chat.Badges.Service
import Simplex.Chat.StoreService
import Simplex.Chat.Controller (ChatCommand (APIDeleteChatItem), ChatConfig (..), ChatController (..), ChatResponse (CRChatItemsDeleted))
import Simplex.Chat.Core (sendChatCmd, sendChatCmdStr)
import Simplex.Chat.Messages (ChatRef (ChatRef), ChatType (CTGroup))
@@ -564,7 +564,7 @@ testBulkIssue ps =
r <- redeemViaService gsKey cc env code
badgeTypeOf r `shouldBe` Just BTLegend
map (\e -> let (c, m, _) = entryOf e in (c, m)) (fst $ statementOf r) `shouldBe` [(2, 2), (-1, 1)]
forM_ codes $ \code -> redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeUsed)
forM_ codes $ \code -> redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeUsed)
countRows cc "sx_badge_service_badge_codes WHERE group_item_id IS NOT NULL" `shouldReturn` 0
testIssueMonths :: HasCallStack => TestParams -> IO ()
@@ -640,7 +640,7 @@ testMultiUseTracker ps =
redeemOk gsKey cc env code
usedUp <- waitItemText cc trackerItemId "All 2 uses redeemed"
sentItemsWithCode cc code `shouldReturn` [usedUp]
redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeUsed)
redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeUsed)
testStaleRefreshIgnored :: HasCallStack => TestParams -> IO ()
testStaleRefreshIgnored ps =
@@ -811,7 +811,7 @@ testGroupRevoke ps =
code <- extractCode <$> replyTo cc alice "/issue supporter"
other <- extractCode <$> replyTo cc alice "/issue legend"
revokeInGroup cc alice code "Revoked."
redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeInvalid)
redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeInvalid)
redeemOk gsKey cc env other
revokeInGroup cc alice code "Already revoked."
-- The reply follows the tracker step, so the revoke has already passed it here.
@@ -959,7 +959,7 @@ testRevokeRetiresTracker ps =
awaitLane cc env
readItemText cc trackerItemId `shouldReturn` retired
revokedTrackerCount cc `shouldReturn` 1
redeemViaService gsKey cc env code >>= (`shouldAnswerError` BSECodeInvalid)
redeemViaService gsKey cc env code >>= (`shouldAnswerError` STECodeInvalid)
testServiceRevokeRetiresTracker :: HasCallStack => TestParams -> IO ()
testServiceRevokeRetiresTracker ps =
@@ -1000,7 +1000,7 @@ redeemOkWith key cc trackerQ_ code = do
r <- redeemWithQueue key cc trackerQ_ code
credentialOf r `shouldSatisfy` isJust
redeemViaService :: HasCallStack => BadgeIssuerKey -> ChatController -> ServiceState -> Text -> IO BadgeServiceResponse
redeemViaService :: HasCallStack => BadgeIssuerKey -> ChatController -> ServiceState -> Text -> IO StoreServiceResponse
redeemViaService key cc env = redeemWithQueue key cc (Just (groupEventQ env))
-- The reply to the made-up request id fails after the redemption, and the service only logs it.
@@ -1008,7 +1008,7 @@ queueRedemption :: HasCallStack => ChatController -> ServiceState -> Text -> IO
queueRedemption cc env codeText = do
user <- readTVarIO (currentUser cc) >>= maybe (error "no current user") pure
(purchaseKey, masterKey) <- newPurchaseKeys
let request = requestObject purchaseKey BSCRedeemBadgeCode {masterKey, code = codeText}
let request = requestObject purchaseKey STCRedeemBadgeCode {masterKey, code = codeText}
atomically $ writeTQueue (serviceRequestQ env) (user, AgentInvId "test-request", Just purchaseKey, request)
redeemLosingRefresh :: HasCallStack => BadgeIssuerKey -> ChatController -> Text -> IO ()
+1 -1
View File
@@ -59,7 +59,7 @@ import Network.HTTP.Types (Header, HeaderName, hCacheControl, hContentType)
import Network.HTTP.Types.Status (statusCode)
import qualified Network.Wai.Handler.Warp as Warp
import Simplex.Chat.Badges (BadgeCredential (..), BadgeInfo (..), BadgeMasterKey (..), BadgeType (..))
import Simplex.Chat.Badges.Service (BadgeOffer (..), BadgePrice (..))
import Simplex.Chat.StoreService (BadgeOffer (..), BadgePrice (..))
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..), BadgeItemStatus (..), BadgeOfferId (..), BadgePriceId (..), OfferDiscount (..))
import Simplex.Chat.PaymentService.Types (CryptoCurrency (..), CurrencyAmount (..), InvoiceId (..), InvoiceStatus (..), PaymentProvider (..), PaymentStatus (..), ServicePaymentDestination (..), ServicePaymentMethod (..))
import Simplex.Messaging.Agent.Store.Common (DBStore (..), withConnection, withTransaction)