Skip to content
Commits on Source (16)
......@@ -2,10 +2,7 @@
namespace Minds\Api;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain\Security as ProDomainSecurity;
use Minds\Interfaces;
use Minds\Helpers;
use Minds\Core\Security;
use Minds\Core\Session;
......@@ -111,11 +108,17 @@ class Factory
static::setCORSHeader();
$code = !Security\XSRF::validateRequest() ? 403 : 401;
if (isset($_SERVER['HTTP_APP_VERSION'])) {
$code = 401; // Mobile requires 401 errors
}
header('Content-type: application/json');
header('HTTP/1.1 401 Unauthorized', true, 401);
http_response_code($code);
echo json_encode([
'error' => 'Sorry, you are not authenticated',
'code' => 401,
'code' => $code,
'loggedin' => false
]);
exit;
......
<?php
namespace Minds\Controllers\api\v2\admin\rewards;
use Minds\Api\Exportable;
use Minds\Core\Rewards\Withdraw\Repository;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use Minds\Core\Rewards\Withdraw\Manager;
use Minds\Core\Rewards\Withdraw\Request;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
......@@ -11,32 +14,38 @@ class withdrawals implements Interfaces\Api, Interfaces\ApiAdminPam
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function get($pages)
{
$repository = new Repository();
$username = $_GET['user'];
/** @var Manager $manager */
$manager = Di::_()->get('Rewards\Withdraw\Manager');
if (!$username) {
return Factory::response([
'withdrawals' => [],
'load-next' => '',
]);
$userGuid = null;
if ($_GET['user']) {
$userGuid = (new User(strtolower($_GET['user'])))->guid;
}
$user = new User(strtolower($username));
$status = $_GET['status'] ?? null;
$withdrawals = $repository->getList([
$opts = [
'status' => $status,
'user_guid' => $userGuid,
'limit' => isset($_GET['limit']) ? (int) $_GET['limit'] : 12,
'offset' => isset($_GET['offset']) ? $_GET['offset'] : '',
'user_guid' => $user->guid
]);
'hydrate' => true,
'admin' => true,
];
/** @var Response $withdrawals */
$withdrawals = $manager->getList($opts);
return Factory::response([
'withdrawals' => Exportable::_($withdrawals['withdrawals']),
'load-next' => (string) base64_encode($withdrawals['token']),
'withdrawals' => $withdrawals,
'load-next' => $withdrawals->getPagingToken(),
]);
}
......@@ -57,6 +66,37 @@ class withdrawals implements Interfaces\Api, Interfaces\ApiAdminPam
*/
public function put($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get('Rewards\Withdraw\Manager');
$request = $manager->get(
(new Request())
->setUserGuid((string) $pages[0] ?? null)
->setTimestamp((int) $pages[1] ?? null)
->setTx((string) $pages[2] ?? null)
);
if (!$request) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Missing request',
]);
}
try {
$success = $manager->approve($request);
} catch (Exception $exception) {
$success = false;
$errorMessage = $exception->getMessage();
}
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Cannot approve request',
]);
}
return Factory::response([]);
}
......@@ -67,6 +107,37 @@ class withdrawals implements Interfaces\Api, Interfaces\ApiAdminPam
*/
public function delete($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get('Rewards\Withdraw\Manager');
$request = $manager->get(
(new Request())
->setUserGuid((string) $pages[0] ?? null)
->setTimestamp((int) $pages[1] ?? null)
->setTx((string) $pages[2] ?? null)
);
if (!$request) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Missing request',
]);
}
try {
$success = $manager->reject($request);
} catch (Exception $exception) {
$success = false;
$errorMessage = $exception->getMessage();
}
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Cannot reject request',
]);
}
return Factory::response([]);
}
}
......@@ -11,6 +11,14 @@ class analytics implements Interfaces\Api, Interfaces\ApiIgnorePam
{
public function get($pages)
{
// Temporary require admin
if (!Core\Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'Only admins can view these analytics. Use the dashboards instead.',
]);
}
if (!isset($pages[0])) {
return Factory::response([
'status' => 'error',
......
......@@ -123,10 +123,11 @@ class transactions implements Interfaces\Api
break;
case "withdraw":
$request = new Withdraw\Request();
$request->setTx($_POST['tx'])
$request
->setUserGuid(Session::getLoggedInUser()->guid)
->setAddress($_POST['address'])
->setTimestamp(time())
->setTx($_POST['tx'])
->setAddress($_POST['address'])
->setGas($_POST['gas'])
->setAmount((string) BigNumber::_($_POST['amount']));
......
......@@ -14,6 +14,7 @@ use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\SapiEmitter;
use Sentry;
class token implements Interfaces\Api, Interfaces\ApiIgnorePam
{
......@@ -70,9 +71,10 @@ class token implements Interfaces\Api, Interfaces\ApiIgnorePam
$refreshTokenRepository->revokeRefreshToken($tokenId);
$response = new JsonResponse([]);
} catch (\Exception $e) {
Sentry\captureException($e); // Log to sentry
$body = [
'status' => 'error',
'message' => $exception->getMessage(),
'message' => $e->getMessage(),
];
$response = new JsonResponse($body, 500);
}
......
......@@ -28,8 +28,6 @@ class channel implements Interfaces\Api
*/
public function get($pages)
{
$currentUser = Session::getLoggedinUser();
$channel = new User(strtolower($pages[0]));
$channel->fullExport = true; //get counts
$channel->exportCounts = true;
......@@ -41,6 +39,8 @@ class channel implements Interfaces\Api
]);
}
$currentUser = Session::getLoggedinUser();
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager->setUser($channel);
......
<?php
/**
* authorize
* @author edgebal
*/
namespace Minds\Controllers\api\v2\sso;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\SSO\Manager;
use Minds\Entities\User;
use Minds\Interfaces;
use Zend\Diactoros\ServerRequest;
class authorize implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/** @var ServerRequest */
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function post($pages)
{
$origin = $this->request->getServerParams()['HTTP_ORIGIN'] ?? '';
if (!$origin) {
return Factory::response([
'status' => 'error',
'message' => 'No HTTP Origin header'
]);
}
$domain = parse_url($origin, PHP_URL_HOST);
/** @var Manager $sso */
$sso = Di::_()->get('SSO');
$sso
->setDomain($domain);
try {
$sso
->authorize($_POST['token']);
} catch (Exception $e) {
error_log((string) $e);
return Factory::response([
'status' => 'error',
'message' => 'Cannot authorize',
]);
}
/** @var User $currentUser */
$currentUser = Session::getLoggedinUser();
return Factory::response([
'user' => $currentUser ? $currentUser->export() : null,
]);
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
return Factory::response([]);
}
}
<?php
/**
* connect
* @author edgebal
*/
namespace Minds\Controllers\api\v2\sso;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\SSO\Manager;
use Minds\Interfaces;
use Zend\Diactoros\ServerRequest;
class connect implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/** @var ServerRequest */
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function post($pages)
{
$origin = $this->request->getServerParams()['HTTP_ORIGIN'] ?? '';
if (!$origin) {
return Factory::response([
'status' => 'error',
'message' => 'No HTTP Origin header'
]);
}
$domain = parse_url($origin, PHP_URL_HOST);
/** @var Manager $sso */
$sso = Di::_()->get('SSO');
$sso
->setDomain($domain);
try {
return Factory::response([
'token' => $sso->generateToken()
]);
} catch (Exception $e) {
error_log((string) $e);
return Factory::response([
'status' => 'error',
'message' => 'Cannot connect',
]);
}
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -8,30 +8,39 @@
namespace Minds\Core\Blockchain\Events;
use Minds\Core\Blockchain\Contracts\MindsToken;
use Minds\Core\Blockchain\Transactions\Manager;
use Exception;
use Minds\Core\Blockchain\Transactions\Repository as TransactionsRepository;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Blockchain\Util;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Rewards\Withdraw;
use Minds\Core\Rewards\Withdraw\Manager;
use Minds\Core\Rewards\Withdraw\Request;
use Minds\Core\Util\BigNumber;
class WithdrawEvent implements BlockchainEventInterface
{
/** @var array $eventsMap */
/** @var array */
public static $eventsMap = [
'0x317c0f5ab60805d3e3fb6aaa61ccb77253bbb20deccbbe49c544de4baa4d7f8f' => 'onRequest',
'blockchain:fail' => 'withdrawFail',
];
/** @var Manager $manager */
private $manager;
/** @var Manager */
protected $manager;
/** @var Repository $repository **/
/** @var TransactionsRepository **/
protected $txRepository;
/** @var Config $config */
private $config;
/** @var Config */
protected $config;
/**
* WithdrawEvent constructor.
* @param Manager $manager
* @param TransactionsRepository $txRepository
* @param Config $config
*/
public function __construct($manager = null, $txRepository = null, $config = null)
{
$this->txRepository = $txRepository ?: Di::_()->get('Blockchain\Transactions\Repository');
......@@ -50,30 +59,31 @@ class WithdrawEvent implements BlockchainEventInterface
/**
* @param $topic
* @param array $log
* @throws \Exception
* @param $transaction
* @throws Exception
*/
public function event($topic, array $log, $transaction)
{
$method = static::$eventsMap[$topic];
if ($log['address'] != $this->config->get('blockchain')['contracts']['withdraw']['contract_address']) {
throw new \Exception('Event does not match address');
throw new Exception('Event does not match address');
}
if (method_exists($this, $method)) {
$this->{$method}($log, $transaction);
} else {
throw new \Exception('Method not found');
throw new Exception('Method not found');
}
}
public function onRequest($log, $transaction)
public function onRequest($log, Transaction $transaction)
{
$address = $log['address'];
if ($address != $this->config->get('blockchain')['contracts']['withdraw']['contract_address']) {
$this->withdrawFail($log, $transaction);
throw new \Exception('Incorrect address sent the withdraw event');
throw new Exception('Incorrect address sent the withdraw event');
}
$tx = $log['transactionHash'];
......@@ -82,29 +92,43 @@ class WithdrawEvent implements BlockchainEventInterface
$gas = (string) BigNumber::fromHex($gas);
$amount = (string) BigNumber::fromHex($amount);
//double check the details of this transaction match with what the user actually requested
$request = new Withdraw\Request();
$request
->setTx($tx)
->setAddress($address)
->setUserGuid($user_guid)
->setGas($gas)
->setTimestamp($transaction->getTimestamp())
->setAmount($amount);
try {
$this->manager->complete($request, $transaction);
} catch (\Exception $e) {
error_log(print_r($e, true));
$request = $this->manager->get(
(new Request())
->setUserGuid($user_guid)
->setTimestamp($transaction->getTimestamp())
->setTx($tx)
);
if (!$request) {
throw new \Exception('Unknown withdrawal');
}
if ((string) $address !== (string) $request->getAddress()) {
throw new \Exception('Wrong address value');
} elseif ((string) $gas !== (string) $request->getGas()) {
throw new \Exception('Wrong gas value');
} elseif ((string) $amount !== (string) $request->getAmount()) {
throw new \Exception('Wrong amount value');
}
$this->manager->confirm($request, $transaction);
} catch (Exception $e) {
$this->manager->fail(
(new Request())
->setUserGuid($user_guid)
->setTimestamp($transaction->getTimestamp())
->setTx($tx)
);
error_log($e);
}
}
public function withdrawFail($log, $transaction)
{
if ($transaction->getContract() !== 'withdraw') {
throw new \Exception("Failed but not a withdrawal");
return;
throw new Exception("Failed but not a withdrawal");
}
$transaction->setFailed(true);
......
......@@ -28,12 +28,6 @@ class Homepage121119 implements HypothesisInterface
(new Bucket)
->setId('form')
->setWeight(25),
(new Bucket)
->setId('base-take-back-control')
->setWeight(25),
(new Bucket)
->setId('form-take-back-control')
->setWeight(25),
];
}
}
......@@ -16,6 +16,7 @@ class Minds extends base
private $modules = [
Events\Module::class,
SSO\Module::class,
Email\Module::class,
Experiments\Module::class,
Helpdesk\Module::class,
......
......@@ -71,6 +71,16 @@ class Domain
return !$settings || ((string) $settings->getUserGuid() === $userGuid);
}
/**
* @param string $domain
* @return bool
*/
public function isRoot(string $domain): bool
{
$rootDomains = $this->config->get('pro')['root_domains'] ?? [];
return in_array(strtolower($domain), $rootDomains, true);
}
/**
* @param Settings $settings
* @param User|null $owner
......
<?php
/**
* Security
* @author edgebal
*/
namespace Minds\Core\Pro\Domain;
use Exception;
use Minds\Common\Cookie;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Zend\Diactoros\ServerRequest;
class Security
{
/** @var string */
const JWT_COOKIE_NAME = 'PRO-XSRF-JWT';
/** @var string */
const XSRF_COOKIE_NAME = 'XSRF-TOKEN';
/** @var Cookie */
protected $cookie;
/** @var Jwt */
protected $jwt;
/** @var Config */
protected $config;
/**
* Security constructor.
* @param Cookie $cookie
* @param Jwt $jwt
* @param Config $config
*/
public function __construct(
$cookie = null,
$jwt = null,
$config = null
) {
$this->cookie = $cookie ?: new Cookie();
$this->jwt = $jwt ?: new Jwt();
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param string $domain
* @return string
* @throws Exception
*/
public function setUp($domain): string
{
$nonce = $this->jwt->randomString();
$nbf = time();
$exp = $nbf + 60;
$jwt = $this->jwt
->setKey($this->getEncryptionKey())
->encode([
'nonce' => $nonce,
], $exp, $nbf);
$this->cookie
->setName(static::JWT_COOKIE_NAME)
->setValue($jwt)
->setExpire($exp)
->setPath('/')
->setHttpOnly(false)
->create();
$this->cookie
->setName(static::XSRF_COOKIE_NAME)
->setValue($nonce)
->setExpire(0)
->setPath('/')
->setHttpOnly(false)
->create();
return $jwt;
}
/**
* @param ServerRequest $request
*/
public function syncCookies(ServerRequest $request): void
{
$jwt = $request->getServerParams()['HTTP_X_PRO_XSRF_JWT'] ?? '';
if (!$jwt) {
return;
}
try {
$data = $this->jwt
->setKey($this->getEncryptionKey())
->decode($jwt);
if (($_COOKIE[static::XSRF_COOKIE_NAME] ?? null) === $data['nonce']) {
return;
}
$this->cookie
->setName(static::XSRF_COOKIE_NAME)
->setValue($data['nonce'])
->setExpire(0)
->setPath('/')
->setHttpOnly(false)
->create();
} catch (Exception $e) {
// Invalid or expired JWT
}
}
/**
* @return string
*/
protected function getEncryptionKey(): string
{
return $this->config->get('oauth')['encryption_key'] ?? '';
}
}
......@@ -24,10 +24,6 @@ class ProProvider extends Provider
return new Domain();
}, ['useFactory' => true]);
$this->di->bind('Pro\Domain\Security', function ($di) {
return new Domain\Security();
}, ['useFactory' => true]);
$this->di->bind('Pro\Domain\Subscription', function ($di) {
return new Domain\Subscription();
}, ['useFactory' => true]);
......
......@@ -1546,3 +1546,12 @@ CREATE TABLE minds.experiments (
AND read_repair_chance = 0.0
AND speculative_retry = '99PERCENTILE';
CREATE INDEX experiments_key_idx ON minds.experiments (key);
ALTER TABLE minds.withdrawals ADD (status text, address text, gas varint);
CREATE MATERIALIZED VIEW minds.withdrawals_by_status AS
SELECT *
FROM minds.withdrawals
WHERE status IS NOT NULL AND user_guid IS NOT NULL AND timestamp IS NOT NULL AND tx IS NOT NULL
PRIMARY KEY (status, timestamp, user_guid, tx)
WITH CLUSTERING ORDER BY (timestamp ASC, user_guid ASC, tx ASC);
<?php
/**
* NotificationsDelegate
* @author edgebal
*/
namespace Minds\Core\Rewards\Withdraw\Delegates;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Events\EventsDispatcher;
use Minds\Core\Rewards\Withdraw\Request;
use Minds\Core\Util\BigNumber;
class NotificationsDelegate
{
/** @var EventsDispatcher */
protected $dispatcher;
/**
* NotificationsDelegate constructor.
* @param EventsDispatcher $dispatcher
*/
public function __construct(
$dispatcher = null
) {
$this->dispatcher = $dispatcher ?: Di::_()->get('EventsDispatcher');
}
/**
* @param Request $request
*/
public function onRequest(Request $request): void
{
$message = 'Your token withdrawal request was submitted successfully.';
$this->dispatcher->trigger('notification', 'all', [
'to' => [ $request->getUserGuid() ],
'from' => 100000000000000519,
'notification_view' => 'custom_message',
'params' => ['message' => $message],
'message' => $message,
]);
}
/**
* @param Request $request
*/
public function onConfirm(Request $request): void
{
$message = 'Your token withdrawal request transaction was confirmed by the blockchain and has been placed onto the review queue.';
$this->dispatcher->trigger('notification', 'all', [
'to' => [ $request->getUserGuid() ],
'from' => 100000000000000519,
'notification_view' => 'custom_message',
'params' => ['message' => $message],
'message' => $message,
]);
}
/**
* @param Request $request
*/
public function onFail(Request $request): void
{
$message = 'Your token withdrawal request transaction failed. Please contact an administrator.';
$this->dispatcher->trigger('notification', 'all', [
'to' => [ $request->getUserGuid() ],
'from' => 100000000000000519,
'notification_view' => 'custom_message',
'params' => ['message' => $message],
'message' => $message,
]);
}
/**
* @param Request $request
* @throws Exception
*/
public function onApprove(Request $request): void
{
$message = sprintf(
"Your withdrawal request has been approved and %g OnChain token(s) were issued.",
BigNumber::fromPlain($request->getAmount(), 18)->toDouble()
);
$this->dispatcher->trigger('notification', 'all', [
'to' => [ $request->getUserGuid() ],
'from' => 100000000000000519,
'notification_view' => 'custom_message',
'params' => ['message' => $message],
'message' => $message,
]);
}
/**
* @param Request $request
* @throws Exception
*/
public function onReject(Request $request): void
{
$message = sprintf(
"Your withdrawal request has been rejected. Your %g OffChain token(s) were refunded.",
BigNumber::fromPlain($request->getAmount(), 18)->toDouble()
);
$this->dispatcher->trigger('notification', 'all', [
'to' => [ $request->getUserGuid() ],
'from' => 100000000000000519,
'notification_view' => 'custom_message',
'params' => ['message' => $message],
'message' => $message,
]);
}
}
<?php
/**
* RequestHydrationDelegate
* @author edgebal
*/
namespace Minds\Core\Rewards\Withdraw\Delegates;
use Exception;
use Minds\Core\Rewards\Withdraw\Request;
use Minds\Entities\User;
class RequestHydrationDelegate
{
/**
* @param Request $request
* @return Request
* @throws Exception
*/
public function hydrate(Request $request)
{
$userGuid = $request->getUserGuid();
if (!$userGuid) {
return $request;
}
try {
$user = new User($userGuid);
} catch (Exception $exception) {
$user = null;
}
return $request
->setUser($user);
}
public function hydrateForAdmin(Request $request)
{
if (!$request->getUser()) {
$request = $this->hydrate($request);
if (!$request->getUser()) {
return $request;
}
}
$referrerGuid = $request->getUser()->referrer;
if (!$referrerGuid) {
return $request;
}
try {
$user = new User($referrerGuid);
} catch (Exception $exception) {
// Faux user in case of banned/deleted accounts
$user = new User();
$user->guid = $referrerGuid;
$user->username = $referrerGuid;
}
return $request
->setReferrer($user);
}
}
......@@ -4,10 +4,13 @@
*/
namespace Minds\Core\Rewards\Withdraw;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Blockchain\Services\Ethereum;
use Minds\Core\Blockchain\Transactions\Manager as TransactionsManager;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Blockchain\Wallets\OffChain\Balance;
use Minds\Core\Blockchain\Wallets\OffChain\Transactions;
use Minds\Core\Blockchain\Wallets\OffChain\Balance as OffchainBalance;
use Minds\Core\Blockchain\Wallets\OffChain\Transactions as OffchainTransactions;
use Minds\Core\Config;
use Minds\Core\Data\Locks\LockFailedException;
use Minds\Core\Di\Di;
......@@ -16,38 +19,48 @@ use Minds\Entities\User;
class Manager
{
/** @var \Minds\Core\Blockchain\Transactions\Manager */
/** @var TransactionsManager */
protected $txManager;
/** @var Transactions $offChainTransactions */
/** @var OffchainTransactions */
protected $offChainTransactions;
/** @var Config $config */
/** @var Config */
protected $config;
/** @var Ethereum $eth */
/** @var Ethereum */
protected $eth;
/** @var \Minds\Core\Rewards\Withdraw\Repository */
protected $repo;
/** @var Repository */
protected $repository;
/** @var Balance */
/** @var OffchainBalance */
protected $offChainBalance;
/** @var Delegates\NotificationsDelegate */
protected $notificationsDelegate;
/** @var Delegates\RequestHydrationDelegate */
protected $requestHydrationDelegate;
public function __construct(
$txManager = null,
$offChainTransactions = null,
$config = null,
$eth = null,
$withdrawRepository = null,
$offChainBalance = null
$repository = null,
$offChainBalance = null,
$notificationsDelegate = null,
$requestHydrationDelegate = null
) {
$this->txManager = $txManager ?: Di::_()->get('Blockchain\Transactions\Manager');
$this->offChainTransactions = $offChainTransactions ?: Di::_()->get('Blockchain\Wallets\OffChain\Transactions');
$this->config = $config ?: Di::_()->get('Config');
$this->eth = $eth ?: Di::_()->get('Blockchain\Services\Ethereum');
$this->repo = $withdrawRepository ?: Di::_()->get('Rewards\Withdraw\Repository');
$this->repository = $repository ?: new Repository();
$this->offChainBalance = $offChainBalance ?: Di::_()->get('Blockchain\Wallets\OffChain\Balance');
$this->notificationsDelegate = $notificationsDelegate ?: new Delegates\NotificationsDelegate();
$this->requestHydrationDelegate = $requestHydrationDelegate ?: new Delegates\RequestHydrationDelegate();
}
/**
......@@ -57,43 +70,126 @@ class Manager
*/
public function check($userGuid)
{
if (isset($this->config->get('blockchain')['contracts']['withdraw']['limit_exemptions'])
&& in_array($userGuid, $this->config->get('blockchain')['contracts']['withdraw']['limit_exemptions'], true)) {
if (
isset($this->config->get('blockchain')['contracts']['withdraw']['limit_exemptions'])
&& in_array($userGuid, $this->config->get('blockchain')['contracts']['withdraw']['limit_exemptions'], true)
) {
return true;
}
$previousRequests = $this->repo->getList([
$previousRequests = $this->repository->getList([
'user_guid' => $userGuid,
'contract' => 'withdraw',
'from' => strtotime('-1 day')
]);
return !isset($previousRequests)
return !$previousRequests
|| !isset($previousRequests['withdrawals'])
|| count($previousRequests['withdrawals']) === 0;
}
/**
* Create a request
* @param array $opts
* @return Response
* @throws Exception
*/
public function getList(array $opts = []): Response
{
$opts = array_merge([
'hydrate' => false,
'admin' => false,
], $opts);
$requests = $this->repository->getList($opts);
$response = new Response();
foreach ($requests['withdrawals'] ?? [] as $request) {
if ($opts['hydrate']) {
$request = $this->requestHydrationDelegate->hydrate($request);
}
if ($opts['admin']) {
$request = $this->requestHydrationDelegate->hydrateForAdmin($request);
}
$response[] = $request;
}
$response
->setPagingToken(base64_encode($requests['load-next'] ?? ''));
return $response;
}
/**
* @param Request $request
* @return void
* @throws \Exception
* @param bool $hydrate
* @return Request|null
* @throws Exception
*/
public function request($request)
public function get(Request $request, $hydrate = false): ?Request
{
if (
!$request->getUserGuid() ||
!$request->getTimestamp() ||
!$request->getTx()
) {
throw new Exception('Missing request keys');
}
$requests = $this->repository->getList([
'user_guid' => $request->getUserGuid(),
'timestamp' => $request->getTimestamp(),
'tx' => $request->getTx(),
'limit' => 1,
]);
/** @var Request|null $request */
$request = $requests['withdrawals'][0] ?? null;
if ($request && $hydrate) {
$request = $this->requestHydrationDelegate->hydrate($request);
}
return $request;
}
/**
* @param Request $request
* @return bool
* @throws Exception
*/
public function request($request): bool
{
if (!$this->check($request->getUserGuid())) {
throw new \Exception('A withdrawal has already been requested in the last 24 hours');
throw new Exception('A withdrawal has already been requested in the last 24 hours');
}
$available = BigNumber::_($this->offChainBalance
->setUser(new User($request->getUserGuid()))
->getAvailable());
$user = new User();
$user->guid = (string) $request->getUserGuid();
// Check how much tokens the user can request
$available = BigNumber::_(
$this->offChainBalance
->setUser($user)
->getAvailable()
);
if ($available->lt($request->getAmount())) {
$readableAvailable = round(BigNumber::fromPlain($available, 18)->toDouble(), 4);
throw new \Exception("You can only request {$readableAvailable} tokens.");
throw new Exception(sprintf(
"You can only request %s tokens.",
round(BigNumber::fromPlain($available, 18)->toDouble(), 4)
));
}
// Set request status
$request
->setStatus('pending');
// Setup transaction entity
$transaction = new Transaction();
$transaction
->setTx($request->getTx())
......@@ -108,55 +204,131 @@ class Manager
'address' => $request->getAddress(),
]);
$this->repo->add($request);
// Update
$this->repository->add($request);
$this->txManager->add($transaction);
// Notify
$this->notificationsDelegate->onRequest($request);
//
return true;
}
/**
* Complete the requested transaction
* @param Request $request
* @param Transaction $transaction - the transaction we store
* @return void
* @return bool
* @throws Exception
*/
public function complete($request, $transaction)
public function confirm(Request $request, Transaction $transaction): bool
{
if ($request->getUserGuid() != $transaction->getUserGuid()) {
throw new \Exception('The user who requested this operation does not match the transaction');
if ($request->getStatus() !== 'pending') {
throw new Exception('Request is not pending');
}
if (strtolower($request->getAddress()) != strtolower($transaction->getData()['address'])) {
throw new \Exception('The address does not match the transaction');
if (BigNumber::_($request->getAmount())->lt(0)) {
throw new Exception('The withdraw amount must be positive');
}
if ($request->getAmount() != $transaction->getData()['amount']) {
throw new \Exception('The amount request does not match the transaction');
if ((string) $request->getUserGuid() !== (string) $transaction->getUserGuid()) {
throw new Exception('The user who requested this operation does not match the transaction');
}
if ($request->getGas() != $transaction->getData()['gas']) {
throw new \Exception('The gas requested does not match the transaction');
if (strtolower($request->getAddress()) !== strtolower($transaction->getData()['address'])) {
throw new Exception('The address does not match the transaction');
}
if (BigNumber::_($request->getAmount())->lt(0)) {
throw new \Exception('The withdraw amount must be positive');
if ($request->getAmount() != $transaction->getData()['amount']) {
throw new Exception('The amount request does not match the transaction');
}
if ($request->getGas() != $transaction->getData()['gas']) {
throw new Exception('The gas requested does not match the transaction');
}
//debit the users balance
$user = new User;
$user->guid = (string) $request->getUserGuid();
// Withhold user tokens
try {
$this->offChainTransactions
->setUser($user)
->setType('withdraw')
//->setTx($request->getTx())
->setAmount((string) BigNumber::_($request->getAmount())->neg())
->create();
} catch (LockFailedException $e) {
$this->txManager->add($transaction);
return;
return false;
}
// Set request status
$request
->setStatus('pending_approval');
// Update
$this->repository->add($request);
// Notify
$this->notificationsDelegate->onConfirm($request);
//
return true;
}
/**
* @param Request $request
* @return bool
* @throws Exception
*/
public function fail(Request $request): bool
{
if ($request->getStatus() !== 'pending') {
throw new Exception('Request is not pending');
}
$user = new User;
$user->guid = (string) $request->getUserGuid();
// Set request status
$request
->setStatus('failed');
// Update
$this->repository->add($request);
// Notify
$this->notificationsDelegate->onFail($request);
//
return true;
}
/**
* @param Request $request
* @return bool
* @throws Exception
*/
public function approve(Request $request): bool
{
if ($request->getStatus() !== 'pending_approval') {
throw new Exception('Request is not pending approval');
}
//now issue the transaction
// Send blockchain transaction
$txHash = $this->eth->sendRawTransaction($this->config->get('blockchain')['contracts']['withdraw']['wallet_pkey'], [
'from' => $this->config->get('blockchain')['contracts']['withdraw']['wallet_address'],
'to' => $this->config->get('blockchain')['contracts']['withdraw']['contract_address'],
......@@ -170,10 +342,67 @@ class Manager
])
]);
// Set request status
$request
->setStatus('approved')
->setCompletedTx($txHash)
->setCompleted(true);
$this->repo->add($request);
// Update
$this->repository->add($request);
// Notify
$this->notificationsDelegate->onApprove($request);
//
return true;
}
/**
* @param Request $request
* @return bool
* @throws Exception
*/
public function reject(Request $request): bool
{
if ($request->getStatus() !== 'pending_approval') {
throw new Exception('Request is not pending approval');
}
$user = new User;
$user->guid = (string) $request->getUserGuid();
// Refund tokens
try {
$this->offChainTransactions
->setUser($user)
->setType('withdraw_refund')
->setAmount((string) BigNumber::_($request->getAmount()))
->create();
} catch (LockFailedException $e) {
throw new Exception('Cannot refund rejected withdrawal tokens');
}
// Set request status
$request
->setStatus('rejected');
// Update
$this->repository->add($request);
// Notify
$this->notificationsDelegate->onReject($request);
//
return true;
}
}
......@@ -3,93 +3,83 @@
namespace Minds\Core\Rewards\Withdraw;
use Cassandra;
use Cassandra\Varint;
use Cassandra\Decimal;
use Cassandra\Timestamp;
use Minds\Core\Blockchain\Transactions\Transaction;
use Exception;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
use Minds\Core\Rewards\Transactions;
use Minds\Core\Util\BigNumber;
use Minds\Entities\User;
class Repository
{
/** @var Client */
private $db;
protected $db;
/**
* Repository constructor.
* @param Client $db
*/
public function __construct($db = null)
{
$this->db = $db ? $db : Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param Transaction[]|Transaction $transactions
* @return $this
* @param array $opts
* @return array
*/
public function add($requests)
{
if (!is_array($requests)) {
$requests = [ $requests ];
}
$queries = [];
$template = "INSERT INTO withdrawals (user_guid, timestamp, amount, tx, completed, completed_tx) VALUES (?,?,?,?,?,?)";
foreach ($requests as $request) {
$queries[] = [
'string' => $template,
'values' => [
new Varint($request->getUserGuid()),
new Timestamp($request->getTimestamp()),
new Varint($request->getAmount()),
$request->getTx(),
(bool) $request->isCompleted(),
$request->getCompletedTx()
]
];
}
$this->db->batchRequest($queries, Cassandra::BATCH_UNLOGGED);
return $this;
}
public function getList($options)
public function getList(array $opts): array
{
$options = array_merge([
$opts = array_merge([
'status' => null,
'user_guid' => null,
'from' => null,
'to' => null,
'completed' => null,
'completed_tx' => null,
'limit' => 12,
'offset' => null
], $options);
'offset' => null,
], $opts);
$cql = "SELECT * from withdrawals";
$where = [];
$values = [];
if ($options['user_guid']) {
if ($opts['status']) {
$cql = "SELECT * from withdrawals_by_status";
$where[] = 'status = ?';
$values[] = (string) $opts['status'];
}
if ($opts['user_guid']) {
$where[] = 'user_guid = ?';
$values[] = new Varint($options['user_guid']);
$values[] = new Varint($opts['user_guid']);
}
if ($opts['timestamp']) {
$where[] = 'timestamp = ?';
$values[] = new Timestamp($opts['timestamp']);
}
if ($opts['tx']) {
$where[] = 'tx = ?';
$values[] = (string) $opts['tx'];
}
if ($options['from']) {
if ($opts['from']) {
$where[] = 'timestamp >= ?';
$values[] = new Timestamp($options['from']);
$values[] = new Timestamp($opts['from']);
}
if ($options['to']) {
if ($opts['to']) {
$where[] = 'timestamp <= ?';
$values[] = new Timestamp($options['to']);
$values[] = new Timestamp($opts['to']);
}
if ($options['completed']) {
if ($opts['completed']) {
$where[] = 'completed = ?';
$values[] = (string) $options['completed'];
$values[] = (string) $opts['completed'];
}
if ($where) {
......@@ -99,47 +89,81 @@ class Repository
$query = new Custom();
$query->query($cql, $values);
$query->setOpts([
'page_size' => (int) $options['limit'],
'paging_state_token' => base64_decode($options['offset'], true)
'page_size' => (int) $opts['limit'],
'paging_state_token' => base64_decode($opts['offset'], true),
]);
try {
$rows = $this->db->request($query);
} catch (\Exception $e) {
$requests = [];
foreach ($rows ?: [] as $row) {
$request = new Request();
$request
->setUserGuid((string) $row['user_guid']->value())
->setTimestamp($row['timestamp']->time())
->setTx($row['tx'])
->setAddress($row['address'] ?: '')
->setAmount((string) BigNumber::_($row['amount']))
->setCompleted((bool) $row['completed'])
->setCompletedTx($row['completed_tx'] ?: null)
->setGas((string) BigNumber::_($row['gas']))
->setStatus($row['status'] ?: '')
;
$requests[] = $request;
}
return [
'withdrawals' => $requests,
'token' => $rows->pagingStateToken(),
];
} catch (Exception $e) {
error_log($e->getMessage());
return [];
}
}
if (!$rows) {
return [];
}
/**
* @param Request $request
* @return bool
*/
public function add(Request $request)
{
$cql = "INSERT INTO withdrawals (user_guid, timestamp, tx, address, amount, completed, completed_tx, gas, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$values = [
new Varint($request->getUserGuid()),
new Timestamp($request->getTimestamp()),
$request->getTx(),
(string) $request->getAddress(),
new Varint($request->getAmount()),
(bool) $request->isCompleted(),
((string) $request->getCompletedTx()) ?: null,
new Varint($request->getGas()),
(string) $request->getStatus(),
];
$requests = [];
foreach ($rows as $row) {
$request = new Request();
$request->setUserGuid((string) $row['user_guid']->value());
$request->setTimestamp($row['timestamp']->time());
$request->setAmount((string) BigNumber::_($row['amount']));
$request->setTx($row['tx']);
$request->setCompleted((bool) $row['completed']);
$request->setCompletedTx($row['completed_tx']);
$requests[] = $request;
}
$prepared = new Custom();
$prepared->query($cql, $values);
return [
'withdrawals' => $requests,
'token' => $rows->pagingStateToken()
];
return $this->db->request($prepared, true);
}
public function update($key, $guids)
/**
* @param Request $request
* @return bool
*/
public function update(Request $request)
{
// TODO: Implement update() method.
return $this->add($request);
}
public function delete($entity)
/**
* @param Request $request
* @throws Exception
*/
public function delete(Request $request)
{
// TODO: Implement delete() method.
throw new Exception('Not allowed');
}
}
<?php
namespace Minds\Core\Rewards\Withdraw;
use JsonSerializable;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
class Request
/**
* Class Request
* @package Minds\Core\Rewards\Withdraw
* @method string getTx()
* @method Request setTx(string $tx)
* @method string getCompletedTx
* @method Request setCompletedTx(string $completedTx)
* @method string getAddress()
* @method Request setAddress(string $address)
* @method int|string getUserGuid()
* @method Request setUserGuid(int|string $userGuid)
* @method double getGas()
* @method Request setGas(double $gas)
* @method string getAmount()
* @method Request setAmount(string $amount)
* @method string getStatus()
* @method Request setStatus(string $status)
* @method bool isCompleted()
* @method Request setCompleted(bool $completed)
* @method int getTimestamp()
* @method Request setTimestamp(int $timestamp)
* @method User|null getUser()
* @method Request setUser(User|null $user)
* @method User|null getReferrer()
* @method Request setReferrer(User|null $referrer)
*/
class Request implements JsonSerializable
{
use MagicAttributes;
/** @var string $tx **/
private $tx;
/** @var string **/
protected $tx;
/** @var string $completed_tx **/
private $completed_tx;
/** @var string **/
protected $completedTx;
/** @var string $address **/
private $address;
/** @var string **/
protected $address;
/** @var int $user_guid **/
private $user_guid;
/** @var int|string **/
protected $userGuid;
/** @var double $gas **/
private $gas;
/** @var double **/
protected $gas;
/** @var string $amount **/
private $amount;
/** @var string **/
protected $amount;
/** @var bool $completed **/
private $completed;
/** @var string */
protected $status;
/** @var int $timestamp **/
private $timestamp;
/** @var bool **/
protected $completed;
public function setUserGuid($user_guid)
{
$this->user_guid = $user_guid;
return $this;
}
/** @var int **/
protected $timestamp;
public function getUserGuid()
{
return $this->user_guid;
}
/** @var User */
protected $user;
public function setCompletedTx($completed_tx)
{
$this->completed_tx = $completed_tx;
return $this;
}
public function getCompletedTx()
{
return $this->completed_tx;
}
/** @var User */
protected $referrer;
/**
* @return array
*/
public function export()
{
return [
$data = [
'timestamp' => $this->timestamp,
'amount' => $this->amount,
'user_guid' => $this->user_guid,
'user_guid' => $this->userGuid,
'tx' => $this->tx,
'status' => $this->status,
'completed' => $this->completed,
'completed_tx' => $this->completed_tx
'completed_tx' => $this->completedTx,
];
if ($this->user) {
$data['user'] = $this->user->export();
}
if ($this->referrer) {
$data['referrer'] = $this->referrer->export();
}
return $data;
}
/**
* 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();
}
}