Ethereum-RPC-Client

 view release on metacpan or  search on metacpan

lib/Ethereum/RPC/Contract.pm  view on Meta::CPAN

use strict;
use warnings;

our $VERSION = '0.05';

=head1 NAME

    Ethereum::Contract - Support for interacting with Ethereum contracts using the geth RPC interface

=cut

use Moo;
use JSON::MaybeXS;
use Math::BigInt;
use Scalar::Util   qw(looks_like_number);
use List::Util     qw(first);
use Digest::Keccak qw(keccak_256_hex);

use Ethereum::RPC::Client;
use Ethereum::RPC::Contract::ContractResponse;
use Ethereum::RPC::Contract::ContractTransaction;
use Ethereum::RPC::Contract::Helper::UnitConversion;

has contract_address => (is => 'rw');
has contract_abi => (
    is       => 'ro',
    required => 1
);
has rpc_client => (
    is => 'lazy',
);

sub _build_rpc_client {
    return Ethereum::RPC::Client->new;
}

has from => (
    is   => 'rw',
    lazy => 1
);

sub _build_from {
    return shift->rpc_client->eth_coinbase();
}

has gas_price => (
    is   => 'rw',
    lazy => 1
);

sub _build_gas_price {
    return shift->rpc_client->eth_gasPrice();
}

has max_fee_per_gas => (is => 'rw');

has max_priority_fee_per_gas => (is => 'rw');

has gas => (is => 'rw');

has contract_decoded => (
    is      => 'rw',
    default => sub { {} },
);

=head2 BUILD

Constructor: Here we get all functions and events from the given ABI and set
it to the contract class.

=over 4

=item contract_address => string (optional)

=item contract_abi => string (required, https://solidity.readthedocs.io/en/develop/abi-spec.html)

=item rpc_client => L<Ethereum::RPC::Client> (optional, default: L<Ethereum::RPC::Client>)

=item from => string (optional)

=item gas => numeric (optional)

=item gas_price => numeric (optional)

=item max_fee_per_gas => numeric (optional)

=item max_priority_fee_per_gas => numeric (optional)

=back

=cut

sub BUILD {
    my ($self) = @_;
    my @decoded_json = @{decode_json($self->contract_abi // "[]")};

    for my $json_input (@decoded_json) {
        if ($json_input->{type} =~ /^function|event|constructor$/) {
            push(@{$self->contract_decoded->{$json_input->{name} // $json_input->{type}}}, $json_input->{inputs});
        }
    }

    unless ($self->contract_decoded->{constructor}) {
        push(@{$self->contract_decoded->{constructor}}, []);
    }

    return;

}

=head2 invoke

Prepare a function to be called/sent to a contract.

=over 4

=item name => string (required)

=item params => array (optional, the function params)

=back

Returns a L<Ethereum::Contract::ContractTransaction> object.

=cut

sub invoke {
    my ($self, $name, @params) = @_;

    my $function_id = substr($self->get_function_id($name, scalar @params), 0, 10);

    my $res = $self->_prepare_transaction($function_id, $name, \@params);

    return $res;
}

=head2 get_function_id

The function ID is derived from the function signature using: SHA3(approve(address,uint256)).

=over 4

=item fuction_name => string (required)

=item params_size => numeric (required, size of inputs called by the function)

=back

Returns a string hash

=cut

sub get_function_id {
    my ($self, $function_name, $params_size) = @_;

    my @inputs = @{$self->contract_decoded->{$function_name}};

    my $selected_data = first { (not $_ and not $params_size) or ($params_size and scalar @{$_} == $params_size) } @inputs;

    $function_name .= sprintf("(%s)", join(",", map { $_->{type} } grep { $_->{type} } @$selected_data));

    my $sha3_hex_function = '0x' . keccak_256_hex($function_name);

    return $sha3_hex_function;
}

=head2 _prepare_transaction

Join the data and parameters and return a prepared transaction to be called as send, call or deploy.

=over 4

=item compiled_data => string (required, function signature or the contract bytecode)

=item function_name => string (contract function as specified in the ABI)

=item params => array (required)

=back

L<Future> object
on_done: L<Ethereum::Contract::ContractTransaction>
on_fail: error string

=cut

sub _prepare_transaction {
    my ($self, $compiled_data, $function_name, $params) = @_;
    $compiled_data =~ s/\s+//g;

    my $encoded = $self->encode($function_name, $params);

    my $data = $compiled_data . $encoded;

    my $transaction = Ethereum::RPC::Contract::ContractTransaction->new(
        contract_address => $self->contract_address,
        rpc_client       => $self->rpc_client,
        data             => $self->append_prefix($data),
        from             => $self->from,
        gas              => $self->gas
    );

    if ($self->gas_price) {
        $transaction->{gas_price} = $self->gas_price;
        # if the gas price is set the transaction type is legacy
        return $transaction;
    }

    # transaction type 2 EIP1559
    $transaction->{max_fee_per_gas}          = $self->max_fee_per_gas          if $self->max_fee_per_gas;
    $transaction->{max_priority_fee_per_gas} = $self->max_priority_fee_per_gas if $self->max_priority_fee_per_gas;
    return $transaction;
}

=head2 encode

Encode function arguments to the ABI format

=over 4

=item C<function_name> ABI function name

=item C<params> all the values for the function in the same order than the ABI

=back

Returns an encoded data string

=cut

sub encode {
    my ($self, $function_name, $params) = @_;

    my $inputs = $self->contract_decoded->{$function_name}->[0];

    # no inputs
    return "" unless $inputs;

    my $offset = $self->get_function_offset($inputs);

    my (@static, @dynamic);
    my @inputs = $inputs->@*;
    for (my $input_index = 0; $input_index < scalar @inputs; $input_index++) {
        my ($static, $dynamic) = $self->get_hex_param($offset, $inputs[$input_index]->{type}, $params->[$input_index]);
        push(@static,  $static->@*);
        push(@dynamic, $dynamic->@*);
        $offset += scalar $dynamic->@*;
    }

    my @data = (@static, @dynamic);
    my $data = join("", @data);

    return $data;
}

=head2 get_function_offset

Get the abi function total offset

For the cases we have arrays as parameters we can have a dynamic size
for the static values, for sample if the basic type has a fixed value
and also the array is fixed, we will have all the items on the array
being added with the static items before the dynamic items in the encoded
data

=over 4

=item C<input_list> the json input from the abi data

=back

return the integer offset

=cut

sub get_function_offset {
    my ($self, $input_list) = @_;
    my $offset = 0;
    for my $input ($input_list->@*) {
        $input->{type} =~ /^([a-z]+)([0-9]+)?\[(\d+)?\]/;
        my $basic_type = $1;
        my $input_size = $2;
        my $array_size = $3;
        if ($input_size && $array_size || ($array_size && $basic_type =~ /^uint|int|fixed/)) {
            $offset += $array_size;
            next;
        }
        $offset += 1;
    }
    return $offset;
}

=head2 get_hex_param



( run in 3.193 seconds using v1.01-cache-2.11-cpan-140bd7fdf52 )