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
48 changes: 0 additions & 48 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
parameters:
ignoreErrors:
-
message: '#^Access to an undefined property neto737\\BitGoSDK\\BitGoExpress\:\:\$coin\.$#'
identifier: property.notFound
count: 37
path: src/BitGoExpress.php

-
message: '#^Access to an undefined property neto737\\BitGoSDK\\BitGoExpress\:\:\$hostname\.$#'
identifier: property.notFound
count: 1
path: src/BitGoExpress.php

-
message: '#^Access to an undefined property neto737\\BitGoSDK\\BitGoExpress\:\:\$port\.$#'
identifier: property.notFound
count: 1
path: src/BitGoExpress.php

-
message: '#^Method neto737\\BitGoSDK\\BitGoExpress\:\:__execute\(\) has no return type specified\.$#'
identifier: missingType.return
Expand Down Expand Up @@ -162,24 +144,12 @@ parameters:
count: 1
path: src/BitGoExpress.php

-
message: '#^Property neto737\\BitGoSDK\\BitGoExpress\:\:\$UTXObased has no type specified\.$#'
identifier: missingType.property
count: 1
path: src/BitGoExpress.php

-
message: '#^Property neto737\\BitGoSDK\\BitGoExpress\:\:\$accessToken has no type specified\.$#'
identifier: missingType.property
count: 1
path: src/BitGoExpress.php

-
message: '#^Property neto737\\BitGoSDK\\BitGoExpress\:\:\$login has no type specified\.$#'
identifier: missingType.property
count: 1
path: src/BitGoExpress.php

-
message: '#^Property neto737\\BitGoSDK\\BitGoExpress\:\:\$params has no type specified\.$#'
identifier: missingType.property
Expand All @@ -198,24 +168,6 @@ parameters:
count: 1
path: src/BitGoExpress.php

-
message: '#^Access to an undefined property neto737\\BitGoSDK\\BitGoSDK\:\:\$accessToken\.$#'
identifier: property.notFound
count: 2
path: src/BitGoSDK.php

-
message: '#^Access to an undefined property neto737\\BitGoSDK\\BitGoSDK\:\:\$coin\.$#'
identifier: property.notFound
count: 11
path: src/BitGoSDK.php

-
message: '#^Access to an undefined property neto737\\BitGoSDK\\BitGoSDK\:\:\$testNet\.$#'
identifier: property.notFound
count: 2
path: src/BitGoSDK.php

-
message: '#^Method neto737\\BitGoSDK\\BitGoSDK\:\:__execute\(\) has no return type specified\.$#'
identifier: missingType.return
Expand Down
258 changes: 176 additions & 82 deletions src/BitGoExpress.php

Large diffs are not rendered by default.

132 changes: 88 additions & 44 deletions src/BitGoSDK.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,23 @@ class BitGoSDK
const DEFAULT_TIMEOUT = 60;
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 XRP_COINS = ['xrp', 'txrp'];
private const XLM_COINS = ['xlm', 'txlm'];

private $APIEndpoint = null;
private $AuthAPIEndpoint = null;
private $url = null;
private $params = [];
public $walletId = null;
private $requestIP = null;
public string $accessToken;
public string $coin;
public bool $testNet;
private int $timeout = self::DEFAULT_TIMEOUT;
private int $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
private bool $forceIPv4 = false;

/**
* BitGoSDK Initialization
Expand Down Expand Up @@ -91,6 +100,16 @@ public function setConnectTimeout(int $seconds): void
$this->connectTimeout = $seconds;
}

/**
* Resolve hostnames to IPv4 addresses only. Off by default, so cURL uses IPv4 or IPv6 as the system allows.
*
* @param bool $force
*/
public function setForceIPv4(bool $force): void
{
$this->forceIPv4 = $force;
}

/**
* Converts value in satoshi to BTC value
*
Expand Down Expand Up @@ -250,7 +269,7 @@ public function listWallets(int $limit = 25, ?string $prevId = null, bool $allTo
$this->params = [
'limit' => $limit,
'prevId' => $prevId,
'allTokens' => $this->coin === CurrencyCode::ETHEREUM || $this->coin === CurrencyCode::ETHEREUM_TESTNET ? $allTokens : null,
'allTokens' => $this->coinIn(self::ETH_COINS) ? $allTokens : null,
];
return $this->__execute('GET');
}
Expand All @@ -265,7 +284,7 @@ public function getWallet(bool $allTokens = false)
{
$this->url = $this->APIEndpoint . '/wallet/' . $this->walletId;
$this->params = [
'allTokens' => $this->coin === CurrencyCode::ETHEREUM || $this->coin === CurrencyCode::ETHEREUM_TESTNET ? $allTokens : null,
'allTokens' => $this->coinIn(self::ETH_COINS) ? $allTokens : null,
];
return $this->__execute('GET');
}
Expand Down Expand Up @@ -294,7 +313,7 @@ public function listWalletTransfers(?string $prevId = null, bool $allTokens = fa
$this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/transfer';
$this->params = [
'prevId' => $prevId,
'allTokens' => $this->coin === CurrencyCode::ETHEREUM || $this->coin === CurrencyCode::ETHEREUM_TESTNET ? $allTokens : null,
'allTokens' => $this->coinIn(self::ETH_COINS) ? $allTokens : null,
];
return $this->__execute('GET');
}
Expand Down Expand Up @@ -339,8 +358,8 @@ public function createWalletAddress(int $chain = AddressType::P2SH_DEPOSIT, ?str
$this->params = [
'chain' => $chain,
'label' => $label,
'lowPriority' => $this->coin === CurrencyCode::ETHEREUM ? $lowPriority : null,
'gasPrice' => $this->coin === CurrencyCode::ETHEREUM ? $gasPrice : null,
'lowPriority' => $this->coinIn(self::ETH_COINS) ? $lowPriority : null,
'gasPrice' => $this->coinIn(self::ETH_COINS) ? $gasPrice : null,
];
return $this->__execute();
}
Expand All @@ -355,8 +374,8 @@ public function getWalletAddress(string $addressOrId, ?string $dt = null, ?strin
{
$this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/address/' . $addressOrId;
$this->params = [
'dt' => $this->coin === CurrencyCode::RIPPLE ? $dt : null,
'memoId' => $this->coin === CurrencyCode::STELLAR ? $memoId : null
'dt' => $this->coinIn(self::XRP_COINS) ? $dt : null,
'memoId' => $this->coinIn(self::XLM_COINS) ? $memoId : null
];
return $this->__execute('GET');
}
Expand All @@ -372,8 +391,8 @@ public function updateWalletAddress(string $addressOrId, ?string $dt = null, ?st
{
$this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/address/' . $addressOrId;
$this->params = [
'dt' => $this->coin === CurrencyCode::RIPPLE ? $dt : null,
'memoId' => $this->coin === CurrencyCode::STELLAR ? $memoId : null,
'dt' => $this->coinIn(self::XRP_COINS) ? $dt : null,
'memoId' => $this->coinIn(self::XLM_COINS) ? $memoId : null,
'label' => $label
];
return $this->__execute('PUT');
Expand Down Expand Up @@ -413,7 +432,7 @@ public function getWalletTransactions(?string $prevId = null, bool $allTokens =
$this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/tx';
$this->params = [
'prevId' => $prevId,
'allTokens' => $this->coin === CurrencyCode::ETHEREUM || $this->coin === CurrencyCode::ETHEREUM_TESTNET ? $allTokens : null,
'allTokens' => $this->coinIn(self::ETH_COINS) ? $allTokens : null,
];
return $this->__execute('GET');
}
Expand Down Expand Up @@ -550,8 +569,8 @@ public function createBitGoKeychain(string $source = 'bitgo', ?string $enterpris
$this->url = $this->APIEndpoint . '/key';
$this->params = [
'source' => $source,
'enterprise' => $this->coin === CurrencyCode::ETHEREUM ? $enterprise : null,
'newFeeAddress' => $this->coin === CurrencyCode::ETHEREUM ? $newFeeAddress : null
'enterprise' => $this->coinIn(self::ETH_COINS) ? $enterprise : null,
'newFeeAddress' => $this->coinIn(self::ETH_COINS) ? $newFeeAddress : null
];
return $this->__execute();
}
Expand Down Expand Up @@ -749,8 +768,8 @@ public function listPendingApprovals(?string $walletID = null, ?string $enterpri
$this->url = $this->APIEndpoint . '/pendingapprovals';
$this->params = [
'walletID' => $walletID,
'enterprise' => $this->coin === CurrencyCode::ETHEREUM ? $enterprise : null,
'allTokens' => $this->coin === CurrencyCode::ETHEREUM || $this->coin === CurrencyCode::ETHEREUM_TESTNET ? $allTokens : null
'enterprise' => $this->coinIn(self::ETH_COINS) ? $enterprise : null,
'allTokens' => $this->coinIn(self::ETH_COINS) ? $allTokens : null
];
return $this->__execute('GET');
}
Expand Down Expand Up @@ -781,7 +800,7 @@ public function listWalletWebhooks(bool $allTokens = false)
{
$this->url = $this->APIEndpoint . '/wallet/' . $this->walletId . '/webhooks';
$this->params = [
'allTokens' => $this->coin === CurrencyCode::ETHEREUM || $this->coin === CurrencyCode::ETHEREUM_TESTNET ? $allTokens : null
'allTokens' => $this->coinIn(self::ETH_COINS) ? $allTokens : null
];
return $this->__execute('GET');
}
Expand Down Expand Up @@ -987,35 +1006,8 @@ private function __execute(string $requestType = 'POST')
$params = self::withoutNulls($this->params);
$this->params = [];

$ch = curl_init($this->url);
if ($requestType === 'GET') {
curl_setopt($ch, CURLOPT_URL, $this->url . '?' . self::buildQuery($params));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
} elseif ($requestType === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
} elseif ($requestType === 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
} elseif ($requestType === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->accessToken
]);
curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/cacert.pem');
if (isset($this->requestIP)) {
curl_setopt($ch, CURLOPT_INTERFACE, $this->requestIP);
}
if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) {
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
}
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout);
$ch = curl_init();
curl_setopt_array($ch, $this->curlOptions($requestType, $params));

$response = curl_exec($ch);
$errno = curl_errno($ch);
Expand All @@ -1029,6 +1021,58 @@ private function __execute(string $requestType = 'POST')
return self::decodeResponse((string) $response, $status);
}

/**
* The cURL options for a request to $this->url.
*
* @param array<string, mixed> $params
* @return array<int, mixed>
*/
private function curlOptions(string $requestType, array $params): array
{
$options = [CURLOPT_URL => $this->url];
if ($requestType === 'GET') {
$options[CURLOPT_URL] = $this->url . '?' . self::buildQuery($params);
$options[CURLOPT_CUSTOMREQUEST] = 'GET';
} elseif ($requestType === 'PUT') {
$options[CURLOPT_CUSTOMREQUEST] = 'PUT';
$options[CURLOPT_POSTFIELDS] = json_encode($params);
} elseif ($requestType === 'DELETE') {
$options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
$options[CURLOPT_POSTFIELDS] = json_encode($params);
} elseif ($requestType === 'POST') {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = json_encode($params);
}
$options[CURLOPT_SSL_VERIFYPEER] = true;
$options[CURLOPT_RETURNTRANSFER] = true;
$options[CURLOPT_HTTPHEADER] = [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->accessToken
];
$options[CURLOPT_CAINFO] = dirname(__FILE__) . '/cacert.pem';
if (isset($this->requestIP)) {
$options[CURLOPT_INTERFACE] = $this->requestIP;
// A bound local address can only reach hosts of the same IP version.
$options[CURLOPT_IPRESOLVE] = filter_var($this->requestIP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ? CURL_IPRESOLVE_V4 : CURL_IPRESOLVE_V6;
}
if ($this->forceIPv4) {
$options[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
}
$options[CURLOPT_TIMEOUT] = $this->timeout;
$options[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
return $options;
}

/**
* Whether this client's coin is one of $coins.
*
* @param list<string> $coins
*/
private function coinIn(array $coins): bool
{
return in_array($this->coin, $coins, true);
}

/**
* Decode a response body. A non-2xx status or an invalid JSON body becomes ['error' => …, 'status' => …],
* keeping any other fields BitGo sent.
Expand Down
2 changes: 1 addition & 1 deletion tests/AmountTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public function testExpressSendsRecipientAmountsAsStrings(): void

public function testExpressRejectsDecimalAmounts(): void
{
$express = @new BitGoExpress('127.0.0.1', 1, 'tbtc');
$express = new BitGoExpress('127.0.0.1', 1, 'tbtc');

$this->expectException(\InvalidArgumentException::class);
$express->sendTransaction('address', '0.5', 'passphrase');
Expand Down
Loading
Loading