Browse Source

server: Add BalancePostgresDataHandler

scalafmt-draft
Alexis Hernandez 7 years ago
parent
commit
4bce1251d1
  1. 21
      server/app/com/xsn/explorer/data/anorm/BalancePostgresDataHandler.scala
  2. 31
      server/app/com/xsn/explorer/data/anorm/dao/BalancePostgresDAO.scala
  3. 16
      server/app/com/xsn/explorer/data/anorm/parsers/BalanceParsers.scala
  4. 13
      server/app/com/xsn/explorer/errors/balanceErrors.scala
  5. 9
      server/app/com/xsn/explorer/models/Balance.scala
  6. 37
      server/conf/evolutions/default/2.sql
  7. 114
      server/test/com/xsn/explorer/data/BalancePostgresDataHandlerSpec.scala

21
server/app/com/xsn/explorer/data/anorm/BalancePostgresDataHandler.scala

@ -0,0 +1,21 @@
package com.xsn.explorer.data.anorm
import javax.inject.Inject
import com.xsn.explorer.data.anorm.dao.BalancePostgresDAO
import com.xsn.explorer.errors.BalanceUnknownError
import com.xsn.explorer.models.Balance
import org.scalactic.{One, Or}
import play.api.db.Database
class BalancePostgresDataHandler @Inject() (
override val database: Database,
balancePostgresDAO: BalancePostgresDAO)
extends AnormPostgresDataHandler {
def upsert(balance: Balance) = withConnection { implicit conn =>
val maybe = balancePostgresDAO.upsert(balance)
Or.from(maybe, One(BalanceUnknownError))
}
}

31
server/app/com/xsn/explorer/data/anorm/dao/BalancePostgresDAO.scala

@ -0,0 +1,31 @@
package com.xsn.explorer.data.anorm.dao
import java.sql.Connection
import anorm._
import com.xsn.explorer.data.anorm.parsers.BalanceParsers._
import com.xsn.explorer.models.Balance
class BalancePostgresDAO {
def upsert(balance: Balance)(implicit conn: Connection): Option[Balance] = {
SQL(
"""
|INSERT INTO balances
| (address, received, spent, available)
|VALUES
| ({address}, {received}, {spent}, {available})
|ON CONFLICT (address) DO UPDATE
| SET received = balances.received + EXCLUDED.received,
| spent = balances.spent + EXCLUDED.spent,
| available = balances.available + EXCLUDED.available
|RETURNING address, received, spent
""".stripMargin
).on(
'address -> balance.address.string,
'received -> balance.received,
'spent -> balance.spent,
'available -> balance.available
).as(parseBalance.singleOpt).flatten
}
}

16
server/app/com/xsn/explorer/data/anorm/parsers/BalanceParsers.scala

@ -0,0 +1,16 @@
package com.xsn.explorer.data.anorm.parsers
import anorm.SqlParser._
import anorm._
import com.xsn.explorer.models.{Address, Balance}
object BalanceParsers {
val parseAddress = str("address").map(Address.from)
val parseReceived = get[BigDecimal]("received")
val parseSpent = get[BigDecimal]("spent")
val parseBalance = (parseAddress ~ parseReceived ~ parseSpent).map { case address ~ received ~ spent =>
address.map { Balance(_, received, spent) }
}
}

13
server/app/com/xsn/explorer/errors/balanceErrors.scala

@ -0,0 +1,13 @@
package com.xsn.explorer.errors
import com.alexitc.playsonify.models.{PublicError, ServerError}
import play.api.i18n.{Lang, MessagesApi}
sealed trait BalanceError
case object BalanceUnknownError extends BalanceError with ServerError {
override def cause: Option[Throwable] = None
override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = List.empty
}

9
server/app/com/xsn/explorer/models/Balance.scala

@ -0,0 +1,9 @@
package com.xsn.explorer.models
case class Balance(
address: Address,
received: BigDecimal = BigDecimal(0),
spent: BigDecimal = BigDecimal(0)) {
def available: BigDecimal = received - spent
}

37
server/conf/evolutions/default/2.sql

@ -0,0 +1,37 @@
# --- !Ups
-- we pre-compute the balances while storing transactions in order to perform
-- simpler queries while requiring addresses and the available amounts.
CREATE TABLE balances(
address VARCHAR(34) NOT NULL,
received DECIMAL(30, 15) NOT NULL,
spent DECIMAL(30, 15) NOT NULL,
available DECIMAL(30, 15) NOT NULL,
-- constraints
CONSTRAINT balances_address_pk PRIMARY KEY (address),
CONSTRAINT balances_address_format CHECK (address ~ '[a-zA-Z0-9]{34}$')
-- TODO: useful to add them if we can support them
-- CONSTRAINT balances_available_check CHECK (available = received - spent),
-- CONSTRAINT balances_received_ge_spent_check CHECK (received >= spent),
-- CONSTRAINT balances_received_positive_check CHECK (received >= 0),
-- CONSTRAINT balances_spent_positive_check CHECK (spent >= 0)
);
CREATE INDEX balances_available_index ON balances (available);
-- there are certain addresses that we need to hide from public, like the one
-- used for the coin swap.
CREATE TABLE hidden_addresses(
address VARCHAR(34) NOT NULL,
-- constraints
CONSTRAINT hidden_addresses_address_pk PRIMARY KEY (address),
CONSTRAINT hidden_addresses_address_format CHECK (address ~ '[a-zA-Z0-9]{34}$')
);
# --- !Downs
DROP TABLE hidden_addresses;
DROP TABLE balances;

114
server/test/com/xsn/explorer/data/BalancePostgresDataHandlerSpec.scala

@ -0,0 +1,114 @@
package com.xsn.explorer.data
import com.xsn.explorer.data.anorm.BalancePostgresDataHandler
import com.xsn.explorer.data.anorm.dao.BalancePostgresDAO
import com.xsn.explorer.data.common.PostgresDataHandlerSpec
import com.xsn.explorer.helpers.DataHelper
import com.xsn.explorer.models.Balance
import org.scalactic.Good
class BalancePostgresDataHandlerSpec extends PostgresDataHandlerSpec {
lazy val dataHandler = new BalancePostgresDataHandler(database, new BalancePostgresDAO)
"upsert" should {
"create an empty balance" in {
val address = DataHelper.createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW85F")
val balance = Balance(address)
val result = dataHandler.upsert(balance)
result mustEqual Good(balance)
}
"set the available amount" in {
val address = DataHelper.createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF")
val balance = Balance(address, received = BigDecimal(10), spent = BigDecimal(5))
val result = dataHandler.upsert(balance)
result mustEqual Good(balance)
database.withConnection { implicit conn =>
val available = _root_.anorm
.SQL(s"SELECT available FROM balances WHERE address = '${address.string}'")
.as(_root_.anorm.SqlParser.get[BigDecimal]("available").single)
available mustEqual balance.available
}
}
"update an existing balance" in {
val address = DataHelper.createAddress("XfAATXtkRgCdMTrj2fxHvLsKLLmqAjhEAt")
val initialBalance = Balance(address, received = BigDecimal(10), spent = BigDecimal(5))
val patch = Balance(address, received = BigDecimal(10), spent = BigDecimal(10))
val expected = combine(initialBalance, patch)
dataHandler.upsert(initialBalance)
val result = dataHandler.upsert(patch)
result mustEqual Good(expected)
}
"allow to set received coins and then, spend them all" in {
pending // TODO: remove it when database checks are enabled
val address = DataHelper.createAddress("XjfNeGJhLgW3egmsZqdbpCNGfysPs7jTNm")
val initialBalance = Balance(address, received = BigDecimal(10))
val patch = Balance(address, spent = BigDecimal(10))
val expected = combine(initialBalance, patch)
dataHandler.upsert(initialBalance).isGood mustEqual true
val result = dataHandler.upsert(patch)
result mustEqual Good(expected)
}
"fail to set received as negative" in {
pending // TODO: remove it when database checks are enabled
val address = DataHelper.createAddress("XdhDFQBfk4E7GE3GVRe4X1bzxiyxRiN2kr")
val balance = Balance(address, received = BigDecimal(-1))
val result = dataHandler.upsert(balance)
println(result)
result.isBad mustEqual true
}
"fail to set spent as negative" in {
pending // TODO: remove it when database checks are enabled
val address = DataHelper.createAddress("Xry2cCLNDMqLmENGW49vYJZPXHPgpqDZ8K")
val balance = Balance(address, spent = BigDecimal(-1))
val result = dataHandler.upsert(balance)
println(result)
result.isBad mustEqual true
}
"fail to set spent > received" in {
pending // TODO: remove it when database checks are enabled
val address = DataHelper.createAddress("XmCEMpTo4r68N7hsmrYhNbfSqSVNJGb6qx")
val balance = Balance(address, received = BigDecimal(9), spent = BigDecimal(10))
val result = dataHandler.upsert(balance)
println(result)
result.isBad mustEqual true
}
"fail to set go negative on received" in {
pending // TODO: remove it when database checks are enabled
val address = DataHelper.createAddress("XauduFtKWMNZaPxqruayxp3S1kj9rvDxjN")
val initialBalance = Balance(address, received = BigDecimal(10))
val patch = Balance(address, spent = BigDecimal(11))
dataHandler.upsert(initialBalance)
val result = dataHandler.upsert(patch)
println(result)
result.isBad mustEqual true
}
}
private def combine(balances: Balance*): Balance = {
balances.reduce { (a, b) =>
Balance(a.address, a.received + b.received, a.spent + b.spent)
}
}
}
Loading…
Cancel
Save