Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
EdouardCourty committed Jan 2, 2025
1 parent f31a7ea commit bf2c1de
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/php_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: PHP CI

on:
push:
branches: [ 'main' ]
pull_request:
branches: [ '*' ]

jobs:
validate:
name: Validate codebase
runs-on: ubuntu-latest
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3

- name: Checkout code
uses: actions/checkout@v4

- name: Validate composer.json
run: composer validate --strict

- name: Audit dependencies
run: composer audit --no-dev

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run PHP CS Fixer
run: php vendor/bin/php-cs-fixer check --config=.php-cs-fixer.php --allow-risky=yes

- name: Run PHPStan
run: php vendor/bin/phpstan

- name: Run PHPUnit
run: php vendor/bin/phpunit tests
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vendor
composer.lock
.idea
42 changes: 42 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "ecourty/xrpl-php",
"description": "A PHP library to interact with an XRP Ledger Node.",
"type": "library",
"homepage": "https://github.com/EdouardCourty/xrpl-php",
"keywords": [
"ripple",
"xrp",
"json-rpc",
"php"
],
"prefer-stable": true,
"require": {
"php": ">=8.3",
"ext-bcmath": "*",
"symfony/http-client": "^7.2"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.5",
"friendsofphp/php-cs-fixer": "^3.65"
},
"license": "MIT",
"autoload": {
"psr-4": {
"XRPL\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"XRPL\\Tests\\": "tests/"
}
},
"authors": [
{
"name": "Edouard Courty",
"email": "[email protected]",
"homepage": "https://github.com/EdouardCourty",
"role": "Author"
}
]
}
56 changes: 56 additions & 0 deletions src/Client/JsonRpcClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace XRPL\Client;

use XRPL\Exception\JsonRpcException;
use XRPL\Utils\JsonRpcRequest;
use XRPL\Utils\UuidGenerator;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* @author Edouard Courty <[email protected]>
*/
readonly class JsonRpcClient
{
private const int DEFAULT_TIMEOUT = 10;

private HttpClientInterface $httpClient;

public function __construct(string $url)
{
$this->httpClient = HttpClient::createForBaseUri($url);
}

public function request(string $method, array $params = [], int $timeout = self::DEFAULT_TIMEOUT): bool|string|int|float|array
{
$requestPayload = new JsonRpcRequest(UuidGenerator::v4(), $method, $params);
$response = null;

try {
$response = $this->httpClient->request('POST', '/', [
'json' => $requestPayload->toArray(),
'timeout' => $timeout,
]);

$statusCode = $response->getStatusCode();

if ($statusCode !== 200) {
throw new JsonRpcException("HTTP error: {$statusCode}", $response);
}

$content = $response->toArray(false);

if (isset($content['error'])) {
$error = $content['error'];
throw new JsonRpcException("RPC Error {$error['code']}: {$error['message']}", $response);
}

return $content['result'];
} catch (\Throwable $e) {
throw new JsonRpcException("Request failed: " . $e->getMessage(), $response, $e);
}
}
}
19 changes: 19 additions & 0 deletions src/Client/XRPLClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace XRPL\Client;

/**
* @author Edouard Courty <[email protected]>
*/
readonly class XRPLClient
{
private JsonRpcClient $jsonRpcClient;

public function __construct(
string $url,
) {
$this->jsonRpcClient = new JsonRpcClient($url);
}
}
29 changes: 29 additions & 0 deletions src/Exception/JsonRpcException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace XRPL\Exception;

use Symfony\Contracts\HttpClient\ResponseInterface;
use Throwable;

/**
* @author Edouard Courty <[email protected]>
*
* @codeCoverageIgnore
*/
class JsonRpcException extends \Exception
{
public function __construct(
string $message,
private readonly ?ResponseInterface $response = null,
?Throwable $previous = null,
) {
parent::__construct($message, $this->response->getStatusCode(), $previous);
}

public function getResponse(): ?ResponseInterface
{
return $this->response;
}
}
31 changes: 31 additions & 0 deletions src/Utils/JsonRpcRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace XRPL\Utils;

/**
* @author Edouard Courty <[email protected]>
*/
readonly class JsonRpcRequest
{
public const string JSON_RPC_VERSION = '2.0';

public function __construct(
private string $id,
private string $method,
private array $params,
private string $version = self::JSON_RPC_VERSION,
) {
}

public function toArray(): array
{
return [
'jsonrpc' => $this->version,
'method' => $this->method,
'params' => $this->params,
'id' => $this->id,
];
}
}
20 changes: 20 additions & 0 deletions src/Utils/UuidGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace XRPL\Utils;

/**
* @author Edouard Courty <[email protected]>
*/
class UuidGenerator
{
final public static function v4(): string
{
$data = \random_bytes(16);
$data[6] = \chr(\ord($data[6]) & 0x0f | 0x40);
$data[8] = \chr(\ord($data[8]) & 0x3f | 0x80);

return \vsprintf('%s%s-%s-%s-%s-%s%s%s', \str_split(\bin2hex($data), 4));
}
}

0 comments on commit bf2c1de

Please sign in to comment.