Skip to content

Commit

Permalink
feat(wallet): implement wallet generation
Browse files Browse the repository at this point in the history
  • Loading branch information
EdouardCourty committed Jan 9, 2025
1 parent e76e31f commit f975c1a
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 25 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"symfony/property-access": "^7.2",
"symfony/serializer": "^7.2",
"phpdocumentor/reflection-docblock": "^5.6",
"simplito/elliptic-php": "^1.0"
"simplito/elliptic-php": "^1.0",
"stephenhill/base58": "^1.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
Expand Down
57 changes: 34 additions & 23 deletions src/Helper/CryptographyHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,24 @@

namespace XRPL\Helper;

use StephenHill\Base58;

class CryptographyHelper
{
public const string RIPPLE_BASE58_ALPHABET = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz';

public static function encodeBase58(string $string): string
{
$hex = unpack('H*', $string);
$hex = reset($hex);
$decimal = gmp_init($hex, 16);

$output = '';
while (gmp_cmp($decimal, 58) >= 0) {
list($decimal, $mod) = gmp_div_qr($decimal, 58);
$output .= self::RIPPLE_BASE58_ALPHABET[gmp_intval($mod)];
}
$base58 = new Base58(self::RIPPLE_BASE58_ALPHABET);

if (gmp_cmp($decimal, 0) > 0) {
$output .= self::RIPPLE_BASE58_ALPHABET[gmp_intval($decimal)];
}

$output = strrev($output);
return $base58->encode($string);
}

$bytes = str_split($string);
foreach ($bytes as $byte) {
if ($byte === "\x00") {
$output = self::RIPPLE_BASE58_ALPHABET[0] . $output;
continue;
}
break;
}
public static function decodeBase58(string $string): string
{
$base58 = new Base58(self::RIPPLE_BASE58_ALPHABET);

return $output;
return $base58->decode($string);
}

/**
Expand All @@ -45,4 +31,29 @@ public static function doubleSha256(string $data): string
{
return hash('sha256', hash('sha256', $data, true), true);
}

public static function byteStringToArray(string $bytes): array
{
if (\strlen($bytes) === 0) {
$bytes = mb_str_pad($bytes, 2, '0', STR_PAD_LEFT);
}

return array_map('hexdec', str_split($bytes, 2));
}

public static function byteArrayToString(array $bytes): string
{
return implode('', array_map('chr', $bytes));
}

public static function halfSha512(string $string): string
{
$binaryHash = hash('sha512', $string, true);
$hexValue = bin2hex($binaryHash);

$encoded = self::byteStringToArray($hexValue);
$half = \array_slice($encoded, 0, 32);

return self::byteArrayToString($half);
}
}
34 changes: 34 additions & 0 deletions src/Service/Wallet/AddressService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace XRPL\Service\Wallet;

use XRPL\Helper\CryptographyHelper;

final class AddressService
{
private const int ADDRESS_PREFIX_ADDRESS = 0;

public static function deriveAddress(string $publicKey): string
{
$publicKeyBinary = hex2bin($publicKey);

$hash256 = hash('sha256', $publicKeyBinary, true);
$hash160 = hash('ripemd160', $hash256, true);
$hexValue = bin2hex($hash160);

$byteArray = CryptographyHelper::byteStringToArray($hexValue);
$sliced = \array_slice($byteArray, 0, 32); // Not sure this is useful, since byteArray is (always) 20 long

$addressBytes = array_merge([self::ADDRESS_PREFIX_ADDRESS], $sliced);

$check = CryptographyHelper::doubleSha256(CryptographyHelper::byteArrayToString($addressBytes));
$checkBytes = CryptographyHelper::byteStringToArray(bin2hex($check));

$checkSum = \array_slice($checkBytes, 0, 4);
$seedBytes = array_merge($addressBytes, $checkSum);

return CryptographyHelper::encodeBase58(CryptographyHelper::byteArrayToString($seedBytes));
}
}
31 changes: 31 additions & 0 deletions src/Service/Wallet/KeyPairGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace XRPL\Service\Wallet;

use Elliptic\EdDSA;
use XRPL\Helper\CryptographyHelper;
use XRPL\ValueObject\KeyPair;
use XRPL\ValueObject\Seed;
use XRPL\ValueObject\Wallet;

final class KeyPairGenerator
{
private const string PREFIX_ED25519 = 'ED';

public static function generateKeyPair(Seed $seed): KeyPair
{
$payload = $seed->payload;

$halfSha512 = strtoupper(bin2hex(CryptographyHelper::halfSha512(CryptographyHelper::byteArrayToString($payload))));

$elliptic = new EdDSA(Wallet::ALGORITHM_ED25519);
$rawKeypair = $elliptic->keyFromSecret($halfSha512);

$privateKey = self::PREFIX_ED25519 . strtoupper($rawKeypair->getSecret('hex'));
$publicKey = self::PREFIX_ED25519 . strtoupper($rawKeypair->getPublic('hex'));

return new KeyPair($privateKey, $publicKey);
}
}
62 changes: 62 additions & 0 deletions src/Service/Wallet/Seeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace XRPL\Service\Wallet;

use XRPL\Helper\CryptographyHelper;
use XRPL\ValueObject\Seed;
use XRPL\ValueObject\Wallet;

final class Seeder
{
public const array ED25519_SEED_PREFIX = [1, 225, 75];
public const array SECP256K1_SEED = [33];

public static function generateSeed(): string
{
$entropy = self::generateEntropy();

$seedData = array_merge(self::ED25519_SEED_PREFIX, $entropy);

$check = CryptographyHelper::doubleSha256(CryptographyHelper::byteArrayToString($seedData));
$checkBytes = CryptographyHelper::byteStringToArray(bin2hex($check));

$checkSum = \array_slice($checkBytes, 0, 4);
$seedBytes = array_merge($seedData, $checkSum);

$seedString = CryptographyHelper::byteArrayToString($seedBytes);
return CryptographyHelper::encodeBase58($seedString);
}

private static function generateEntropy(int $length = 16): array
{
$bytes = bin2hex(random_bytes($length));

return CryptographyHelper::byteStringToArray($bytes);
}

public static function decodeSeed(string $seed): Seed
{
$clearSeed = CryptographyHelper::decodeBase58($seed);

$byteArray = CryptographyHelper::byteStringToArray(bin2hex($clearSeed));
$checkSum = \array_slice($byteArray, -4);
$withoutChecksum = \array_slice($byteArray, 0, -4);

$versionBytesCount = \count(self::ED25519_SEED_PREFIX);
$prefix = \array_slice($withoutChecksum, 0, $versionBytesCount);
$payload = \array_slice($withoutChecksum, $versionBytesCount);

if (\count($payload) !== 16) {
throw new \Exception('Invalid seed length');
}

return new Seed(
Wallet::ALGORITHM_ED25519,
$prefix,
$payload,
$checkSum,
);
}
}
25 changes: 25 additions & 0 deletions src/Service/Wallet/WalletGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace XRPL\Service\Wallet;

use XRPL\ValueObject\Wallet;

final class WalletGenerator
{
public static function generate(): Wallet
{
$seed = Seeder::generateSeed();

return self::generateFromSeed($seed);
}

public static function generateFromSeed(string $seed): Wallet
{
$seed = Seeder::decodeSeed($seed);
$keyPair = KeyPairGenerator::generateKeyPair($seed);

return new Wallet($seed, $keyPair, AddressService::deriveAddress($keyPair->publicKey));
}
}
16 changes: 16 additions & 0 deletions src/ValueObject/Seed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace XRPL\ValueObject;

final readonly class Seed
{
public function __construct(
public string $algorithm,
public array $prefix,
public array $payload,
public array $checksum,
) {
}
}
5 changes: 4 additions & 1 deletion src/ValueObject/Wallet.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

final readonly class Wallet
{
public const string ALGORITHM_ED25519 = 'ed25519';
public const string ALGORITHM_SECP256K1 = 'secp256k1';

public function __construct(
public Seed $seed,
public KeyPair $keyPair,
public string $classicAddress,
public string $seed,
) {
}
}

0 comments on commit f975c1a

Please sign in to comment.