diff --git a/.gitignore b/.gitignore index c1c6fef4..aa9bf3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ composer.phar # PHPUnit .phpunit.result.cache -test/unit/_html \ No newline at end of file +test/unit/_html + +PrivateKey.key \ No newline at end of file diff --git a/composer.json b/composer.json index 6853c62a..94148989 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,9 @@ "ext-reflection": "*", "bitpay/key-utils": "^1.1", "guzzlehttp/guzzle": "^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^6.1 || ^6.2", - "netresearch/jsonmapper": "^4.1" + "symfony/yaml": "^5.4 || ^6.0 || ^7.0", + "netresearch/jsonmapper": "^4.1", + "symfony/console": "^6.0" }, "authors": [ { diff --git a/composer.lock b/composer.lock index f57c69fb..52b4f2fc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ec3ee155f901809c88d2e0b55e084128", + "content-hash": "06cf0fa37ff2686fcef0c147ec44b7b4", "packages": [ { "name": "bitpay/key-utils", @@ -432,6 +432,59 @@ }, "time": "2024-01-31T06:18:54+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, { "name": "psr/http-client", "version": "1.0.3", @@ -636,6 +689,100 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "symfony/console", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", + "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v3.4.0", @@ -782,6 +929,413 @@ ], "time": "2024-01-29T20:11:03+00:00" }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "7a14736fb179876575464e4658fce0c304e8c15b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/7a14736fb179876575464e4658fce0c304e8c15b", + "reference": "7a14736fb179876575464e4658fce0c304e8c15b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-25T09:26:29+00:00" + }, { "name": "symfony/yaml", "version": "v6.4.3", diff --git a/setup/ConfigGenerator.php b/setup/ConfigGenerator.php index 6cb00700..4b752cc2 100644 --- a/setup/ConfigGenerator.php +++ b/setup/ConfigGenerator.php @@ -1,243 +1,289 @@ [ + "Environment" => $env === 'P' ? 'Prod' : 'Test', + "EnvConfig" => [ + 'Test' => [ + "PrivateKeyPath" => $isProd ? null : $privateKeyLocation, + "PrivateKeySecret" => $isProd ? null : $password, + "ApiTokens" => [ + "merchant" => $isProd ? null : $merchantToken, + "payout" => $isProd ? null : $payoutToken, + ], + "Proxy" => null, + ], + 'Prod' => [ + "PrivateKeyPath" => $isProd ? $privateKeyLocation : null, + "PrivateKeySecret" => $isProd ? $password : null, + "ApiTokens" => [ + "merchant" => $isProd ? $merchantToken : null, + "payout" => $isProd ? $payoutToken : null, + ], + "Proxy" => null, + ], + ], + ], + ]; -$privateKeyname = 'PrivateKeyName.key'; // Add here the name for your Private key + $json_data = json_encode($config, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + file_put_contents('BitPay.config.json', $json_data); -$generateMerchantToken = true; // Set to true to generate a token for the Merchant facade -$generatePayoutToken = true; // Set to true to generate a token for the Payout facade (Request to Support if you need it) + $yml_data = Yaml::dump($config, 8, 4, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + file_put_contents('BitPay.config.yml', $yml_data); +} -$yourMasterPassword = 'YourMasterPassword'; //Will be used to encrypt your PrivateKey +function selectTokens(OutputInterface $output, mixed $helper, InputInterface $input): array +{ + $question = new Question('Select the tokens that you would like to request: '); + $output->writeln('Press M for merchant, P for payout, or B for both: '); + $possibleAnswers = ['M', 'P', 'B']; + $question->setAutocompleterValues($possibleAnswers); + $result = $helper->ask($input, $output, $question); + if (!$result) { + throw new \InvalidArgumentException('Missing answer'); + } -$generateJSONfile = true; // Set to true to generate the Configuration File in Json format -$generateYMLfile = true; // Set to true to generate the Configuration File in Yml format + $result = strtoupper($result); -$proxy = null; // The url of your proxy to forward requests through. Example: http://********.com:3128 + validateAnswer($result, $possibleAnswers); + $shouldGenerateMerchant = false; + $shouldGeneratePayout = false; -/** - * WARNING: DO NOT CHANGE ANYTHING FROM HERE ON - */ + if ($result === 'M') { + $shouldGenerateMerchant = true; + } -/** - * Generate new private key. - * Make sure you provide an easy recognizable name to your private key - * NOTE: In case you are providing the BitPay services to your clients, - * you MUST generate a different key per each of your clients - * - * WARNING: It is EXTREMELY IMPORTANT to place this key files in a very SECURE location - **/ -$privateKey = new PrivateKey($privateKeyname); -$storageEngine = new EncryptedFilesystemStorage($yourMasterPassword); - -try { -// Use the EncryptedFilesystemStorage to load the Merchant's encrypted private key with the Master Password. - $privateKey = $storageEngine->load($privateKeyname); -} catch (Exception $ex) { -// Check if the loaded keys is a valid key - if (!$privateKey->isValid()) { - $privateKey->generate(); + if ($result === 'P') { + $shouldGeneratePayout = true; } -// Encrypt and store it securely. -// This Master password could be one for all keys or a different one for each Private Key - $storageEngine->persist($privateKey); + if ($result === 'B') { + $shouldGenerateMerchant = true; + $shouldGeneratePayout = true; + } + + return [$shouldGenerateMerchant, $shouldGeneratePayout]; } -/** - * Generate the public key from the private key every time (no need to store the public key). - **/ -try { - $publicKey = $privateKey->getPublicKey(); -} catch (Exception $ex) { - echo $ex->getMessage(); +function validateAnswer(string $result, array $possibleAnswers): void +{ + if (!\in_array($result, $possibleAnswers, true)) { + throw new \InvalidArgumentException('Wrong answer ' . $result . ' possible answers: ' . implode(',', $possibleAnswers)); + } } -/** - * Derive the SIN from the public key. - **/ -try { - $sin = $publicKey->getSin()->__toString(); -} catch (Exception $ex) { - echo $ex->getMessage(); -} +function getEnv(OutputInterface $output, QuestionHelper $helper, InputInterface $input): string +{ + $question = new Question('Select target environment: '); + $output->writeln('Press T for testing or P for production:'); + $possibleAnswers = ['T', 'P']; + $question->setAutocompleterValues($possibleAnswers); + $result = $helper->ask($input, $output, $question); + if (!$result) { + throw new \RuntimeException('Missing answer'); + } + $result = strtoupper($result); -/** - * Use the SIN to request a pairing code and token. - * The pairing code has to be approved in the BitPay Dashboard - * THIS is just a cUrl example, which explains how to use the key pair for signing requests - **/ -$baseUrl = $isProd ? 'https://bitpay.com' : 'https://test.bitpay.com'; -$env = $isProd ? 'Prod' : 'Test'; + validateAnswer($result, $possibleAnswers); + return $result; +} -$merchantToken = null; -$payoutToken = null; +function getPrivateKeyPassword(QuestionHelper $helper, InputInterface $input, OutputInterface $output): ?string +{ + $question = new Question('Please write password to encrypt your PrivateKey: '); + $password = $helper->ask($input, $output, $question); + if (!$password) { + throw new \InvalidArgumentException('Encrypt password cannot be empty'); + } + return $password; +} +function getPrivateKeyLocation(QuestionHelper $helper, InputInterface $input, OutputInterface $output): string +{ + $question = new Question('Please write full path with filename for private key or press enter to generate private key in root directory: '); + $privateKeyLocation = $helper->ask($input, $output, $question); + if (!$privateKeyLocation) { + $privateKeyLocation = __DIR__ . '/PrivateKey.key'; + } -/** - * Request a token for the Merchant facade - */ + return $privateKeyLocation; +} -try { - if ($generateMerchantToken) { - $facade = 'merchant'; - - $postData = json_encode( - [ - 'id' => $sin, - 'facade' => $facade, - ]); - - $curlCli = curl_init($baseUrl . "/tokens"); - - curl_setopt( - $curlCli, CURLOPT_HTTPHEADER, [ - 'x-accept-version: 2.0.0', - 'Content-Type: application/json', - 'x-identity' => $publicKey->__toString(), - 'x-signature' => $privateKey->sign($baseUrl . "/tokens".$postData), - ]); - - curl_setopt($curlCli, CURLOPT_CUSTOMREQUEST, 'POST'); - curl_setopt($curlCli, CURLOPT_POSTFIELDS, stripslashes($postData)); - curl_setopt($curlCli, CURLOPT_RETURNTRANSFER, true); - - $result = curl_exec($curlCli); - $resultData = json_decode($result, true); - curl_close($curlCli); - - if (array_key_exists('error', $resultData)) { - echo $resultData['error']; - exit; +function getKeys(string $privateKeyLocation, string $password): array { + $privateKey = new PrivateKey($privateKeyLocation); + $storageEngine = new EncryptedFilesystemStorage($password); + try { + // Use the EncryptedFilesystemStorage to load the Merchant's encrypted private key with the Master Password. + $privateKey = $storageEngine->load($privateKeyLocation); + } catch (\Exception $ex) { + // Check if the loaded keys is a valid key + if (!$privateKey->isValid()) { + $privateKey->generate(); } - /** - * Example of a pairing Code returned from the BitPay API - * which needs to be APPROVED on the BitPay Dashboard before being able to use it. - **/ - $merchantToken = $resultData['data'][0]['token']; - echo "\r\nMerchant Facade\r\n"; - echo " -> Pairing Code: "; - echo $resultData['data'][0]['pairingCode']; - echo "\r\n -> Token: "; - echo $merchantToken; - echo "\r\n"; - - /** End of request **/ + // Encrypt and store it securely. + // This Master password could be one for all keys or a different one for each Private Key + $storageEngine->persist($privateKey); } - /** - * Repeat the process for the Payout facade - */ - - if ($generatePayoutToken) { - - $facade = 'payout'; - - $postData = json_encode( - [ - 'id' => $sin, - 'facade' => $facade, - ]); + // Generate the public key from the private key every time (no need to store the public key). + $publicKey = $privateKey->getPublicKey(); - $curlCli = curl_init($baseUrl . "/tokens"); + return [$privateKey, $publicKey]; +} - curl_setopt( - $curlCli, CURLOPT_HTTPHEADER, [ - 'x-accept-version: 2.0.0', - 'Content-Type: application/json', - 'x-identity' => $publicKey->__toString(), - 'x-signature' => $privateKey->sign($baseUrl . "/tokens".$postData), - ]); +function getSin(PublicKey $publicKey): string +{ + return $publicKey->getSin()->__toString(); +} - curl_setopt($curlCli, CURLOPT_CUSTOMREQUEST, 'POST'); - curl_setopt($curlCli, CURLOPT_POSTFIELDS, stripslashes($postData)); - curl_setopt($curlCli, CURLOPT_RETURNTRANSFER, true); +function generateToken( + OutputInterface $output, + string $facade, + PrivateKey $privateKey, + PublicKey $publicKey, + string $sin, + string $apiUrl +): ?string { + $url = $apiUrl . '/tokens'; + + $postData = json_encode([ + 'id' => $sin, + 'facade' => $facade, + ], JSON_THROW_ON_ERROR); + $headers = [ + 'Content-Type' => 'application/json', + 0 => 'x-accept-version: ' . Env::BITPAY_API_VERSION, + 1 => 'X-Identity: ' . $publicKey->__toString(), + 2 => 'X-Signature: ' . $privateKey->sign($url . $postData), + 3 => 'x-bitpay-plugin-info: ' . Env::BITPAY_PLUGIN_INFO, + 4 => 'x-bitpay-api-frame: ' . Env::BITPAY_API_FRAME, + 5 => 'x-bitpay-api-frame-version: ' . Env::BITPAY_API_FRAME_VERSION + ]; + + $client = new GuzzleHttpClient(); + $response = $client->request('POST', $url, [ + 'headers' => $headers, + 'body' => stripslashes($postData) + ])->getBody()->__toString(); + + $resultData = json_decode($response, true, 512, JSON_THROW_ON_ERROR); + if (array_key_exists('error', $resultData)) { + throw new \RuntimeException($resultData['error']); + } - $result = curl_exec($curlCli); - $resultData = json_decode($result, true); - curl_close($curlCli); + $token = $resultData['data'][0]['token']; - if (array_key_exists('error', $resultData)) { - echo $resultData['error']; - exit; - } + $output->writeln(strtoupper($facade) . ' facade'); + $output->writeln(' -> Pairing code: ' . $resultData['data'][0]['pairingCode']); + $output->writeln(' -> Token: ' . $token); + $output->writeln(''); - /** - * Example of a pairing Code returned from the BitPay API - * which needs to be APPROVED on the BitPay Dashboard before being able to use it. - **/ - $payoutToken = $resultData['data'][0]['token']; - echo "\r\nPayout Facade\r\n"; - echo " -> Pairing Code: "; - echo $resultData['data'][0]['pairingCode']; - echo "\r\n -> Token: "; - echo $payoutToken; - echo "\r\n"; - - /** End of request **/ - } -} catch (Exception $ex) { - echo $ex->getMessage(); + return $token; } -echo "\r\nPlease, copy the above pairing code/s and approve on your BitPay Account at the following link:\r\n"; -echo $baseUrl . "/dashboard/merchant/api-tokens\r\n"; -echo "\r\nOnce you have this Pairing Code/s approved you can move the generated files to a secure location and start using the Client.\r\n"; - /** - * Generate Config File + * @param OutputInterface $output + * @param string $apiUrl + * @return void */ +function successMessage(OutputInterface $output, string $apiUrl): void +{ + $output->writeln('Configuration generated successfully!'); + $output->writeln('Please, copy the above pairing code/s and approve on your BitPay Account at the following link:'); + $output->writeln($apiUrl . '/dashboard/merchant/api-tokens'); + $output->writeln('Once you have this Pairing Code/s approved you can move the generated files to a secure location and start using the Client'); +} -$config = [ - "BitPayConfiguration" => [ - "Environment" => $env, - "EnvConfig" => [ - 'Test' => [ - "PrivateKeyPath" => $isProd ? null : __DIR__."/".$privateKeyname, - "PrivateKeySecret" => $isProd ? null : $yourMasterPassword, - "ApiTokens" => [ - "merchant" => $isProd ? null : $merchantToken, - "payout" => $isProd ? null : $payoutToken, - ], - "Proxy" => $proxy, - ], - 'Prod' => [ - "PrivateKeyPath" => $isProd ? __DIR__."/".$privateKeyname : null, - "PrivateKeySecret" => $isProd ? $yourMasterPassword : null, - "ApiTokens" => [ - "merchant" => $isProd ? $merchantToken : null, - "payout" => $isProd ? $payoutToken : null, - ], - "Proxy" => $proxy, - ], - ], - ], -]; - -try { - if ($generateJSONfile) { - $json_data = json_encode($config, JSON_PRETTY_PRINT); - file_put_contents('BitPay.config.json', $json_data); +$help = "Generate new private key. Make sure you provide an easy recognizable name to your private key\n"; +$help .= "NOTE: In case you are providing the BitPay services to your clients,\n"; +$help .= "you MUST generate a different key per each of your clients\n"; +$help .= "WARNING: It is EXTREMELY IMPORTANT to place this key files in a very SECURE location"; + +function getMerchantToken( + bool $shouldGenerateMerchant, + OutputInterface $output, + mixed $privateKey, + mixed $publicKey, + mixed $sin, + string $apiUrl +): ?string { + if ($shouldGenerateMerchant) { + return generateToken($output, 'merchant', $privateKey, $publicKey, $sin, $apiUrl); } - if ($generateYMLfile) { - $yml_data = Yaml::dump($config, 8, 4, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); - file_put_contents('BitPay.config.yml', $yml_data); + return null; +} + +function getPayoutToken( + bool $shouldGeneratePayout, + OutputInterface $output, + mixed $privateKey, + mixed $publicKey, + mixed $sin, + string $apiUrl +): ?string { + if ($shouldGeneratePayout) { + return generateToken($output, 'payout', $privateKey, $publicKey, $sin, $apiUrl); } -} catch (Exception $ex) { - echo $ex->getMessage(); + return null; } + +(new SingleCommandApplication()) + ->setName('Generate new private key') + ->setDescription('Generate new private key. Make sure you provide an easy recognizable name to your private key') + ->setHelp($help) + ->setCode(function (InputInterface $input, OutputInterface $output): int { + $helper = $this->getHelper('question'); + + try { + $env = getEnv($output, $helper, $input); + $apiUrl = $env === 'P' ? 'https://bitpay.com' : 'https://test.bitpay.com'; + $password = getPrivateKeyPassword($helper, $input, $output); + $privateKeyLocation = getPrivateKeyLocation($helper, $input, $output); + + [$shouldGenerateMerchant, $shouldGeneratePayout] = selectTokens($output, $helper, $input); + [$privateKey, $publicKey] = getKeys($privateKeyLocation, $password); + $sin = getSin($publicKey); + + $merchantToken = getMerchantToken($shouldGenerateMerchant, $output, $privateKey, $publicKey, $sin, $apiUrl); + $payoutToken = getPayoutToken($shouldGeneratePayout, $output, $privateKey, $publicKey, $sin, $apiUrl); + + createConfigFile($env, $env === 'P', $privateKeyLocation, $password, $merchantToken, $payoutToken); + successMessage($output, $apiUrl); + return Command::SUCCESS; + } catch (\Exception $e) { + $output->writeln($e->getMessage()); + return Command::FAILURE; + } + })->run();