Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Entries for 2.2.3 and earlier were reconstructed from the git history in October
- Request timeouts: `setTimeout()` and `setConnectTimeout()` on both clients. The defaults are a 10 second connect timeout and a 60 second (`BitGoSDK`) or 310 second (`BitGoExpress`) request timeout.
- `BitGoExpress` can reach Express over TLS. Prefix the hostname with `https://`, trust a custom or self-signed certificate with `setCaFile()`, or, for local testing only, turn off verification with `setVerifyTls(false)`.
- `setForceIPv4()` on both clients, to get back the old IPv4-only behaviour.
- `BitGoSDK::setCaFile()`, to trust a custom CA file, for example behind a TLS-inspecting proxy.
- Both clients send a `User-Agent: BitGoSDK-PHP/<version>` header. The version is in `BitGoSDK::VERSION`.
- `BitGoExpress::pingExpress()`, which checks that Express is running without contacting BitGo.
- `BitGoExpress::fanoutWalletUnspents()`, replacing the misspelled `funoutWalletUnspents()`.
- `AddressType` constants named after each chain's script type: `P2SH_P2WSH_*` (10/11), `P2WSH_*` (20/21) and `P2TR_*` (30/31).
Expand All @@ -32,11 +34,13 @@ Entries for 2.2.3 and earlier were reconstructed from the git history in October
- Amounts in `sendTransaction()`, `sendTransactionToMany()` and `buildTransaction()` accept an `int` or a string of digits, and are always sent as strings, so 18-decimal amounts no longer overflow `PHP_INT_MAX`. `toSatoshi()` accepts strings and no longer breaks under locales that use a decimal comma. `toBTC()` no longer converts through a float.
- Requests no longer force IPv4. cURL uses IPv4 or IPv6 as the system allows. A local address bound with `setRequestIP()` decides the IP version.
- `BitGoSDK::setRequestIP()` returns `true` if the IP was applied, and `false` on testnet or for an invalid IP.
- **TLS certificates are checked against the system CA store.** The bundled 2022 `cacert.pem` is gone. If PHP finds no system CA store, the Mozilla bundle from the new `composer/ca-bundle` dependency is used.
- `ext-bcmath` is no longer required.
- **The minimum PHP version is now 7.4** (was 7.0).
- The project is now licensed under MIT. 2.2.3 and earlier were released under GPL-3.0. The file headers say so too.
- The README lists the current requirements and links to the docs on developers.bitgo.com.
- The README lists the current requirements, links to the docs on developers.bitgo.com, explains how to reach Express over TLS and names the minimum Express version (14.1.3).
- `src/` and `examples/` were reformatted to PSR-12. This is whitespace and brace placement only, with no behaviour change.
- The examples run without notices on PHP 8.4, from any directory. They pass BTC amounts to `toSatoshi()` as strings and check for errors after `unlockSession()`.
- Composer dist archives no longer include `tests/`, `examples/`, `docs/` or dotfiles. The examples are still available in the repository.

### Deprecated
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ CI runs these checks on every pull request. PHPUnit and `php -l` run on PHP 7.4

This section is for maintainers. Releases are tagged from `main` for now, and from `2.x` for 2.x releases once that branch exists.

1. **Prepare the changelog.** In a pull request, rename `## [Unreleased]` in `CHANGELOG.md` to `## [X.Y.Z] - YYYY-MM-DD`, add a new empty `## [Unreleased]` section above it, and update the compare links at the bottom of the file. Merge it once all required checks pass.
1. **Prepare the changelog.** In a pull request, rename `## [Unreleased]` in `CHANGELOG.md` to `## [X.Y.Z] - YYYY-MM-DD`, add a new empty `## [Unreleased]` section above it, and update the compare links at the bottom of the file. Set `BitGoSDK::VERSION` in `src/BitGoSDK.php` to `X.Y.Z`, since it is sent in the `User-Agent` header. Merge it once all required checks pass.
2. **Tag the release** on the merged commit. Tags use plain `X.Y.Z` with no `v` prefix, matching the existing tags.

```sh
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,38 @@ More examples are in [`examples/`](examples/).

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.

### Use TLS unless Express is on the same host

Requests to Express carry wallet passphrases and private keys. Over plain `http://` that is only safe when Express listens on `localhost`. Otherwise, run Express with a TLS certificate and prefix the hostname with `https://`:

```php
use neto737\BitGoSDK\BitGoExpress;
use neto737\BitGoSDK\Enum\CurrencyCode;

$express = new BitGoExpress('https://express.internal', 3080, CurrencyCode::BITCOIN);

// For a self-signed or private-CA certificate, trust that certificate or CA file.
$express->setCaFile('/path/to/express.crt');
```

Certificates are verified by default. `setVerifyTls(false)` turns verification off, so use it only for local testing.

### Minimum Express version

Use BitGo Express **14.1.3 or newer**. Older versions fail on two changes BitGo made on its side:

- **UTXO withdrawals from cold wallets** must use PSBT in production. BitGo needs Express 14.1.3 or newer for this ([changelog](https://developers.bitgo.com/changelog/20251007)).
- **Send-to-many on EVM assets** (`sendTransactionToMany()` on ETH and other EVM coins) uses a new batcher contract since May 22, 2025, so Express releases from before that date can't send it ([changelog](https://developers.bitgo.com/changelog/20250522)).

## 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
## TLS certificates

Keep your `cacert.pem` up to date. You can download the latest one from [curl.se](https://curl.se/docs/caextract.html).
Both clients check certificates against your system's CA store. If PHP can't find one, they use the Mozilla CA bundle from [`composer/ca-bundle`](https://github.com/composer/ca-bundle), so keep your dependencies up to date. To trust a different CA, for example behind a TLS-inspecting proxy, call `setCaFile('/path/to/ca.pem')`.

## License

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@

"require": {
"php": ">=7.4",
"ext-curl": "*"
"ext-curl": "*",
"composer/ca-bundle": "^1.5"
},

"require-dev": {
Expand Down
2 changes: 1 addition & 1 deletion examples/createAddress.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

use neto737\BitGoSDK\BitGoSDK;
use neto737\BitGoSDK\Enum\CurrencyCode;
Expand Down
2 changes: 1 addition & 1 deletion examples/generateWallet.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

use neto737\BitGoSDK\BitGoExpress;
use neto737\BitGoSDK\Enum\CurrencyCode;
Expand Down
2 changes: 1 addition & 1 deletion examples/getWalletByAddress.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

use neto737\BitGoSDK\BitGoSDK;
use neto737\BitGoSDK\Enum\CurrencyCode;
Expand Down
9 changes: 7 additions & 2 deletions examples/receiveWebhook.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

use neto737\BitGoSDK\BitGoSDK;
use neto737\BitGoSDK\Enum\CurrencyCode;
Expand All @@ -10,7 +10,12 @@
$bitgo = new BitGoSDK('YOUR_API_KEY_HERE', $coin, true);
$bitgo->walletId = 'WALLET_ID_HERE';

$payload = $bitgo->getWebhookPayload();
$payload = BitGoSDK::getWebhookPayload();

if (!is_array($payload) || !isset($payload['hash'])) {
http_response_code(400);
exit('Not a BitGo transfer webhook.');
}

$txDetails = $bitgo->getWalletTransaction($payload['hash']);

Expand Down
17 changes: 9 additions & 8 deletions examples/sendTransaction.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

use neto737\BitGoSDK\BitGoSDK;
use neto737\BitGoSDK\BitGoExpress;
Expand All @@ -13,21 +13,22 @@
$bitgo = new BitGoSDK('YOUR_API_KEY_HERE', $coin, true);

/**
* To send any transaction with BitGoExpress SDK you need to unlock your wallet
* If you're not using testnet to send coins, you need to unlock your wallet with
* your OTP password (2FA)
* To send any transaction with BitGoExpress SDK you need to unlock your session
* with your OTP code (2FA). On testnet the OTP code is always 000000.
*/
$bitgo->unlockSession('0000000');
$unlock = $bitgo->unlockSession('000000');
if (isset($unlock['error'])) {
exit('Unlock failed: ' . $unlock['error'] . PHP_EOL);
}

$bitgoExpress = new BitGoExpress($hostname, $port, $coin);
$bitgoExpress->accessToken = 'YOUR_API_KEY_HERE';
$bitgoExpress->walletId = 'YOUR_WALLET_ID_HERE';

/**
* Send the amount in satoshi
* Send the amount in satoshi. Pass BTC values as strings to avoid float rounding.
*/
$value_in_btc = 0.25;
$amount = BitGoSDK::toSatoshi($value_in_btc);
$amount = BitGoSDK::toSatoshi('0.25');

$sendTransaction = $bitgoExpress->sendTransaction('DESTINATION_ADDRESS', $amount, 'YOUR_WALLET_PASSPHRASE');
var_dump($sendTransaction);
22 changes: 12 additions & 10 deletions examples/sendTransactionToMany.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';

use neto737\BitGoSDK\BitGoSDK;
use neto737\BitGoSDK\BitGoExpress;
Expand All @@ -13,33 +13,35 @@
$bitgo = new BitGoSDK('YOUR_API_KEY_HERE', $coin, true);

/**
* To send any transaction with BitGoExpress SDK you need to unlock your wallet
* If you're not using testnet to send coins, you need to unlock your wallet with
* your OTP password (2FA)
* To send any transaction with BitGoExpress SDK you need to unlock your session
* with your OTP code (2FA). On testnet the OTP code is always 000000.
*/
$bitgo->unlockSession('0000000');
$unlock = $bitgo->unlockSession('000000');
if (isset($unlock['error'])) {
exit('Unlock failed: ' . $unlock['error'] . PHP_EOL);
}

$bitgoExpress = new BitGoExpress($hostname, $port, $coin);
$bitgoExpress->accessToken = 'YOUR_API_KEY_HERE';
$bitgoExpress->walletId = 'YOUR_WALLET_ID_HERE';

/**
* $sendTo array needs to be a multidimensional array
* All amounts in this array needs to be integer (int) values
* If you need convert bitcoin value into satoshi value just use toSatoshi function
* $sendTo is a list of recipients, each with an address and an amount in base units (satoshi)
* Amounts can be integers or strings of digits. Use toSatoshi() to convert a BTC value,
* passing it as a string to avoid float rounding
*/
$sendTo = [
[
'address' => 'ADDRESS_1',
'amount' => BitGoSDK::toSatoshi(0.25)
'amount' => BitGoSDK::toSatoshi('0.25')
],
[
'address' => 'ADDRESS_2',
'amount' => 20000
],
[
'address' => 'ADDRESS_3',
'amount' => BitGoSDK::toSatoshi(0.5)
'amount' => BitGoSDK::toSatoshi('0.5')
],
//...
];
Expand Down
6 changes: 5 additions & 1 deletion src/BitGoExpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace neto737\BitGoSDK;

use Composer\CaBundle\CaBundle;
use neto737\BitGoSDK\Enum\CurrencyCode;

class BitGoExpress
Expand Down Expand Up @@ -711,8 +712,11 @@ private function curlOptions(string $requestType, array $params, bool $authentic
if ($authenticate && isset($this->accessToken)) {
$headers[] = 'Authorization: Bearer ' . $this->accessToken;
}
$headers[] = 'User-Agent: BitGoSDK-PHP/' . BitGoSDK::VERSION;
$options[CURLOPT_HTTPHEADER] = $headers;
$options[CURLOPT_CAINFO] = $this->caFile ?? dirname(__FILE__) . '/cacert.pem';
// Without a custom CA file, use the system CA store, or composer/ca-bundle's Mozilla bundle if there is none.
$caPath = $this->caFile ?? CaBundle::getSystemCaRootBundlePath();
$options[is_dir($caPath) ? CURLOPT_CAPATH : CURLOPT_CAINFO] = $caPath;
if ($this->forceIPv4) {
$options[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
}
Expand Down
25 changes: 23 additions & 2 deletions src/BitGoSDK.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@

namespace neto737\BitGoSDK;

use Composer\CaBundle\CaBundle;
use neto737\BitGoSDK\Enum\CurrencyCode;
use neto737\BitGoSDK\Enum\AddressType;

class BitGoSDK
{
// Sent in the User-Agent header. Bump it with every release.
const VERSION = '2.3.0';
const BITGO_PRODUCTION_API_ENDPOINT = 'https://app.bitgo.com/api/v2/';
const BITGO_TESTNET_API_ENDPOINT = 'https://app.bitgo-test.com/api/v2/';
const DEFAULT_TIMEOUT = 60;
Expand All @@ -40,6 +43,7 @@ class BitGoSDK
public bool $testNet;
private int $timeout = self::DEFAULT_TIMEOUT;
private int $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
private ?string $caFile = null;
private bool $forceIPv4 = false;

/**
Expand Down Expand Up @@ -75,6 +79,20 @@ public function setRequestIP(string $ipAddress)
return true;
}

/**
* Trust the CA certificates in a PEM file instead of the system CA store, for example behind a TLS-inspecting proxy
*
* @param string $path Path to a PEM file
* @throws \InvalidArgumentException if the file cannot be read
*/
public function setCaFile(string $path): void
{
if (!is_file($path) || !is_readable($path)) {
throw new \InvalidArgumentException('CA file not found or not readable: ' . $path);
}
$this->caFile = $path;
}

/**
* Set the maximum time a request may take, connection included
*
Expand Down Expand Up @@ -1044,9 +1062,12 @@ private function curlOptions(string $requestType, array $params): array
$options[CURLOPT_RETURNTRANSFER] = true;
$options[CURLOPT_HTTPHEADER] = [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->accessToken
'Authorization: Bearer ' . $this->accessToken,
'User-Agent: BitGoSDK-PHP/' . self::VERSION
];
$options[CURLOPT_CAINFO] = dirname(__FILE__) . '/cacert.pem';
// Without a custom CA file, use the system CA store, or composer/ca-bundle's Mozilla bundle if there is none.
$caPath = $this->caFile ?? CaBundle::getSystemCaRootBundlePath();
$options[is_dir($caPath) ? CURLOPT_CAPATH : CURLOPT_CAINFO] = $caPath;
if (isset($this->requestIP)) {
$options[CURLOPT_INTERFACE] = $this->requestIP;
// A bound local address can only reach hosts of the same IP version.
Expand Down
Loading
Loading