From 98e9d4e10b52886f7ff86548674e89628b454cd6 Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 18:58:07 +1030 Subject: [PATCH 1/7] Add BitGoExpress::pingExpress() and fix the ping() docblock. ping() calls /api/v2/ping, which pings BitGo through Express, but its docblock described /pingexpress, which checks Express alone without contacting BitGo. ping() keeps its endpoint so existing callers are unaffected, its docblock now says what it does, and the new pingExpress() calls /api/v2/pingexpress. --- src/BitGoExpress.php | 14 +++++++++++++- tests/ExpressPingTest.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/ExpressPingTest.php diff --git a/src/BitGoExpress.php b/src/BitGoExpress.php index 2b8fbf4..c0a61e9 100644 --- a/src/BitGoExpress.php +++ b/src/BitGoExpress.php @@ -160,7 +160,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 +171,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. * 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')); + } +} From 1e77fd04ededd9eedb03f8a2283f844d70d40095 Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 18:59:35 +1030 Subject: [PATCH 2/7] Add fanoutWalletUnspents() and fix docblock typos. BitGoExpress::funoutWalletUnspents() was misspelled. fanoutWalletUnspents() now does the work, and funoutWalletUnspents() stays as a @deprecated alias that forwards its arguments unchanged. Docblock fixes: "his SDK call", "pendingaapproval" (the webhook type is pendingapproval), "transaaction", "the coin what you want", the copy-pasted listKeychains() and listWalletWebhooks() descriptions, a stray tab, and three "@return type" placeholders. The PHPStan baseline entries those caused are gone. --- phpstan-baseline.neon | 30 ---------------- src/BitGoExpress.php | 20 ++++++++--- src/BitGoSDK.php | 18 +++++----- tests/DeprecatedAliasTest.php | 66 +++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 44 deletions(-) create mode 100644 tests/DeprecatedAliasTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa86f97..fa39b19 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 @@ -252,12 +246,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,18 +324,6 @@ 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 @@ -360,12 +336,6 @@ parameters: 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 diff --git a/src/BitGoExpress.php b/src/BitGoExpress.php index c0a61e9..3af6723 100644 --- a/src/BitGoExpress.php +++ b/src/BitGoExpress.php @@ -50,7 +50,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) @@ -396,9 +396,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 = [ @@ -419,7 +419,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. @@ -520,7 +530,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..56e0600 100644 --- a/src/BitGoSDK.php +++ b/src/BitGoSDK.php @@ -47,7 +47,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 */ @@ -493,7 +493,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 +528,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 +578,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 */ @@ -761,7 +761,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 +779,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 +793,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 +809,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 +828,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) diff --git a/tests/DeprecatedAliasTest.php b/tests/DeprecatedAliasTest.php new file mode 100644 index 0000000..51a0b8d --- /dev/null +++ b/tests/DeprecatedAliasTest.php @@ -0,0 +1,66 @@ +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); + } + + /** + * @return array + */ + public static function deprecatedMethods(): array + { + return [ + 'funoutWalletUnspents' => [BitGoExpress::class, 'funoutWalletUnspents', 'fanoutWalletUnspents'], + ]; + } + + /** + * @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()); + } +} From 861303df200e5c954decb3d2bfad0ae50b64ea1d Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 19:01:04 +1030 Subject: [PATCH 3/7] Deprecate the duplicate block-webhook methods. addBlockWebhook(), listBlockWebhooks() and removeBlockWebhook() hit the same /webhooks endpoint as addUserWebhook(), listUserWebhooks() and removeUserWebhook(). The User names are kept: with simulateUserWebhook() they form a complete set, and they send $type as given. The Block names are now @deprecated aliases that forward to the User methods. They still send any type other than 'block' or 'wallet_confirmation' as 'block', so existing callers see no change. --- phpstan-baseline.neon | 18 --------- src/BitGoSDK.php | 54 ++++++++++++-------------- tests/DeprecatedAliasTest.php | 41 +++++++++++++++++++ tests/Fixtures/RecordingWebhookSdk.php | 46 ++++++++++++++++++++++ 4 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 tests/Fixtures/RecordingWebhookSdk.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa39b19..937e18f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -168,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 @@ -324,12 +318,6 @@ parameters: 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 @@ -384,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 diff --git a/src/BitGoSDK.php b/src/BitGoSDK.php index 56e0600..e6e3442 100644 --- a/src/BitGoSDK.php +++ b/src/BitGoSDK.php @@ -861,7 +861,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 +875,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 +896,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 +926,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 +1049,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/tests/DeprecatedAliasTest.php b/tests/DeprecatedAliasTest.php index 51a0b8d..3917d77 100644 --- a/tests/DeprecatedAliasTest.php +++ b/tests/DeprecatedAliasTest.php @@ -3,6 +3,8 @@ namespace neto737\BitGoSDK\Tests; use neto737\BitGoSDK\BitGoExpress; +use neto737\BitGoSDK\BitGoSDK; +use neto737\BitGoSDK\Tests\Fixtures\RecordingWebhookSdk; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -41,6 +43,42 @@ public function fanoutWalletUnspents(string $walletPassphrase, ?string $xprv = n 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 */ @@ -48,6 +86,9 @@ 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'], ]; } 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]; + } +} From 521c751e640f02734b1ce12d516cc8313d26c278 Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 19:02:00 +1030 Subject: [PATCH 4/7] Name the AddressType chains after their script types. AddressType::P2SH_DEPOSIT is chain 10, which is SegWit wrapped in P2SH, while plain P2SH is chain 0 (LEGACY_DEPOSIT). The new constants say what each chain is: P2SH_P2WSH_* (10/11), P2WSH_* (20/21) and P2TR_* (30/31). P2SH_DEPOSIT and P2SH_CHANGE are @deprecated with their values unchanged. BECH32_* and BECH32M_* stay as aliases of P2WSH_* and P2TR_*. The createWalletAddress() docblock said the chain defaulted to 0, but it has defaulted to 10 since 2.2.0. The docblock now says so and points to chain 0 for coins without SegWit. The default is unchanged, now spelled P2SH_P2WSH_DEPOSIT, and its $lowPriority and $gasPrice docs are in signature order. --- src/BitGoSDK.php | 7 ++-- src/Enum/AddressType.php | 43 ++++++++++++++++++----- tests/AddressTypeTest.php | 74 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 tests/AddressTypeTest.php diff --git a/src/BitGoSDK.php b/src/BitGoSDK.php index e6e3442..f761476 100644 --- a/src/BitGoSDK.php +++ b/src/BitGoSDK.php @@ -346,13 +346,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 = [ diff --git a/src/Enum/AddressType.php b/src/Enum/AddressType.php index 33dfe0e..0cf98ac 100644 --- a/src/Enum/AddressType.php +++ b/src/Enum/AddressType.php @@ -15,18 +15,43 @@ 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/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()); + } +} From 6163bd1035afa4bc8b3ff09487eb1b378210a353 Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 19:04:36 +1030 Subject: [PATCH 5/7] Bring CurrencyCode up to date with BitGo's coin codes. Adds Aptos, Arbitrum, Avalanche C-Chain and P-Chain (avaxc, avaxp), BNB Smart Chain, Cardano, Cosmos, Dogecoin, NEAR, Optimism, Polkadot, Polygon, Solana, Sui and TON with their testnets, plus the Bitcoin Gold testnet, Bitcoin testnet4 and signet, and the Ethereum Hoodi (hteth) and Sepolia (sepeth) testnets. Also adds USDT, USDC, DAI, LINK, WBTC, UNI and AAVE, and GLM, REPv2 and KNC2, the tokens that replaced GNT, REP and KNC. Nothing is removed. These are now @deprecated: - AVAX and AVAX_TESTNET: 'avax' and 'tavax' are not BitGo coin codes. - ETHEREUM_TESTNET: 'teth' is Kovan, which is retired. - OMG, GNT, REP, KNC, QRL, PAY and BRD: tokens that migrated or wound down. The ETH coin family now includes sepeth, and the BTC, UTXO and SegWit families include tbtc4 and tbtcsig, so wallets on those testnets keep their coin-specific params. --- src/BitGoExpress.php | 8 +-- src/BitGoSDK.php | 2 +- src/Enum/CurrencyCode.php | 105 ++++++++++++++++++++++++++++++++++--- tests/CoinFamilyTest.php | 6 +++ tests/CurrencyCodeTest.php | 90 +++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 tests/CurrencyCodeTest.php diff --git a/src/BitGoExpress.php b/src/BitGoExpress.php index 3af6723..fb35dca 100644 --- a/src/BitGoExpress.php +++ b/src/BitGoExpress.php @@ -24,10 +24,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; diff --git a/src/BitGoSDK.php b/src/BitGoSDK.php index f761476..b485fb3 100644 --- a/src/BitGoSDK.php +++ b/src/BitGoSDK.php @@ -26,7 +26,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']; diff --git a/src/Enum/CurrencyCode.php b/src/Enum/CurrencyCode.php index 3970a20..6f027e9 100644 --- a/src/Enum/CurrencyCode.php +++ b/src/Enum/CurrencyCode.php @@ -15,61 +15,150 @@ 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/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); + } +} From 8965ba62bc3d31a592eb5628b4fc48ea007acbc7 Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 19:06:06 +1030 Subject: [PATCH 6/7] Fix the file headers and update the README. The src/ file headers still said GPL v3 and @version 2.2. They now say MIT, matching LICENSE and composer.json, and drop @version so it can't go stale. The policy-rule docblocks linked to the retired www.bitgo.com/api/v2 docs and now link to developers.bitgo.com. README: lists the requirements (PHP 7.4+ with cURL, an access token, and Express for key operations), installs with a plain `composer require`, pins ^2.3 rather than "*", fixes the "Or if put" typo, links to the Express, API reference, webhook and policy docs on developers.bitgo.com, replaces the curl.haxx.se link with curl.se, and adds a License section. --- README.md | 41 ++++++++++++++++++++++++--------------- src/BitGoExpress.php | 3 +-- src/BitGoSDK.php | 9 ++++----- src/Enum/AddressType.php | 3 +-- src/Enum/CurrencyCode.php | 3 +-- 5 files changed, 32 insertions(+), 27 deletions(-) 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/src/BitGoExpress.php b/src/BitGoExpress.php index fb35dca..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; diff --git a/src/BitGoSDK.php b/src/BitGoSDK.php index b485fb3..b69bf60 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; @@ -617,8 +616,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 */ @@ -639,7 +638,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 */ diff --git a/src/Enum/AddressType.php b/src/Enum/AddressType.php index 0cf98ac..79d7b52 100644 --- a/src/Enum/AddressType.php +++ b/src/Enum/AddressType.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\Enum; diff --git a/src/Enum/CurrencyCode.php b/src/Enum/CurrencyCode.php index 6f027e9..076ff50 100644 --- a/src/Enum/CurrencyCode.php +++ b/src/Enum/CurrencyCode.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\Enum; From d98b852e24420a7aa8277c7053ca5c5e1485a15c Mon Sep 17 00:00:00 2001 From: Neto Date: Sun, 18 Oct 2026 19:07:08 +1030 Subject: [PATCH 7/7] Return whether setRequestIP() applied the IP. setRequestIP() silently ignored an invalid IP and any IP on testnet. It now returns true when the IP is applied and false otherwise, and a rejected IP leaves any earlier one in place. The return type is declared in the docblock only, so subclasses that override setRequestIP() keep working. --- phpstan-baseline.neon | 6 ---- src/BitGoSDK.php | 12 +++++--- tests/RequestIpTest.php | 61 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 tests/RequestIpTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 937e18f..49a86fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -402,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/BitGoSDK.php b/src/BitGoSDK.php index b69bf60..b65ee96 100644 --- a/src/BitGoSDK.php +++ b/src/BitGoSDK.php @@ -60,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; } /** 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')); + } +}