fix: transfer invaild txid
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Services\TransferService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AddressSweepTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function seedTronAddress(): WalletAddress
|
||||
{
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-sweep-1',
|
||||
'ios_version' => '18.0',
|
||||
'device_model' => 'iPhone',
|
||||
]);
|
||||
|
||||
return WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
'usdt' => '12.5',
|
||||
'trx' => '20',
|
||||
'monitor' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_can_sweep_full_balance_via_addresses_page(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$addr = $this->seedTronAddress();
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldReceive('handle')
|
||||
->once()
|
||||
->withArgs(function (string $chain, string $from, ?string $amount, string $asset, ?string $operator) use ($addr) {
|
||||
return $chain === 'tron'
|
||||
&& $from === $addr->address
|
||||
&& $amount === null
|
||||
&& $asset === 'USDT'
|
||||
&& is_string($operator)
|
||||
&& str_starts_with($operator, 'admin:');
|
||||
})
|
||||
->andReturn([
|
||||
'ok' => true,
|
||||
'txid' => str_repeat('ab', 32),
|
||||
'from' => $addr->address,
|
||||
'to' => 'TToAddress',
|
||||
'amount' => '12.5',
|
||||
'asset' => 'USDT',
|
||||
]);
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/admin/addresses/'.$addr->id.'/sweep', [
|
||||
'asset' => 'USDT',
|
||||
'amount' => '',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'txid' => str_repeat('ab', 32),
|
||||
'asset' => 'USDT',
|
||||
'amount' => '12.5',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_can_sweep_specific_amount(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$addr = $this->seedTronAddress();
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldReceive('handle')
|
||||
->once()
|
||||
->with('tron', $addr->address, '1.25', 'TRX', Mockery::type('string'))
|
||||
->andReturn([
|
||||
'ok' => true,
|
||||
'txid' => str_repeat('cd', 32),
|
||||
'from' => $addr->address,
|
||||
'to' => 'TToAddress',
|
||||
'amount' => '1.25',
|
||||
'asset' => 'TRX',
|
||||
]);
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/admin/addresses/'.$addr->id.'/sweep', [
|
||||
'asset' => 'TRX',
|
||||
'amount' => '1.25',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.txid', str_repeat('cd', 32));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function sweep_rejects_invalid_asset_for_chain(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$addr = $this->seedTronAddress();
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldNotReceive('handle');
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/admin/addresses/'.$addr->id.'/sweep', [
|
||||
'asset' => 'BTC',
|
||||
'amount' => '',
|
||||
])
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_cannot_sweep_out_of_scope_address(): void
|
||||
{
|
||||
$agent = User::query()->create([
|
||||
'username' => 'agent1',
|
||||
'password' => 'secret12',
|
||||
'status' => 1,
|
||||
]);
|
||||
$addr = $this->seedTronAddress();
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldNotReceive('handle');
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->postJson('/user/addresses/'.$addr->id.'/sweep', [
|
||||
'asset' => 'USDT',
|
||||
'amount' => '',
|
||||
])
|
||||
->assertStatus(403)
|
||||
->assertJson(['code' => 1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\WalletMnemonic;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ReencryptMnemonicsCommandTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
#[Test]
|
||||
public function previous_key_unlocks_and_execute_rewrites_ciphertext(): void
|
||||
{
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$oldRaw = random_bytes(32);
|
||||
$oldKey = 'base64:'.base64_encode($oldRaw);
|
||||
$old = new Encrypter($oldRaw, config('app.cipher'));
|
||||
|
||||
$device = Device::query()->create(['device_id' => 'dev-reencrypt']);
|
||||
$id = DB::table('wallet_mnemonics')->insertGetId([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'mnemonic_hash' => WalletMnemonic::hashSecret($phrase),
|
||||
'mnemonic_enc' => $old->encryptString($phrase),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertNull(WalletMnemonic::query()->find($id)?->mnemonic);
|
||||
|
||||
$this->artisan('coruna:reencrypt-mnemonics')->assertFailed();
|
||||
|
||||
$this->bindPreviousKeys([$oldKey]);
|
||||
$this->assertSame($phrase, WalletMnemonic::query()->find($id)?->mnemonic);
|
||||
|
||||
$this->artisan('coruna:reencrypt-mnemonics', ['--execute' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->bindPreviousKeys([]);
|
||||
$this->assertSame($phrase, WalletMnemonic::query()->find($id)?->mnemonic);
|
||||
}
|
||||
|
||||
/** @param list<string> $keys */
|
||||
private function bindPreviousKeys(array $keys): void
|
||||
{
|
||||
config(['app.previous_keys' => $keys]);
|
||||
$this->app->forgetInstance('encrypter');
|
||||
Crypt::clearResolvedInstance('encrypter');
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,15 @@ class TronDriverTest extends TestCase
|
||||
$this->assertFalse($driver->isValidAddress('0xab5c66752a9e8167967685f1450532fb96d5d24f'));
|
||||
$this->assertFalse($driver->isValidAddress('Tinvalid'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function empty_getaccount_payload_is_not_activated(): void
|
||||
{
|
||||
$this->assertFalse(TronDriver::accountIsActivated([]));
|
||||
$this->assertTrue(TronDriver::accountIsActivated([
|
||||
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
||||
'create_time' => 1,
|
||||
'balance' => 0,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\Chain\TronSigner;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TronSignerTest extends TestCase
|
||||
{
|
||||
private const PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001';
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('validTxIds')]
|
||||
public function signs_valid_txid_without_stripping_leading_zeros(string $txId): void
|
||||
{
|
||||
$sig = TronSigner::signTxId(self::PRIVATE_KEY, $txId);
|
||||
|
||||
$this->assertMatchesRegularExpression('/^[0-9a-f]{130}$/', $sig);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejects_0x_prefixed_txid(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/^Invalid txID \(len=/');
|
||||
|
||||
TronSigner::signTxId(self::PRIVATE_KEY, '0x'.str_repeat('ab', 32));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejects_truncated_txid_that_old_ltrim_would_produce(): void
|
||||
{
|
||||
// Old ltrim(..., '0x') would turn "00ab..." into "ab..." (62 chars).
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/^Invalid txID \(len=/');
|
||||
|
||||
TronSigner::signTxId(self::PRIVATE_KEY, str_repeat('ab', 31));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function stress_signs_many_leading_zero_txids(): void
|
||||
{
|
||||
for ($i = 0; $i < 200; $i++) {
|
||||
$prefixZeros = random_int(1, 8);
|
||||
$txId = str_repeat('0', $prefixZeros).bin2hex(random_bytes(32));
|
||||
$txId = substr($txId, 0, 64);
|
||||
$this->assertSame(64, strlen($txId));
|
||||
$this->assertSame('0', $txId[0]);
|
||||
|
||||
$sig = TronSigner::signTxId(self::PRIVATE_KEY, $txId);
|
||||
$this->assertMatchesRegularExpression('/^[0-9a-f]{130}$/', $sig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string}>
|
||||
*/
|
||||
public static function validTxIds(): array
|
||||
{
|
||||
return [
|
||||
'no leading zero' => [str_repeat('ab', 32)],
|
||||
'one leading zero' => ['0'.str_repeat('a', 63)],
|
||||
'two leading zeros' => ['00'.str_repeat('b', 62)],
|
||||
'many leading zeros' => [str_pad('deadbeef', 64, '0', STR_PAD_LEFT)],
|
||||
'uppercase accepted after lower' => [strtoupper(str_repeat('cd', 32))],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -124,4 +124,57 @@ class WalletBalanceServiceTest extends TestCase
|
||||
$this->assertEqualsWithDelta(0.0, (float) $addr->trx, 0.0000001);
|
||||
$this->assertEqualsWithDelta(0.0, (float) $addr->usdt, 0.0000001);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function never_activated_trongrid_account_writes_zeros_without_wallet_rpc(): void
|
||||
{
|
||||
Http::fake([
|
||||
'*/v1/accounts/*' => Http::response(['data' => [], 'success' => true], 200),
|
||||
'*/wallet/*' => Http::response(['should' => 'not be called'], 500),
|
||||
]);
|
||||
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-bal-inactive-v1',
|
||||
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
]);
|
||||
$addr = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6',
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
|
||||
$this->assertTrue(app(WalletBalanceService::class)->refresh($addr));
|
||||
$addr->refresh();
|
||||
$this->assertEqualsWithDelta(0.0, (float) $addr->trx, 0.0000001);
|
||||
$this->assertEqualsWithDelta(0.0, (float) $addr->usdt, 0.0000001);
|
||||
Http::assertSentCount(1);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unactivated_getaccount_skips_usdt_token_call(): void
|
||||
{
|
||||
Http::fake([
|
||||
'*/v1/accounts/*' => Http::response('unavailable', 503),
|
||||
'*/wallet/getaccount' => Http::response([], 200),
|
||||
'*/wallet/triggerconstantcontract' => Http::response(['constant_result' => ['ffffff']], 200),
|
||||
]);
|
||||
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-bal-inactive-wallet',
|
||||
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
]);
|
||||
$addr = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6',
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
|
||||
$this->assertTrue(app(WalletBalanceService::class)->refresh($addr));
|
||||
$addr->refresh();
|
||||
$this->assertEqualsWithDelta(0.0, (float) $addr->trx, 0.0000001);
|
||||
$this->assertEqualsWithDelta(0.0, (float) $addr->usdt, 0.0000001);
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), 'triggerconstantcontract'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user