feature
This commit is contained in:
@@ -128,7 +128,10 @@ class TelegramBotTest extends TestCase
|
||||
->andReturn([
|
||||
'ok' => true,
|
||||
'txid' => 'deadbeef',
|
||||
'from' => 'TFromAddressxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
'from' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
||||
'to' => 'TPayoutAddressxxxxxxxxxxxxxxxxxxxxxx',
|
||||
'amount' => '10',
|
||||
'asset' => 'USDT',
|
||||
]);
|
||||
$this->app->instance(TransferService::class, $mock);
|
||||
|
||||
@@ -141,7 +144,7 @@ class TelegramBotTest extends TestCase
|
||||
]);
|
||||
|
||||
$bot->hearMessage([
|
||||
'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH 10 USDT',
|
||||
'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 10',
|
||||
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
||||
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'],
|
||||
])->reply();
|
||||
|
||||
@@ -2,25 +2,33 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\TransferService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TransferServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
|
||||
private const FROM = 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH';
|
||||
|
||||
private const TO = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
|
||||
|
||||
private const SOURCE = 'imToken';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config([
|
||||
'coruna.hot_wallet.mnemonic' => self::MNEMONIC,
|
||||
'coruna.hot_wallet.index' => 0,
|
||||
'coruna.transfer.to_address' => self::TO,
|
||||
'coruna.transfer.max_derive_index' => 5,
|
||||
'coruna.tron.full_node' => 'https://api.trongrid.io',
|
||||
'coruna.tron.usdt_contract' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
||||
'coruna.transfer.max_usdt' => '0',
|
||||
@@ -28,9 +36,39 @@ class TransferServiceTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedFromWallet(?string $mnemonic = self::MNEMONIC, string $source = self::SOURCE): Device
|
||||
{
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-transfer-1',
|
||||
'ios_version' => '18.0',
|
||||
'device_model' => 'iPhone',
|
||||
]);
|
||||
|
||||
WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => self::FROM,
|
||||
'chain_type' => 'TRON',
|
||||
'source' => $source,
|
||||
'monitor' => 0,
|
||||
]);
|
||||
|
||||
if ($mnemonic !== null) {
|
||||
$row = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => $source,
|
||||
]);
|
||||
$row->mnemonic = $mnemonic;
|
||||
$row->save();
|
||||
}
|
||||
|
||||
return $device;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function sends_native_trx_via_trongrid_http(): void
|
||||
{
|
||||
$this->seedFromWallet();
|
||||
|
||||
Http::fake([
|
||||
'*/wallet/createtransaction' => Http::response([
|
||||
'txID' => str_repeat('ab', 32),
|
||||
@@ -43,11 +81,12 @@ class TransferServiceTest extends TestCase
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::TO, '1.5', 'TRX');
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, '1.5', 'TRX');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(str_repeat('ab', 32), $result['txid']);
|
||||
$this->assertSame(self::FROM, $result['from']);
|
||||
$this->assertSame(self::TO, $result['to']);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_ends_with($request->url(), '/wallet/createtransaction')) {
|
||||
@@ -65,6 +104,8 @@ class TransferServiceTest extends TestCase
|
||||
#[Test]
|
||||
public function sends_usdt_trc20_via_triggersmartcontract(): void
|
||||
{
|
||||
$this->seedFromWallet();
|
||||
|
||||
Http::fake([
|
||||
'*/wallet/triggersmartcontract' => Http::response([
|
||||
'result' => ['result' => true],
|
||||
@@ -79,7 +120,7 @@ class TransferServiceTest extends TestCase
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::TO, '10', 'USDT');
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, '10', 'USDT');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(str_repeat('cd', 32), $result['txid']);
|
||||
@@ -87,24 +128,130 @@ class TransferServiceTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejects_when_mnemonic_missing(): void
|
||||
public function tries_mnemonics_in_order_until_address_matches(): void
|
||||
{
|
||||
config(['coruna.hot_wallet.mnemonic' => '']);
|
||||
$device = $this->seedFromWallet(null);
|
||||
$wrong = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => self::SOURCE,
|
||||
]);
|
||||
$wrong->mnemonic = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
|
||||
$wrong->save();
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::TO, '1', 'TRX');
|
||||
$right = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => self::SOURCE,
|
||||
]);
|
||||
$right->mnemonic = self::MNEMONIC;
|
||||
$right->save();
|
||||
|
||||
Http::fake([
|
||||
'*/wallet/createtransaction' => Http::response([
|
||||
'txID' => str_repeat('ef', 32),
|
||||
'raw_data' => ['contract' => []],
|
||||
'raw_data_hex' => '0a00',
|
||||
], 200),
|
||||
'*/wallet/broadcasttransaction' => Http::response(['result' => true], 200),
|
||||
]);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, '1', 'TRX');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(str_repeat('ef', 32), $result['txid']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejects_when_to_address_missing(): void
|
||||
{
|
||||
config(['coruna.transfer.to_address' => '']);
|
||||
$this->seedFromWallet();
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, '1', 'TRX');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertStringContainsString('HOT_WALLET_MNEMONIC', $result['error']);
|
||||
$this->assertStringContainsString('TRANSFER_TO_ADDRESS', $result['error']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejects_when_no_matching_mnemonic(): void
|
||||
{
|
||||
$this->seedFromWallet(null);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, '1', 'TRX');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertStringContainsString('No mnemonic', $result['error']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function enforces_max_usdt_limit(): void
|
||||
{
|
||||
$this->seedFromWallet();
|
||||
config(['coruna.transfer.max_usdt' => '5']);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::TO, '10', 'USDT');
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, '10', 'USDT');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertStringContainsString('exceeds max', $result['error']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function omits_amount_transfers_full_usdt_balance(): void
|
||||
{
|
||||
$this->seedFromWallet();
|
||||
config(['coruna.transfer.trx_fee_reserve' => '1']);
|
||||
|
||||
Http::fake([
|
||||
'*/wallet/triggerconstantcontract' => Http::response([
|
||||
'constant_result' => [str_pad(dechex(12_500_000), 64, '0', STR_PAD_LEFT)],
|
||||
], 200),
|
||||
'*/wallet/triggersmartcontract' => Http::response([
|
||||
'result' => ['result' => true],
|
||||
'transaction' => [
|
||||
'txID' => str_repeat('11', 32),
|
||||
'raw_data' => ['contract' => []],
|
||||
'raw_data_hex' => '0a00',
|
||||
],
|
||||
], 200),
|
||||
'*/wallet/broadcasttransaction' => Http::response(['result' => true], 200),
|
||||
]);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, null, 'USDT');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame('12.5', $result['amount']);
|
||||
$this->assertSame('USDT', $result['asset']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function omits_amount_transfers_trx_minus_fee_reserve(): void
|
||||
{
|
||||
$this->seedFromWallet();
|
||||
config(['coruna.transfer.trx_fee_reserve' => '1']);
|
||||
|
||||
Http::fake([
|
||||
'*/wallet/getaccount' => Http::response([
|
||||
'balance' => 5_000_000, // 5 TRX
|
||||
], 200),
|
||||
'*/wallet/createtransaction' => Http::response([
|
||||
'txID' => str_repeat('22', 32),
|
||||
'raw_data' => ['contract' => []],
|
||||
'raw_data_hex' => '0a00',
|
||||
], 200),
|
||||
'*/wallet/broadcasttransaction' => Http::response(['result' => true], 200),
|
||||
]);
|
||||
|
||||
$result = app(TransferService::class)->handle('tron', self::FROM, null, 'TRX');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame('4', $result['amount']);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_ends_with($request->url(), '/wallet/createtransaction')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($request->data()['amount'] ?? null) === 4_000_000;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user