{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Aftok.Payments.Bitcoin where
import Aftok.Billing
( Billable,
amount,
project,
requestExpiryPeriod,
)
import Aftok.Currency (Currency (BTC))
import Aftok.Currency.Bitcoin
( NetworkMode,
_Satoshi,
getNetwork,
)
import Aftok.Currency.Bitcoin.Payments (PaymentKey (..), PaymentRequest (..))
import Aftok.Database (MonadDB)
import Aftok.Payments.Types
( NativeRequest (Bip70Request),
PaymentOps (..),
PaymentRequestError,
)
import Aftok.Payments.Util (MinPayout (..), getPayouts, getProjectPayoutFractions)
import Aftok.Types (AccountId)
import qualified Bippy as B
import qualified Bippy.Proto as P
import Bippy.Types
( Expiry (Expiry),
Output (Output),
PKIData,
Satoshi (Satoshi),
expiryTime,
getExpires,
getPaymentDetails,
)
import Control.Lens
( (^.),
makeLenses,
)
import Control.Monad.Except (throwError)
import Control.Monad.Trans.Except (except, withExceptT)
import qualified Crypto.PubKey.RSA.Types as RSA
( Error (..),
PrivateKey,
)
import Crypto.Random.Types
( MonadRandom,
getRandomBytes,
)
import Data.AffineSpace ((.+^))
import Data.Map.Strict (assocs)
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.Thyme.Time as C
import Haskoin.Address (Address (..), encodeBase58Check)
import Haskoin.Script (ScriptOutput (..))
import Network.URI (URI)
data BillingOps (m :: * -> *)
= BillingOps
{ memoGen ::
Billable Satoshi -> C.Day -> C.UTCTime -> m (Maybe Text),
uriGen ::
PaymentKey -> m (Maybe URI),
payloadGen ::
Billable Satoshi -> C.Day -> C.UTCTime -> m (Maybe ByteString)
}
data PaymentsConfig
= PaymentsConfig
{ _networkMode :: !NetworkMode,
_signingKey :: !RSA.PrivateKey,
_pkiData :: !PKIData,
_minPayment :: !Satoshi
}
makeLenses ''PaymentsConfig
data PaymentError
= RequestError !PaymentRequestError
| SigningError !RSA.Error
| IllegalAddress !Address
isExpired :: C.UTCTime -> P.PaymentRequest -> Bool
isExpired now req =
let check = any ((now >) . C.toThyme . expiryTime)
in either (error . T.pack) (check . getExpires) $
getPaymentDetails req
paymentOps ::
( MonadRandom m,
MonadDB m
) =>
BillingOps m ->
PaymentsConfig ->
PaymentOps Satoshi (ExceptT PaymentError m)
paymentOps ops cfg =
PaymentOps
{ newPaymentRequest = (((fmap Bip70Request) .) .) . bip70PaymentRequest ops cfg
}
bip70PaymentRequest ::
( MonadRandom m,
MonadDB m
) =>
BillingOps m ->
PaymentsConfig ->
Billable Satoshi ->
C.Day ->
UTCTime ->
ExceptT PaymentError m PaymentRequest
bip70PaymentRequest ops cfg billable billingDay billingTime = do
let billTotal = billable ^. amount
payoutTime = C.mkUTCTime billingDay (fromInteger 0)
payoutFractions <- lift $ getProjectPayoutFractions payoutTime (billable ^. project)
payouts <- withExceptT RequestError $ getPayouts payoutTime BTC (MinPayout $ cfg ^. minPayment) billTotal payoutFractions
outputs <- except $ traverse toOutput (assocs payouts)
pkey <- PaymentKey . encodeBase58Check <$> lift (getRandomBytes 32)
memo <- lift $ memoGen ops billable billingDay billingTime
uri <- lift $ uriGen ops pkey
payload <- lift $ payloadGen ops billable billingDay billingTime
let expiry = Expiry . C.fromThyme $ billingTime .+^ (billable ^. requestExpiryPeriod)
let details =
B.createPaymentDetails
(getNetwork (cfg ^. networkMode))
outputs
(C.fromThyme billingTime)
(Just expiry)
memo
uri
payload
resp <- lift $ B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) details
either (throwError . SigningError) (pure . PaymentRequest pkey) resp
toOutput :: ((AccountId, Address), Satoshi) -> Either PaymentError Output
toOutput ((_, addr), amt) = case addr of
(PubKeyAddress a) -> Right (Output amt (PayPKHash a))
other -> Left $ IllegalAddress other
outputAmount :: Satoshi -> Rational -> Satoshi
outputAmount i r = Satoshi . round $ toRational (i ^. _Satoshi) * r