diff --git a/README.md b/README.md index cce6cfe..fe6b0ea 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # BitGoSDK PHP BitGoSDK written in PHP. This SDK contains methods for easily interacting with the BitGo API. @@ -11,21 +10,25 @@ BitGoSDK written in PHP. This SDK contains methods for easily interacting with t ## Requirements -- PHP 7.4 or newer with the cURL extension +- PHP 7.4 or newer, with the cURL extension +- A BitGo account and an [access token](https://developers.bitgo.com/docs/get-started-access-tokens) +- A [BitGo Express](#bitgo-express) instance, for anything that needs your keys, such as creating wallets or sending ## Installation -To install the SDK, you will need to be using [Composer](http://getcomposer.org/) in your project. If you aren't using Composer yet, it's really simple! Here's how to install composer and the BitGoSDK PHP. +Install the SDK with [Composer](https://getcomposer.org/): ```sh -# Install Composer -curl -sS https://getcomposer.org/installer | php +composer require neto737/bitgosdk-php +``` + +Or add it to the `require` section of your `composer.json`: -# Add the BitGoSDK as a dependency -php composer.phar require neto737/bitgosdk-php +```json +"neto737/bitgosdk-php": "^2.3" ``` -Next, require Composer's autoloader, in your application, to automatically load the BitGoSDK in your project: +Then load Composer's autoloader in your application: ```php require 'vendor/autoload.php'; @@ -33,12 +36,6 @@ require 'vendor/autoload.php'; use neto737\BitGoSDK\BitGoSDK; ``` -Or if put the following in your `composer.json`: - -```json -"neto737/bitgosdk-php": "*" -``` - ## Example ```php @@ -53,13 +50,25 @@ $bitgo->walletId = 'YOUR_WALLET_ID_HERE'; $createAddress = $bitgo->createWalletAddress(); ``` +More examples are in [`examples/`](examples/). + ## BitGo Express -If you are going to use BitGo Express you have to run a BitGo Express node. You can find how to run your own BitGo Express node [here](https://github.com/BitGo/BitGoJS/blob/master/modules/express/README.md). +BitGo signs transactions on the client, so sending coins, creating wallets and other calls that use your keys go through `BitGoExpress`, which talks to your own BitGo Express instance. See [Install BitGo Express](https://developers.bitgo.com/docs/get-started-express-install) to run one, and the [Express API reference](https://developers.bitgo.com/reference/express-overview) for its endpoints. + +## Documentation + +- [BitGo developer portal](https://developers.bitgo.com/) +- [BitGo API reference](https://developers.bitgo.com/reference/overview) +- [Webhooks](https://developers.bitgo.com/docs/webhooks-overview) and [policies](https://developers.bitgo.com/docs/policies-overview) ## Attention -Keep your cacert.pem always up to date. You can find updates on the site [curl.haxx.se](https://curl.haxx.se/docs/caextract.html). +Keep your `cacert.pem` up to date. You can download the latest one from [curl.se](https://curl.se/docs/caextract.html). + +## License + +MIT. See [LICENSE](LICENSE). Versions 2.2.3 and earlier were released under GPL-3.0. ## Credits diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa86f97..49a86fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -48,12 +48,6 @@ parameters: count: 1 path: src/BitGoExpress.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoExpress\:\:funoutWalletUnspents\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/BitGoExpress.php - - message: '#^Method neto737\\BitGoSDK\\BitGoExpress\:\:generateWallet\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -174,12 +168,6 @@ parameters: count: 1 path: src/BitGoSDK.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:addBlockWebhook\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/BitGoSDK.php - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:addKeychain\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -252,12 +240,6 @@ parameters: count: 1 path: src/BitGoSDK.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:getMaximumSpendable\(\) has invalid return type neto737\\BitGoSDK\\type\.$#' - identifier: class.notFound - count: 1 - path: src/BitGoSDK.php - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:getPendingApproval\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -336,36 +318,12 @@ parameters: count: 1 path: src/BitGoSDK.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:getWebhookPayload\(\) has invalid return type neto737\\BitGoSDK\\type\.$#' - identifier: class.notFound - count: 1 - path: src/BitGoSDK.php - - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:getWebhookPayload\(\) should return neto737\\BitGoSDK\\type but returns string\|false\.$#' - identifier: return.type - count: 1 - path: src/BitGoSDK.php - - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:listBlockWebhooks\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/BitGoSDK.php - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:listKeychains\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/BitGoSDK.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:listPendingApprovals\(\) has invalid return type neto737\\BitGoSDK\\type\.$#' - identifier: class.notFound - count: 1 - path: src/BitGoSDK.php - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:listUserWebhooks\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -414,12 +372,6 @@ parameters: count: 1 path: src/BitGoSDK.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:removeBlockWebhook\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/BitGoSDK.php - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:removePolicyRule\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -450,12 +402,6 @@ parameters: count: 1 path: src/BitGoSDK.php - - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:setRequestIP\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/BitGoSDK.php - - message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:setVelocityPolicyRule\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/BitGoExpress.php b/src/BitGoExpress.php index 2b8fbf4..f59c4d8 100644 --- a/src/BitGoExpress.php +++ b/src/BitGoExpress.php @@ -9,8 +9,7 @@ * * @package BitGoSDK PHP * @author Neto Melo - * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3 - * @version 2.2 + * @license https://opensource.org/licenses/MIT MIT License */ namespace neto737\BitGoSDK; @@ -24,10 +23,10 @@ class BitGoExpress const DEFAULT_CONNECT_TIMEOUT = 10; // Coin families for coin-specific params. Each lists mainnet and testnet codes, so testnet wallets get the same params. - private const UTXO_COINS = ['btc', 'tbtc', 'bch', 'tbch', 'btg', 'tbtg', 'dash', 'tdash', 'doge', 'tdoge', 'ltc', 'tltc', 'zec', 'tzec']; - private const BTC_COINS = ['btc', 'tbtc']; - private const SEGWIT_COINS = ['btc', 'tbtc', 'btg', 'tbtg', 'ltc', 'tltc']; - private const ETH_COINS = ['eth', 'teth', 'gteth', 'hteth']; + private const UTXO_COINS = ['btc', 'tbtc', 'tbtc4', 'tbtcsig', 'bch', 'tbch', 'btg', 'tbtg', 'dash', 'tdash', 'doge', 'tdoge', 'ltc', 'tltc', 'zec', 'tzec']; + private const BTC_COINS = ['btc', 'tbtc', 'tbtc4', 'tbtcsig']; + private const SEGWIT_COINS = ['btc', 'tbtc', 'tbtc4', 'tbtcsig', 'btg', 'tbtg', 'ltc', 'tltc']; + private const ETH_COINS = ['eth', 'teth', 'gteth', 'hteth', 'sepeth']; private const XRP_COINS = ['xrp', 'txrp']; private $APIEndpoint = null; @@ -50,7 +49,7 @@ class BitGoExpress * * @param string $hostname Set the hostname of your BitGo Express instance. Prefix it with https:// if Express serves TLS. * @param int $port Set the port of your BitGo Express instance - * @param string $coin Select the coin what you want to use with the BitGOSDK (use CurrencyCode class to select) + * @param string $coin Select the coin you want to use with the BitGoSDK (use CurrencyCode class to select) * @throws \InvalidArgumentException if the hostname has a scheme other than http:// or https:// */ public function __construct(string $hostname, int $port, string $coin = CurrencyCode::BITCOIN) @@ -160,7 +159,8 @@ public function login(string $email, string $password, string $otp, ?bool $exten } /** - * Ping bitgo express to ensure that it is still running. Unlike /ping, this does not try connecting to bitgo.com. + * Ping BitGo through BitGo Express, to check that Express is running and can reach BitGo. + * Use pingExpress() to check Express alone. * * @return array */ @@ -170,6 +170,17 @@ public function ping() return $this->__execute('GET'); } + /** + * Ping BitGo Express to ensure that it is still running. Unlike ping(), this does not try connecting to bitgo.com. + * + * @return array + */ + public function pingExpress() + { + $this->url = $this->AuthAPIEndpoint . '/pingexpress'; + return $this->__execute('GET'); + } + /** * This API call creates a new wallet. * @@ -384,9 +395,9 @@ public function consolidateWalletUnspents(string $walletPassphrase, ?int $numUns * @param bool $enforceMinConfirmsForChange Apply the required confirmations set in minConfirms for change outputs * @param int $feeRate The desired fee rate for the transaction in satoshis/kB * @param int $feeTxConfirmTarget Fee rate is automatically chosen by targeting a transaction confirmation in this number of blocks (Only available on BTC, feeRate takes precedence if also set) - * @return array + * @return array */ - public function funoutWalletUnspents(string $walletPassphrase, ?string $xprv = null, ?int $maxNumInputsToUse = null, ?int $numUnspentsToMake = null, ?int $minValue = null, ?int $maxValue = null, ?int $minHeight = null, ?int $maxFeePercentage = null, ?int $minConfirms = null, ?bool $enforceMinConfirmsForChange = null, ?int $feeRate = null, ?int $feeTxConfirmTarget = null) + public function fanoutWalletUnspents(string $walletPassphrase, ?string $xprv = null, ?int $maxNumInputsToUse = null, ?int $numUnspentsToMake = null, ?int $minValue = null, ?int $maxValue = null, ?int $minHeight = null, ?int $maxFeePercentage = null, ?int $minConfirms = null, ?bool $enforceMinConfirmsForChange = null, ?int $feeRate = null, ?int $feeTxConfirmTarget = null) { $this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/fanoutunspents'; $this->params = [ @@ -407,7 +418,17 @@ public function funoutWalletUnspents(string $walletPassphrase, ?string $xprv = n } /** - * his SDK call attempts to move all of the funds of the wallet into the address provided. + * @deprecated 2.3.0 Misspelled. Use fanoutWalletUnspents(), which takes the same arguments. + * + * @return array + */ + public function funoutWalletUnspents(string $walletPassphrase, ?string $xprv = null, ?int $maxNumInputsToUse = null, ?int $numUnspentsToMake = null, ?int $minValue = null, ?int $maxValue = null, ?int $minHeight = null, ?int $maxFeePercentage = null, ?int $minConfirms = null, ?bool $enforceMinConfirmsForChange = null, ?int $feeRate = null, ?int $feeTxConfirmTarget = null) + { + return $this->fanoutWalletUnspents($walletPassphrase, $xprv, $maxNumInputsToUse, $numUnspentsToMake, $minValue, $maxValue, $minHeight, $maxFeePercentage, $minConfirms, $enforceMinConfirmsForChange, $feeRate, $feeTxConfirmTarget); + } + + /** + * This SDK call attempts to move all of the funds of the wallet into the address provided. * * @param string $address The address to send all the funds in the wallet to. * @param string $walletPassphrase Passphrase to decrypt the wallet’s private key. @@ -508,7 +529,7 @@ public function signBuildTransaction($txPrebuild, ?string $prv = null, ?string $ * @param object $halfSigned The half-signed info returned from 'Sign Transaction’ * @param string $otp The current 2FA code * @param string $txHex The half-signed, serialized transaction hex (alternative to halfSigned) - * @param string $comment Any additional comment to attach to the transaaction + * @param string $comment Any additional comment to attach to the transaction * @return array */ public function sendBuildTransaction($halfSigned, string $otp, ?string $txHex = null, ?string $comment = null) diff --git a/src/BitGoSDK.php b/src/BitGoSDK.php index f1eb365..b65ee96 100644 --- a/src/BitGoSDK.php +++ b/src/BitGoSDK.php @@ -9,8 +9,7 @@ * * @package BitGoSDK PHP * @author Neto Melo - * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3 - * @version 2.2 + * @license https://opensource.org/licenses/MIT MIT License */ namespace neto737\BitGoSDK; @@ -26,7 +25,7 @@ class BitGoSDK const DEFAULT_CONNECT_TIMEOUT = 10; // Coin families for coin-specific params. Each lists mainnet and testnet codes, so testnet wallets get the same params. - private const ETH_COINS = ['eth', 'teth', 'gteth', 'hteth']; + private const ETH_COINS = ['eth', 'teth', 'gteth', 'hteth', 'sepeth']; private const XRP_COINS = ['xrp', 'txrp']; private const XLM_COINS = ['xlm', 'txlm']; @@ -47,7 +46,7 @@ class BitGoSDK * BitGoSDK Initialization * * @param string $accessToken Set your Access Token (API Key) - * @param string $coin Select the coin what you want to use with the BitGOSDK (use CurrencyCode class to select) + * @param string $coin Select the coin you want to use with the BitGoSDK (use CurrencyCode class to select) * @param bool $testNet Start instance as testnet (true or false), default is false * @throws \Exception */ @@ -61,15 +60,19 @@ public function __construct(string $accessToken, string $coin = CurrencyCode::BI } /** - * Set the IP that will make requests on BitGo servers (useful feature to people who uses shared hosting) + * Set the local IP that requests to BitGo are sent from (useful on hosts with several IPs, such as shared hosting) * - * @param string $ipAddress + * @param string $ipAddress An IPv4 or IPv6 address of this host + * @return bool Whether the IP was applied. It is not applied on testnet, or if $ipAddress is not a valid IP, + * and any IP set earlier is kept. */ public function setRequestIP(string $ipAddress) { - if (!$this->testNet && filter_var($ipAddress, FILTER_VALIDATE_IP)) { - $this->requestIP = $ipAddress; + if ($this->testNet || filter_var($ipAddress, FILTER_VALIDATE_IP) === false) { + return false; } + $this->requestIP = $ipAddress; + return true; } /** @@ -346,13 +349,14 @@ public function getWalletTransferBySequenceId(string $sequenceId) /** * This API call is used to create a new receive address for your wallet, which enhances your privacy. * - * @param int $chain Specifies the address format, defaults to 0, use 10 for SegWit (only on BTC and BTG) + * @param int $chain The address chain, see AddressType. Defaults to 10 (AddressType::P2SH_P2WSH_DEPOSIT, SegWit wrapped in P2SH). + * Pass AddressType::LEGACY_DEPOSIT (0) for coins without SegWit support, such as BCH. * @param string $label Human-readable name for the address - * @param int $gasPrice Custom gas price to be used for deployment of receive addresses (only for Ethereum) * @param bool $lowPriority Whether the deployment of the address forwarder contract should use a low priority fee key (ETH only) + * @param int $gasPrice Custom gas price to be used for deployment of receive addresses (ETH only) * @return array */ - public function createWalletAddress(int $chain = AddressType::P2SH_DEPOSIT, ?string $label = null, bool $lowPriority = false, ?int $gasPrice = null) + public function createWalletAddress(int $chain = AddressType::P2SH_P2WSH_DEPOSIT, ?string $label = null, bool $lowPriority = false, ?int $gasPrice = null) { $this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/address'; $this->params = [ @@ -493,7 +497,7 @@ public function getTotalBalances() * @param int $feeRate The desired fee rate for the transaction in satoshis/kB * @param int $minConfirms The required number of confirmations for each non-change unspent * @param bool $enforceMinConfirmsForChange Apply the required confirmations set in minConfirms for change outputs - * @return type + * @return array */ public function getMaximumSpendable(?int $limit = null, ?int $minValue = null, ?int $maxValue = null, ?int $minHeight = null, ?int $feeRate = null, ?int $minConfirms = null, ?bool $enforceMinConfirmsForChange = null) { @@ -528,7 +532,7 @@ public function freezeWallet(string $otp, int $duration = 3600) } /** - * Retrieve a keychain based on its key id. + * List the current user's keychains for this coin. * * @param int $limit Max number of results in a single call. Defaults to 25. * @param string $prevId Continue iterating (provided by nextBatchPrevId in the previous list) @@ -578,7 +582,7 @@ public function createBitGoKeychain(string $source = 'bitgo', ?string $enterpris /** * Creates a new backup keychain on a third party specializing in key recovery services. * - * @param string $provider The backup provider for the keychain, e. g. 'cme' + * @param string $provider The backup provider for the keychain, e. g. 'cme' * @param string $source The origin of the keychain. Must be 'backup' for a backup key. * @return array */ @@ -616,8 +620,8 @@ public function addKeychain(string $pub, ?string $encryptedPrv = null, string $s * its single key to sign a transaction. * * @param string $id The id of the policy rule - * @param string $type The type of the policy. See Policy types (https://www.bitgo.com/api/v2/#policy-types) - * @param string $condition The condition that triggers the policy. See Policy types (https://www.bitgo.com/api/v2/#policy-types) + * @param string $type The type of the policy. See the policy docs (https://developers.bitgo.com/docs/policies-overview) + * @param string $condition The condition that triggers the policy. See the policy docs (https://developers.bitgo.com/docs/policies-overview) * @param string $action The action to take when the policy is triggered. Should be either 'deny' or 'getapproval' * @return array */ @@ -638,7 +642,7 @@ public function setVelocityPolicyRule(string $id, string $type, string $conditio * * @param string $id The id of the policy rule to be updated. (This value can’t be changed) * @param string $type The type of the policy. (This value can’t be changed) - * @param string $condition The new condition that triggers the policy. See Policy types (https://www.bitgo.com/api/v2/#policy-types) + * @param string $condition The new condition that triggers the policy. See the policy docs (https://developers.bitgo.com/docs/policies-overview) * @param string $action The new action to take when the policy is triggered. Should be either 'deny' or 'getapproval' * @return array */ @@ -761,7 +765,7 @@ public function getPendingApproval(string $pendingApprovalId) * @param string $walletID Base address of the wallet * @param string $enterprise The public ID of the enterprise * @param bool $allTokens Gets details of all token pending approvals. Only valid for ETH/TETH - * @return type + * @return array */ public function listPendingApprovals(?string $walletID = null, ?string $enterprise = null, bool $allTokens = false) { @@ -779,7 +783,7 @@ public function listPendingApprovals(?string $walletID = null, ?string $enterpri * * @param bool $decodeJson Set true to decode the JSON * @param bool $decodeAsArray Set true to decode the JSON as array - * @return type + * @return mixed The decoded payload, or the raw body if $decodeJson is false */ public static function getWebhookPayload(bool $decodeJson = true, bool $decodeAsArray = true) { @@ -793,7 +797,7 @@ public static function getWebhookPayload(bool $decodeJson = true, bool $decodeAs /** * List all webhooks set up on the wallet. * - * @param bool $allTokens Gets details of all token pending approvals. Only valid for ETH/TETH + * @param bool $allTokens Gets details of all token webhooks. Only valid for ETH/TETH * @return array */ public function listWalletWebhooks(bool $allTokens = false) @@ -809,7 +813,7 @@ public function listWalletWebhooks(bool $allTokens = false) * Add a webhook that will result in an HTTP callback at the specified URL from BitGo when events are triggered. * * @param string $url URL to fire the webhook to. - * @param string $type Type of event to listen to (can be 'transfer' or 'pendingaapproval'). + * @param string $type Type of event to listen to (can be 'transfer' or 'pendingapproval'). * @param int $numConfirmations Number of confirmations before triggering the webhook. If 0 or unspecified, requests will be sent to the callback endpoint when the transfer is first seen and when it is confirmed. * @return array */ @@ -828,7 +832,7 @@ public function addWalletWebhook(string $url, string $type, ?int $numConfirmatio * Removing a webhook will cause new events of the specified type to no longer trigger HTTP callbacks to your URLs. * * @param string $url URL for callback requests to be made at. - * @param string $type Type of event to listen to (can be 'transfer' or 'pendingaapproval'). + * @param string $type Type of event to listen to (can be 'transfer' or 'pendingapproval'). * @return array */ public function removeWalletWebhook(string $url, string $type) @@ -861,7 +865,7 @@ public function simulateWalletWebhook(string $webhookId, ?string $transferId = n } /** - * List all webhooks set up on the user. + * List the user's webhooks for this coin, such as block and wallet_confirmation webhooks. * * @return array */ @@ -875,7 +879,7 @@ public function listUserWebhooks() * Add a webhook that will result in an HTTP callback at the specified URL from BitGo when events are triggered. * * @param string $url URL to fire the webhook to. - * @param string $type Type of event to listen to (can be of type 'block'). + * @param string $type Type of event to listen to, e.g. 'block' or 'wallet_confirmation'. * @param string $label Label of the new webhook. * @param int $numConfirmations Number of confirmations before triggering the webhook. * @return array @@ -896,7 +900,7 @@ public function addUserWebhook(string $url, string $type, ?string $label = null, * Removing a webhook will cause new events of the specified type to no longer trigger HTTP callbacks to your URLs. * * @param string $url URL for callback requests to be made at. - * @param string $type Type of event to listen to (can be of type 'block'). + * @param string $type Type of event to listen to, e.g. 'block' or 'wallet_confirmation'. * @return array */ public function removeUserWebhook(string $url, string $type) @@ -926,53 +930,39 @@ public function simulateUserWebhook(string $webhookId, ?string $blockId = null) } /** - * Adds a webhook that will result in an HTTP callback at the specified URL from BitGo when events are triggered. + * @deprecated 2.3.0 Same endpoint as addUserWebhook(). Use addUserWebhook(), which sends $type unchanged. * * @param string $url URL to fire the webhook to. - * @param string $type Type of event to listen to (can be 'block' or 'wallet_confirmation'). + * @param string $type 'block' or 'wallet_confirmation'. Any other value is sent as 'block'. * @param string $label Label of the new webhook. - * @param int $numConfirmations Number of confirmations before triggering the webhook. If 0 or unspecified, requests will be sent to the callback endpoint when the transfer is first seen and when it is confirmed. - - * @return array + * @param int $numConfirmations Number of confirmations before triggering the webhook. + * @return array */ public function addBlockWebhook(string $url, string $type = 'block', ?string $label = null, ?int $numConfirmations = null) { - $this->url = $this->APIEndpoint . '/webhooks'; - $this->params = [ - 'url' => $url, - 'type' => (in_array($type, ['block', 'wallet_confirmation'])) ? $type : 'block', - 'label' => $label, - 'numConfirmations' => $numConfirmations - ]; - return $this->__execute(); + return $this->addUserWebhook($url, self::blockWebhookType($type), $label, $numConfirmations); } /** - * Returns block webhooks. The types of webhooks are block and wallet_confirmation notifications. + * @deprecated 2.3.0 Same endpoint as listUserWebhooks(). Use listUserWebhooks(). * - * @return array + * @return array */ public function listBlockWebhooks() { - $this->url = $this->APIEndpoint . '/webhooks'; - return $this->__execute('GET'); + return $this->listUserWebhooks(); } /** - * Removing a webhook will cause new events of the specified type to no longer trigger HTTP callbacks to your URLs. + * @deprecated 2.3.0 Same endpoint as removeUserWebhook(). Use removeUserWebhook(), which sends $type unchanged. * - * @param string $url - * @param string $type - * @return array + * @param string $url URL for callback requests to be made at. + * @param string $type 'block' or 'wallet_confirmation'. Any other value is sent as 'block'. + * @return array */ public function removeBlockWebhook(string $url, string $type = 'block') { - $this->url = $this->APIEndpoint . '/webhooks'; - $this->params = [ - 'url' => $url, - 'type' => (in_array($type, ['block', 'wallet_confirmation'])) ? $type : 'block' - ]; - return $this->__execute('DELETE'); + return $this->removeUserWebhook($url, self::blockWebhookType($type)); } /** @@ -1063,6 +1053,14 @@ private function curlOptions(string $requestType, array $params): array return $options; } + /** + * The type the deprecated *BlockWebhook() methods send: 'block' unless 'wallet_confirmation' was asked for. + */ + private static function blockWebhookType(string $type): string + { + return in_array($type, ['block', 'wallet_confirmation'], true) ? $type : 'block'; + } + /** * Whether this client's coin is one of $coins. * diff --git a/src/Enum/AddressType.php b/src/Enum/AddressType.php index 33dfe0e..79d7b52 100644 --- a/src/Enum/AddressType.php +++ b/src/Enum/AddressType.php @@ -9,24 +9,48 @@ * * @package BitGoSDK PHP * @author Neto Melo - * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3 - * @version 2.2 + * @license https://opensource.org/licenses/MIT MIT License */ namespace neto737\BitGoSDK\Enum; +/** + * BitGo address chains for UTXO wallets. Even chains are receive (deposit) addresses, odd chains are change addresses. + * Which chains a wallet supports depends on the coin: chains 10 and up need SegWit, 30 and up need Taproot. + */ abstract class AddressType { - const LEGACY_DEPOSIT = 0; - const LEGACY_CHANGE = 1; + // P2SH multisig, the original BitGo address format + const LEGACY_DEPOSIT = 0; + const LEGACY_CHANGE = 1; + + // SegWit multisig wrapped in P2SH (P2SH-P2WSH) + const P2SH_P2WSH_DEPOSIT = 10; + const P2SH_P2WSH_CHANGE = 11; + + // Native SegWit multisig (P2WSH, bech32 addresses) + const P2WSH_DEPOSIT = 20; + const P2WSH_CHANGE = 21; + + // Taproot multisig (P2TR, bech32m addresses) + const P2TR_DEPOSIT = 30; + const P2TR_CHANGE = 31; + + // Same values as P2WSH_DEPOSIT and P2WSH_CHANGE + const BECH32_DEPOSIT = 20; + const BECH32_CHANGE = 21; - const P2SH_DEPOSIT = 10; - const P2SH_CHANGE = 11; + // Same values as P2TR_DEPOSIT and P2TR_CHANGE + const BECH32M_DEPOSIT = 30; + const BECH32M_CHANGE = 31; - const BECH32_DEPOSIT = 20; - const BECH32_CHANGE = 21; + /** + * @deprecated 2.3.0 Chain 10 is SegWit wrapped in P2SH, not plain P2SH. Use P2SH_P2WSH_DEPOSIT, or LEGACY_DEPOSIT for plain P2SH. + */ + const P2SH_DEPOSIT = 10; - // Taproot fork address format - const BECH32M_DEPOSIT = 30; - const BECH32M_CHANGE = 31; + /** + * @deprecated 2.3.0 Chain 11 is SegWit wrapped in P2SH, not plain P2SH. Use P2SH_P2WSH_CHANGE, or LEGACY_CHANGE for plain P2SH. + */ + const P2SH_CHANGE = 11; } diff --git a/src/Enum/CurrencyCode.php b/src/Enum/CurrencyCode.php index 3970a20..076ff50 100644 --- a/src/Enum/CurrencyCode.php +++ b/src/Enum/CurrencyCode.php @@ -9,67 +9,155 @@ * * @package BitGoSDK PHP * @author Neto Melo - * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3 - * @version 2.2 + * @license https://opensource.org/licenses/MIT MIT License */ namespace neto737\BitGoSDK\Enum; +/** + * BitGo coin codes, as used in the API path (/api/v2/{coin}/...). BitGo supports many more coins and tokens than + * are listed here, and any BitGo coin code can be passed as a string. + */ abstract class CurrencyCode { // Mainnet const ALGORAND = 'algo'; - const AVAX = 'avax'; + const APTOS = 'apt'; + const ARBITRUM = 'arbeth'; + const AVALANCHE_C_CHAIN = 'avaxc'; + const AVALANCHE_P_CHAIN = 'avaxp'; const BITCOIN = 'btc'; const BITCOIN_CASH = 'bch'; const BITCOIN_GOLD = 'btg'; + const BNB_SMART_CHAIN = 'bsc'; + const CARDANO = 'ada'; const CASPER = 'cspr'; const CELO = 'celo'; + const COSMOS = 'atom'; const DASH = 'dash'; + const DOGECOIN = 'doge'; const EOS = 'eos'; const ETHEREUM = 'eth'; const HEDERA = 'hbar'; const LITECOIN = 'ltc'; + const NEAR = 'near'; + const OPTIMISM = 'opeth'; + const POLKADOT = 'dot'; + const POLYGON = 'polygon'; const RSK_SMART_BTC = 'rbtc'; + const SOLANA = 'sol'; const STACKS = 'stx'; const STELLAR = 'xlm'; + const SUI = 'sui'; const TEZOS = 'xtz'; + const TON = 'ton'; const TRON = 'trx'; const RIPPLE = 'xrp'; const ZCASH = 'zec'; + /** + * @deprecated 2.3.0 'avax' is not a BitGo coin code. Use AVALANCHE_C_CHAIN ('avaxc'). + */ + const AVAX = 'avax'; + // Testnet const ALGORAND_TESTNET = 'talgo'; - const AVAX_TESTNET = 'tavax'; + const APTOS_TESTNET = 'tapt'; + const ARBITRUM_TESTNET = 'tarbeth'; + const AVALANCHE_C_CHAIN_TESTNET = 'tavaxc'; + const AVALANCHE_P_CHAIN_TESTNET = 'tavaxp'; const BITCOIN_TESTNET = 'tbtc'; + const BITCOIN_TESTNET4 = 'tbtc4'; + const BITCOIN_SIGNET = 'tbtcsig'; const BITCOIN_CASH_TESTNET = 'tbch'; + const BITCOIN_GOLD_TESTNET = 'tbtg'; + const BNB_SMART_CHAIN_TESTNET = 'tbsc'; + const CARDANO_TESTNET = 'tada'; const CASPER_TESTNET = 'tcspr'; const CELO_TESTNET = 'tcelo'; + const COSMOS_TESTNET = 'tatom'; const DASH_TESTNET = 'tdash'; + const DOGECOIN_TESTNET = 'tdoge'; const EOS_TESTNET = 'teos'; - const ETHEREUM_TESTNET = 'teth'; + const ETHEREUM_HOODI_TESTNET = 'hteth'; + const ETHEREUM_SEPOLIA_TESTNET = 'sepeth'; const HEDERA_TESTNET = 'thbar'; const LITECOIN_TESTNET = 'tltc'; + const NEAR_TESTNET = 'tnear'; + const OPTIMISM_TESTNET = 'topeth'; + const POLKADOT_TESTNET = 'tdot'; + const POLYGON_TESTNET = 'tpolygon'; const RSK_SMART_BTC_TESTNET = 'trbtc'; + const SOLANA_TESTNET = 'tsol'; const STACKS_TESTNET = 'tstx'; const STELLAR_TESTNET = 'txlm'; + const SUI_TESTNET = 'tsui'; const TEZOS_TESTNET = 'txtz'; + const TON_TESTNET = 'tton'; const TRON_TESTNET = 'ttrx'; const RIPPLE_TESTNET = 'txrp'; const ZCASH_TESTNET = 'tzec'; + /** + * @deprecated 2.3.0 'tavax' is not a BitGo coin code. Use AVALANCHE_C_CHAIN_TESTNET ('tavaxc'). + */ + const AVAX_TESTNET = 'tavax'; + + /** + * @deprecated 2.3.0 'teth' is the retired Kovan testnet. Use ETHEREUM_HOODI_TESTNET ('hteth') or ETHEREUM_SEPOLIA_TESTNET ('sepeth'). + */ + const ETHEREUM_TESTNET = 'teth'; + //ERC20 TOKENS const ERC = 'erc'; //BitGo’s sample ERC20 token - const OMISE_GO = 'omg'; - const ZRX = 'zrx'; //0x token + const AAVE = 'aave'; + const AUGUR_REPV2 = 'repv2'; + const BASIC_ATTENTION_TOKEN = 'bat'; + const CHAINLINK = 'link'; + const CIVIC_REP = 'cvc'; + const DAI = 'dai'; const FUNFAIR = 'fun'; + const GOLEM_GLM = 'glm'; + const KYBER_NETWORK_V2 = 'knc2'; + const NUMERAIRE = 'nmr'; + const TETHER_USDT = 'usdt'; + const UNISWAP = 'uni'; + const USD_COIN = 'usdc'; + const WRAPPED_BITCOIN = 'wbtc'; + const ZRX = 'zrx'; //0x token + + /** + * @deprecated 2.3.0 The OMG Network has wound down and the token is widely delisted. + */ + const OMISE_GO = 'omg'; + + /** + * @deprecated 2.3.0 Golem migrated from GNT to GLM. Use GOLEM_GLM. + */ const GOLEM = 'gnt'; + + /** + * @deprecated 2.3.0 Augur migrated from REP to REPv2. Use AUGUR_REPV2. + */ const AUGUR_REP = 'rep'; - const BASIC_ATTENTION_TOKEN = 'bat'; + + /** + * @deprecated 2.3.0 Kyber Network migrated KNC to a new token contract. Use KYBER_NETWORK_V2. + */ const KYBER_NETWORK = 'knc'; - const CIVIC_REP = 'cvc'; + + /** + * @deprecated 2.3.0 The ERC-20 QRL token was swapped for coins on QRL's own chain. + */ const QRL = 'qrl'; - const NUMERAIRE = 'nmr'; + + /** + * @deprecated 2.3.0 TenX has wound down and the token is widely delisted. + */ const TENXPAY = 'pay'; + + /** + * @deprecated 2.3.0 BRD was acquired by Coinbase, and the token is discontinued and widely delisted. + */ const BREAD = 'brd'; } diff --git a/tests/AddressTypeTest.php b/tests/AddressTypeTest.php new file mode 100644 index 0000000..8c092c7 --- /dev/null +++ b/tests/AddressTypeTest.php @@ -0,0 +1,74 @@ + + */ + public static function chains(): array + { + return [ + 'P2SH' => [AddressType::LEGACY_DEPOSIT, 0], + 'P2SH change' => [AddressType::LEGACY_CHANGE, 1], + 'P2SH-P2WSH' => [AddressType::P2SH_P2WSH_DEPOSIT, 10], + 'P2SH-P2WSH change' => [AddressType::P2SH_P2WSH_CHANGE, 11], + 'P2WSH' => [AddressType::P2WSH_DEPOSIT, 20], + 'P2WSH change' => [AddressType::P2WSH_CHANGE, 21], + 'P2TR' => [AddressType::P2TR_DEPOSIT, 30], + 'P2TR change' => [AddressType::P2TR_CHANGE, 31], + 'bech32' => [AddressType::BECH32_DEPOSIT, 20], + 'bech32 change' => [AddressType::BECH32_CHANGE, 21], + 'bech32m' => [AddressType::BECH32M_DEPOSIT, 30], + 'bech32m change' => [AddressType::BECH32M_CHANGE, 31], + ]; + } + + /** + * @dataProvider chains + */ + public function testChainValues(int $constant, int $chain): void + { + self::assertSame($chain, $constant); + } + + /** + * @return array + */ + public static function deprecatedConstants(): array + { + return [ + 'P2SH_DEPOSIT' => ['P2SH_DEPOSIT', 'P2SH_P2WSH_DEPOSIT'], + 'P2SH_CHANGE' => ['P2SH_CHANGE', 'P2SH_P2WSH_CHANGE'], + ]; + } + + /** + * @dataProvider deprecatedConstants + */ + public function testMisleadingNamesAreDeprecatedButKeepTheirValue(string $deprecated, string $replacement): void + { + $constant = new ReflectionClassConstant(AddressType::class, $deprecated); + + self::assertSame((new ReflectionClassConstant(AddressType::class, $replacement))->getValue(), $constant->getValue()); + self::assertMatchesRegularExpression('/@deprecated\s+2\.3\.0\b.*\b' . $replacement . '\b/', (string) $constant->getDocComment()); + } + + public function testCreateWalletAddressStillDefaultsToChain10(): void + { + $chain = new ReflectionParameter([BitGoSDK::class, 'createWalletAddress'], 'chain'); + + self::assertSame(AddressType::P2SH_P2WSH_DEPOSIT, $chain->getDefaultValue()); + self::assertSame('neto737\BitGoSDK\Enum\AddressType::P2SH_P2WSH_DEPOSIT', $chain->getDefaultValueConstantName()); + } +} diff --git a/tests/CoinFamilyTest.php b/tests/CoinFamilyTest.php index e286289..2396392 100644 --- a/tests/CoinFamilyTest.php +++ b/tests/CoinFamilyTest.php @@ -29,12 +29,17 @@ public function expressProvider(): array 'tstx is not UTXO' => ['UTXO_COINS', 'tstx', false], 'eth is not UTXO' => ['UTXO_COINS', 'eth', false], 'tbtc is BTC' => ['BTC_COINS', 'tbtc', true], + 'tbtc4 is BTC' => ['BTC_COINS', 'tbtc4', true], + 'tbtcsig is BTC' => ['BTC_COINS', 'tbtcsig', true], + 'tbtc4 is UTXO' => ['UTXO_COINS', 'tbtc4', true], + 'tbtcsig is SegWit' => ['SEGWIT_COINS', 'tbtcsig', true], 'ltc is not BTC' => ['BTC_COINS', 'ltc', false], 'tltc is SegWit' => ['SEGWIT_COINS', 'tltc', true], 'tbtg is SegWit' => ['SEGWIT_COINS', 'tbtg', true], 'bch is not SegWit' => ['SEGWIT_COINS', 'bch', false], 'teth is ETH' => ['ETH_COINS', 'teth', true], 'hteth is ETH' => ['ETH_COINS', 'hteth', true], + 'sepeth is ETH' => ['ETH_COINS', 'sepeth', true], 'txrp is XRP' => ['XRP_COINS', 'txrp', true], ]; } @@ -59,6 +64,7 @@ public function sdkProvider(): array 'teth is ETH' => ['ETH_COINS', 'teth', true], 'gteth is ETH' => ['ETH_COINS', 'gteth', true], 'hteth is ETH' => ['ETH_COINS', 'hteth', true], + 'sepeth is ETH' => ['ETH_COINS', 'sepeth', true], 'tbtc is not ETH' => ['ETH_COINS', 'tbtc', false], 'txrp is XRP' => ['XRP_COINS', 'txrp', true], 'txlm is XLM' => ['XLM_COINS', 'txlm', true], diff --git a/tests/CurrencyCodeTest.php b/tests/CurrencyCodeTest.php new file mode 100644 index 0000000..eb93ff1 --- /dev/null +++ b/tests/CurrencyCodeTest.php @@ -0,0 +1,90 @@ + + */ + public static function codes(): array + { + return [ + 'Dogecoin' => [CurrencyCode::DOGECOIN, 'doge'], + 'Avalanche C-Chain' => [CurrencyCode::AVALANCHE_C_CHAIN, 'avaxc'], + 'Polygon' => [CurrencyCode::POLYGON, 'polygon'], + 'BNB Smart Chain' => [CurrencyCode::BNB_SMART_CHAIN, 'bsc'], + 'Solana testnet' => [CurrencyCode::SOLANA_TESTNET, 'tsol'], + 'Ethereum Hoodi testnet' => [CurrencyCode::ETHEREUM_HOODI_TESTNET, 'hteth'], + 'Ethereum Sepolia testnet' => [CurrencyCode::ETHEREUM_SEPOLIA_TESTNET, 'sepeth'], + 'Bitcoin testnet4' => [CurrencyCode::BITCOIN_TESTNET4, 'tbtc4'], + 'USD Coin' => [CurrencyCode::USD_COIN, 'usdc'], + 'Golem GLM' => [CurrencyCode::GOLEM_GLM, 'glm'], + ]; + } + + /** + * @dataProvider codes + */ + public function testCodes(string $constant, string $code): void + { + self::assertSame($code, $constant); + } + + /** + * @return array + */ + public static function deprecatedConstants(): array + { + return [ + 'AVAX' => ['AVAX', 'avax'], + 'AVAX_TESTNET' => ['AVAX_TESTNET', 'tavax'], + 'ETHEREUM_TESTNET' => ['ETHEREUM_TESTNET', 'teth'], + 'OMISE_GO' => ['OMISE_GO', 'omg'], + 'GOLEM' => ['GOLEM', 'gnt'], + 'AUGUR_REP' => ['AUGUR_REP', 'rep'], + 'KYBER_NETWORK' => ['KYBER_NETWORK', 'knc'], + 'QRL' => ['QRL', 'qrl'], + 'TENXPAY' => ['TENXPAY', 'pay'], + 'BREAD' => ['BREAD', 'brd'], + ]; + } + + /** + * @dataProvider deprecatedConstants + */ + public function testOutdatedCodesAreDeprecatedButKept(string $name, string $code): void + { + $constant = new ReflectionClassConstant(CurrencyCode::class, $name); + + self::assertSame($code, $constant->getValue()); + self::assertMatchesRegularExpression('/@deprecated\s+2\.3\.0\b/', (string) $constant->getDocComment()); + } + + public function testDeprecationsNameAnExistingReplacement(): void + { + $class = new ReflectionClass(CurrencyCode::class); + + foreach ($class->getReflectionConstants() as $constant) { + preg_match_all('/\b(?:Use|or) ([A-Z][A-Z0-9_]+)\b/', (string) $constant->getDocComment(), $matches); + foreach ($matches[1] as $replacement) { + self::assertTrue($class->hasConstant($replacement), $constant->getName() . ' points to missing ' . $replacement); + } + } + } + + public function testCodesAreUnique(): void + { + $codes = (new ReflectionClass(CurrencyCode::class))->getConstants(); + + self::assertSame(array_unique($codes), $codes); + } +} diff --git a/tests/DeprecatedAliasTest.php b/tests/DeprecatedAliasTest.php new file mode 100644 index 0000000..3917d77 --- /dev/null +++ b/tests/DeprecatedAliasTest.php @@ -0,0 +1,107 @@ +walletId = 'wallet-id'; + + $express->fanoutWalletUnspents('passphrase'); + + self::assertSame('http://127.0.0.1:1/api/v2/tbtc/wallet/wallet-id/fanoutunspents', $this->getPrivate($express, 'url')); + } + + public function testFunoutForwardsEveryArgumentToFanout(): void + { + $express = new class ('127.0.0.1', 1, 'tbtc') extends BitGoExpress { + /** @var list */ + public array $forwarded = []; + + public function fanoutWalletUnspents(string $walletPassphrase, ?string $xprv = null, ?int $maxNumInputsToUse = null, ?int $numUnspentsToMake = null, ?int $minValue = null, ?int $maxValue = null, ?int $minHeight = null, ?int $maxFeePercentage = null, ?int $minConfirms = null, ?bool $enforceMinConfirmsForChange = null, ?int $feeRate = null, ?int $feeTxConfirmTarget = null) + { + $this->forwarded = func_get_args(); + return ['forwarded' => true]; + } + }; + $args = ['passphrase', 'xprv', 20, 200, 1000, 5000, 700000, 10, 0, false, 2000, 3]; + + self::assertSame(['forwarded' => true], $express->funoutWalletUnspents(...$args)); + self::assertSame($args, $express->forwarded); + } + + public function testAddBlockWebhookForwardsToAddUserWebhook(): void + { + $sdk = new RecordingWebhookSdk(); + + self::assertSame(['forwarded' => true], $sdk->addBlockWebhook('https://example.com/hook', 'wallet_confirmation', 'label', 0)); + self::assertSame(['addUserWebhook', ['https://example.com/hook', 'wallet_confirmation', 'label', 0]], $sdk->forwarded); + } + + public function testAddBlockWebhookStillSendsUnknownTypesAsBlock(): void + { + $sdk = new RecordingWebhookSdk(); + + $sdk->addBlockWebhook('https://example.com/hook', 'transfer'); + + self::assertSame(['addUserWebhook', ['https://example.com/hook', 'block', null, null]], $sdk->forwarded); + } + + public function testListBlockWebhooksForwardsToListUserWebhooks(): void + { + $sdk = new RecordingWebhookSdk(); + + self::assertSame(['forwarded' => true], $sdk->listBlockWebhooks()); + self::assertSame(['listUserWebhooks', []], $sdk->forwarded); + } + + public function testRemoveBlockWebhookForwardsToRemoveUserWebhook(): void + { + $sdk = new RecordingWebhookSdk(); + + self::assertSame(['forwarded' => true], $sdk->removeBlockWebhook('https://example.com/hook')); + self::assertSame(['removeUserWebhook', ['https://example.com/hook', 'block']], $sdk->forwarded); + + $sdk->removeBlockWebhook('https://example.com/hook', 'wallet_confirmation'); + self::assertSame(['removeUserWebhook', ['https://example.com/hook', 'wallet_confirmation']], $sdk->forwarded); + } + + /** + * @return array + */ + public static function deprecatedMethods(): array + { + return [ + 'funoutWalletUnspents' => [BitGoExpress::class, 'funoutWalletUnspents', 'fanoutWalletUnspents'], + 'addBlockWebhook' => [BitGoSDK::class, 'addBlockWebhook', 'addUserWebhook'], + 'listBlockWebhooks' => [BitGoSDK::class, 'listBlockWebhooks', 'listUserWebhooks'], + 'removeBlockWebhook' => [BitGoSDK::class, 'removeBlockWebhook', 'removeUserWebhook'], + ]; + } + + /** + * @dataProvider deprecatedMethods + * @param class-string $class + */ + public function testDeprecatedMethodNamesItsReplacement(string $class, string $deprecated, string $replacement): void + { + $doc = (string) (new ReflectionMethod($class, $deprecated))->getDocComment(); + + self::assertMatchesRegularExpression('/@deprecated\s+2\.3\.0\b.*\b' . $replacement . '\(\)/', $doc); + self::assertTrue(method_exists($class, $replacement)); + self::assertStringNotContainsString('@deprecated', (string) (new ReflectionMethod($class, $replacement))->getDocComment()); + } +} diff --git a/tests/ExpressPingTest.php b/tests/ExpressPingTest.php new file mode 100644 index 0000000..0754db2 --- /dev/null +++ b/tests/ExpressPingTest.php @@ -0,0 +1,34 @@ +ping(); + + self::assertSame('http://127.0.0.1:1/api/v2/ping', $this->getPrivate($express, 'url')); + } + + public function testPingExpressPingsExpressOnly(): void + { + $express = new BitGoExpress('127.0.0.1', 1, 'tbtc'); + + $express->pingExpress(); + + self::assertSame('http://127.0.0.1:1/api/v2/pingexpress', $this->getPrivate($express, 'url')); + } +} diff --git a/tests/Fixtures/RecordingWebhookSdk.php b/tests/Fixtures/RecordingWebhookSdk.php new file mode 100644 index 0000000..bfaeea7 --- /dev/null +++ b/tests/Fixtures/RecordingWebhookSdk.php @@ -0,0 +1,46 @@ +}|array{} */ + public array $forwarded = []; + + public function __construct() + { + parent::__construct('token', 'tbtc', true); + } + + /** + * @return array{forwarded: true} + */ + public function listUserWebhooks() + { + $this->forwarded = [__FUNCTION__, func_get_args()]; + return ['forwarded' => true]; + } + + /** + * @return array{forwarded: true} + */ + public function addUserWebhook(string $url, string $type, ?string $label = null, ?int $numConfirmations = null) + { + $this->forwarded = [__FUNCTION__, func_get_args()]; + return ['forwarded' => true]; + } + + /** + * @return array{forwarded: true} + */ + public function removeUserWebhook(string $url, string $type) + { + $this->forwarded = [__FUNCTION__, func_get_args()]; + return ['forwarded' => true]; + } +} diff --git a/tests/RequestIpTest.php b/tests/RequestIpTest.php new file mode 100644 index 0000000..7cde3b4 --- /dev/null +++ b/tests/RequestIpTest.php @@ -0,0 +1,61 @@ + + */ + public static function validIps(): array + { + return [ + 'IPv4' => ['192.0.2.10'], + 'IPv6' => ['2001:db8::10'], + ]; + } + + /** + * @dataProvider validIps + */ + public function testValidIpOnMainnetIsApplied(string $ip): void + { + $sdk = new BitGoSDK('token', 'btc'); + + self::assertTrue($sdk->setRequestIP($ip)); + self::assertSame($ip, $this->getPrivate($sdk, 'requestIP')); + } + + public function testInvalidIpIsNotApplied(): void + { + $sdk = new BitGoSDK('token', 'btc'); + + self::assertFalse($sdk->setRequestIP('not-an-ip')); + self::assertNull($this->getPrivate($sdk, 'requestIP')); + } + + public function testRejectedIpKeepsTheEarlierOne(): void + { + $sdk = new BitGoSDK('token', 'btc'); + $sdk->setRequestIP('192.0.2.10'); + + self::assertFalse($sdk->setRequestIP('192.0.2.300')); + self::assertSame('192.0.2.10', $this->getPrivate($sdk, 'requestIP')); + } + + public function testIpIsNotAppliedOnTestnet(): void + { + $sdk = new BitGoSDK('token', 'tbtc', true); + + self::assertFalse($sdk->setRequestIP('192.0.2.10')); + self::assertNull($this->getPrivate($sdk, 'requestIP')); + } +}