Commit 1cc3cedd authored by xiphon's avatar xiphon

Abstraction layer to support other coins, Github and Zcoin support

parent 8cdb68ee
...@@ -38,10 +38,10 @@ PUSHER_APP_CLUSTER=mt1 ...@@ -38,10 +38,10 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MONERO_USERNAME= COIN=monero
MONERO_PASSWORD=
RPC_URL=http://127.0.0.1:28080/json_rpc RPC_URL=http://127.0.0.1:28080/json_rpc
RPC_USER=
RPC_PASSWORD=
WALLET_ADDRESS= REPOSITORY_URL=https://repo.getmonero.org/api/v4/projects/54
GITHUB_ACCESS_TOKEN=
GITLAB_URL=https://repo.getmonero.org/api/v4/projects/54
[submodule "storage/app/proposals"]
path = storage/app/proposals
url = https://repo.getmonero.org/monero-project/ccs-proposals.git
branch = master
<?php
namespace App\Coin;
use Illuminate\Console\Command;
use Monero\WalletCommon;
interface Coin
{
public function newWallet() : WalletCommon;
public function onNotifyGetTransactions(Command $command, WalletCommon $wallet);
public function subaddrIndex($addressDetails, $project);
}
class CoinAuto
{
public static function newCoin() : Coin
{
$coin = env('COIN', 'monero');
switch ($coin) {
case 'monero':
return new CoinMonero();
case 'zcoin':
return new CoinZcoin();
default:
throw new \Exception('Unsupported COIN ' . $coin);
}
}
}
<?php
namespace App\Coin;
use Illuminate\Console\Command;
use Monero\WalletCommon;
use Monero\WalletOld;
class CoinMonero implements Coin
{
public function newWallet() : WalletCommon
{
return new WalletOld();
}
public function onNotifyGetTransactions(Command $command, WalletCommon $wallet)
{
$min_height = $command->argument('height') ?? Deposit::max('block_received');
return $wallet->scanIncomingTransfers(max($min_height, 50) - 50);
}
public function subaddrIndex($addressDetails, $project)
{
return $addressDetails['subaddr_index'];
}
}
<?php
namespace App\Coin;
use App\Deposit;
use App\Project;
use Illuminate\Console\Command;
use Monero\WalletCommon;
use Monero\WalletZcoin;
class CoinZcoin implements Coin
{
public function newWallet() : WalletCommon
{
return new WalletZcoin();
}
public function onNotifyGetTransactions(Command $command, WalletCommon $wallet)
{
$skip_txes = Deposit::whereNotNull('tx_id')->where('confirmations', '>', 10)->count();
return $wallet->scanIncomingTransfers($skip_txes)->each(function ($tx) {
$project = Project::where('address', $tx->address)->first();
if ($project) {
$tx->subaddr_index = $project->subaddr_index;
}
});
}
public function subaddrIndex($addressDetails, $project)
{
return $project->id;
}
}
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Coin\CoinAuto;
use App\Project; use App\Project;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Monero\WalletOld;
class GenerateAddresses extends Command class GenerateAddresses extends Command
{ {
...@@ -40,17 +40,21 @@ class GenerateAddresses extends Command ...@@ -40,17 +40,21 @@ class GenerateAddresses extends Command
*/ */
public function handle() public function handle()
{ {
$coin = CoinAuto::newCoin();
$wallet = $coin->newWallet();
$projects = Project::whereNotNull('filename')->whereNull('address')->where('state', 'FUNDING-REQUIRED')->get(); $projects = Project::whereNotNull('filename')->whereNull('address')->where('state', 'FUNDING-REQUIRED')->get();
$wallet = new WalletOld();
foreach ($projects as $project) { foreach ($projects as $project) {
$addressDetails = $wallet->getPaymentAddress(); $addressDetails = $wallet->getPaymentAddress();
$project->address_uri = $wallet->createQrCodeString($addressDetails['address']); $project->address_uri = $wallet->createQrCodeString($addressDetails['address']);
$project->address = $addressDetails['address']; $project->address = $addressDetails['address'];
$project->subaddr_index = $addressDetails['subaddr_index']; $project->subaddr_index = $coin->subaddrIndex($addressDetails, $project);
Storage::disk('public')->put("/img/qrcodes/{$project->subaddr_index}.png", $project->generateQrcode()); Storage::disk('public')->put("/img/qrcodes/{$project->subaddr_index}.png", $project->generateQrcode());
$project->qr_code = "img/qrcodes/{$project->subaddr_index}.png"; $project->qr_code = "img/qrcodes/{$project->subaddr_index}.png";
$project->raised_amount = 0; $project->raised_amount = 0;
$project->save(); $project->save();
$this->info('Project: ' . $project->filename . ', address: ' . $project->address);
} }
} }
......
<?php
namespace App\Console\Commands;
class moneroNotify extends walletNotify
{
private $coin;
private $wallet;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monero:notify
{height? : Scan wallet transactions starting from the specified height}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Checks the monero blockchain for transactions';
}
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Project; use App\Project;
use GitLab\Connection; use App\Repository\State;
use App\Repository\Connection;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
...@@ -40,20 +41,20 @@ class ProcessProposals extends Command ...@@ -40,20 +41,20 @@ class ProcessProposals extends Command
$result = []; $result = [];
$connection = new Connection(new Client()); $connection = new Connection(new Client());
$merged = $connection->mergeRequests('merged'); $merged = $connection->mergeRequests(State::Merged);
foreach ($merged as $request) { foreach ($merged as $request) {
$newFiles = $connection->getNewFiles($request->iid); $newFiles = $connection->getNewFiles($request);
if (sizeof($newFiles) != 1) { if ($newFiles->count() != 1) {
continue; continue;
} }
$filename = $newFiles[0]; $filename = $newFiles->first();
if (!preg_match('/.+\.md$/', $filename)) { if (!preg_match('/.+\.md$/', $filename)) {
continue; continue;
} }
if (basename($filename) != $filename) { if (basename($filename) != $filename) {
continue; continue;
} }
$result[$filename] = $request->web_url; $result[$filename] = $request->url();
} }
return $result; return $result;
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Project; use App\Project;
use App\Repository\State;
use App\Repository\Connection;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use GitLab\Connection;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use stdClass; use stdClass;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
...@@ -76,38 +77,38 @@ class UpdateSiteProposals extends Command ...@@ -76,38 +77,38 @@ class UpdateSiteProposals extends Command
$ideas = []; $ideas = [];
$connection = new Connection(new Client()); $connection = new Connection(new Client());
$mergeRequests = $connection->mergeRequests('opened'); $mergeRequests = $connection->mergeRequests(State::Opened);
foreach ($mergeRequests as $mergeRequest) { foreach ($mergeRequests as $mergeRequest) {
$newFiles = $connection->getNewFiles($mergeRequest->iid); $newFiles = $connection->getNewFiles($mergeRequest);
if (sizeof($newFiles) != 1) { if ($newFiles->count() != 1) {
$this->error ("Skipping MR #$mergeRequest->id '$mergeRequest->title': contains multiple files"); $this->error ("Skipping MR #{$mergeRequest->id()} '{$mergeRequest->title()}': contains multiple files");
continue; continue;
} }
$filename = $newFiles[0]; $filename = $newFiles->first();
if (!preg_match('/.+\.md$/', $filename)) { if (!preg_match('/.+\.md$/', $filename)) {
$this->error("Skipping MR #$mergeRequest->id '$mergeRequest->title': doesn't contain any .md file"); $this->error("Skipping MR #{$mergeRequest->id()} '{$mergeRequest->title()}': doesn't contain any .md file");
continue; continue;
} }
if (basename($filename) != $filename) { if (basename($filename) != $filename) {
$this->error("Skipping MR #$mergeRequest->id '$mergeRequest->title': $filename must be in the root folder"); $this->error("Skipping MR #{$mergeRequest->id()} '{$mergeRequest->title()}': $filename must be in the root folder");
continue; continue;
} }
if (in_array($filename, $ideas)) { if (in_array($filename, $ideas)) {
$this->error("Skipping MR #$mergeRequest->id '$mergeRequest->title': duplicated $filename, another MR #$ideas[$filename]->id"); $this->error("Skipping MR #{$mergeRequest->id()} '{$mergeRequest->title()}': duplicated $filename, another MR #$ideas[$filename]->id");
continue; continue;
} }
$project = Project::where('filename', $filename)->first(); $project = Project::where('filename', $filename)->first();
if ($project && $this->proposalFileExists($filename)) { if ($project && $this->proposalFileExists($filename)) {
$this->error("Skipping MR #$mergeRequest->id '$mergeRequest->title': already have a project $filename"); $this->error("Skipping MR #{$mergeRequest->id()} '{$mergeRequest->title()}': already have a project $filename");
continue; continue;
} }
$this->info("Idea MR #$mergeRequest->id '$mergeRequest->title': $filename"); $this->info("Idea MR #{$mergeRequest->id()} '{$mergeRequest->title()}': $filename");
$prop = new stdClass(); $prop = new stdClass();
$prop->name = htmlspecialchars(trim($mergeRequest->title), ENT_QUOTES); $prop->name = htmlspecialchars(trim($mergeRequest->title()), ENT_QUOTES);
$prop->{'gitlab-url'} = htmlspecialchars($mergeRequest->web_url, ENT_QUOTES); $prop->{'gitlab-url'} = htmlspecialchars($mergeRequest->url(), ENT_QUOTES);
$prop->author = htmlspecialchars($mergeRequest->author->username, ENT_QUOTES); $prop->author = htmlspecialchars($mergeRequest->author(), ENT_QUOTES);
$prop->date = date('F j, Y', strtotime($mergeRequest->created_at)); $prop->date = date('F j, Y', $mergeRequest->created_at());
$responseProposals[] = $prop; $responseProposals[] = $prop;
} }
...@@ -119,7 +120,8 @@ class UpdateSiteProposals extends Command ...@@ -119,7 +120,8 @@ class UpdateSiteProposals extends Command
{ {
$prop = new stdClass(); $prop = new stdClass();
$prop->name = $proposal->title; $prop->name = $proposal->title;
$prop->{'donate-url'} = url("projects/{$proposal->subaddr_index}/donate"); $prop->{'donate-address'} = $proposal->address;
$prop->{'donate-qr-code'} = $proposal->address_uri ? $proposal->getQrCodeSrcAttribute() : null;
$prop->{'gitlab-url'} = $proposal->gitlab_url; $prop->{'gitlab-url'} = $proposal->gitlab_url;
$prop->{'local-url'} = '/proposals/'. pathinfo($proposal->filename, PATHINFO_FILENAME) . '.html'; $prop->{'local-url'} = '/proposals/'. pathinfo($proposal->filename, PATHINFO_FILENAME) . '.html';
$prop->contributions = $proposal->contributions; $prop->contributions = $proposal->contributions;
......
...@@ -2,28 +2,30 @@ ...@@ -2,28 +2,30 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Coin\CoinAuto;
use App\Deposit; use App\Deposit;
use App\Project; use App\Project;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Monero\Transaction; use Monero\Transaction;
use Monero\WalletOld;
class walletNotify extends Command class walletNotify extends Command
{ {
private $coin;
private $wallet;
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'monero:notify protected $signature = 'wallet:notify';
{height? : Scan wallet transactions starting from the specified height}';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Checks the monero blockchain for transactions'; protected $description = 'Checks the blockchain for transactions';
/** /**
* Create a new command instance. * Create a new command instance.
...@@ -33,6 +35,9 @@ class walletNotify extends Command ...@@ -33,6 +35,9 @@ class walletNotify extends Command
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->coin = CoinAuto::newCoin();
$this->wallet = $this->coin->newWallet();
} }
/** /**
...@@ -42,18 +47,15 @@ class walletNotify extends Command ...@@ -42,18 +47,15 @@ class walletNotify extends Command
*/ */
public function handle() public function handle()
{ {
$wallet = new WalletOld(); $blockheight = $this->wallet->blockHeight();
$blockheight = $wallet->blockHeight();
if ($blockheight < 1) { if ($blockheight < 1) {
$this->error('monero daemon down or wrong port in db ?'); $this->error('failed to fetch blockchain height');
return; return;
} }
$min_height = $this->argument('height') ?? Deposit::max('block_received'); $transactions = $this->coin->onNotifyGetTransactions($this, $this->wallet);
$transactions = $wallet->scanIncomingTransfers(max($min_height, 50) - 50); $transactions->each(function ($transaction) {
$transactions->each(function ($transaction) use ($wallet) {
$this->processPayment($transaction); $this->processPayment($transaction);
}); });
...@@ -67,6 +69,9 @@ class walletNotify extends Command ...@@ -67,6 +69,9 @@ class walletNotify extends Command
*/ */
public function processPayment(Transaction $transaction) public function processPayment(Transaction $transaction)
{ {
$amountCoins = $transaction->amount / 10 ** $this->wallet->digitsAfterTheRadixPoint();
$details = 'address: ' . $transaction->address . ' amount: '. $amountCoins . ' txid: '.$transaction->id;
$deposit = Deposit::where('tx_id', $transaction->id)->where('subaddr_index', $transaction->subaddr_index)->first(); $deposit = Deposit::where('tx_id', $transaction->id)->where('subaddr_index', $transaction->subaddr_index)->first();
if ($deposit) { if ($deposit) {
if ($deposit->block_received == 0) { if ($deposit->block_received == 0) {
...@@ -76,16 +81,15 @@ class walletNotify extends Command ...@@ -76,16 +81,15 @@ class walletNotify extends Command
return null; return null;
} }
$this->info('amount: '.$transaction->amount / 1000000000000 .' confirmations:'.$transaction->confirmations.' tx_hash:'.$transaction->id);
$this->info('subaddr_index: '.$transaction->subaddr_index);
$this->createDeposit($transaction); $this->createDeposit($transaction);
$project = Project::where('subaddr_index', $transaction->subaddr_index)->first(); if ($project = Project::where('subaddr_index', $transaction->subaddr_index)->first()) {
if ($project) {
// update the project total // update the project total
$project->raised_amount = $project->raised_amount + $transaction->amount * 1e-12; $project->raised_amount = $project->raised_amount + $amountCoins;
$project->save(); $project->save();
$this->info('Donation to "' . $project->filename . '" '. $details);
} else {
$this->error('Unrecognized donation, ' . $details);
} }
return; return;
...@@ -122,7 +126,7 @@ class walletNotify extends Command ...@@ -122,7 +126,7 @@ class walletNotify extends Command
*/ */
public function updateConfirmation($blockheight, Deposit $deposit) public function updateConfirmation($blockheight, Deposit $deposit)
{ {
$diff = $blockheight - $deposit->block_received; $diff = $blockheight - $deposit->block_received + 1;
$deposit->confirmations = $diff; $deposit->confirmations = $diff;
$deposit->save(); $deposit->save();
......
...@@ -46,10 +46,6 @@ class Project extends Model ...@@ -46,10 +46,6 @@ class Project extends Model
return $this->hasMany(Deposit::class, 'subaddr_index', 'subaddr_index'); return $this->hasMany(Deposit::class, 'subaddr_index', 'subaddr_index');
} }
public function getAmountReceivedAttribute() {
return $this->deposits->sum('amount') * 1e-12;
}
public function getPercentageFundedAttribute() { public function getPercentageFundedAttribute() {
return min(100, round($this->raised_amount / $this->target_amount * 100)); return min(100, round($this->raised_amount / $this->target_amount * 100));
} }
......
<?php
namespace App\Repository;
use GuzzleHttp\Client;
class Connection
{
private $repo;
public function __construct(Client $client)
{
$url = env('REPOSITORY_URL') ?? env('GITLAB_URL');
if (parse_url($url, PHP_URL_HOST) == 'github.com') {
$this->repo = new Github($client, $url);
} else {
$this->repo = new Gitlab($client, $url);
}
}
public function mergeRequests($state) {
return $this->repo->mergeRequests($state);
}
public function getNewFiles($merge_request_iid) {
return $this->repo->getNewFiles($merge_request_iid);
}
}
<?php
namespace App\Repository;
use GuzzleHttp\Client;
class PullRequest implements Proposal
{
private $pull_request;
public function __construct($pull_request)
{
$this->pull_request = $pull_request;
}
public function id() : int
{
return $this->pull_request->number;
}
public function url() : string
{
return $this->pull_request->html_url;
}
public function title() : string
{
return $this->pull_request->title;
}
public function author() : string
{
return $this->pull_request->user->login;
}
public function created_at() : int
{
return strtotime($this->pull_request->created_at);
}
}
class Github implements Repository
{
private $client;
private $options = [];
private $owner_repo;
private const stateToString = [ State::Opened => 'open' ,
State::Merged => 'closed'];
public function __construct(Client $client, string $repository_url)
{
$this->client = $client;
$this->owner_repo = parse_url($repository_url, PHP_URL_PATH);
if ($token = env('GITHUB_ACCESS_TOKEN')) {
$this->options = ['headers' => ['Authorization' => 'token ' . $token]];
}
}
private function getUrl($url)
{
return $this->client->request('GET', $url, $this->options);
}
public function mergeRequests($state)
{
$url = 'https://api.github.com/repos' . $this->owner_repo . '/pulls?state=' . Self::stateToString[$state];
$response = $this->getUrl($url);
$result = collect(json_decode($response->getBody()));
if ($state == State::Merged) {
$result = $result->filter(function ($pull_request) {
return $pull_request->merged_at !== null;
});
}
return $result->map(function ($pull_request) {