Commit cb11f518 authored by xiphon's avatar xiphon
Browse files

Subaddresses: implement Subaddress donations, drop PaymentId support

parent adca731a
......@@ -40,16 +40,15 @@ class GenerateAddresses extends Command
*/
public function handle()
{
$projects = Project::whereNotNull('filename')->whereNull('payment_id')->where('state', 'FUNDING-REQUIRED')->get();
$projects = Project::whereNotNull('filename')->whereNull('address')->where('state', 'FUNDING-REQUIRED')->get();
$wallet = new WalletOld();
foreach ($projects as $project) {
$addressDetails = $wallet->getPaymentAddress();
$project->address_uri = $wallet->createQrCodeString($addressDetails['address']);
$project->address = $addressDetails['address'];
$project->payment_id = $addressDetails['paymentId'];
Storage::disk('public')->put("/img/qrcodes/{$project->payment_id}.png", $project->generateQrcode());
$project->qr_code = "img/qrcodes/{$project->payment_id}.png";
$project->subaddr_index = $addressDetails['subaddr_index'];
Storage::disk('public')->put("/img/qrcodes/{$project->subaddr_index}.png", $project->generateQrcode());
$project->qr_code = "img/qrcodes/{$project->subaddr_index}.png";
$project->raised_amount = 0;
$project->save();
}
......
......@@ -106,7 +106,7 @@ class UpdateSiteProposals extends Command
{
$prop = new stdClass();
$prop->name = $proposal->title;
$prop->{'donate-url'} = url("projects/{$proposal->payment_id}/donate");
$prop->{'donate-url'} = url("projects/{$proposal->subaddr_index}/donate");
$prop->{'gitlab-url'} = $proposal->gitlab_url;
$prop->{'local-url'} = '/forum-funding-system/proposals/'. pathinfo($proposal->filename, PATHINFO_FILENAME) . '.html';
$prop->milestones = $proposal->milestones;
......
......@@ -50,21 +50,12 @@ class walletNotify extends Command
return;
}
// check mempool
$transactionsMempool = $wallet->scanMempool($blockheight);
$transactionsMempool->each(function ($transaction) use ($wallet) {
$min_height = Deposit::max('block_received');
$transactions = $wallet->scanIncomingTransfers(max($min_height, 10) - 10);
$transactions->each(function ($transaction) use ($wallet) {
$this->processPayment($transaction);
});
$paymentIDs = $wallet->getPaymentIds();
if (count($paymentIDs)) {
// check blockchain
$transactions = $wallet->scanBlocks($blockheight, $paymentIDs);
$transactions->each(function ($transaction) use ($wallet) {
$this->processPayment($transaction);
});
}
$this->updateAllConfirmations($blockheight);
}
......@@ -80,14 +71,14 @@ class walletNotify extends Command
return null;
}
$this->info('amount: '.$transaction->amount / 1000000000000 .' confirmations:'.$transaction->confirmations.' tx_hash:'.$transaction->id);
$this->info('paymentid: '.$transaction->paymentId);
$this->info('subaddr_index: '.$transaction->subaddr_index);
$this->createDeposit($transaction);
$project = Project::where('payment_id', $transaction->paymentId)->first();
$project = Project::where('subaddr_index', $transaction->subaddr_index)->first();
if ($project) {
// update the project total
$project->raised_amount = $project->raised_amount + $transaction->amount;
$project->raised_amount = $project->raised_amount + $transaction->amount * 1e-12;
$project->save();
}
......@@ -144,7 +135,7 @@ class walletNotify extends Command
$deposit->tx_id = $transaction->id;
$deposit->amount = $transaction->amount;
$deposit->confirmations = $transaction->confirmations;
$deposit->payment_id = $transaction->paymentId;
$deposit->subaddr_index = $transaction->subaddr_index;
$deposit->time_received = $transaction->time_received;
$deposit->block_received = $transaction->block_height;
$deposit->save();
......
......@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Model;
* App\Deposit
*
* @property int $id
* @property string $payment_id
* @property int $subaddr_index
* @property string $amount
* @property string $time_received
* @property string $tx_id
......
......@@ -31,12 +31,12 @@ class FundingController extends Controller
* Shows the project based on the payment id
*
* @param Request $request
* @param $paymentId
* @param $subaddr_index
* @return ProjectResource|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function show(Request $request, $paymentId)
public function show(Request $request, $subaddr_index)
{
$project = Project::where('payment_id', $paymentId)->firstOrFail();
$project = Project::where('subaddr_index', $subaddr_index)->firstOrFail();
if ($request->wantsJson())
{
......@@ -47,9 +47,9 @@ class FundingController extends Controller
->with('project', $project);
}
public function donate(Request $request, $paymentId)
public function donate(Request $request, $subaddr_index)
{
$project = Project::where('payment_id', $paymentId)->firstOrFail();
$project = Project::where('subaddr_index', $subaddr_index)->firstOrFail();
if ($request->wantsJson())
{
......
......@@ -15,7 +15,7 @@ class ProjectResource extends JsonResource
public function toArray($request)
{
return [
'payment_id' => $this->payment_id,
'subaddr_index' => $this->subaddr_index,
'status' => $this->status,
'amount_received' => $this->amount_received,
'target_amount' => $this->target_amount,
......
......@@ -9,14 +9,14 @@ use SimpleSoftwareIO\QrCode\Facades\QrCode;
* App\ProjectResource
*
* @property int $id
* @property string $payment_id
* @property int $subaddr_index
* @property string $target_amount
* @property string $status
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Deposit[] $deposits
* @property-read mixed $amount_received
* @property-read string $uri
* @property-read string $address_uri
* @property-read int $percentage_funded
* @property-read int $contributions
* @property-read string $qrcode
......@@ -43,7 +43,7 @@ class Project extends Model
*/
public function deposits()
{
return $this->hasMany(Deposit::class, 'payment_id', 'payment_id');
return $this->hasMany(Deposit::class, 'subaddr_index', 'subaddr_index');
}
public function getAmountReceivedAttribute() {
......
......@@ -4,7 +4,7 @@ use Faker\Generator as Faker;
$factory->define(\App\Deposit::class, function (Faker $faker) {
return [
'payment_id' => $faker->sha256,
'subaddr_index' => $faker->randomNumber(),
'amount' => $faker->randomNumber(2),
'time_received' => $faker->dateTime,
'tx_id' => $faker->sha256,
......
......@@ -7,7 +7,7 @@ $factory->define(\App\Project::class, function (Faker $faker) {
$status = $faker->randomElement(['opened', 'closed', 'locked', 'merged']);
return [
'title' => $faker->sentence(),
'payment_id' => $faker->sha256,
'subaddr_index' => $faker->randomNumber(),
'address' => $faker->sha256,
'address_uri' => "monero:{$faker->sha256}",
'qr_code' => $faker->file(),
......
......@@ -17,7 +17,7 @@ class CreateProjectsTable extends Migration
$table->increments('id');
$table->string('author');
$table->string('title');
$table->string('payment_id')->nullable();
$table->unsignedInteger('subaddr_index')->nullable();
$table->string('address')->nullable();
$table->string('address_uri')->nullable();
$table->string('qr_code')->nullable();
......
......@@ -15,8 +15,8 @@ class CreateDepositsTable extends Migration
{
Schema::create('deposits', function (Blueprint $table) {
$table->increments('id');
$table->string('payment_id');
$table->unsignedInteger('confirmations')->default(0);
$table->unsignedInteger('subaddr_index');
$table->string('amount');
$table->dateTime('time_received');
$table->string('tx_id');
......
......@@ -12,7 +12,7 @@ class ProjectsTableSeeder extends Seeder
public function run()
{
factory(\App\Project::class, 20)->create()->each(function ($p) {
$p->deposits()->saveMany(factory(\App\Deposit::class, rand(0,12))->make(['payment_id' => $p->payment_id]));
$p->deposits()->saveMany(factory(\App\Deposit::class, rand(0,12))->make(['subaddr_index' => $p->subaddr_index]));
});
}
}
......@@ -37,11 +37,11 @@ interface WalletManager
public function blockHeight();
/**
* Creates a new integrated address
* Creates a new subaddress
*
* @return array ['integrated_address', 'payment_id']
* @return array ['address', 'address_index']
*/
public function createIntegratedAddress();
public function createSubaddress();
/**
* Gets any incoming transactions
......@@ -64,12 +64,12 @@ interface WalletManager
* 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
* @param string $paymentId payment id when not using integrated addresses
*
* @return string the uri string which can be used to generate a QR code
*/
public function createUri($address, $paymentId = null, $amount = null);
public function createUri($address, $amount = null, $paymentId = null);
/**
* creates a random 64 char payment id
......
......@@ -16,7 +16,7 @@ class Transaction
public $confirmations;
public $payment_id;
public $subaddr_index;
public $block_height;
......@@ -31,7 +31,7 @@ class Transaction
* @param $timeReceived
* @param $paymentId
*/
public function __construct($id, $amount, $address, $confirmations, $time, $timeReceived, $paymentId = null, $blockheight = null)
public function __construct($id, $amount, $address, $confirmations, $time, $timeReceived, $subaddr_index = null, $blockheight = null)
{
$this->amount = $amount;
$this->time_received = $timeReceived;
......@@ -39,7 +39,7 @@ class Transaction
$this->address = $address;
$this->id = $id;
$this->confirmations = $confirmations;
$this->payment_id = $paymentId;
$this->subaddr_index = $subaddr_index;
$this->block_height = $blockheight;
$this->correctTimeRecieved();
}
......
......@@ -28,15 +28,15 @@ class Wallet
public function getPaymentAddress()
{
$integratedAddress = $this->createIntegratedAddress();
if (!$integratedAddress) {
$subaddress = $this->createSubaddress();
if (!$subaddress) {
return ['address' => 'not valid', 'expiration_time' => 900];
}
$project = new Project();
$project->payment_id = $integratedAddress['payment_id'];
$project->subaddr_index = $subaddress['address_index'];
$project->save();
return ['address' => $integratedAddress['integrated_address'], 'paymentId' => $integratedAddress['payment_id']];
return ['address' => $subaddress['address'], 'subaddr_index' => $subaddress['address_index']];
}
/**
......@@ -49,71 +49,38 @@ class Wallet
return $this->client->balance();
}
public function mempoolTransfers()
{
return $this->client->incomingTransfers();
}
public function bulkPayments($paymentIds)
{
$blockBuffer = 10;
return $this->client->payments($paymentIds, intval($this->wallet->last_scanned_block_height) - $blockBuffer);
}
/**
* Scans the monero blockchain for transactions for the payment ids
*
* @param $blockheight
* @param $paymentIDs
* @param $min_height
* @param $account_index
*
* @return array|Transaction
* @return \Illuminate\Support\Collection
*/
public function scanBlocks($blockheight, $paymentIDs)
public function scanIncomingTransfers($min_height = 0, $account_index = 0)
{
$response = $this->bulkPayments($paymentIDs);
$address = $this->getAddress();
$transactions = [];
if ($response && isset($response['payments'])) {
foreach ($response['payments'] as $payment) {
$transaction = new Transaction(
$payment['tx_hash'],
$payment['amount'],
$address,
$blockheight - $payment['block_height'],
0,
Carbon::now(),
$payment['payment_id'],
$payment['block_height']
);
$transactions[] = $transaction;
}
$response = $this->client->incomingTransfers($min_height);
if (!$response) {
return collect([]);
}
return collect($transactions);
}
/**
* @param $blockheight
*
* @return \Illuminate\Support\Collection
*/
public function scanMempool($blockheight)
{
$address = $this->getAddress();
$transactions = [];
$response = $this->mempoolTransfers();
if ($response && isset($response['pool'])) {
foreach ($response['pool'] as $payment) {
const toScan = ['pool', 'in'];
foreach (toScan as $entry) {
if (!isset($response[$entry])) {
continue;
}
foreach ($response[$entry] as $payment) {
if $payment['subaddr_index']['major'] != $account_index {
continue;
}
$transaction = new Transaction(
$payment['txid'],
$payment['amount'],
$address,
0,
$payment['address'],
$payment['confirmations'],
0,
Carbon::now(),
$payment['payment_id'],
$blockheight
$payment['subaddr_index']['minor'],
$payment['height']
);
$transactions[] = $transaction;
}
......@@ -143,37 +110,34 @@ class Wallet
}
/**
* Returns XMR integrated address
* Returns XMR subaddress
*
* @return mixed
*/
public function createIntegratedAddress()
public function createSubaddress()
{
return $this->client->createIntegratedAddress();
return $this->client->createSubaddress();
}
/**
* @param $amount
* @param $address
* @param $paymentId
* @param $amount
*
* @return string
*/
public function createQrCodeString($amount, $address, $paymentId = ''): string
public function createQrCodeString($address, $amount): string
{
// @todo add tx_payment_id support
// monero payment_id is passed through the address
return 'monero:'.$address.'?tx_amount='.$amount;
}
/**
* gets all the payment_ids outstanding from the address_pool, we use these to check against the latest mined blocks
* gets all the subaddr_indexes outstanding from the address_pool, we use these to check against the latest mined blocks
*
* @return Collection
*/
public function getPaymentIds()
public function getSubaddressIndexes()
{
return Project::pluck('payment_id'); //stop scanning for payment_ids after 24h
return Project::pluck('subaddr_index'); //stop scanning for subaddr_index after 24h
}
}
......@@ -28,12 +28,12 @@ class WalletOld
public function getPaymentAddress()
{
$integratedAddress = $this->createIntegratedAddress();
if (!$integratedAddress) {
$subaddress = $this->createSubaddress();
if (!$subaddress) {
return ['address' => 'not valid'];
}
return ['address' => $integratedAddress['integrated_address'], 'paymentId' => $integratedAddress['payment_id']];
return ['address' => $subaddress['address'], 'subaddr_index' => $subaddress['address_index']];
}
/**
......@@ -46,71 +46,37 @@ class WalletOld
return $this->client->balance();
}
public function mempoolTransfers()
{
return $this->client->incomingTransfers();
}
public function bulkPayments($paymentIds)
{
$blockBuffer = 10;
return $this->client->payments($paymentIds, intval($this->wallet->last_scanned_block_height) - $blockBuffer);
}
/**
* Scans the monero blockchain for transactions for the payment ids
*
* @param int $blockheight
* @param Collection $paymentIDs
*
* @return array|Transaction
*/
public function scanBlocks($blockheight, $paymentIDs)
{
$response = $this->bulkPayments($paymentIDs);
$address = $this->getAddress();
$transactions = [];
if ($response && isset($response['payments'])) {
foreach ($response['payments'] as $payment) {
$transaction = new Transaction(
$payment['tx_hash'],
$payment['amount'],
$address,
$blockheight - $payment['block_height'],
0,
Carbon::now(),
$payment['payment_id'],
$payment['block_height']
);
$transactions[] = $transaction;
}
}
return collect($transactions);
}
/**
* @param $blockheight
* @param $account_index
*
* @return \Illuminate\Support\Collection
*/
public function scanMempool($blockheight)
public function scanIncomingTransfers($min_height = 0, $account_index = 0)
{
$address = $this->getAddress();
$response = $this->client->incomingTransfers($min_height);
if (!$response) {
return collect([]);
}
$transactions = [];
$response = $this->mempoolTransfers();
if ($response && isset($response['pool'])) {
foreach ($response['pool'] as $payment) {
foreach (['pool', 'in'] as $entry) {
if (!isset($response[$entry])) {
continue;
}
foreach ($response[$entry] as $payment) {
if ($payment['subaddr_index']['major'] != $account_index) {
continue;
}
$transaction = new Transaction(
$payment['txid'],
$payment['amount'],
$address,
0,
$payment['address'],
$payment['confirmations'],
0,
Carbon::now(),
$payment['payment_id'],
$blockheight
$payment['subaddr_index']['minor'],
$payment['height']
);
$transactions[] = $transaction;
}
......@@ -140,35 +106,34 @@ class WalletOld
}
/**
* Returns XMR integrated address
* Returns XMR subaddress
*
* @return mixed
*/
public function createIntegratedAddress()
public function createSubaddress()
{
return $this->client->createIntegratedAddress();
return $this->client->createSubaddress();
}
/**
* @param $amount
* @param $address
* @param $paymentId
* @param $amount
*
* @return string
*/
public function createQrCodeString($address, $amount = null, $paymentId = null): string
public function createQrCodeString($address, $amount = null): string
{
return $this->client->createUri($address, $amount, $paymentId);
return $this->client->createUri($address, $amount);
}
/**
* gets all the payment_ids outstanding from the address_pool, we use these to check against the latest mined blocks
* gets all the subaddr_indexes outstanding from the address_pool, we use these to check against the latest mined blocks
*
* @return Collection
*/
public function getPaymentIds()
public function getSubaddressIndexes()
{
return Project::pluck('payment_id'); //stop scanning for payment_ids after 24h
return Project::pluck('subaddr_index'); //stop scanning for subaddr_index after 24h
}
}
......@@ -91,14 +91,18 @@ class jsonRPCClient implements Contracts\WalletManager
$response = $this->request('get_height');
return $response['height'];
}
/**
* Creates a new integrated address
* Creates a new subaddress
*
* @return array ['integrated_address', 'payment_id']
* @param int $account_index account index to create subaddress (maajor index)
* @param string $label label to assign to new subaddress
*
* @return array ['address', 'address_index']
*/
public function createIntegratedAddress() : array
public function createSubaddress($account_index = 0, $label = '') : array
{
$response = $this->request('make_integrated_address');
$response = $this->request('create_address', ['account_index' => $account_index, 'label' => $label]);
return $response;
}
......@@ -133,12 +137,12 @@ class jsonRPCClient implements Contracts\WalletManager
* 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
* @param string $paymentId payment id when not using integrated addresses
*