...
 
......@@ -40,3 +40,6 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
RPC_URL=
WALLET_ADDRESS=
GITLAB_URL=
GITLAB_ACCESS_TOKEN=
\ No newline at end of file
<?php
namespace App\Console\Commands;
use App\Project;
use GitLab\Connection;
use GuzzleHttp\Client;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class fetchMergeRequests extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'gitlab:fetch-proposals';
/**
* The console command description.
*
* @var string
*/
protected $description = 'fetch all the proposal merge requests from gitlab';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = new Connection(new Client());
$projects = $connection->mergeRequests('all');
foreach ($projects as $project) {
// create requests that are still pending
$project = Project::firstOrNew([
'merge_request_id' => $project->id
],[
'title' => $project->title,
'state' => $project->state
]);
// check if there is a payment_id and an address
// check if the amount field has been supplied
$project->save();
}
// id, title, state, username,
// save merge requests
// compare to current merge requests
// if merge request is missing is it closed or merged
return;
}
/**
* Gets the ffs amount requested from a file
*
* @param string $filename
* @return int
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getAmountFromText($filename = 'test-ffs.md')
{
$input = Storage::get($filename);
$lines = preg_split('/\r\n|\r|\n/', $input);
foreach($lines as $line) {
$line = str_replace(' ','', $line);
$details = explode(':', $line);
if ($details[0] === 'amount') {
return $details[1];
}
}
return 0;
}
}
......@@ -27,9 +27,16 @@ use SimpleSoftwareIO\QrCode\Facades\QrCode;
* @method static \Illuminate\Database\Eloquent\Builder|\App\Project whereTargetAmount($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Project whereUpdatedAt($value)
* @mixin \Eloquent
* @property string $title
* @property int|null $merge_request_id
* @property string|null $commit_sha
* @method static \Illuminate\Database\Eloquent\Builder|\App\Project whereCommitSha($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Project whereMergeRequestId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Project whereTitle($value)
*/
class Project extends Model
{
protected $guarded = ['id'];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
......
......@@ -6,9 +6,12 @@
"type": "project",
"require": {
"php": "^7.1.3",
"ext-curl": "*",
"ext-json": "*",
"barryvdh/laravel-ide-helper": "^2.5",
"doctrine/dbal": "^2.8",
"fideloper/proxy": "^4.0",
"guzzlehttp/guzzle": "^6.3",
"laravel/framework": "5.7.*",
"laravel/tinker": "^1.0",
"simplesoftwareio/simple-qrcode": "2.0.*"
......@@ -28,7 +31,8 @@
],
"psr-4": {
"App\\": "app/",
"Monero\\": "monero/"
"Monero\\": "monero/",
"GitLab\\": "gitlab/"
}
},
"autoload-dev": {
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7a5d7380ba557e7bdee9c9e7a997e459",
"content-hash": "37462f3e9af90094d49d90514b64ae96",
"packages": [
{
"name": "bacon/bacon-qr-code",
......@@ -1066,6 +1066,187 @@
],
"time": "2018-02-07T20:20:57+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"shasum": ""
},
"require": {
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.4",
"php": ">=5.5"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.0"
},
"suggest": {
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.3-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "[email protected]",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"time": "2018-04-22T15:46:56+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "[email protected]",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"time": "2016-12-20T10:07:11+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "[email protected]",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2017-03-20T17:10:46+00:00"
},
{
"name": "jakub-onderka/php-console-color",
"version": "v0.2",
......@@ -1846,6 +2027,56 @@
],
"time": "2017-02-14T16:28:37+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/log",
"version": "1.0.2",
......
......@@ -3,11 +3,14 @@
use Faker\Generator as Faker;
$factory->define(\App\Project::class, function (Faker $faker) {
$status = $faker->randomElement(['opened', 'closed', 'locked', 'merged']);
return [
'title' => $faker->sentence(),
'payment_id' => $faker->sha256,
'target_amount' => $faker->randomNumber(),
'status' => $faker->randomElement(['new', 'open', 'funded']),
'created_at' => $faker->dateTime,
'updated_at' => $faker->dateTime,
'target_amount' => $faker->randomFloat(2, 0, 2000),
'state' => $status,
'merge_request_id' => $faker->randomNumber(6),
'created_at' => $faker->dateTimeThisYear,
'updated_at' => $faker->dateTimeThisYear,
];
});
......@@ -15,9 +15,12 @@ class CreateProjectsTable extends Migration
{
Schema::create('projects', function (Blueprint $table) {
$table->increments('id');
$table->string('payment_id')->unique();
$table->string('target_amount');
$table->string('status')->default('open');
$table->string('title');
$table->string('payment_id')->nullable();
$table->string('address')->nullable();
$table->string('target_amount')->nullable();
$table->string('state')->default('opened');
$table->unsignedInteger('merge_request_id')->unique();
$table->timestamps();
});
}
......
<?php
namespace GitLab;
use GuzzleHttp\Client;
class Connection
{
/** @var Client */
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function mergeRequests($state = 'all') {
$url = env('GITLAB_URL') . '/merge_requests?scope=all&per_page=50&state='. $state;
$response = $this->client->request('GET', $url, ['headers' => ['Private-Token' => env('GITLAB_ACCESS_TOKEN')]]);
return collect(json_decode($response->getBody()));
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ namespace Monero;
use App\Project;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class Wallet
{
......@@ -38,27 +39,6 @@ class Wallet
return ['address' => $integratedAddress['integrated_address'], 'paymentId' => $integratedAddress['payment_id']];
}
/**
* Returns all balances (locked/unlocked) or exception state for site monitor
*
* @return array
*/
public function balanceSiteMonitor()
{
$result = [];
$balance = $this->client->getbalance();
if (!$balance) {
$result['unlocked_balance'] = 'DOWN';
$result['balance'] = 'DOWN';
return $result;
}
$result['unlocked_balance'] = $balance['unlocked_balance'] / 1000000000000;
$result['balance'] = $balance['balance'] / 1000000000000;
return $result;
}
/**
* Returns the actual available and useable balance (unlocked balance)
*
......@@ -66,22 +46,19 @@ class Wallet
*/
public function balance()
{
$balance = $this->client->getbalance();
$result = $balance['unlocked_balance'] / 1000000000000;
return $result;
return $this->client->balance();
}
public function mempoolTransfers()
{
return $this->client->mempoolTransfers();
return $this->client->incomingTransfers();
}
public function bulkPayments($paymentIds)
{
$blockBuffer = 10;
return $this->client->bulk_payments($paymentIds, intval($this->wallet->last_scanned_block_height) - $blockBuffer);
return $this->client->payments($paymentIds, intval($this->wallet->last_scanned_block_height) - $blockBuffer);
}
/**
......@@ -152,12 +129,7 @@ class Wallet
*/
public function blockHeight()
{
$result = $this->client->getheight();
if ($result && isset($result['height'])) {
return $result['height'];
}
return 0;
return $this->client->blockHeight();
}
/**
......@@ -167,12 +139,7 @@ class Wallet
*/
public function getAddress()
{
$address = $this->client->getaddress();
if ($address != null) {
return $address['address'];
}
return 'Invalid';
return $this->client->address();
}
/**
......@@ -182,11 +149,7 @@ class Wallet
*/
public function createIntegratedAddress()
{
try {
return $this->client->make_integrated_address();
} catch (\Throwable $e) {
return false;
}
return $this->client->createIntegratedAddress();
}
/**
......@@ -206,12 +169,11 @@ class Wallet
/**
* gets all the payment_ids outstanding from the address_pool, we use these to check against the latest mined blocks
*
* @return array
* @return Collection
*/
public function getPaymentIds()
{
$paymentIds = Project::pluck('payment_id'); //stop scanning for payment_ids after 24h
return $paymentIds;
return Project::pluck('payment_id'); //stop scanning for payment_ids after 24h
}
}
......@@ -2,8 +2,9 @@
namespace Monero;
//json 2.0 rpc client
use App\Exceptions\ConnectionException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
/**
* Class jsonRPCClient
......@@ -11,215 +12,181 @@ use App\Exceptions\ConnectionException;
*/
class jsonRPCClient
{
/**
* @var
*/
private $url;
/**
* @var int
*/
private $timeout;
/** @var string */
private $username;
/**
* @var bool
*/
private $debug;
/** @var string */
private $password;
/** @var Client|null */
private $client;
/**
* @var
* JsonRPCClient constructor.
* @param null $client
*/
private $username;
public function __construct($client = null)
{
if (empty($client)) {
$client = new Client([
'base_uri' => env('RPC_URL'),
]);
}
$this->username = env('MONERO_USERNAME');
$this->password = env('MONERO_PASSWORD');
$this->client = $client;
}
/**
* @var
* Gets the balance
*
* @return int the overall value after inputs unlock
*/
private $password;
public function balance() : int
{
$response = $this->request('get_balance');
return $response['balance'];
}
/**
* @var array
* Gets the unlocked balance
*
* @return int the spendable balance
*/
private $headers = [
'Connection: close',
'Content-Type: application/json',
'Accept: application/json',
];
public function unlockedBalance() : int
{
$response = $this->request('get_balance');
return $response['unlocked_balance'];
}
/**
* jsonRPCClient constructor.
* Gets the primary address
*
* @param $url
* @param int $timeout
* @param bool|false $debug
* @param array $headers
* @return string wallets primary address
*/
public function __construct($url, $timeout = 20, $debug = false, $headers = [])
public function address() : string
{
$this->url = $url;
$this->timeout = $timeout;
$this->debug = $debug;
$this->headers = array_merge($this->headers, $headers);
$response = $this->request('get_address');
return $response['address'];
}
/**
* Generic method executor
* Gets the current block height
*
* @param $method
* @param $params
*
* @return null
* @return int block height
*/
public function __call($method, $params)
public function blockHeight() : int
{
return $this->execute($method, $params);
$response = $this->request('get_height');
return $response['height'];
}
/**
* Set auth credentials
* Creates a new integrated address
*
* @param $username
* @param $password
* @return array ['integrated_address', 'payment_id']
*/
public function authentication($username, $password)
public function createIntegratedAddress() : array
{
$this->username = $username;
$this->password = $password;
$response = $this->request('make_integrated_address');
return $response;
}
/**
* Get XMR bulk payments
*
* @param $payment_ids
* @param $block_height
* Gets any incoming transactions
*
* @return null
* @return array
*/
public function bulk_payments($payment_ids, $block_height)
public function incomingTransfers() : array
{
return $this->execute('get_bulk_payments', $payment_ids, $block_height);
$response = $this->request('get_transfers', ['pool' => true, 'in' => true]);
return $response;
}
/**
* Transfer XMR to another destination
* Checks for any payments made to the paymentIds
*
* @param $amount
* @param $destination
* @param $payment_id
* @param $mixin
* @param int $unlock_time
* @param array $paymentIds list of payment ids to be searched for
* @param int $minHeight the lowest block the search should start with
*
* @return string
* @return array payments received since min block height with a payment id provided
*/
public function transferXMR($amount, $destination, $payment_id, $mixin, $unlock_time = 0)
public function payments($paymentIds, $minHeight) : array
{
$dest = ['amount' => intval(0 + $amount * 1000000000000), 'address' => $destination];
$params = [
'destinations' => [$dest],
'payment_id' => $payment_id,
'mixin' => $mixin,
'unlock_time' => $unlock_time,
];
$response = $this->execute('transfer', $params);
$tx = trim($response['tx_hash'], '<>');
$response = $this->request('get_bulk_payments', ['payment_ids' => $paymentIds, 'min_block_height' => $minHeight]);
return $tx;
return $response;
}
public function mempoolTransfers()
/**
* creates a uri for easier wallet parsing
*
* @param string $address address comprising of primary, sub or integrated address
* @param string $paymentId payment id when not using integrated addresses
* @param int $amount atomic amount requested
*
* @return string the uri string which can be used to generate a QR code
*/
public function createUri($address, $paymentId = null, $amount = null) : string
{
$response = $this->execute('get_transfers', ['pool' => true]);
$response = $this->request('make_uri', ['address' => $address, 'amount' => $amount, 'payment_id' => $paymentId]);
return $response;
return $response['uri'];
}
/**
* Prepares the payload for CURL and evaluates the result from the RPC
*
* @param $procedure
* @param $params
* @param null $params2
* Sets up the request data body
*
* @return null
* @param string $method name of the rpc command
* @param array $params associative array of variables being passed to the method
*
* @throws WalletErrorException
* @return false|string will return a json string or false
*/
public function execute($procedure, $params, $params2 = null)
private function preparePayload($method, $params)
{
$id = mt_rand();
$payload = [
'jsonrpc' => '2.0',
'method' => $procedure,
'id' => $id,
'id' => '0',
'method' => $method,
'params' => $params,
];
if (!empty($params)) {
if ($params2 != null) {
$payload['params']['payment_ids'] = $params;
$payload['params']['min_block_height'] = $params2;
} else {
if (is_array($params)) {
// no keys
//$params = array_values($params);
$payload['params'] = $params;
}
}
}
if ($this->debug) {
print_r($payload);
}
$result = $this->doRequest($payload);
if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) {
if ($this->debug) {
print_r($result['result']);
}
return $result['result'];
}
if (isset($result['error'])) {
throw new ConnectionException($result['error']['message']);
}
throw new ConnectionException('no response');
return json_encode($payload);
}
/**
* Executes the CURL request.
*
* @param $payload
* @param string $method name of the rpc command
* @param array $params associative array of variables being passed to the method
*
* @return array|mixed
* @return mixed the rpc query result
*
* @throws \RuntimeException
*/
public function doRequest($payload)
protected function request(string $method, array $params = [])
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_USERAGENT, 'JSON-RPC PHP Client');
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_VERBOSE, true);
if ($this->username && $this->password) {
curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
}
$payload = $this->preparePayload($method, $params);
try {
$response = $this->client->request('POST', '',[
'auth' => [$this->username, $this->password, 'digest'],
'body' => $payload,
'headers' => [
'Content-Type' => 'application/json',
]
]);
$body = $response->getBody();
} catch (GuzzleException $exception) {
Log::error($exception);
throw new \RuntimeException('Connection to node unsuccessful');
}
$result = json_decode((string) $body, true);
if (isset($result['error'])) {
if ($this->debug) {
print_r(json_encode($payload)."\n");
print_r($ch);
throw new \RuntimeException($result['error']['message']);
}
$result = curl_exec($ch);
$response = json_decode($result, true);
curl_close($ch);
return is_array($response) ? $response : [];
return $result['result'];
}
}