mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 06:32:59 +00:00
958 lines
40 KiB
Haskell
958 lines
40 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
module StoreService.Names.Store
|
|
( NamePhase (..),
|
|
RegistrationPayment (..),
|
|
NameReveal (..),
|
|
NameRegistration (..),
|
|
NewRegistration (..),
|
|
PaidOrder (..),
|
|
RegistrationRefused (..),
|
|
VersionWrite (..),
|
|
LiveOrder (..),
|
|
OrderTerms (..),
|
|
PrepaidCode (..),
|
|
NewPrepaidCode (..),
|
|
CodeHashTaken (..),
|
|
TxFor (..),
|
|
TxStatus (..),
|
|
NameTransaction (..),
|
|
NewTransaction (..),
|
|
RecordChange (..),
|
|
EditState (..),
|
|
RecordEdit (..),
|
|
NewRecordEdit (..),
|
|
findLimit,
|
|
orderHash,
|
|
orderLive,
|
|
newRegistrationId,
|
|
methodText,
|
|
textMethod,
|
|
createPaidRegistration,
|
|
createCodeRegistration,
|
|
getRegistration,
|
|
getRegistrationByOrderHash,
|
|
getLiveOrders,
|
|
closeOverdue,
|
|
updateRegistration,
|
|
retargetRegistration,
|
|
findRegistrations,
|
|
isLabelRegistering,
|
|
activeRegistrations,
|
|
unpaidCleanupCandidates,
|
|
deleteUnpaidRegistration,
|
|
failedCodeOrdersBefore,
|
|
insertPrepaidCode,
|
|
getPrepaidCode,
|
|
getPrepaidCodeById,
|
|
reservePrepaidCode,
|
|
releasePrepaidCode,
|
|
spendPrepaidCode,
|
|
revokePrepaidCode,
|
|
insertTransaction,
|
|
recordTransactionOutcome,
|
|
pendingTransactions,
|
|
registrationTransactions,
|
|
editTransactions,
|
|
insertRecordEdit,
|
|
getRecordEdit,
|
|
getRecordEditByBody,
|
|
getRecordEditByNonce,
|
|
settleRecordEdit,
|
|
relayedEditTimes,
|
|
)
|
|
where
|
|
|
|
import qualified Control.Exception as E
|
|
import Control.Monad (when)
|
|
import Crypto.Number.Serialize (i2ospOf_)
|
|
import Data.ByteArray.Encoding (Base (Base16), convertToBase)
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.ByteString as B
|
|
import Data.Int (Int64)
|
|
import Data.Maybe (isJust, listToMaybe)
|
|
import Data.Text (Text)
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
import Data.Time.Clock (UTCTime)
|
|
import Data.Word (Word64)
|
|
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..))
|
|
import Simplex.Chat.PaymentService.Types (CardProvider (..), CryptoCurrency (..), InvoiceId (..), InvoiceStatus (..), ServicePaymentMethod (..))
|
|
import Simplex.Chat.Store.Shared (insertedRowId)
|
|
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..), FromField (..), ToField (..), fromTextField_)
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
|
import Simplex.Messaging.Crypto (sha256Hash)
|
|
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature)
|
|
import Simplex.Messaging.Encoding.String (TextEncoding (..), strDecode)
|
|
import Simplex.Messaging.Eth.Address (Address, unAddress)
|
|
import Simplex.Messaging.Eth.EIP712 (serializeSignature)
|
|
import Simplex.Messaging.SimplexName (LabelHash (..))
|
|
import Simplex.Messaging.Util (maybeFirstRow, tshow)
|
|
import StoreService.Names.Abi (hex)
|
|
import StoreService.Names.Chain (TxHash (..))
|
|
import StoreService.Names.Phases (RegistrationId (..))
|
|
import StoreService.Names.Signatures (OrderFields (..), parseSignature)
|
|
import StoreService.Store (RevokeResult (..))
|
|
import StoreService.Store.Invoices (StoreDecodeError (..), executeChanging, invoiceStatusText, lockInvoice, newInvoiceId, truncateToSecond, unfundedQuery)
|
|
|
|
#if defined(dbPostgres)
|
|
import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..))
|
|
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
|
#else
|
|
import Database.SQLite.Simple (Only (..), Query, (:.) (..))
|
|
import Database.SQLite.Simple.QQ (sql)
|
|
#endif
|
|
|
|
data NamePhase
|
|
= PUnpaid
|
|
| PQueued
|
|
| PCommitting
|
|
| PCommitted
|
|
| PRegistering
|
|
| PRegistered
|
|
| PTaken
|
|
| PFailed
|
|
| PCodeIssued
|
|
deriving (Eq, Show, Enum, Bounded)
|
|
|
|
instance TextEncoding NamePhase where
|
|
textEncode = \case
|
|
PUnpaid -> "unpaid"
|
|
PQueued -> "queued"
|
|
PCommitting -> "committing"
|
|
PCommitted -> "committed"
|
|
PRegistering -> "registering"
|
|
PRegistered -> "registered"
|
|
PTaken -> "taken"
|
|
PFailed -> "failed"
|
|
PCodeIssued -> "code_issued"
|
|
textDecode t = lookup t [(textEncode p, p) | p <- [minBound .. maxBound]]
|
|
|
|
instance FromField NamePhase where fromField = fromTextField_ textDecode
|
|
|
|
instance ToField NamePhase where toField = toField . textEncode
|
|
|
|
data RegistrationPayment
|
|
= RPInvoice ServicePaymentMethod InvoiceId
|
|
| RPCode Int64
|
|
deriving (Eq, Show)
|
|
|
|
-- | The name, links and owner's intent, all set by an accepted reveal and cleared together.
|
|
data NameReveal = NameReveal
|
|
{ rvLabel :: Text,
|
|
rvSecret :: ByteString,
|
|
rvContactLink :: Maybe Text,
|
|
rvChannelLink :: Maybe Text,
|
|
rvNonce :: Word64,
|
|
rvDeadline :: Word64,
|
|
rvSignature :: RecoverableSignature
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data NameRegistration = NameRegistration
|
|
{ nrId :: RegistrationId,
|
|
nrVersion :: Int64,
|
|
nrLabelHash :: LabelHash,
|
|
nrLength :: Int,
|
|
nrDuration :: Word64,
|
|
nrOwner :: Address,
|
|
nrCommitment :: ByteString,
|
|
nrSealed :: Text,
|
|
nrPhase :: NamePhase,
|
|
nrYears :: Int,
|
|
nrPrice :: Int,
|
|
nrPayment :: RegistrationPayment,
|
|
nrReveal :: Maybe NameReveal,
|
|
nrCommitTx :: Maybe TxHash,
|
|
nrCommitBlock :: Maybe Word64,
|
|
nrRegisterTx :: Maybe TxHash,
|
|
nrRegisterBlock :: Maybe Word64,
|
|
nrRevealAfter :: Maybe Word64,
|
|
nrRevealBefore :: Maybe Word64,
|
|
nrExpiresAt :: Maybe Word64,
|
|
nrIssuedCodeId :: Maybe Int64,
|
|
nrRegisteredAt :: Maybe UTCTime,
|
|
nrFailedAt :: Maybe UTCTime,
|
|
nrError :: Maybe Text,
|
|
nrLive :: Bool,
|
|
nrOrderHash :: ByteString,
|
|
nrMoves :: Int,
|
|
nrLinksRefusedAt :: Maybe UTCTime,
|
|
nrCreatedAt :: UTCTime,
|
|
nrUpdatedAt :: UTCTime
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data NewRegistration = NewRegistration
|
|
{ nrgId :: RegistrationId,
|
|
nrgOwner :: Address,
|
|
nrgOrder :: OrderFields,
|
|
nrgYears :: Int
|
|
}
|
|
|
|
-- | The invoice row is inserted by the caller, in the same transaction and before the registration.
|
|
data PaidOrder = PaidOrder
|
|
{ poMethod :: ServicePaymentMethod,
|
|
poPrice :: Int,
|
|
poInvoiceId :: InvoiceId,
|
|
poProviderRef :: Text
|
|
}
|
|
|
|
data RegistrationRefused = RRLiveOrder | RRCodeNotReservable
|
|
deriving (Eq, Show)
|
|
|
|
-- | NotLive: the write would make a closed order live. LiveOrderExists: another live order holds its commitment, or its owner and label hash.
|
|
-- NotOverdue: closeOverdue found the order's invoice no longer open, or holding money.
|
|
data VersionWrite = Written Int64 | VersionChanged | NotLive | LiveOrderExists | NotOverdue
|
|
deriving (Eq, Show)
|
|
|
|
-- | A live order an order request meets; overdue when its open invoice is past its expiry time.
|
|
data LiveOrder = LiveOrder
|
|
{ loRegistration :: NameRegistration,
|
|
loOverdue :: Bool
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
-- | What an order pays with, as its hash covers it.
|
|
data OrderTerms = OTInvoice ServicePaymentMethod Int | OTCode ByteString
|
|
|
|
data PrepaidCode = PrepaidCode
|
|
{ pcId :: Int64,
|
|
pcCodeHash :: ByteString,
|
|
pcMinLength :: Int,
|
|
pcYears :: Int,
|
|
pcPaymentStatus :: BadgeCodePaymentStatus,
|
|
pcReservedBy :: Maybe RegistrationId,
|
|
pcReservedAt :: Maybe UTCTime,
|
|
pcSpentAt :: Maybe UTCTime,
|
|
pcRevokedAt :: Maybe UTCTime,
|
|
pcExpiresAt :: Maybe UTCTime,
|
|
pcCreatedAt :: UTCTime
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data NewPrepaidCode = NewPrepaidCode
|
|
{ npcCodeHash :: ByteString,
|
|
npcMinLength :: Int,
|
|
npcYears :: Int,
|
|
npcPaymentStatus :: BadgeCodePaymentStatus,
|
|
npcExpiresAt :: Maybe UTCTime
|
|
}
|
|
|
|
data CodeHashTaken = CodeHashTaken
|
|
deriving (Eq, Show)
|
|
|
|
data TxFor = TxCommit RegistrationId | TxRegister RegistrationId | TxEdit Int64
|
|
deriving (Eq, Show)
|
|
|
|
-- | A dropped transaction has no receipt and its registrar nonce is used.
|
|
data TxStatus = TSPending | TSMined | TSReverted | TSDropped
|
|
deriving (Eq, Show, Enum, Bounded)
|
|
|
|
instance TextEncoding TxStatus where
|
|
textEncode = \case
|
|
TSPending -> "pending"
|
|
TSMined -> "mined"
|
|
TSReverted -> "reverted"
|
|
TSDropped -> "dropped"
|
|
textDecode t = lookup t [(textEncode s, s) | s <- [minBound .. maxBound]]
|
|
|
|
instance FromField TxStatus where fromField = fromTextField_ textDecode
|
|
|
|
instance ToField TxStatus where toField = toField . textEncode
|
|
|
|
data NameTransaction = NameTransaction
|
|
{ ntId :: Int64,
|
|
ntFor :: TxFor,
|
|
ntRaw :: ByteString,
|
|
ntHash :: TxHash,
|
|
ntNonce :: Word64,
|
|
ntSentAt :: UTCTime,
|
|
ntMinedBlock :: Maybe Word64,
|
|
ntStatus :: TxStatus
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data NewTransaction = NewTransaction
|
|
{ ntxRaw :: ByteString,
|
|
ntxHash :: TxHash,
|
|
ntxNonce :: Word64
|
|
}
|
|
|
|
-- | A text record set to a value, "" removing it, or every record cleared.
|
|
data RecordChange = RCSetText Text Text | RCClear
|
|
deriving (Eq, Show)
|
|
|
|
data EditState = ESPending | ESMined (Maybe TxHash) | ESReverted (Maybe Text)
|
|
deriving (Eq, Show)
|
|
|
|
data RecordEdit = RecordEdit
|
|
{ reId :: Int64,
|
|
reLabelHash :: LabelHash,
|
|
reOwner :: Address,
|
|
reNonce :: Word64,
|
|
reChange :: RecordChange,
|
|
reDeadline :: Word64,
|
|
reSignature :: RecoverableSignature,
|
|
reBodyHash :: ByteString,
|
|
reState :: EditState,
|
|
reCreatedAt :: UTCTime
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data NewRecordEdit = NewRecordEdit
|
|
{ nreLabelHash :: LabelHash,
|
|
nreOwner :: Address,
|
|
nreNonce :: Word64,
|
|
nreChange :: RecordChange,
|
|
nreDeadline :: Word64,
|
|
nreSignature :: RecoverableSignature,
|
|
nreBodyHash :: ByteString
|
|
}
|
|
|
|
findLimit :: Int
|
|
findLimit = 50
|
|
|
|
initialVersion :: Int64
|
|
initialVersion = 1
|
|
|
|
orderHashFieldBytes :: Int
|
|
orderHashFieldBytes = 4
|
|
|
|
-- | Each field is length-prefixed, so no two different orders encode the same.
|
|
orderHash :: Address -> OrderFields -> OrderTerms -> ByteString
|
|
orderHash owner OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} terms =
|
|
sha256Hash . B.concat . map field $ [tag, lh, num ofLength, num ofDuration, unAddress owner, ofCommitment, encodeUtf8 ofSealed] <> payment
|
|
where
|
|
(tag, payment) = case terms of
|
|
OTInvoice method price -> ("name-invoice", [encodeUtf8 $ methodText method, num price])
|
|
OTCode codeHash -> ("name-code", [codeHash])
|
|
field bs = i2ospOf_ orderHashFieldBytes (toInteger $ B.length bs) <> bs
|
|
num :: Show a => a -> ByteString
|
|
num = encodeUtf8 . tshow
|
|
|
|
newRegistrationId :: IO RegistrationId
|
|
newRegistrationId = (\(InvoiceId t) -> RegistrationId t) <$> newInvoiceId
|
|
|
|
methodText :: ServicePaymentMethod -> Text
|
|
methodText = \case
|
|
SPMCard CPStripe -> "card"
|
|
SPMCrypto CCBtc -> "btc"
|
|
SPMCrypto CCXmr -> "xmr"
|
|
|
|
textMethod :: Text -> Maybe ServicePaymentMethod
|
|
textMethod t = lookup t [(methodText m, m) | m <- [SPMCard CPStripe, SPMCrypto CCBtc, SPMCrypto CCXmr]]
|
|
|
|
createPaidRegistration :: DB.Connection -> NewRegistration -> PaidOrder -> UTCTime -> IO (Either RegistrationRefused ())
|
|
createPaidRegistration db new@NewRegistration {nrgId = RegistrationId rid, nrgOwner, nrgOrder} PaidOrder {poMethod, poPrice, poInvoiceId = InvoiceId invId, poProviderRef} now' = do
|
|
let now = truncateToSecond now'
|
|
hash = orderHash nrgOwner nrgOrder (OTInvoice poMethod poPrice)
|
|
inserted <- insertRegistration db new PUnpaid poPrice (Just poMethod) Nothing hash now
|
|
when inserted $
|
|
DB.execute
|
|
db
|
|
"INSERT INTO sx_badge_service_name_invoices (invoice_id, registration_id, provider_ref, created_at) VALUES (?,?,?,?)"
|
|
(invId, rid, poProviderRef, now)
|
|
pure $ if inserted then Right () else Left RRLiveOrder
|
|
|
|
-- | Reserving the code is the conditional update of the transaction that creates the registration.
|
|
createCodeRegistration :: DB.Connection -> NewRegistration -> PrepaidCode -> UTCTime -> IO (Either RegistrationRefused ())
|
|
createCodeRegistration db new@NewRegistration {nrgId = regId@(RegistrationId rid), nrgOwner, nrgOrder} PrepaidCode {pcId, pcCodeHash} now' = do
|
|
let now = truncateToSecond now'
|
|
inserted <- insertRegistration db new PQueued 0 Nothing (Just pcId) (orderHash nrgOwner nrgOrder (OTCode pcCodeHash)) now
|
|
if not inserted
|
|
then pure $ Left RRLiveOrder
|
|
else
|
|
reservePrepaidCode db pcId regId now >>= \case
|
|
True -> pure $ Right ()
|
|
False -> do
|
|
DB.execute db "DELETE FROM sx_badge_service_name_registrations WHERE registration_id = ?" (Only rid)
|
|
pure $ Left RRCodeNotReservable
|
|
|
|
-- | False when the registration would be a second live one for its commitment, or for its owner and label hash.
|
|
insertRegistration :: DB.Connection -> NewRegistration -> NamePhase -> Int -> Maybe ServicePaymentMethod -> Maybe Int64 -> ByteString -> UTCTime -> IO Bool
|
|
insertRegistration db NewRegistration {nrgId = RegistrationId rid, nrgOwner, nrgOrder, nrgYears} phase price method codeId hash now =
|
|
(> 0)
|
|
<$> executeChanging
|
|
db
|
|
[sql|
|
|
INSERT INTO sx_badge_service_name_registrations
|
|
(registration_id, version, label_hash, length, duration, owner, commitment, sealed, phase, years,
|
|
price, method, name_prepaid_code_id, live, order_hash, moves, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?)
|
|
ON CONFLICT DO NOTHING
|
|
|]
|
|
( (rid, initialVersion, Binary lh, ofLength, int64 ofDuration, Binary $ unAddress nrgOwner, Binary ofCommitment, ofSealed, phase, nrgYears)
|
|
:. (price, methodText <$> method, codeId, BI True, Binary hash, 0 :: Int, now, now)
|
|
)
|
|
where
|
|
OrderFields {ofLabelHash = LabelHash lh, ofLength, ofDuration, ofCommitment, ofSealed} = nrgOrder
|
|
|
|
registrationColumns :: Query
|
|
registrationColumns =
|
|
[sql|
|
|
SELECT r.registration_id, r.version, r.label_hash, r.length, r.duration, r.owner, r.commitment, r.sealed, r.phase, r.years,
|
|
r.price, r.method, ni.invoice_id, r.name_prepaid_code_id, r.label, r.secret, r.contact_link, r.channel_link, r.intent_nonce, r.intent_deadline,
|
|
r.intent_signature, r.commit_tx, r.commit_block, r.register_tx, r.register_block, r.reveal_after, r.reveal_before, r.expires_at, r.issued_code_id, r.registered_at,
|
|
r.failed_at, r.error, r.live, r.order_hash, r.moves, r.links_refused_at, r.created_at, r.updated_at
|
|
|]
|
|
|
|
registrationFrom :: Query
|
|
registrationFrom =
|
|
" "
|
|
<> [sql|
|
|
FROM sx_badge_service_name_registrations r
|
|
LEFT JOIN sx_badge_service_name_invoices ni ON ni.registration_id = r.registration_id
|
|
|]
|
|
|
|
selectRegistrations :: Query
|
|
selectRegistrations = registrationColumns <> registrationFrom
|
|
|
|
type RegistrationRow =
|
|
(Text, Int64, Binary ByteString, Int, Int64, Binary ByteString, Binary ByteString, Text, NamePhase, Int)
|
|
:. (Int, Maybe Text, Maybe Text, Maybe Int64, Maybe Text, Maybe (Binary ByteString), Maybe Text, Maybe Text, Maybe Int64, Maybe Int64)
|
|
:. (Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe Int64, Maybe (Binary ByteString), Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime)
|
|
:. (Maybe UTCTime, Maybe Text, BoolInt, Binary ByteString, Int, Maybe UTCTime, UTCTime, UTCTime)
|
|
|
|
toRegistration :: RegistrationRow -> Either Text NameRegistration
|
|
toRegistration
|
|
( (rid, nrVersion, Binary lh, nrLength, duration, Binary owner, Binary nrCommitment, nrSealed, nrPhase, nrYears)
|
|
:. (nrPrice, method_, invoiceId_, codeId_, label_, secret_, nrContactLink, nrChannelLink, nonce_, deadline_)
|
|
:. (signature_, commitTx, commitBlock, registerTx, registerBlock, revealAfter, revealBefore, expiresAt, nrIssuedCodeId, nrRegisteredAt)
|
|
:. (nrFailedAt, nrError, BI nrLive, Binary nrOrderHash, nrMoves, nrLinksRefusedAt, nrCreatedAt, nrUpdatedAt)
|
|
) = do
|
|
nrOwner <- decodeAddress owner
|
|
nrPayment <- case (method_, invoiceId_, codeId_) of
|
|
(Just m, Just invId, Nothing) -> (`RPInvoice` InvoiceId invId) <$> note ("name_registrations.method: " <> m) (textMethod m)
|
|
(Nothing, Nothing, Just codeId) -> Right $ RPCode codeId
|
|
_ -> Left $ "name_registrations payment: " <> rid
|
|
nrReveal <- case (label_, secret_, nonce_, deadline_, signature_) of
|
|
(Nothing, Nothing, Nothing, Nothing, Nothing) -> Right Nothing
|
|
(Just rvLabel, Just (Binary rvSecret), Just nonce, Just deadline, Just (Binary sig)) -> do
|
|
rvSignature <- decodeSignature sig
|
|
Right $ Just NameReveal {rvLabel, rvSecret, rvContactLink = nrContactLink, rvChannelLink = nrChannelLink, rvNonce = word64 nonce, rvDeadline = word64 deadline, rvSignature}
|
|
_ -> Left $ "name_registrations reveal: " <> rid
|
|
pure
|
|
NameRegistration
|
|
{ nrId = RegistrationId rid,
|
|
nrVersion,
|
|
nrLabelHash = LabelHash lh,
|
|
nrLength,
|
|
nrDuration = word64 duration,
|
|
nrOwner,
|
|
nrCommitment,
|
|
nrSealed,
|
|
nrPhase,
|
|
nrYears,
|
|
nrPrice,
|
|
nrPayment,
|
|
nrReveal,
|
|
nrCommitTx = txHash <$> commitTx,
|
|
nrCommitBlock = word64 <$> commitBlock,
|
|
nrRegisterTx = txHash <$> registerTx,
|
|
nrRegisterBlock = word64 <$> registerBlock,
|
|
nrRevealAfter = word64 <$> revealAfter,
|
|
nrRevealBefore = word64 <$> revealBefore,
|
|
nrExpiresAt = word64 <$> expiresAt,
|
|
nrIssuedCodeId,
|
|
nrRegisteredAt,
|
|
nrFailedAt,
|
|
nrError,
|
|
nrLive,
|
|
nrOrderHash,
|
|
nrMoves,
|
|
nrLinksRefusedAt,
|
|
nrCreatedAt,
|
|
nrUpdatedAt
|
|
}
|
|
|
|
getRegistration :: DB.Connection -> RegistrationId -> IO (Maybe NameRegistration)
|
|
getRegistration db (RegistrationId rid) =
|
|
firstDecoded toRegistration $ DB.query db (selectRegistrations <> " WHERE r.registration_id = ?") (Only rid)
|
|
|
|
-- | The newest stored order, live or not, with exactly these fields.
|
|
getRegistrationByOrderHash :: DB.Connection -> ByteString -> IO (Maybe NameRegistration)
|
|
getRegistrationByOrderHash db hash =
|
|
firstDecoded toRegistration $
|
|
DB.query db (selectRegistrations <> " WHERE r.order_hash = ? ORDER BY r.created_at DESC, r.registration_id DESC LIMIT 1") (Only $ Binary hash)
|
|
|
|
-- | Overdue whatever status the poller has marked, as long as the invoice is not paid and holds no money, as the sweep decides.
|
|
getLiveOrders :: DB.Connection -> ByteString -> Address -> LabelHash -> UTCTime -> IO [LiveOrder]
|
|
getLiveOrders db commitment owner (LabelHash lh) now =
|
|
DB.query
|
|
db
|
|
( registrationColumns
|
|
<> ", CASE WHEN r.phase = ? AND i.status <> ? AND i.expires_at < ?"
|
|
<> unfunded
|
|
<> " THEN 1 ELSE 0 END"
|
|
<> registrationFrom
|
|
<> " LEFT JOIN sx_badge_service_invoices i ON i.invoice_id = ni.invoice_id"
|
|
<> " WHERE r.live = 1 AND (r.commitment = ? OR (r.owner = ? AND r.label_hash = ?))"
|
|
)
|
|
(PUnpaid, invoiceStatusText ISPaid, truncateToSecond now, Binary commitment, Binary $ unAddress owner, Binary lh)
|
|
>>= decodeAll toLiveOrder
|
|
where
|
|
toLiveOrder (row :. Only (BI loOverdue)) = (\loRegistration -> LiveOrder {loRegistration, loOverdue}) <$> toRegistration row
|
|
|
|
-- | The sweep's own check, on the invoice joined as i.
|
|
unfunded :: Query
|
|
unfunded = unfundedQuery "i.invoice_id"
|
|
|
|
-- | Run in the transaction that inserts the next order, so the unique constraints let it in. The invoice is written before
|
|
-- the registration, as settlement, cancel and the sweep write them; a registration write that loses after it leaves the
|
|
-- caller to roll back.
|
|
closeOverdue :: DB.Connection -> NameRegistration -> UTCTime -> IO VersionWrite
|
|
closeOverdue db r@NameRegistration {nrId = RegistrationId rid, nrVersion, nrPayment} now = do
|
|
-- locked first, so the funding check below, a later statement, sees a payment a concurrent settlement committed
|
|
mapM_ (\invId -> lockInvoice db invId now) $ case nrPayment of
|
|
RPInvoice _ invId -> Just invId
|
|
RPCode _ -> Nothing
|
|
expired <-
|
|
executeChanging
|
|
db
|
|
( [sql|
|
|
UPDATE sx_badge_service_invoices SET status = ?, updated_at = ?
|
|
WHERE status = ? AND invoice_id IN (
|
|
SELECT i.invoice_id FROM sx_badge_service_name_invoices ni
|
|
JOIN sx_badge_service_name_registrations r ON r.registration_id = ni.registration_id
|
|
JOIN sx_badge_service_invoices i ON i.invoice_id = ni.invoice_id
|
|
WHERE ni.registration_id = ? AND r.version = ? AND r.live = 1
|
|
|]
|
|
<> unfunded
|
|
<> " )"
|
|
)
|
|
(invoiceStatusText ISExpired, truncateToSecond now, invoiceStatusText ISOpen, rid, nrVersion)
|
|
if expired > 0
|
|
then updateRegistration db r {nrLive = False} now
|
|
else
|
|
DB.query db "SELECT version FROM sx_badge_service_name_registrations WHERE registration_id = ?" (Only rid) >>= \case
|
|
[Only version] | version == nrVersion -> pure NotOverdue
|
|
_ -> pure VersionChanged
|
|
|
|
-- | Whether an order can still progress to a registration for its name. The unique live indexes allow one such order per commitment,
|
|
-- and one per owner and label hash, so a wallet can order a name again once its registration is done.
|
|
-- An unpaid order is live while its invoice is open, which nrLive carries; a paid order that lost its name can move to another one.
|
|
orderLive :: NameRegistration -> Bool
|
|
orderLive NameRegistration {nrPhase, nrPayment, nrLive} = case nrPhase of
|
|
PUnpaid -> nrLive
|
|
PQueued -> True
|
|
PCommitting -> True
|
|
PCommitted -> True
|
|
PRegistering -> True
|
|
PFailed -> True
|
|
PTaken -> case nrPayment of
|
|
RPInvoice {} -> True
|
|
RPCode _ -> False
|
|
PRegistered -> False
|
|
PCodeIssued -> False
|
|
|
|
-- | Writes every changeable field of a registration read at its nrVersion, unless it changed since, with live as orderLive decides.
|
|
-- A closed order is never made live again, and a write that would make the order meet another live one changes nothing.
|
|
updateRegistration :: DB.Connection -> NameRegistration -> UTCTime -> IO VersionWrite
|
|
updateRegistration db r now = do
|
|
changed <-
|
|
executeChanging
|
|
db
|
|
[sql|
|
|
UPDATE sx_badge_service_name_registrations
|
|
SET version = version + 1, label_hash = ?, length = ?, duration = ?, commitment = ?, sealed = ?, phase = ?, price = ?,
|
|
label = ?, secret = ?, contact_link = ?, channel_link = ?, intent_nonce = ?, intent_deadline = ?, intent_signature = ?,
|
|
commit_tx = ?, commit_block = ?, register_tx = ?, register_block = ?, reveal_after = ?, reveal_before = ?, expires_at = ?,
|
|
issued_code_id = ?, registered_at = ?, failed_at = ?, error = ?, live = ?, moves = ?, links_refused_at = ?, updated_at = ?
|
|
WHERE registration_id = ? AND version = ?
|
|
AND (? = 0 OR (live = 1 AND NOT EXISTS (
|
|
SELECT 1 FROM sx_badge_service_name_registrations o
|
|
WHERE o.live = 1 AND o.registration_id <> ? AND (o.commitment = ? OR (o.owner = ? AND o.label_hash = ?))
|
|
)))
|
|
|]
|
|
( (Binary lh, nrLength, int64 nrDuration, Binary nrCommitment, nrSealed, nrPhase, nrPrice)
|
|
:. (rvLabel <$> nrReveal, Binary . rvSecret <$> nrReveal, rvContactLink =<< nrReveal, rvChannelLink =<< nrReveal, int64 . rvNonce <$> nrReveal, int64 . rvDeadline <$> nrReveal, Binary . serializeSignature . rvSignature <$> nrReveal)
|
|
:. (txHashField <$> nrCommitTx, int64 <$> nrCommitBlock, txHashField <$> nrRegisterTx, int64 <$> nrRegisterBlock, int64 <$> nrRevealAfter, int64 <$> nrRevealBefore, int64 <$> nrExpiresAt)
|
|
:. (nrIssuedCodeId, truncateToSecond <$> nrRegisteredAt, truncateToSecond <$> nrFailedAt, nrError, BI live, nrMoves, truncateToSecond <$> nrLinksRefusedAt, truncateToSecond now)
|
|
:. (rid, nrVersion, BI live, rid, Binary nrCommitment, Binary $ unAddress nrOwner, Binary lh)
|
|
)
|
|
if changed > 0 then pure $ Written (nrVersion + 1) else refusal
|
|
where
|
|
NameRegistration {nrId = RegistrationId rid, nrVersion, nrLabelHash = LabelHash lh, nrLength, nrDuration, nrOwner, nrCommitment, nrSealed, nrPhase, nrPrice, nrReveal} = r
|
|
NameRegistration {nrCommitTx, nrCommitBlock, nrRegisterTx, nrRegisterBlock, nrRevealAfter, nrRevealBefore, nrExpiresAt} = r
|
|
NameRegistration {nrIssuedCodeId, nrRegisteredAt, nrFailedAt, nrError, nrMoves, nrLinksRefusedAt} = r
|
|
live = orderLive r
|
|
refusal =
|
|
DB.query db "SELECT version, live FROM sx_badge_service_name_registrations WHERE registration_id = ?" (Only rid) >>= \case
|
|
[(version, BI storedLive)] | version == nrVersion -> pure $ if storedLive then LiveOrderExists else NotLive
|
|
_ -> pure VersionChanged
|
|
|
|
-- | Another name, or other links: queued again with the reveal, transactions, reveal times and link refusal cleared.
|
|
-- The order hash stays the original order's, so a resend of that order still finds it.
|
|
retargetRegistration :: DB.Connection -> NameRegistration -> OrderFields -> Int -> UTCTime -> IO VersionWrite
|
|
retargetRegistration db r OrderFields {ofLabelHash, ofLength, ofDuration, ofCommitment, ofSealed} price =
|
|
updateRegistration
|
|
db
|
|
r
|
|
{ nrLabelHash = ofLabelHash,
|
|
nrLength = ofLength,
|
|
nrDuration = ofDuration,
|
|
nrCommitment = ofCommitment,
|
|
nrSealed = ofSealed,
|
|
nrPrice = price,
|
|
nrPhase = PQueued,
|
|
nrMoves = nrMoves r + 1,
|
|
nrReveal = Nothing,
|
|
nrCommitTx = Nothing,
|
|
nrCommitBlock = Nothing,
|
|
nrRegisterTx = Nothing,
|
|
nrRegisterBlock = Nothing,
|
|
nrRevealAfter = Nothing,
|
|
nrRevealBefore = Nothing,
|
|
nrLinksRefusedAt = Nothing,
|
|
nrFailedAt = Nothing,
|
|
nrError = Nothing
|
|
}
|
|
|
|
-- | Newest first, at most findLimit.
|
|
findRegistrations :: DB.Connection -> Address -> IO [NameRegistration]
|
|
findRegistrations db owner =
|
|
DB.query db (selectRegistrations <> " WHERE r.owner = ? ORDER BY r.created_at DESC, r.registration_id DESC LIMIT ?") (Binary $ unAddress owner, findLimit)
|
|
>>= decodeAll toRegistration
|
|
|
|
isLabelRegistering :: DB.Connection -> LabelHash -> IO Bool
|
|
isLabelRegistering db (LabelHash lh) =
|
|
not . null
|
|
<$> ( DB.query db "SELECT 1 FROM sx_badge_service_name_registrations WHERE label_hash = ? AND phase = ? LIMIT 1" (Binary lh, PRegistering) ::
|
|
IO [Only Int]
|
|
)
|
|
|
|
-- | The registrations a worker moves on, oldest first.
|
|
activeRegistrations :: DB.Connection -> IO [NameRegistration]
|
|
activeRegistrations db =
|
|
DB.query db (selectRegistrations <> " WHERE r.phase IN (?,?,?,?) ORDER BY r.created_at, r.registration_id") (PQueued, PCommitting, PCommitted, PRegistering)
|
|
>>= decodeAll toRegistration
|
|
|
|
-- | Unpaid orders whose invoice expired before the cutoff, without a recorded payment.
|
|
unpaidCleanupCandidates :: DB.Connection -> UTCTime -> IO [(RegistrationId, InvoiceId)]
|
|
unpaidCleanupCandidates db expiredBefore =
|
|
map (\(rid, invId) -> (RegistrationId rid, InvoiceId invId))
|
|
<$> DB.query
|
|
db
|
|
[sql|
|
|
SELECT r.registration_id, ni.invoice_id
|
|
FROM sx_badge_service_name_registrations r
|
|
JOIN sx_badge_service_name_invoices ni ON ni.registration_id = r.registration_id
|
|
JOIN sx_badge_service_invoices i ON i.invoice_id = ni.invoice_id
|
|
WHERE r.phase = ? AND i.expires_at < ?
|
|
AND NOT EXISTS (SELECT 1 FROM sx_badge_service_payments p WHERE p.invoice_id = ni.invoice_id)
|
|
ORDER BY i.expires_at, r.registration_id
|
|
|]
|
|
(PUnpaid, truncateToSecond expiredBefore)
|
|
|
|
-- | Deletes an unpaid registration and its invoice, unless a payment was recorded for it.
|
|
deleteUnpaidRegistration :: DB.Connection -> RegistrationId -> IO Bool
|
|
deleteUnpaidRegistration db (RegistrationId rid) = do
|
|
invoiceIds <- DB.query db "SELECT invoice_id FROM sx_badge_service_name_invoices WHERE registration_id = ?" (Only rid)
|
|
deleted <-
|
|
executeChanging
|
|
db
|
|
[sql|
|
|
DELETE FROM sx_badge_service_name_registrations
|
|
WHERE registration_id = ? AND phase = ?
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM sx_badge_service_name_invoices ni
|
|
JOIN sx_badge_service_payments p ON p.invoice_id = ni.invoice_id
|
|
WHERE ni.registration_id = ?
|
|
)
|
|
|]
|
|
(rid, PUnpaid, rid)
|
|
when (deleted > 0) $
|
|
mapM_ (DB.execute db "DELETE FROM sx_badge_service_invoices WHERE invoice_id = ?" . Only . fromOnly) (invoiceIds :: [Only Text])
|
|
pure $ deleted > 0
|
|
|
|
-- | Code orders failed before the cutoff that still hold their unspent code.
|
|
failedCodeOrdersBefore :: DB.Connection -> UTCTime -> IO [(RegistrationId, Int64)]
|
|
failedCodeOrdersBefore db cutoff =
|
|
map (\(rid, codeId) -> (RegistrationId rid, codeId))
|
|
<$> DB.query
|
|
db
|
|
[sql|
|
|
SELECT r.registration_id, c.name_prepaid_code_id
|
|
FROM sx_badge_service_name_registrations r
|
|
JOIN sx_badge_service_name_prepaid_codes c ON c.reserved_by = r.registration_id
|
|
WHERE r.phase = ? AND r.failed_at < ? AND c.spent_at IS NULL
|
|
ORDER BY r.failed_at, r.registration_id
|
|
|]
|
|
(PFailed, truncateToSecond cutoff)
|
|
|
|
insertPrepaidCode :: DB.Connection -> NewPrepaidCode -> UTCTime -> IO (Either CodeHashTaken Int64)
|
|
insertPrepaidCode db NewPrepaidCode {npcCodeHash, npcMinLength, npcYears, npcPaymentStatus, npcExpiresAt} now = do
|
|
inserted <-
|
|
executeChanging
|
|
db
|
|
[sql|
|
|
INSERT INTO sx_badge_service_name_prepaid_codes (code_hash, min_length, years, code_payment_status, expires_at, created_at)
|
|
VALUES (?,?,?,?,?,?)
|
|
ON CONFLICT DO NOTHING
|
|
|]
|
|
(Binary npcCodeHash, npcMinLength, npcYears, npcPaymentStatus, truncateToSecond <$> npcExpiresAt, truncateToSecond now)
|
|
if inserted > 0 then Right <$> insertedRowId db else pure $ Left CodeHashTaken
|
|
|
|
prepaidCodeColumns :: Query
|
|
prepaidCodeColumns =
|
|
[sql|
|
|
SELECT name_prepaid_code_id, code_hash, min_length, years, code_payment_status, reserved_by, reserved_at, spent_at, revoked_at, expires_at, created_at
|
|
FROM sx_badge_service_name_prepaid_codes
|
|
|]
|
|
|
|
type PrepaidCodeRow = (Int64, Binary ByteString, Int, Int, BadgeCodePaymentStatus, Maybe Text, Maybe UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe UTCTime) :. Only UTCTime
|
|
|
|
toPrepaidCode :: PrepaidCodeRow -> PrepaidCode
|
|
toPrepaidCode ((pcId, Binary pcCodeHash, pcMinLength, pcYears, pcPaymentStatus, reservedBy, pcReservedAt, pcSpentAt, pcRevokedAt, pcExpiresAt) :. Only pcCreatedAt) =
|
|
PrepaidCode {pcId, pcCodeHash, pcMinLength, pcYears, pcPaymentStatus, pcReservedBy = RegistrationId <$> reservedBy, pcReservedAt, pcSpentAt, pcRevokedAt, pcExpiresAt, pcCreatedAt}
|
|
|
|
getPrepaidCode :: DB.Connection -> ByteString -> IO (Maybe PrepaidCode)
|
|
getPrepaidCode db codeHash =
|
|
maybeFirstRow toPrepaidCode $ DB.query db (prepaidCodeColumns <> " WHERE code_hash = ?") (Only $ Binary codeHash)
|
|
|
|
getPrepaidCodeById :: DB.Connection -> Int64 -> IO (Maybe PrepaidCode)
|
|
getPrepaidCodeById db codeId =
|
|
maybeFirstRow toPrepaidCode $ DB.query db (prepaidCodeColumns <> " WHERE name_prepaid_code_id = ?") (Only codeId)
|
|
|
|
-- | Only an unused code: not reserved, spent or revoked, paid or free, and not expired.
|
|
reservePrepaidCode :: DB.Connection -> Int64 -> RegistrationId -> UTCTime -> IO Bool
|
|
reservePrepaidCode db codeId (RegistrationId rid) now' =
|
|
(> 0)
|
|
<$> executeChanging
|
|
db
|
|
[sql|
|
|
UPDATE sx_badge_service_name_prepaid_codes SET reserved_by = ?, reserved_at = ?
|
|
WHERE name_prepaid_code_id = ? AND reserved_by IS NULL AND spent_at IS NULL AND revoked_at IS NULL
|
|
AND code_payment_status <> ? AND (expires_at IS NULL OR expires_at > ?)
|
|
|]
|
|
(rid, now, codeId, CPSUnpaid, now)
|
|
where
|
|
now = truncateToSecond now'
|
|
|
|
releasePrepaidCode :: DB.Connection -> Int64 -> RegistrationId -> IO Bool
|
|
releasePrepaidCode db codeId (RegistrationId rid) =
|
|
(> 0)
|
|
<$> executeChanging
|
|
db
|
|
[sql|
|
|
UPDATE sx_badge_service_name_prepaid_codes SET reserved_by = NULL, reserved_at = NULL
|
|
WHERE name_prepaid_code_id = ? AND reserved_by = ? AND spent_at IS NULL
|
|
|]
|
|
(codeId, rid)
|
|
|
|
-- | A spent code keeps reserved_by, naming the registration it paid for.
|
|
spendPrepaidCode :: DB.Connection -> Int64 -> RegistrationId -> UTCTime -> IO Bool
|
|
spendPrepaidCode db codeId (RegistrationId rid) now =
|
|
(> 0)
|
|
<$> executeChanging
|
|
db
|
|
"UPDATE sx_badge_service_name_prepaid_codes SET spent_at = ? WHERE name_prepaid_code_id = ? AND reserved_by = ? AND spent_at IS NULL"
|
|
(truncateToSecond now, codeId, rid)
|
|
|
|
-- | A reserved code can be revoked, and its order keeps it; a spent one cannot.
|
|
revokePrepaidCode :: DB.Connection -> ByteString -> UTCTime -> IO RevokeResult
|
|
revokePrepaidCode db codeHash now = do
|
|
revoked <-
|
|
executeChanging
|
|
db
|
|
"UPDATE sx_badge_service_name_prepaid_codes SET revoked_at = ? WHERE code_hash = ? AND revoked_at IS NULL AND spent_at IS NULL"
|
|
(truncateToSecond now, Binary codeHash)
|
|
maybe NoSuchCode (result revoked) <$> getPrepaidCode db codeHash
|
|
where
|
|
result revoked PrepaidCode {pcId, pcRevokedAt}
|
|
| revoked > 0 = Revoked pcId
|
|
| isJust pcRevokedAt = AlreadyRevoked pcId
|
|
| otherwise = AlreadyRedeemed
|
|
|
|
txKind :: TxFor -> (Text, Maybe Text, Maybe Int64)
|
|
txKind = \case
|
|
TxCommit (RegistrationId rid) -> ("commit", Just rid, Nothing)
|
|
TxRegister (RegistrationId rid) -> ("register", Just rid, Nothing)
|
|
TxEdit editId -> ("edit", Nothing, Just editId)
|
|
|
|
-- | Saved once signed and before it is sent, so a restart can rebroadcast it.
|
|
insertTransaction :: DB.Connection -> TxFor -> NewTransaction -> UTCTime -> IO Int64
|
|
insertTransaction db for NewTransaction {ntxRaw, ntxHash, ntxNonce} now = do
|
|
let (kind, rid, editId) = txKind for
|
|
DB.execute
|
|
db
|
|
[sql|
|
|
INSERT INTO sx_badge_service_name_transactions
|
|
(registration_id, name_record_edit_id, kind, raw, tx_hash, registrar_nonce, sent_at, status)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
|]
|
|
(rid, editId, kind, Binary ntxRaw, txHashField ntxHash, int64 ntxNonce, truncateToSecond now, TSPending)
|
|
insertedRowId db
|
|
|
|
-- | Moves a transaction on from the status the caller read, so an outcome is never overwritten unseen; False when it is not in that status.
|
|
recordTransactionOutcome :: DB.Connection -> TxHash -> TxStatus -> TxStatus -> Maybe Word64 -> IO Bool
|
|
recordTransactionOutcome db hash from status block =
|
|
(> 0)
|
|
<$> executeChanging
|
|
db
|
|
"UPDATE sx_badge_service_name_transactions SET status = ?, mined_block = ? WHERE tx_hash = ? AND status = ?"
|
|
(status, int64 <$> block, txHashField hash, from)
|
|
|
|
transactionColumns :: Query
|
|
transactionColumns =
|
|
[sql|
|
|
SELECT name_transaction_id, kind, registration_id, name_record_edit_id, raw, tx_hash, registrar_nonce, sent_at, mined_block, status
|
|
FROM sx_badge_service_name_transactions
|
|
|]
|
|
|
|
type TransactionRow = (Int64, Text, Maybe Text, Maybe Int64, Binary ByteString, Binary ByteString, Int64, UTCTime, Maybe Int64, TxStatus)
|
|
|
|
toTransaction :: TransactionRow -> Either Text NameTransaction
|
|
toTransaction (ntId, kind, rid_, editId_, Binary ntRaw, hash, nonce, ntSentAt, block, ntStatus) = do
|
|
ntFor <- case (kind, rid_, editId_) of
|
|
("commit", Just rid, Nothing) -> Right $ TxCommit $ RegistrationId rid
|
|
("register", Just rid, Nothing) -> Right $ TxRegister $ RegistrationId rid
|
|
("edit", Nothing, Just editId) -> Right $ TxEdit editId
|
|
_ -> Left $ "name_transactions.kind: " <> kind
|
|
pure NameTransaction {ntId, ntFor, ntRaw, ntHash = txHash hash, ntNonce = word64 nonce, ntSentAt, ntMinedBlock = word64 <$> block, ntStatus}
|
|
|
|
-- | Those without a receipt, in nonce order, for the sender to resume.
|
|
pendingTransactions :: DB.Connection -> IO [NameTransaction]
|
|
pendingTransactions db =
|
|
DB.query db (transactionColumns <> " WHERE status = ? ORDER BY registrar_nonce, name_transaction_id") (Only TSPending)
|
|
>>= decodeAll toTransaction
|
|
|
|
registrationTransactions :: DB.Connection -> RegistrationId -> IO [NameTransaction]
|
|
registrationTransactions db (RegistrationId rid) =
|
|
DB.query db (transactionColumns <> " WHERE registration_id = ? ORDER BY name_transaction_id") (Only rid)
|
|
>>= decodeAll toTransaction
|
|
|
|
editTransactions :: DB.Connection -> Int64 -> IO [NameTransaction]
|
|
editTransactions db editId =
|
|
DB.query db (transactionColumns <> " WHERE name_record_edit_id = ? ORDER BY name_transaction_id") (Only editId)
|
|
>>= decodeAll toTransaction
|
|
|
|
pendingEdit :: Text
|
|
pendingEdit = "pending"
|
|
|
|
minedEdit :: Text
|
|
minedEdit = "mined"
|
|
|
|
revertedEdit :: Text
|
|
revertedEdit = "reverted"
|
|
|
|
-- | Nothing when a pending or mined edit already holds the owner's nonce.
|
|
insertRecordEdit :: DB.Connection -> NewRecordEdit -> UTCTime -> IO (Maybe Int64)
|
|
insertRecordEdit db NewRecordEdit {nreLabelHash = LabelHash lh, nreOwner, nreNonce, nreChange, nreDeadline, nreSignature, nreBodyHash} now = do
|
|
let (key, value) = case nreChange of
|
|
RCSetText k v -> (Just k, Just v)
|
|
RCClear -> (Nothing, Nothing)
|
|
inserted <-
|
|
executeChanging
|
|
db
|
|
[sql|
|
|
INSERT INTO sx_badge_service_name_record_edits
|
|
(label_hash, owner, nonce, record_key, record_value, deadline, signature, body_hash, state, created_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
ON CONFLICT DO NOTHING
|
|
|]
|
|
( (Binary lh, Binary $ unAddress nreOwner, int64 nreNonce, key, value, int64 nreDeadline, Binary $ serializeSignature nreSignature, Binary nreBodyHash)
|
|
:. (pendingEdit, truncateToSecond now)
|
|
)
|
|
if inserted > 0 then Just <$> insertedRowId db else pure Nothing
|
|
|
|
recordEditColumns :: Query
|
|
recordEditColumns =
|
|
[sql|
|
|
SELECT name_record_edit_id, label_hash, owner, nonce, record_key, record_value, deadline, signature, body_hash, state, revert, mined_tx, created_at
|
|
FROM sx_badge_service_name_record_edits
|
|
|]
|
|
|
|
type RecordEditRow =
|
|
(Int64, Binary ByteString, Binary ByteString, Int64, Maybe Text, Maybe Text, Int64, Binary ByteString, Binary ByteString, Text)
|
|
:. (Maybe Text, Maybe (Binary ByteString), UTCTime)
|
|
|
|
toRecordEdit :: RecordEditRow -> Either Text RecordEdit
|
|
toRecordEdit ((reId, Binary lh, Binary owner, nonce, key_, value_, deadline, Binary sig, Binary reBodyHash, state) :. (revert, minedTx, reCreatedAt)) = do
|
|
reOwner <- decodeAddress owner
|
|
reSignature <- decodeSignature sig
|
|
reChange <- case (key_, value_) of
|
|
(Just key, Just value) -> Right $ RCSetText key value
|
|
(Nothing, Nothing) -> Right RCClear
|
|
_ -> Left $ "name_record_edits record: " <> tshow reId
|
|
reState <- decodeState
|
|
pure RecordEdit {reId, reLabelHash = LabelHash lh, reOwner, reNonce = word64 nonce, reChange, reDeadline = word64 deadline, reSignature, reBodyHash, reState, reCreatedAt}
|
|
where
|
|
decodeState
|
|
| state == pendingEdit = Right ESPending
|
|
| state == minedEdit = Right $ ESMined $ txHash <$> minedTx
|
|
| state == revertedEdit = Right $ ESReverted revert
|
|
| otherwise = Left $ "name_record_edits.state: " <> state
|
|
|
|
getRecordEdit :: DB.Connection -> Int64 -> IO (Maybe RecordEdit)
|
|
getRecordEdit db editId =
|
|
firstDecoded toRecordEdit $ DB.query db (recordEditColumns <> " WHERE name_record_edit_id = ?") (Only editId)
|
|
|
|
-- | The newest edit sent with this signed body.
|
|
getRecordEditByBody :: DB.Connection -> ByteString -> IO (Maybe RecordEdit)
|
|
getRecordEditByBody db bodyHash =
|
|
firstDecoded toRecordEdit $
|
|
DB.query db (recordEditColumns <> " WHERE body_hash = ? ORDER BY name_record_edit_id DESC LIMIT 1") (Only $ Binary bodyHash)
|
|
|
|
-- | The pending or mined edit holding the owner's nonce.
|
|
getRecordEditByNonce :: DB.Connection -> Address -> Word64 -> IO (Maybe RecordEdit)
|
|
getRecordEditByNonce db owner nonce =
|
|
firstDecoded toRecordEdit $
|
|
DB.query db (recordEditColumns <> " WHERE owner = ? AND nonce = ? AND state IN (?,?)") (Binary $ unAddress owner, int64 nonce, pendingEdit, minedEdit)
|
|
|
|
-- | Settles a pending edit; False when it was settled already.
|
|
settleRecordEdit :: DB.Connection -> Int64 -> EditState -> IO Bool
|
|
settleRecordEdit db editId state =
|
|
(> 0)
|
|
<$> executeChanging
|
|
db
|
|
"UPDATE sx_badge_service_name_record_edits SET state = ?, revert = ?, mined_tx = ? WHERE name_record_edit_id = ? AND state = ?"
|
|
(stateText, revert, txHashField <$> minedTx, editId, pendingEdit)
|
|
where
|
|
(stateText, revert, minedTx) = case state of
|
|
ESPending -> (pendingEdit, Nothing, Nothing)
|
|
ESMined tx -> (minedEdit, Nothing, tx)
|
|
ESReverted name -> (revertedEdit, name, Nothing)
|
|
|
|
-- | When each edit relayed for the name since the given time was sent, oldest first.
|
|
relayedEditTimes :: DB.Connection -> LabelHash -> UTCTime -> IO [UTCTime]
|
|
relayedEditTimes db (LabelHash lh) since =
|
|
map fromOnly
|
|
<$> DB.query
|
|
db
|
|
"SELECT created_at FROM sx_badge_service_name_record_edits WHERE label_hash = ? AND created_at > ? ORDER BY created_at"
|
|
(Binary lh, truncateToSecond since)
|
|
|
|
int64 :: Word64 -> Int64
|
|
int64 = fromIntegral
|
|
|
|
txHash :: Binary ByteString -> TxHash
|
|
txHash (Binary h) = TxHash h
|
|
|
|
txHashField :: TxHash -> Binary ByteString
|
|
txHashField (TxHash h) = Binary h
|
|
|
|
word64 :: Int64 -> Word64
|
|
word64 = fromIntegral
|
|
|
|
-- | Address has no exported constructor; its parser takes lowercase hex without the EIP-55 check.
|
|
decodeAddress :: ByteString -> Either Text Address
|
|
decodeAddress bs = either (const $ Left "address column") Right $ strDecode $ convertToBase Base16 bs
|
|
|
|
decodeSignature :: ByteString -> Either Text RecoverableSignature
|
|
decodeSignature bs = either (Left . ("signature column: " <>) . tshow) Right $ parseSignature $ hex bs
|
|
|
|
note :: Text -> Maybe a -> Either Text a
|
|
note err = maybe (Left err) Right
|
|
|
|
decodeAll :: (row -> Either Text a) -> [row] -> IO [a]
|
|
decodeAll decode = either (E.throwIO . StoreDecodeError) pure . traverse decode
|
|
|
|
firstDecoded :: (row -> Either Text a) -> IO [row] -> IO (Maybe a)
|
|
firstDecoded decode rows = rows >>= fmap listToMaybe . decodeAll decode . take 1
|