From ca11c4cceead3aa412ddb2626805c923f8c9d14e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Apr 2026 11:24:27 +0100 Subject: [PATCH] refactor: extract Actions for CheckAvailability, SubmitClaim, RegisterName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CorePHP Actions pattern — single-purpose classes with static ::run(). Controller methods now delegate to Actions. Each Action validates, executes, and returns typed results. Enables reuse from commands, jobs, and tests without going through HTTP. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/Mod/Names/Actions/CheckAvailability.php | 51 ++++++++++ app/Mod/Names/Actions/RegisterName.php | 90 ++++++++++++++++++ app/Mod/Names/Actions/SubmitClaim.php | 75 +++++++++++++++ app/Mod/Names/Controllers/NamesController.php | 59 ++---------- app/Mod/Names/Models/NameClaim.php | 4 + database/database.sqlite | Bin 28672 -> 28672 bytes 6 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 app/Mod/Names/Actions/CheckAvailability.php create mode 100644 app/Mod/Names/Actions/RegisterName.php create mode 100644 app/Mod/Names/Actions/SubmitClaim.php diff --git a/app/Mod/Names/Actions/CheckAvailability.php b/app/Mod/Names/Actions/CheckAvailability.php new file mode 100644 index 0000000..01fad4c --- /dev/null +++ b/app/Mod/Names/Actions/CheckAvailability.php @@ -0,0 +1,51 @@ + $name, + 'available' => false, + 'reserved' => false, + 'reason' => 'Invalid name format.', + 'fqdn' => "{$name}.lthn", + ]; + } + + $alias = $this->rpc->getAliasByName($name); + $reserved = Cache::has("name_lock:{$name}"); + + return [ + 'name' => $name, + 'available' => $alias === null && ! $reserved, + 'reserved' => $reserved, + 'fqdn' => "{$name}.lthn", + ]; + } + + public static function run(string $name): array + { + return app(static::class)->handle($name); + } +} diff --git a/app/Mod/Names/Actions/RegisterName.php b/app/Mod/Names/Actions/RegisterName.php new file mode 100644 index 0000000..7a97423 --- /dev/null +++ b/app/Mod/Names/Actions/RegisterName.php @@ -0,0 +1,90 @@ +validate($name); + + // Atomic reservation lock (10 min) + $lockKey = "name_lock:{$name}"; + if (! Cache::add($lockKey, true, 600)) { + throw ValidationException::withMessages([ + 'name' => 'Name is being registered by another request.', + ]); + } + + // Use registrar wallet if no address provided + if (empty($address)) { + $address = $this->wallet->getAddress(); + } + + $comment = 'v=lthn1;type=user'; + $result = $this->wallet->registerAlias($name, $address, $comment); + $ticketId = bin2hex(random_bytes(6)); + + if (isset($result['tx_id'])) { + return [ + 'name' => $name, + 'fqdn' => "{$name}.lthn", + 'ticket_id' => $ticketId, + 'tx_id' => $result['tx_id'], + 'status' => 'pending', + 'address' => $address, + ]; + } + + // Registration queued (chain busy) + Cache::forget($lockKey); + + return [ + 'name' => $name, + 'fqdn' => "{$name}.lthn", + 'ticket_id' => $ticketId, + 'status' => 'queued', + 'message' => $result['error'] ?? 'Queued for next block.', + ]; + } + + private function validate(string $name): void + { + if (! preg_match('/^[a-z0-9][a-z0-9.\-]{0,254}$/', $name) || strlen($name) < 6) { + throw ValidationException::withMessages([ + 'name' => 'Invalid name. Use 6+ lowercase alphanumeric characters.', + ]); + } + + if ($this->rpc->getAliasByName($name) !== null) { + throw ValidationException::withMessages([ + 'name' => 'Name already registered.', + ]); + } + } + + public static function run(string $name, string $address = ''): array + { + return app(static::class)->handle($name, $address); + } +} diff --git a/app/Mod/Names/Actions/SubmitClaim.php b/app/Mod/Names/Actions/SubmitClaim.php new file mode 100644 index 0000000..77a89d9 --- /dev/null +++ b/app/Mod/Names/Actions/SubmitClaim.php @@ -0,0 +1,75 @@ + 'mybrand', 'email' => 'me@example.com']); + * echo $claim->claim_id; + */ +class SubmitClaim +{ + public function __construct( + private readonly DaemonRpc $rpc, + ) {} + + public function handle(array $data): NameClaim + { + $name = strtolower(trim($data['name'] ?? '')); + $email = trim($data['email'] ?? ''); + + $this->validate($name, $email); + + return NameClaim::create([ + 'name' => $name, + 'email' => $email, + ]); + } + + private function validate(string $name, string $email): void + { + if (! preg_match('/^[a-z0-9][a-z0-9.\-]{0,254}$/', $name)) { + throw ValidationException::withMessages([ + 'name' => 'Invalid name. Use lowercase alphanumeric characters.', + ]); + } + + if (strlen($name) < 6) { + throw ValidationException::withMessages([ + 'name' => 'Name must be at least 6 characters.', + ]); + } + + if (empty($email) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw ValidationException::withMessages([ + 'email' => 'Valid email required for claim notification.', + ]); + } + + // Not already registered on chain + if ($this->rpc->getAliasByName($name) !== null) { + throw ValidationException::withMessages([ + 'name' => 'Name already registered.', + ]); + } + + // Not already claimed + if (NameClaim::where('name', $name)->exists()) { + throw ValidationException::withMessages([ + 'name' => 'Name already claimed. Awaiting approval.', + ]); + } + } + + public static function run(array $data): NameClaim + { + return app(static::class)->handle($data); + } +} diff --git a/app/Mod/Names/Controllers/NamesController.php b/app/Mod/Names/Controllers/NamesController.php index 17cf34b..e66f878 100644 --- a/app/Mod/Names/Controllers/NamesController.php +++ b/app/Mod/Names/Controllers/NamesController.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Mod\Chain\Services\DaemonRpc; use Mod\Chain\Services\WalletRpc; +use Mod\Names\Actions; use Mod\Names\Models\NameClaim; /** @@ -34,24 +35,7 @@ class NamesController extends Controller */ public function available(string $name): JsonResponse { - $name = strtolower(trim($name)); - - if (! $this->isValidName($name)) { - return response()->json([ - 'available' => false, - 'reason' => 'Invalid name. Use 1-63 lowercase alphanumeric characters.', - ]); - } - - $alias = $this->rpc->getAliasByName($name); - $reserved = Cache::has("name_lock:{$name}"); - - return response()->json([ - 'name' => $name, - 'available' => $alias === null && ! $reserved, - 'reserved' => $reserved, - 'fqdn' => "{$name}.lthn", - ]); + return response()->json(Actions\CheckAvailability::run($name)); } /** @@ -543,43 +527,14 @@ class NamesController extends Controller */ public function claim(Request $request): JsonResponse { - $name = strtolower(trim((string) $request->input('name'))); - $email = trim((string) $request->input('email')); - - if (! $this->isValidName($name)) { - return response()->json(['error' => 'Invalid name. Use 6+ lowercase alphanumeric characters.'], 422); - } - - if (strlen($name) < 6) { - return response()->json(['error' => 'Name must be at least 6 characters.'], 422); - } - - if (empty($email) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { - return response()->json(['error' => 'Valid email required for claim notification.'], 422); - } - - // Check not already registered on chain - $alias = $this->rpc->getAliasByName($name); - if ($alias !== null) { - return response()->json(['error' => 'Name already registered.', 'name' => $name], 409); - } - - // Check not already claimed in database - if (NameClaim::where('name', $name)->exists()) { - return response()->json(['error' => 'Name already claimed. Awaiting approval.', 'name' => $name], 409); - } - - $claim = NameClaim::create([ - 'name' => $name, - 'email' => $email, - ]); + $claim = Actions\SubmitClaim::run($request->only(['name', 'email'])); return response()->json([ 'claim_id' => $claim->claim_id, - 'name' => $name, - 'fqdn' => "{$name}.lthn", - 'status' => 'pending', - 'message' => 'Your claim has been submitted. We will notify you at ' . $email . ' when approved.', + 'name' => $claim->name, + 'fqdn' => "{$claim->name}.lthn", + 'status' => $claim->status, + 'message' => "Your claim has been submitted. We will notify you at {$claim->email} when approved.", ], 201); } diff --git a/app/Mod/Names/Models/NameClaim.php b/app/Mod/Names/Models/NameClaim.php index 5377394..5eab753 100644 --- a/app/Mod/Names/Models/NameClaim.php +++ b/app/Mod/Names/Models/NameClaim.php @@ -22,6 +22,10 @@ class NameClaim extends Model 'status', ]; + protected $attributes = [ + 'status' => 'pending', + ]; + protected static function booted(): void { static::creating(function (self $claim) { diff --git a/database/database.sqlite b/database/database.sqlite index bd15511ebe022bd2e989cd355ed8b60fe6fce822..476186a8ddb5e8059e0c51f3fe7d0160e2aeb610 100644 GIT binary patch delta 258 zcmZp8z}WDBae_1>$3z)tMvjdM%jDS}Fz|omf3R85;R?SXKNGVuV_srzYJ74|VrFhJ z)8vo(!W>Nez6|^;`F%GF3OMqsMKN(Os48kp8ylM%npqf|8dw^bn5HHsmt^MWm82Gz zKvt delta 146 zcmZp8z}WDBae_1>`$QRMM)r*f%jDS}GVp)of4EsN;VQoX9}}}OXI^4%YJ74|VrFjf zfOX8TeOj78IDzKY68nG?4#+f&T+g!wvq40eq7`_=^CAJ~Qxth6qg%6z5@< o=S)c|Ni8lhG&1I5U|?Y6|H{Ds6)G>x&BZLuS&*8Sl9`td0J;k