Commit f9ea874b authored by Emiliano Balbuena's avatar Emiliano Balbuena

(wip)(refactor): Payments

1 merge request!235WIP: Boost Campaigns (&24)
Pipeline #69643524 failed with stages
in 3 minutes and 1 second
......@@ -100,22 +100,15 @@ class campaigns implements Interfaces\Api
->setEnd((int) ($_POST['end'] ?? 0))
->setBudget((float) ($_POST['budget'] ?? 0));
$payment = $_POST['payment'] ?? null;
if ($payment) {
$campaign
->pushPayment($payment);
}
/** @var Manager $manager */
$manager = Di::_()->get('Boost\Campaigns\Manager');
$manager->setActor(Session::getLoggedInUser());
try {
if (!$isEditing) {
$campaign = $manager->create($campaign);
$campaign = $manager->create($campaign, $_POST['payment'] ?? null);
} else {
$campaign = $manager->update($campaign);
$campaign = $manager->update($campaign, $_POST['payment'] ?? null);
}
return Factory::response([
......
......@@ -8,6 +8,7 @@ use Cassandra;
use Cassandra\Varint;
use Cassandra\Decimal;
use Cassandra\Timestamp;
use Exception;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
......@@ -155,7 +156,7 @@ class Repository
try{
$rows = $this->db->request($query);
} catch(\Exception $e) {
} catch(Exception $e) {
error_log($e->getMessage());
return [];
}
......@@ -197,7 +198,7 @@ class Repository
try{
$rows = $this->db->request($query);
} catch(\Exception $e) {
} catch(Exception $e) {
error_log($e->getMessage());
return [];
}
......@@ -224,6 +225,23 @@ class Repository
}
public function exists($tx)
{
$cql = "SELECT count(*) AS total FROM blockchain_transactions_mainnet_by_tx WHERE tx = ?";
$values = [ (string) $tx ];
$query = new Custom();
$query->query($cql, $values);
$result = $this->db->request($query);
if (!isset($result[0]['total'])) {
throw new Exception('Error checking transaction existence');
}
return (int) $result[0]['total'];
}
public function update($transaction, array $dirty = [])
{
$template = "UPDATE blockchain_transactions_mainnet";
......@@ -260,7 +278,7 @@ class Repository
try {
$success = $this->db->request($query);
} catch (\Exception $e) {
} catch (Exception $e) {
return false;
}
......@@ -276,7 +294,7 @@ class Repository
try{
$rows = $this->db->request($query);
} catch(\Exception $e) {
} catch(Exception $e) {
error_log($e->getMessage());
return [];
}
......
......@@ -9,6 +9,7 @@ namespace Minds\Core\Boost\Campaigns;
use Exception;
use JsonSerializable;
use Minds\Common\Urn;
use Minds\Core\Boost\Campaigns\Payments\Payment;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
......@@ -37,7 +38,8 @@ use Minds\Traits\MagicAttributes;
* @method Campaign setBudget(string $budget)
* @method string getBudgetType()
* @method Campaign setBudgetType(string $budgetType)
* @method array getPayments()
* @method Payment[] getPayments()
* @method Campaign setPayments(Payment[] $payments)
* @method string getChecksum()
* @method Campaign setChecksum(string $checksum)
* @method int getImpressions()
......@@ -164,20 +166,10 @@ class Campaign implements JsonSerializable
}
/**
* @param array|string $payments
* @param Payment $payment
* @return $this
*/
public function setPayments($payments = [])
{
$this->payments = (is_string($payments) ? json_decode($payments, true) : $payments) ?: [];
return $this;
}
/**
* @param mixed $payment
* @return $this
*/
public function pushPayment($payment)
public function pushPayment(Payment $payment)
{
$this->payments[] = $payment;
return $this;
......
......@@ -6,42 +6,50 @@
namespace Minds\Core\Boost\Campaigns\Delegates;
use Exception;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
use Minds\Core\Boost\Campaigns\Payments;
use Minds\Core\Boost\Campaigns\Payments\Payment;
use Minds\Core\Config;
use Minds\Core\Di\Di;
class PaymentsDelegate
{
/**
* @var Config
*/
/** @var Config */
protected $config;
/** @var Payments\Onchain */
protected $onchainPayments;
/**
* PaymentsDelegate constructor.
* @param Config $config
* @param Payments\Onchain $onchainPayments
*/
public function __construct(
$config = null
$config = null,
$onchainPayments = null
)
{
$this->config = $config ?: Di::_()->get('Config');
$this->onchainPayments = $onchainPayments ?: new Payments\Onchain();
}
/**
* @param Campaign $campaign
* @param mixed $paymentPayload
* @return Campaign
* @throws CampaignException
*/
public function onCreate(Campaign $campaign)
public function onCreate(Campaign $campaign, $paymentPayload = null)
{
// NOTE: Do not mock
// NOTE: Do not spec test
$this->validateBudget($campaign);
$campaign = $this->updateImpressionsByCpm($campaign);
$this->registerCampaignPayment($campaign, $paymentPayload);
// TODO: Validate offchain balance, or set as pending for onchain
$campaign = $this->updateImpressionsByCpm($campaign);
return $campaign;
}
......@@ -49,18 +57,21 @@ class PaymentsDelegate
/**
* @param Campaign $campaign
* @param Campaign $campaignRef
* @param mixed $paymentPayload
* @return Campaign
* @throws CampaignException
*/
public function onUpdate(Campaign $campaign, Campaign $campaignRef)
public function onUpdate(Campaign $campaign, Campaign $campaignRef, $paymentPayload = null)
{
// NOTE: Do not mock
// NOTE: Do not spec test
$campaignRef
->setBudgetType($campaign->getBudgetType());
$this->validateBudget($campaignRef);
$campaign = $this->updateImpressionsByCpm($campaign);
$this->registerCampaignPayment($campaignRef, $paymentPayload);
// TODO: Validate balance, set as pending for onchain, refund if needed
// TODO: Ensure budget didn't go lower than impressions met threshold
$campaign = $this->updateImpressionsByCpm($campaign);
return $campaign;
}
......@@ -71,6 +82,8 @@ class PaymentsDelegate
*/
public function onStateChange(Campaign $campaign)
{
// NOTE: Do not spec test
// TODO: Check that campaign is in a final complete or incomplete status (revoked/rejected)
// TODO: Refund!
// TODO: Store refund info onto Campaign/Boost metadata
......@@ -96,6 +109,50 @@ class PaymentsDelegate
return $campaign;
}
/**
* @param Campaign $campaign
* @param mixed $payload
* @return Campaign
* @throws CampaignException
*/
public function registerCampaignPayment(Campaign $campaign, $payload)
{
if (!$payload) {
return $campaign;
}
switch ($campaign->getBudgetType()) {
case 'tokens':
if (!$payload || !$payload['txHash'] || !$payload['address'] || !$payload['amount']) {
throw new CampaignException('Invalid payment signature');
}
$payment = new Payment();
$payment
->setOwnerGuid($campaign->getOwnerGuid())
->setCampaignGuid($campaign->getGuid())
->setTx($payload['txHash'])
->setSource($payload['address'])
->setAmount($payload['amount'])
->setTimeCreated(time());
try {
$this->onchainPayments->register($payment);
} catch (Exception $e) {
throw new CampaignException("Error registering payment: {$e->getMessage()}");
}
$campaign->pushPayment($payment);
break;
default:
throw new CampaignException('Unknown budget type');
}
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
......
......@@ -178,7 +178,6 @@ class ElasticRepository
->setEnd((int) $doc['_source']['end'])
->setBudget((string) $doc['_source']['budget'])
->setBudgetType($doc['_source']['budget_type'])
->setPayments($doc['_source']['payments'])
->setChecksum($doc['_source']['checksum'])
->setImpressions((int) $doc['_source']['impressions'])
->setImpressionsMet($doc['_source']['impressions_met'])
......@@ -216,7 +215,6 @@ class ElasticRepository
'end' => (int) $campaign->getEnd(),
'budget' => $campaign->getBudget(),
'budget_type' => $campaign->getBudgetType(),
'payments' => json_encode($campaign->getPayments()),
'checksum' => $campaign->getChecksum(),
'impressions' => $campaign->getImpressions(),
'@timestamp' => $campaign->getCreatedTimestamp(),
......
......@@ -9,6 +9,7 @@ namespace Minds\Core\Boost\Campaigns;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Common\Urn;
use Minds\Core\Boost\Campaigns\Payments\Repository as PaymentsRepository;
use Minds\Entities\User;
class Manager
......@@ -19,6 +20,9 @@ class Manager
/** @var ElasticRepository */
protected $elasticRepository;
/** @var PaymentsRepository */
protected $paymentsRepository;
/** @var Delegates\CampaignUrnDelegate */
protected $campaignUrnDelegate;
......@@ -41,6 +45,7 @@ class Manager
* Manager constructor.
* @param Repository $repository
* @param ElasticRepository $elasticRepository
* @param PaymentsRepository $paymentsRepository
* @param Delegates\CampaignUrnDelegate $campaignUrnDelegate
* @param Delegates\NormalizeDatesDelegate $normalizeDatesDelegate
* @param Delegates\NormalizeEntityUrnsDelegate $normalizeEntityUrnsDelegate
......@@ -50,6 +55,7 @@ class Manager
public function __construct(
$repository = null,
$elasticRepository = null,
$paymentsRepository = null,
$campaignUrnDelegate = null,
$normalizeDatesDelegate = null,
$normalizeEntityUrnsDelegate = null,
......@@ -59,6 +65,7 @@ class Manager
{
$this->repository = $repository ?: new Repository();
$this->elasticRepository = $elasticRepository ?: new ElasticRepository();
$this->paymentsRepository = $paymentsRepository ?: new PaymentsRepository();
// Delegates
......@@ -85,7 +92,21 @@ class Manager
*/
public function getList(array $opts = [])
{
return $this->elasticRepository->getList($opts);
return $this->elasticRepository->getList($opts)->map(function (Campaign $campaign) {
try {
$campaign
->setPayments(
$this->paymentsRepository->getList([
'owner_guid' => $campaign->getOwnerGuid(),
'campaign_guid' => $campaign->getGuid(),
])->toArray()
);
} catch (Exception $e) {
error_log("[BoostCampaignsManager] {$e}");
}
return $campaign;
});
}
/**
......@@ -102,7 +123,7 @@ class Manager
return null;
}
$campaigns = $this->elasticRepository->getList([
$campaigns = $this->getList([
'guid' => $guid
])->toArray();
......@@ -115,11 +136,11 @@ class Manager
/**
* @param Campaign $campaign
* @param mixed $paymentPayload
* @return Campaign
* @throws CampaignException
* @throws Exception
*/
public function create(Campaign $campaign)
public function create(Campaign $campaign, $paymentPayload = null)
{
$campaign = $this->campaignUrnDelegate->onCreate($campaign);
......@@ -168,7 +189,7 @@ class Manager
// Run payments delegate (should be ALWAYS called after normalizing dates)
$campaign = $this->paymentsDelegate->onCreate($campaign);
$campaign = $this->paymentsDelegate->onCreate($campaign, $paymentPayload);
// Write
......@@ -186,7 +207,7 @@ class Manager
* @throws CampaignException
* @throws Exception
*/
public function update(Campaign $campaignRef)
public function update(Campaign $campaignRef, $paymentPayload = null)
{
// Load campaign
......@@ -226,7 +247,7 @@ class Manager
// Run payments delegate (should be ALWAYS called after normalizing dates)
$campaign = $this->paymentsDelegate->onUpdate($campaign, $campaignRef);
$campaign = $this->paymentsDelegate->onUpdate($campaign, $campaignRef, $paymentPayload);
// Write
......
<?php
/**
* Onchain
* @author edgebal
*/
namespace Minds\Core\Boost\Campaigns\Payments;
use Exception;
use Minds\Core\Blockchain\Services\Ethereum;
use Minds\Core\Blockchain\Transactions\Repository as TransactionsRepository;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Util\BigNumber;
class Onchain
{
/** @var Config */
protected $config;
/** @var Ethereum */
protected $eth;
/** @var Repository */
protected $repository;
/** @var TransactionsRepository */
protected $txRepository;
public function __construct(
$config = null,
$eth = null,
$repository = null,
$txRepository = null
)
{
$this->config = $config ?: Di::_()->get('Config');
$this->eth = $eth ?: Di::_()->get('Blockchain\Services\Ethereum');
$this->repository = $repository ?: new Repository();
$this->txRepository = $txRepository ?: Di::_()->get('Blockchain\Transactions\Repository');
}
/**
* @param Payment $payment
* @return bool
* @throws Exception
*/
public function register(Payment $payment)
{
if ($this->txRepository->exists($payment->getTx())) {
throw new Exception('Payment transaction already exists');
}
$transaction = new Transaction();
$transaction
->setTx($payment->getTx())
->setContract('boost_campaign')
->setAmount((string) BigNumber::toPlain($payment->getAmount(), 18))
->setWalletAddress($payment->getSource())
->setTimestamp($payment->getTimeCreated())
->setUserGuid($payment->getOwnerGuid())
->setData([]);
$this->repository->add($payment);
$this->txRepository->add($transaction);
return true;
}
// public function validate($from, $tx)
// {
// $tokenAddress = $this->config->get('blockchain')['token_address'] ?? null;
// $boostCampaignWalletAddress = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_address'] ?? null;
//
// if (!$tokenAddress || !$boostCampaignWalletAddress) {
// throw new Exception('Missing token or boost campaign wallet addresses');
// }
//
// $receipt = $this->eth->request('eth_getTransactionReceipt', [ $tx ]);
//
// // TODO: Skip failed transactions
//
// // TODO: Review this
// $destinationAddress = sprintf("0x%s", substr($receipt['logs'][0]['topics'][2] ?? '', -40));
//
// // TODO: Review this
// $hexWei = ltrim(str_replace('0x', '', $receipt['logs'][0]['data'] ?? ''), '0');
// $amount = BigNumber::fromPlain(BigNumber::fromHex($hexWei), 18)->toDouble();
//
// if (!$receipt) {
// throw new Exception("Invalid payment: {$tx}");
// } elseif (strtolower($receipt['from']) !== strtolower($from)) {
// throw new Exception("Invalid payment source for {$tx}");
// } elseif (strtolower($receipt['to']) !== strtolower($tokenAddress)) {
// throw new Exception("Invalid token address for {$tx}");
// } elseif (strtolower($destinationAddress) !== strtolower($boostCampaignWalletAddress)) {
// throw new Exception("Invalid boost campaign address for {$tx}");
// }
//
// return $amount;
// }
}
<?php
/**
* Payment
* @author edgebal
*/
namespace Minds\Core\Boost\Campaigns\Payments;
use JsonSerializable;
use Minds\Traits\MagicAttributes;
/**
* Class Payment
* @package Minds\Core\Boost\Campaigns\Payments
* @method int|string getOwnerGuid()
* @method Payment setOwnerGuid(int|string $ownerGuid)
* @method int|string getCampaignGuid()
* @method Payment setCampaignGuid(int|string $campaignGuid)
* @method string getTx()
* @method Payment setTx(string $tx)
* @method string getSource()
* @method Payment setSource(string $source)
* @method string getAmount()
* @method Payment setAmount(string $amount)
* @method int getTimeCreated()
* @method Payment setTimeCreated(int $timeCreated)
*/
class Payment implements JsonSerializable
{
use MagicAttributes;
/** @var int|string */
protected $ownerGuid;
/** @var int|string */
protected $campaignGuid;
/** @var string */
protected $tx;
/** @var string */
protected $source;
/** @var string */
protected $amount;
/** @var int */
protected $timeCreated;
/**
* @return array
*/
public function export()
{
return [
'owner_guid' => (string) $this->ownerGuid,
'campaign_guid' => (string) $this->campaignGuid,
'tx' => $this->tx,
'source' => $this->source,
'amount' => $this->amount,
'time_created' => $this->timeCreated,
];
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return $this->export();
}
}
<?php
/**
* Repository
* @author edgebal
*/
namespace Minds\Core\Boost\Campaigns\Payments;
use Cassandra\Bigint;
use Cassandra\Timestamp;
use Cassandra\Varint;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\Cassandra\Client as CassandraClient;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
use NotImplementedException;
class Repository
{
/** @var CassandraClient */
protected $db;
/**
* Repository constructor.
* @param CassandraClient $db
*/
public function __construct(
$db = null
)
{
$this->db = $db ?: Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param array $opts
* @return Response
*/
public function getList(array $opts = [])
{
$opts = array_merge([
'owner_guid' => null,
'campaign_guid' => null,
'tx' => null,
], $opts);
$cql = "SELECT * FROM boost_campaigns_payments";
$where = [];
$values = [];
if ($opts['owner_guid']) {
$where[] = 'owner_guid = ?';
$values[] = new Bigint($opts['owner_guid']);
}
if ($opts['campaign_guid']) {
$where[] = 'campaign_guid = ?';
$values[] = new Bigint($opts['campaign_guid']);
}
if ($opts['tx']) {
$where[] = 'tx = ?';
$values[] = (string) $opts['tx'];
}
if ($where) {
$cql .= sprintf(" WHERE %s", implode(' AND ', $where));
}
$prepared = new Custom();
$prepared->query($cql, $values);
$response = new Response();
try {
// TODO: Use Cassandra Scroll for getList
$rows = $this->db->request($prepared);
foreach ($rows ?: [] as $row) {
$payment = new Payment();
$payment
->setOwnerGuid($row['owner_guid']->toInt())
->setCampaignGuid($row['campaign_guid']->toInt())
->setTx($row['tx'])
->setSource($row['source'])
->setAmount($row['amount'])
->setTimeCreated($row['time_created']->time());
$response[] = $payment;
}
} catch (Exception $e) {
$response->setException($e);
}
return $response;
}
/**
* @param Payment $payment
* @return bool
*/
public function add(Payment $payment)
{
$cql = "INSERT INTO boost_campaigns_payments (owner_guid, campaign_guid, tx, source, amount, time_created) VALUES (?, ?, ?, ?, ?, ?)";
$values = [
new Bigint($payment->getOwnerGuid()),
new Bigint($payment->getCampaignGuid()),
(string) $payment->getTx(),
(string) $payment->getSource(),
new Varint($payment->getAmount()),
new Timestamp($payment->getTimeCreated())
];
$prepared = new Custom();
$prepared->query($cql, $values);
return (bool) $this->db->request($prepared, true);
}
/**
* @param Payment $payment
* @return bool
*/
public function update(Payment $payment)
{
return $this->add($payment);
}
/**
* @param Payment $payment
* @throws NotImplementedException
*/
public function delete(Payment $payment)
{
throw new NotImplementedException();
}
}
......@@ -63,7 +63,6 @@ class Repository
'end' => $campaign->getEnd(),
'budget' => $campaign->getBudget(),
'budget_type' => $campaign->getBudgetType(),
'payments' => $campaign->getPayments(),
'checksum' => $campaign->getChecksum(),
'impressions' => $campaign->getImpressions(),
'impressions_met' => $campaign->getImpressionsMet(),
......
......@@ -1430,3 +1430,13 @@ CREATE MATERIALIZED VIEW minds.boost_campaigns_by_owner AS
WHERE type IS NOT null AND owner_guid IS NOT null AND guid IS NOT null
PRIMARY KEY (type, owner_guid, guid)
WITH CLUSTERING ORDER BY (owner_guid ASC, guid DESC);
CREATE TABLE minds.boost_campaigns_payments (
owner_guid bigint,
campaign_guid bigint,
tx text,
source text,
amount varint,
time_created timestamp,
PRIMARY KEY (owner_guid, campaign_guid, tx)
) WITH CLUSTERING ORDER BY (campaign_guid ASC, tx ASC);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment