Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add multipayment details #1142

Merged
merged 11 commits into from
Jan 16, 2025
6 changes: 0 additions & 6 deletions app/Services/BigNumber.php
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@
namespace App\Services;

use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
use Stringable;

final class BigNumber implements Stringable
@@ -91,9 +90,4 @@ public function valueOf(): BigDecimal
{
return $this->value;
}

public function toHex(): string
{
return $this->value->toScale(0, RoundingMode::DOWN)->toBigInteger()->toBase(16);
}
}
5 changes: 5 additions & 0 deletions app/ViewModels/Concerns/Transaction/HasMethod.php
Original file line number Diff line number Diff line change
@@ -58,6 +58,11 @@ public function isContractDeployment(): bool
return $this->method->isContractDeployment();
}

public function isMultiPayment(): bool
{
return $this->method->isMultiPayment();
}

public function isSelfReceiving(): bool
{
if ($this->isValidatorRegistration()) {
31 changes: 31 additions & 0 deletions app/ViewModels/Concerns/Transaction/HasPayload.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

namespace App\ViewModels\Concerns\Transaction;

use ArkEcosystem\Crypto\Enums\ContractAbiType;
use ArkEcosystem\Crypto\Utils\AbiDecoder;

trait HasPayload
@@ -84,6 +85,36 @@ public function formattedPayload(): ?string
])->render());
}

public function multiPaymentRecipients(): ?array
{
if (! $this->isMultiPayment()) {
throw new \Exception('This transaction is not a multi-payment.');
}

/**
* @var string $payload
*/
$payload = $this->rawPayload();

$method = (new AbiDecoder(ContractAbiType::MULTIPAYMENT))->decodeFunctionData($payload);

$recipients = [];

$addresses = $method['args'][0];
$amounts = $method['args'][1];

foreach ($addresses as $index => $address) {
if (isset($amounts[$index])) {
$recipients[] = [
'address' => $address,
'amount' => $amounts[$index],
];
}
}

return $recipients;
}

private function getMethodData(): ?array
{
$payload = $this->rawPayload();
10 changes: 5 additions & 5 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 14 additions & 4 deletions database/factories/TransactionFactory.php
Original file line number Diff line number Diff line change
@@ -11,6 +11,9 @@
use App\Models\Transaction;
use App\Models\Wallet;
use App\Services\BigNumber;
use ArkEcosystem\Crypto\Enums\AbiFunction;
use ArkEcosystem\Crypto\Enums\ContractAbiType;
use ArkEcosystem\Crypto\Utils\AbiEncoder;
use Illuminate\Database\Eloquent\Factories\Factory;
use function Tests\faker;

@@ -77,12 +80,19 @@ public function tokenTransfer(string $address, int $amount): Factory
*/
public function multiPayment(array $recipients, array $amounts): Factory
{
$method = ContractMethod::multiPayment();
$pay = [];

$method .= implode('', array_map(fn (string $recipient) => str_pad(preg_replace('/^0x/', '', $recipient), 64, '0', STR_PAD_LEFT), $recipients));
$method .= implode('', array_map(fn (BigNumber $amount) => str_pad($amount->toHex(), 64, '0', STR_PAD_LEFT), $amounts));
foreach ($recipients as $index => $recipient) {
$pay[0][] = $recipient;
$pay[1][] = $amounts[$index]->__toString();
}

return $this->withPayload($method)
$payload = (new AbiEncoder(ContractAbiType::MULTIPAYMENT))
->encodeFunctionCall(AbiFunction::MULTIPAYMENT->value, $pay);

$payload = preg_replace('/^0x/', '', $payload);

return $this->withPayload($payload)
->state(fn () => [
'recipient_address' => Network::knownContract('multipayment'),
]);
1 change: 1 addition & 0 deletions resources/lang/en/general.php
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
return [
'ardent' => 'Ardent',
'address' => 'Address',
'recipients' => 'Recipients',
'beta_uppercase' => 'BETA',
'optional' => 'Optional',
'or' => 'or',
4 changes: 4 additions & 0 deletions resources/views/app/transaction.blade.php
Original file line number Diff line number Diff line change
@@ -18,6 +18,10 @@
<x-transaction.page.summary :transaction="$transaction" />

<x-transaction.page.status :model="$transaction" />

@if ($transaction->isMultiPayment())
<x-transaction.page.recipients :model="$transaction" />
@endif
</div>

<div class="mb-8">
21 changes: 15 additions & 6 deletions resources/views/components/general/identity.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@props([
'model',
'model' => null,
'address' => null,
'prefix' => false,
'isListing' => false,
'address' => false,
@@ -14,6 +15,14 @@
'validatorNameClass' => null,
])

@php
if ($model === null && $address === null) {
throw new Exception('You must provide a model or an address');
}

$address = $model ? $model->address() : $address;
@endphp

<div @class($containerClass)>
<div {{ $attributes->class('flex items-center md:flex-row md:justify-start') }}>
<div @class([
@@ -28,18 +37,18 @@
<div @class(['font-semibold sm:hidden md:flex', $linkClass])>
@else
<a
href="{{ route('wallet', $model->address()) }}"
href="{{ route('wallet', $address) }}"
@class(['font-semibold sm:hidden md:flex link', $linkClass])
>
@endif
@if ($address)
{{ $address }}
@else
@if($withoutTruncate)
{{ $model->address() }}
{{ $address }}
@else
<x-truncate-middle :length="$truncateLength">
{{ $model->address() }}
{{ $address }}
</x-truncate-middle>
@endisset
@endif
@@ -53,11 +62,11 @@
<div @class(['hidden font-semibold sm:flex md:hidden', $linkClass])>
@else
<a
href="{{ route('wallet', $model->address()) }}"
href="{{ route('wallet', $address) }}"
@class(['hidden font-semibold sm:flex md:hidden link', $linkClass])
>
@endif
{{ $model->address() }}
{{ $address }}
@if ($withoutLink)
</div>
@else
Original file line number Diff line number Diff line change
@@ -1,43 +1,53 @@
@props([
'model',
'model' => null,
'withoutTruncate' => false,
'withoutClipboard' => false,
'truncateBreakpoint' => 'xl',
'withoutTransactionCount' => true,
'validatorNameClass' => null,
'address' => null
])

@php
if ($model === null && $address === null) {
throw new Exception('You must provide a model or an address');
}

$truncateHiddenBreakpoint = [
'sm' => 'sm:hidden',
'lg' => 'lg:hidden',
'xl' => 'xl:hidden',
][$truncateBreakpoint];

$truncateShowBreakpoint = [
'sm' => 'hidden sm:inline',
'lg' => 'hidden lg:inline',
'xl' => 'hidden xl:inline',
][$truncateBreakpoint];

$address = $model ? $model->address() : $address;
@endphp

<div class="flex flex-col">
<span {{ $attributes->class('flex justify-between w-full text-sm leading-4.25') }}>
<span>
<x-general.identity
:model="$model"
:address="$address"
:without-truncate="$withoutTruncate"
:validator-name-class="$validatorNameClass"
>
<x-slot name="address">
@unless ($withoutTruncate)
<span @class($truncateHiddenBreakpoint)>
<x-truncate-middle>{{ $model->address() }}</x-truncate-middle>
<x-truncate-middle>{{ $address }}</x-truncate-middle>
</span>
<span @class($truncateShowBreakpoint)>
{{ $model->address() }}
{{ $address }}
</span>
@else
<span class="inline">
{{ $model->address() }}
{{ $address }}
</span>
@endif
</x-slot>
@@ -46,7 +56,7 @@

@unless ($withoutClipboard)
<x-clipboard
:value="$model->address()"
:value="$address"
:tooltip="trans('pages.wallet.address_copied')"
class="mr-3"
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@props(['model'])

@php
$recepients = $model->multiPaymentRecipients();
@endphp

<x-general.page-section.container
:title="trans('general.recipients')"
wrapper-class="flex flex-col flex-1 whitespace-nowrap"
no-border
>
<x-tables.encapsulated-table>
<thead class="dark:bg-black bg-theme-secondary-100">
<tr class="border-b-none">
<x-tables.headers.desktop.address name="general.address" class="w-full" />

<x-tables.headers.desktop.number name="general.transaction.amount" class="text-right" />
</tr>
</thead>
<tbody>
@foreach($recepients as $recipient)
<x-ark-tables.row wire:key="recipient-{{ $recipient['address'] }}-{{ $recipient['amount'] }}-{{ $loop->index }}">
<x-ark-tables.cell>
<x-tables.rows.desktop.encapsulated.address truncateBreakpoint="lg" :address="$recipient['address']" />
</x-ark-tables.cell>

<x-ark-tables.cell class="text-right">
<x-general.amount-small :amount="$recipient['amount']" />
</x-ark-tables.cell>
</x-ark-tables.row>
@endforeach
</tbody>
</x-ark-tables.table>

</x-general.page-section.container>
39 changes: 39 additions & 0 deletions tests/Unit/ViewModels/TransactionViewModelTest.php
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
use App\Models\Receipt;
use App\Models\Transaction;
use App\Models\Wallet;
use App\Services\BigNumber;
use App\Services\Cache\CryptoDataCache;
use App\Services\Cache\NetworkCache;
use App\ViewModels\TransactionViewModel;
@@ -312,6 +313,44 @@

expect($transaction->formattedPayload())->toBe('MethodID: 0x12341234');
});

it('should get formatted multi payment receipts', function () {
$transaction = new TransactionViewModel(Transaction::factory()
->multiPayment([
'0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A',
'0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A',
'0xEd0C906b8fcCDe71A19322DFfe929c6e04460cFF',
], [
BigNumber::new(100000000),
BigNumber::new(200000000),
BigNumber::new(1234567),
])->create());

expect($transaction->multiPaymentRecipients())->toEqual([
'0' => [
'address' => '0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A',
'amount' => '100000000',
],
'1' => [
'address' => '0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A',
'amount' => '200000000',
],
'2' => [
'address' => '0xEd0C906b8fcCDe71A19322DFfe929c6e04460cFF',
'amount' => '1234567',
],
]);
});

it('should fail to get formatted multi payment receipts if not a multi payment', function () {
$transaction = new TransactionViewModel(Transaction::factory()
->withPayload('123456')
->create());

expect(function () use ($transaction) {
$transaction->multiPaymentRecipients();
})->toThrow(Exception::class);
});
});

it('should calculate fee with receipt', function () {