I have Monero on my web app so users can deposit and withdraw Monero at will. My issue is that sometimes it will transfer the users funds and other times it will give an error saying "invalid params".
If anyone can see what the issue is or point me in the right direction, that would be fantastic, this is driving me crazy.
The app uses two files, the first is the JSON RPC:
namespace App\Helpers;
use GuzzleHttp\Client;
class XMRWalletRPC
{
private $username;
private $password;
private $client;
/**
* Wallet constructor.
* @param string $hostname
* @param int $port
*/
function __construct($hostname = 'http://127.0.0.1', $port = 18088, $user, $pass)
{
$this->username = $user;
$this->password = $pass;
$this->client = new Client(['base_uri' => $hostname . ':' . $port]);
}
/**
* Helper function for creating wallet requests
* @param array $body
* @return string
*/
public function _request($body)
{
$body['id'] = 0;
$response = $this->client->request('POST', '/json_rpc', [
'auth' => [$this->username, $this->password, 'digest'],
'body' => json_encode($body)
]);
$response = json_decode($response->getBody());
/** Log::info('Response: ' . var_export($response, true));*/
if(property_exists($response, 'error')){
return $response->error;
} else {
return $response->result;
}
}
/**
* Helper function for building transfer or transfer split request body
* @param array $options
* @return string
*/
public function _buildTransfer($options)
{
$destinations = $options['destinations'];
// Convert Monero amount to atomic units
if(gettype($destinations) == "object"){
$destinations->amount = $destinations->amount * 1e12;
$destinations = array($destinations);
}
else if(gettype($destinations) == "array"){
$destinations['amount'] = $destinations['amount'] * 1e12;
$destinations = array($destinations);
} else {
foreach ($destinations as &$destination){
$destination->amount = $destination->amount * 1e12;
}
}
// Define Mixin
$mixin = (isset($options['mixin']) ? $options['mixin'] : 4);
// Define Unlock Time
$unlock_time = (isset($options['unlock_time']) ? $options['unlock_time'] : 0);
// Define Payment ID
$payment_id = (isset($options['payment_id']) ? $options['payment_id'] : null);
// Build params array
$params = [
'destinations' => $destinations,
'mixin' => $mixin,
'unlock_time' => $unlock_time,
'payment_id' => $payment_id
];
// Set algorithm type if using transfer_split method
if($options['method'] == "transfer_split"){
$new_algorithm = (isset($options['new_algorithm']) ? $options['new_algorithm'] : false);
$params['new_algorithm'] = $new_algorithm;
}
// Build request body
$body = [
'method' => $options['method'],
'params' => $params
];
return $body;
}
/**
* Return total balance and unlocked balance of wallet
* @return string
*/
public function getBalance()
{
$body = ['method' => 'getbalance'];
$balance = $this->_request($body);
return $balance;
}
/**
* Return the address of the wallet
*/
public function getAddress()
{
$body = ['method' => 'getaddress'];
return $this->_request($body);
}
/**
* Return the current block height.
* @return string
*/
public function getHeight()
{
$body = ['method' => 'getheight'];
return $this->_request($body);
}
/**
* Transfer Monero to a single recipient or group of recipients
* @param array $options
* @return string
*/
public function transfer($options)
{
$options['method'] = 'transfer';
$body = $this->_buildTransfer($options);
return $this->_request($body);
}
public function getTransfers($options)
{
$options['method'] = 'get_transfers';
return $this->_request($options);
}
/**
* Same as transfer(), but can split into more than one transaction if necessary.
* @param array $options
* @return string
*/
public function transferSplit($options)
{
$options['method'] = 'transfer_split';
$body = $this->_buildTransfer($options);
return $this->_request($body);
}
/**
* Send all dust output back to the wallet with mixin 0
* @return string
*/
public function sweepDust()
{
$body = ['method' => 'sweep_dust'];
return $this->_request($body);
}
/**
* Save the blockchain.
* @return string
*/
public function store()
{
$body = ['method' => 'store'];
return $this->_request($body);
}
/**
* Get a list of incoming payments from a given payment ID
* @param $payment_id
* @return string
*/
public function getPayments($payment_id)
{
$params = ['payment_id' => $payment_id];
$body = [
'method' => 'get_payments',
'params'=> $params
];
return $this->_request($body);
}
/**
* Get a list of incoming payments from a single payment ID or list of payment IDs from a given height.
* @param $payment_ids array
* @param $height int
* @return string
*/
public function getBulkPayments($payment_ids, $height)
{
$params = [
'payment_ids' => $payment_ids,
'min_block_height' => $height
];
$body = [
'method' => 'get_bulk_payments',
'params' => $params
];
return $this->_request($body);
}
/**
* Return a list of incoming transfers to the wallet.
* @param $type string
* @return string
*/
public function incomingTransfers($type)
{
$params = ['transfer_type' => $type];
$body = [
'method' => 'incoming_transfers',
'params' => $params
];
return $this->_request($body);
}
/**
* Return the spend or view private key.
* @param $key_type string
* @return string
*/
public function queryKey($key_type)
{
$params = ['key_type' => $key_type];
$body = [
'method' => 'query_key',
'params' => $params
];
return $this->_request($body);
}
/**
* Make an integrated address from the wallet address and a payment id.
* @param string $payment_id
* @return string
*/
public function integratedAddress($payment_id = null)
{
$params = ['payment_id' => $payment_id];
$body = [
'method' => 'make_integrated_address',
'params' => $params
];
return $this->_request($body);
}
/**
* Retrieve the standard address and payment id corresponding to an integrated address.
* @param $address string
* @return string
*/
public function splitIntegratedAddress($address)
{
$params = ['integrated_address' => $address];
$body = [
'method' => 'split_integrated_address',
'params' => $params
];
return $this->_request($body);
}
/**
* Stops the wallet, storing the current state.
* @return string
*/
public function stopWallet()
{
$body = ['method' => 'stop_wallet'];
return $this->_request($body);
}
}
The second file has a section that deals with Monero withdrawals:
public function WithdrawXMR(){
$validator= Validator::make(Input::all(), array(
'amount' => 'required|numeric',
'address' => 'required|min:95|max:106',
'pin' => 'required',
));
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
if (!preg_match("/^([4]|[8])([0-9]|[A-B])(.){93}/", Input::get('address')) && !preg_match("/^([4]|[8])(.){105}/", Input::get('address')))
{
return redirect()->back()->withInput()->with('error','XMR address invalid!');
}
$amount = Input::get('amount');
$address = Input::get('address');
$pin = Input::get('pin');
$wallet = XMRWallet::where('type','=','XMR')->first();
$wallet->connectJSONRPC();
if(empty($address))
return redirect()->back()->withInput()->with('error','XMR address invalid!');
$setting = new Setting();
$user = \Auth::user();
$balance = new BalanceXMR();
if(hash('sha256', $pin) == $user->pin) {
$balance_amount = $balance->getBalance();
$fee_withdraw = $setting->getSetting('xmr_fee_withdraw',0);
$net_total = $amount-$fee_withdraw;
$min_amount = $fee_withdraw;
if($amount <= $min_amount){
return redirect('balance')
->with( 'error', "Amount withdraw must be equal to or great than ".$min_amount."." );
}elseif($balance_amount >= $amount){
try{
$txid = $wallet->transfer(array('destinations' => array('amount' => $net_total, 'address' => $address)));
if($txid){
if(isset($txid->tx_hash)){
$user_id=$user->id;
$updated = BalanceXMR::where('user_id', '=', $user_id)->where('amount', '>', $amount)->decrement('amount', $amount);
if ($updated > 0) {
$new_amount = $balance_amount - $amount;
Log::info("Take Money -- User: ".$user_id.": new_amount: ".$new_amount.' -- amount: '.$amount);
}
Withdraw::insert(array('user_id' => $user->id, 'to_address'=>$address, 'amount'=>$amount, 'fee_amount'=>$fee_withdraw,'receive_amount'=>$net_total,'created_at'=>date('Y-m-d H:i:s'),'status'=>1,'transaction_id'=>$txid->tx_hash));
return redirect('balance')
->with( 'success', "<strong>Success!</strong> You withdrew ".sprintf('%.5f',$net_total)." ".$wallet->type." to address: ".$address.". Fee: ".$fee_withdraw );
}
else{
return redirect('balance')->with( 'error', '<strong>Error!</strong> '.$txid->message);
}
}else{
return redirect('balance')
->with( 'error', "<strong>Error!</strong> Can not send." );
}
}catch (\Exception $e) {
return redirect('balance')->with( 'error', '<strong>Error!</strong> Wallet error.'); //'Caught exception: '.$e->getMessage()
}
}else{
return redirect('balance')->with( 'error', "Attempted to withdraw more funds than you have." );
}
}else{
return redirect('balance')->with( 'error', "Pin invalid." );
}
}
And here is the model file:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Helpers\XMRWalletRPC;
class XMRWallet extends Model
{
protected $table = 'wallets_xmr';
public $timestamps = false;
private $client;
public function getType($wallet_id){
$type = XMRWallet::where('id', '=', $wallet_id)->select('type')->first();
if(isset($type->type))
return $type->type;
else return '';
}
public function connectJSONRPC() {
$this->client = new XMRWalletRPC('http://127.0.0.1', 18088, $this->wallet_username, $this->wallet_password);
}
public function height() {
return $this->client->getHeight();
}
public function transfer($options) {
return $this->client->transfer($options);
}
public function getPayments($payment_id) {
return $this->client->getPayments($payment_id);
}
public function incomingTransfers($type) {
return $this->client->incomingTransfers($type);
}
public function getTransfers($options) {
return $this->client->getTransfers($options);
}
public function integratedAddress($payment_id = null) {
return $this->client->integratedAddress($payment_id);
}
public function splitIntegratedAddress($address) {
return $this->client->splitIntegratedAddress($address);
}
}
?>
Here is the log from a failed amount and a working amount. The failed amount always fails, the working amount always works.
Failed amount = 0.1253721 Working amount = 0.1253728
[2019-08-23 22:32:10] local.INFO: Input:
array (
'_token' => 'vZmZ0W7NKJXxdad4qwwNvRmRsyorL8UACRHrInRb',
'address' =>
'8BTD44tHsQjK3Y7mgEEXE8FhJor47tX9zEsjJUnkHcDo5qvjkZohNSiUJj9J779HpBVFqc2NxGxSvLPs9xRV1FnmSFkGk7l',
'amount' => '0.1253721',
'pin' => '1234',
)
[2019-08-23 22:32:10] local.INFO: Params: array (
'destinations' =>
array (
0 =>
array (
'amount' => 106372099999.99998,
'address' =>
'8BTD44tHsQjK3Y7mgEEXE8FhJor47tX9zEsjJUnkHcDo5qvjkZohNSiUJj9J779HpBVFqc2NxGxSvLPs9xRV1FnmSFkGk7l',
),
),
'mixin' => 10,
'unlock_time' => 0,
'payment_id' => NULL,
)
[2019-08-23 22:32:10] local.INFO: Response: (object) array(
'error' =>
(object) array(
'code' => -32602,
'message' => 'Invalid params',
),
'id' => 0,
'jsonrpc' => '2.0',
)
[2019-08-23 22:32:10] local.INFO: Can not send money:
Invalid params
Working amount
[2019-08-23 22:32:34] local.INFO: Input:
array (
'_token' => 'vZmZ0W7NKJXxdad4qwwNvRmRsyorL8UACRHrInRb',
'address' =>
'8BTD44tHsQjK3Y7mgEEXE8FhJor47tX9zEsjJUnkHcDo5qvjkZohNSiUJj9J779HpBVFqc2NxGxSvLPs9xRV1FnmSFkGk7l',
'amount' => '0.1253728',
'pin' => '1234',
)
[2019-08-23 22:32:34] local.INFO: Params: array (
'destinations' =>
array (
0 =>
array (
'amount' => 106372800000.0,
'address' =>
'8BTD44tHsQjK3Y7mgEEXE8FhJor47tX9zEsjJUnkHcDo5qvjkZohNSiUJj9J779HpBVFqc2NxGxSvLPs9xRV1FnmSFkGk7l',
),
),
'mixin' => 10,
'unlock_time' => 0,
'payment_id' => NULL,
)
[2019-08-23 22:34:18] local.INFO: Response: (object) array(
'id' => 0,
'jsonrpc' => '2.0',
'result' =>
(object) array(
'amount' => 106372800000,
'fee' => 28730000,
'multisig_txset' => '',
'tx_blob' => '',
'tx_hash' =>
'032fd127e50dc5f0c0f5dd094a4f40405b0dbdc012f18f86aa5199a3036ee7db',
'tx_key' => '',
'tx_metadata' => '',
'unsigned_txset' => '',
),
)
[2019-08-23 22:34:18] local.INFO: Take Money -- User: 1664: new_amount:
0.5023544 -- amount: 0.1253728
_buildTransfer
function looks iffy. It would be useful to see the data (json) it actually creates/sends, as this looks likely your error (so log the $body at the end of _buildTransfer).