1

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
7
  • Post logs from the monero-wallet-rpc. That will be more useful than partial chunks of code and your custom log.
    – jtgrassie
    Commented Aug 22, 2019 at 21:24
  • added the rpc log to the bottom of the post Commented Aug 23, 2019 at 12:39
  • I don't think you understand. The precise logs when it fails. Aside from this, your _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).
    – jtgrassie
    Commented Aug 23, 2019 at 15:18
  • Added the new log at the bottom again. Commented Aug 23, 2019 at 19:57
  • 1
    You need to do intval($destinations->amount * 1e12) everywhere you are assigning $destinations->amount * 1e12
    – jtgrassie
    Commented Aug 23, 2019 at 22:55

1 Answer 1

1

Your log shows you converting the string '0.1253721' to 106372099999.99998, which is an invalid amount. Whereas your other string '0.1253728' gets converted to 106372800000.0, which is passing through OK. The number sent to the wallet RPC should be an integer.

Your function _buildTransfer is the culprit. Either cast when assigning or wrap in intval(...). E.g.

$destinations->amount = $destinations->amount * 1e12;

should be:

$destinations->amount = intval($destinations->amount * 1e12); 

or:

$destinations->amount = (int)($destinations->amount * 1e12);

You'll need to change all the lines you are setting the amounts in that function.

2
  • That's worked. Thank you very much. One final question is it worth replacing mixin with ringsize? Or leave mixin in and also add ringsize. Commented Aug 23, 2019 at 23:31
  • Doesn't matter. Monero uses a fixed ring size now of 11. So putting anything else will be ignored anyway.
    – jtgrassie
    Commented Aug 23, 2019 at 23:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.