handle($method, $path); if ($method === 'GET' && $path === '/api/v1/live') { JsonResponse::success(['alive' => true]); } $requestCredentials = RequestCredentials::fromHeaders(new NativeCrypto()); $application = ApplicationFactory::createFromConfig( $config, $requestCredentials->handshakeProof, ); $router = new Router(); $router->get('/api/v1', static function () use ($application): never { JsonResponse::success([ 'name' => 'Contractless PHP API', 'version' => 'v1', 'rpc_endpoints' => count($application->transport->endpointIds()), ]); }); $router->get('/api/v1/health', static function () use ($application): never { $height = RpcReplyDecoder::unsigned32( $application->client->blockHeight(), 'block height', ); JsonResponse::success(['online' => true, 'height' => $height]); }); $router->get('/api/v1/ready', static function () use ($application): never { $height = RpcReplyDecoder::unsigned32( $application->client->blockHeight(), 'block height', ); JsonResponse::success([ 'ready' => true, 'height' => $height, 'rpc_endpoints' => $application->endpointHealth->status( $application->transport->endpointIds(), ), ]); }); $router->get('/api/v1/network', static function () use ($application): never { JsonResponse::success( RpcReplyDecoder::networkInfo($application->client->networkInfo()), ); }); $router->get('/api/v1/height', static function () use ($application): never { JsonResponse::success([ 'height' => RpcReplyDecoder::unsigned32( $application->client->blockHeight(), 'block height', ), ]); }); $router->get('/api/v1/time', static function () use ($application): never { JsonResponse::success([ 'timestamp' => RpcReplyDecoder::unsigned32( $application->client->nodeTime(), 'node time', ), ]); }); $router->get('/api/v1/addresses/validate', static function () use ($application): never { $address = Request::canonicalAddress(Request::queryString('address', 45)); JsonResponse::success([ 'address' => $address, 'valid' => RpcReplyDecoder::validStatus( $application->client->validateAddress($address), ), ]); }); $router->get('/api/v1/addresses/vanity/resolve', static function () use ($application): never { $address = strtolower(Request::queryString('address', 25)); if (preg_match('/^([a-z]{1,20})\.(clc|cltc)$/', $address, $matches) !== 1) { throw new HttpException(422, 'Enter a valid Contractless vanity address.'); } $fixedWidthAddress = str_pad($matches[1], 20, ' ', STR_PAD_LEFT) . '.' . $matches[2]; JsonResponse::success([ 'vanity_address' => $address, 'owner_address' => RpcReplyDecoder::optionalText( $application->client->vanityOwner($fixedWidthAddress), ), ]); }); $router->get('/api/v1/addresses/registration', static function () use ($application): never { $address = Request::canonicalAddress(Request::queryString('address', 45)); JsonResponse::success([ 'address' => $address, 'registered' => RpcReplyDecoder::registrationStatus( $application->client->walletRegistrationStatus($address), ), ]); }); $router->post( '/api/v1/addresses/register', static function () use ($application, $requestCredentials): never { $body = Request::json(); $address = Request::canonicalAddress( Request::bodyString($body, 'address', 45), ); $publicKeyHex = strtolower( Request::bodyString($body, 'public_key', 1_794), ); $signature = Request::signature( Request::bodyString($body, 'signature', 1_332), ); if ( strlen($publicKeyHex) !== 1_794 || !ctype_xdigit($publicKeyHex) ) { throw new HttpException( 422, 'The public key must contain 1,794 hexadecimal characters.', ); } if (!hash_equals($requestCredentials->address, $address)) { throw new HttpException( 403, 'A wallet may only submit its own registration.', ); } $publicKey = hex2bin($publicKeyHex); if ( $publicKey === false || !hash_equals( $requestCredentials->handshakeProof->publicKey, $publicKey, ) ) { throw new HttpException( 403, 'The registration public key does not match the authenticated wallet.', ); } JsonResponse::success([ 'address' => $address, 'registered' => RpcReplyDecoder::registrationStatus( $application->client->registerWallet( $address, $publicKey, $signature, ), ), ]); }, ); $router->get('/api/v1/balances/base', static function () use ($application): never { $address = Request::canonicalAddress(Request::queryString('address', 45)); $coin = strtoupper(Request::queryString('coin', 15)); $expectedCoin = str_ends_with($address, '.cltc') ? 'CLTC' : 'CLC'; if ($coin !== $expectedCoin) { throw new HttpException( 422, "Base coin must be $expectedCoin for this address.", ); } JsonResponse::success([ 'address' => $address, 'coin' => $coin, 'balance' => RpcReplyDecoder::balance( $application->client->coinBalance($coin, $address), ), ]); }); $router->get('/api/v1/balances', static function () use ($application): never { $address = Request::canonicalAddress(Request::queryString('address', 45)); JsonResponse::success([ 'address' => $address, 'balances' => RpcReplyDecoder::balances( $application->client->totalBalance($address), ), ]); }); $router->get('/api/v1/transactions/lookup', static function () use ($application): never { $txid = Request::hash(Request::queryString('txid', 64), 'txid'); JsonResponse::success([ 'txid' => $txid, ] + RpcReplyDecoder::transaction( $application->client->transactionById($txid), )); }); $router->get('/api/v1/addresses/history', static function () use ($application): never { $address = Request::canonicalAddress(Request::queryString('address', 45)); $skip = Request::queryInt('skip', 0, 0, 4_294_967_295); $limit = Request::queryInt('limit', 100, 1, 1_000); JsonResponse::success([ 'address' => $address, 'skip' => $skip, 'limit' => $limit, 'transactions' => RpcReplyDecoder::addressHistory( $application->client->addressHistory($address, $skip, $limit), ), ]); }); $router->post('/api/v1/messages/verify', static function () use ($application): never { $body = Request::json(); // Message bytes must remain exactly as signed, including surrounding whitespace. $message = Request::bodyString($body, 'message', 65_535, trim: false); $address = Request::canonicalAddress( Request::bodyString($body, 'address', 45), ); $signature = Request::signature( Request::bodyString($body, 'signature', 1_332), ); JsonResponse::success([ 'valid' => RpcReplyDecoder::validStatus( $application->client->validateMessage($message, $address, $signature), ), ]); }); $router->post('/api/v1/transactions/broadcast', static function () use ($application): never { $body = Request::json(); $transaction = Request::transactionHex( Request::bodyString($body, 'transaction_hex', 1_900_000), ); $transactionType = TransactionPolicy::validate($transaction); JsonResponse::success( ['transaction_type' => $transactionType] + RpcReplyDecoder::broadcast( $application->client->submitTransaction($transaction), ), ); }); RemainingRoutes::register($router, $application); NftMediaRoutes::register( $router, $application, new NftMediaService($config->nftMedia), ); $router->dispatch($method, $path); } catch (Throwable $error) { ExceptionResponder::respond($error); }