mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 08:37:30 +00:00
store: rename badge service to StoreService
This commit is contained in:
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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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 ()
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user