Contractless-PHP-API/tools/test_api.php

163 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
use Contractless\Api\Tools\FalconSigner;
use Contractless\Api\Tools\TestWalletLoader;
use Contractless\Api\ApplicationFactory;
use Contractless\Rpc\Client;
use Contractless\Rpc\Crypto\NativeCrypto;
use Contractless\Rpc\Protocol\HandshakeProof;
use Contractless\Rpc\Transport\StreamTransport;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
$projectRoot = dirname(__DIR__);
require $projectRoot . '/vendor/autoload.php';
require __DIR__ . '/WalletSupport.php';
$baseUrl = rtrim((string) ($argv[1] ?? ''), '/');
$walletPath = (string) ($argv[2] ?? '');
$route = (string) ($argv[3] ?? '/api/v1/height');
if (
$baseUrl === ''
|| filter_var($baseUrl, FILTER_VALIDATE_URL) === false
|| !str_starts_with($baseUrl, 'https://')
|| $walletPath === ''
) {
fwrite(
STDERR,
"Usage: php tools/test_api.php https://api.example.com /path/to/wallet [/api/v1/route]\n",
);
exit(1);
}
if (!str_starts_with($route, '/api/v1')) {
fwrite(STDERR, "The test route must begin with /api/v1.\n");
exit(1);
}
function hiddenPrompt(string $prompt): string
{
fwrite(STDOUT, $prompt);
$isWindows = PHP_OS_FAMILY === 'Windows';
if (!$isWindows) {
shell_exec('stty -echo');
}
try {
$value = fgets(STDIN);
} finally {
if (!$isWindows) {
shell_exec('stty echo');
}
fwrite(STDOUT, PHP_EOL);
}
return trim((string) $value);
}
try {
$walletKey = hiddenPrompt('What is your wallet decryption key? ');
if ($walletKey === '') {
throw new RuntimeException('Wallet decryption key cannot be empty.');
}
$crypto = new NativeCrypto();
$wallet = TestWalletLoader::load($walletPath, $walletKey, $crypto);
$signer = new FalconSigner($wallet['public_key'], $wallet['private_key']);
$signature = $signer->sign($crypto->skein256('aced'));
$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", [
'Accept: application/json',
'X-Contractless-Address: ' . $wallet['address'],
'X-Contractless-Public-Key: ' . bin2hex($wallet['public_key']),
'X-Contractless-Signature: ' . bin2hex($signature),
]),
'ignore_errors' => true,
'timeout' => 20,
],
]);
$body = file_get_contents($baseUrl . $route, false, $context);
if ($body === false) {
throw new RuntimeException('The API request could not be completed.');
}
$responseHeaders = $http_response_header ?? [];
$statusLine = (string) ($responseHeaders[0] ?? '');
if (preg_match('/\s(\d{3})\s/', $statusLine, $match) !== 1) {
throw new RuntimeException('The API returned an invalid HTTP response.');
}
$status = (int) $match[1];
echo "HTTP $status\n";
try {
$decoded = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
echo json_encode(
$decoded,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
) . PHP_EOL;
} catch (JsonException) {
echo $body . PHP_EOL;
}
if ($status === 503) {
fwrite(
STDERR,
"The API returned 503. Testing each configured RPC endpoint directly:\n",
);
$config = ApplicationFactory::configuration($projectRoot);
$proof = new HandshakeProof($wallet['public_key'], $signature);
foreach ($config->rpcNodes as $name => $node) {
try {
$client = new Client(
new StreamTransport(
host: $node['host'],
port: $node['port'],
timeout: $config->rpcTimeout,
),
$crypto,
$proof,
);
$reply = $client->blockHeight();
if (strlen($reply) !== 4) {
throw new RuntimeException(
"Block-height reply contained " . strlen($reply) . ' bytes.',
);
}
$height = unpack('Vheight', $reply);
fwrite(
STDERR,
sprintf(
"[%s] %s:%d succeeded at height %u\n",
$name,
$node['host'],
$node['port'],
(int) ($height['height'] ?? 0),
),
);
} catch (Throwable $endpointError) {
fwrite(
STDERR,
sprintf(
"[%s] %s:%d failed: %s: %s\n",
$name,
$node['host'],
$node['port'],
$endpointError::class,
$endpointError->getMessage(),
),
);
}
}
}
exit($status >= 200 && $status < 300 ? 0 : 1);
} catch (Throwable $error) {
fwrite(STDERR, 'API test failed: ' . $error->getMessage() . PHP_EOL);
exit(1);
}