diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 9e21c7b..8dd3b13 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -44,4 +44,5 @@ jobs: env: ENV: "${{ github.event.inputs.environment == 'prod' && 'prod' || 'sandbox'}}" SECRET: "${{ github.event.inputs.environment == 'prod' && secrets.PRODSECRET || secrets.SANDBOXSECRET}}" - run: php -denv=$ENV -dsecret=$SECRET phpunit.phar ClientTest.php + MPCSECRET: "${{secrets.SANDBOXMPCSECRET}}" + run: php -denv=$ENV -dsecret=$SECRET -dMPCApiSecret=$MPCSECRET phpunit.phar ClientTest.php diff --git a/MPCClient.php b/MPCClient.php new file mode 100644 index 0000000..9a490bf --- /dev/null +++ b/MPCClient.php @@ -0,0 +1,512 @@ +apiKey = $apiSigner->getPublicKey(); + $this->apiSigner = $apiSigner; + $this->coboPub = $config['coboPub']; + $this->host = $config['host']; + $this->debug = $debug; + } + + /** + * @throws Exception + */ + function request(string $method, string $path, array $data) + { + $ch = curl_init(); + $sorted_data = $this->sortData($data); + $nonce = time() * 1000; + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Biz-Api-Key:" . $this->apiKey, + "Biz-Api-Nonce:" . $nonce, + "Biz-Api-Signature:" . $this->apiSigner->sign(join("|", [$method, $path, $nonce, $sorted_data])) + ]); + + + if ($method == "POST") { + curl_setopt($ch, CURLOPT_URL, $this->host . $path); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } else { + curl_setopt($ch, CURLOPT_URL, $this->host . $path . "?" . $sorted_data); + } + if ($this->debug) { + echo "request >>>>>>>>\n"; + echo join("|", [$method, $path, $nonce, $sorted_data]), "\n"; + } + + list($header, $body) = explode("\r\n\r\n", curl_exec($ch), 2); + preg_match("/biz_timestamp: (?[0-9]*)/i", $header, $match); + $timestamp = $match["timestamp"]; + preg_match("/biz_resp_signature: (?[0-9abcdef]*)/i", $header, $match); + $signature = $match["signature"]; + + if ($this->debug) { + echo "response <<<<<<<<\n"; + echo "$body|$timestamp", "\n"; + echo "$signature", "\n"; + } + if ($this->verifyEcdsa($body, $timestamp, $signature) != 1) { + throw new Exception("signature verify fail"); + } + curl_close($ch); + return json_decode($body); + } + + private function sortData(array $data): string + { + ksort($data); + $result = []; + foreach ($data as $key => $val) { + array_push($result, $key . "=" . urlencode($val)); + } + return join("&", $result); + } + + function verifyEcdsa(string $message, string $timestamp, string $signature): bool + { + $message = hash("sha256", hash("sha256", "$message|$timestamp", True), True); + $ec = new EC('secp256k1'); + $key = $ec->keyFromPublic($this->coboPub, "hex"); + return $key->verify(bin2hex($message), $signature); + } + + /*** + * get supported chains + * @return mixed|string + */ + function getSupportedChains() + { + return $this->request("GET", "/v1/custody/mpc/get_supported_chains/", []); + } + + /*** + * get supported coins + * @param string $chainCode + * @return mixed|string + */ + function getSupportedCoins(string $chainCode) + { + $params = [ + "chain_code" => $chainCode, + ]; + return $this->request("GET", "/v1/custody/mpc/get_supported_coins/", $params); + } + + /*** + * check valid address + * @param string $coin + * @param string $address + * @return mixed|string + */ + function isValidAddress(string $coin, string $address) + { + $params = [ + "coin" => $coin, + "address" => $address, + ]; + return $this->request("GET", "/v1/custody/mpc/is_valid_address/", $params); + } + + /*** + * get main address + * string $chainCode + * @return mixed|string + */ + function getMainAddress(string $chainCode) + { + $params = [ + "chain_code" => $chainCode, + ]; + return $this->request("GET", "/v1/custody/mpc/get_main_address/", $params); + } + + /*** + * generate address + * string $chainCode + * int $count + * @return mixed|string + */ + function generateAddresses(string $chainCode, int $count) + { + $params = [ + "chain_code" => $chainCode, + "count" => $count, + ]; + return $this->request("POST", "/v1/custody/mpc/generate_addresses/", $params); + } + + /*** + * list addresses + * string $chainCode + * string $startId + * string $endId + * int $limit + * int $sort 0:DESCENDING 1:ASCENDING + * @return mixed|string + */ + function listAddresses(string $chainCode, string $startId = null, string $endId = null, int $limit = 50, int $sort = null) + { + $params = [ + "chain_code" => $chainCode, + "limit" => $limit, + ]; + + if ($startId) { + $params = array_merge($params, ["start_id" => $startId]); + } + if ($endId) { + $params = array_merge($params, ["end_id" => $endId]); + } + if ($sort) { + $params = array_merge($params, ["sort" => $sort]); + } + + return $this->request("GET", "/v1/custody/mpc/list_addresses/", $params); + } + + /*** + * get balance + * string $address + * string $chainCode + * string $coin + * @return mixed|string + */ + function getBalance(string $address, string $chainCode = null, string $coin = null) + { + $params = [ + "address" => $address, + ]; + + if ($chainCode) { + $params = array_merge($params, ["chain_code" => $chainCode]); + } + if ($coin) { + $params = array_merge($params, ["coin" => $coin]); + } + + return $this->request("GET", "/v1/custody/mpc/get_balance/", $params); + } + + /*** + * list balances + * string $pageIndex + * string $pageLength + * string $coin + * @return mixed|string + */ + function listBalances(int $pageIndex, int $pageLength, string $coin = null) + { + $params = [ + "page_index" => $pageIndex, + "page_length" => $pageLength, + ]; + + if ($coin) { + $params = array_merge($params, ["coin" => $coin]); + } + + return $this->request("GET", "/v1/custody/mpc/list_balances/", $params); + } + + /*** + * list spendable + * string $coin + * string $address + * @return mixed|string + */ + function listSpendable(string $coin, string $address = null) + { + $params = [ + "coin" => $coin, + ]; + + if ($address) { + $params = array_merge($params, ["address" => $address]); + } + + return $this->request("GET", "/v1/custody/mpc/list_spendable/", $params); + } + + /*** + * create transaction + * string $coin + * string $requestId + * BigInteger $amount + * string $fromAddr + * string $toAddr + * string $toAddressDetails + * BigInteger $fee + * BigInteger $gasPrice + * BigInteger $gasLimit + * int $operation + * string $extraParameters + * @return mixed|string + */ + function createTransaction(string $coin, string $requestId, BigInteger $amount, string $fromAddr = null, + string $toAddr = null, string $toAddressDetails = null, BigInteger $fee = null, + BigInteger $gasPrice = null, BigInteger $gasLimit = null, int $operation = null, + string $extraParameters = null) + { + $params = [ + "coin" => $coin, + "request_id" => $requestId, + "amount" => $amount->toString(), + ]; + + if ($fromAddr) { + $params = array_merge($params, ["from_address" => $fromAddr]); + } + if ($toAddr) { + $params = array_merge($params, ["to_address" => $toAddr]); + } + if ($toAddressDetails) { + $params = array_merge($params, ["to_address_details" => $toAddressDetails]); + } + if ($fee) { + $params = array_merge($params, ["fee" => $fee->toString()]); + } + if ($gasPrice) { + $params = array_merge($params, ["gas_price" => $gasPrice->toString()]); + } + if ($gasLimit) { + $params = array_merge($params, ["gas_limit" => $gasLimit->toString()]); + } + if ($operation) { + $params = array_merge($params, ["operation" => $operation]); + } + if ($extraParameters) { + $params = array_merge($params, ["extra_parameters" => $extraParameters]); + } + + return $this->request("POST", "/v1/custody/mpc/create_transaction/", $params); + } + + /*** + * drop transaction + * string $coboId + * string $requestId + * BigInteger $fee + * BigInteger $gasPrice + * BigInteger $gasLimit + * @return mixed|string + */ + function dropTransaction(string $coboId, string $requestId, BigInteger $fee = null, BigInteger $gasPrice = null, + BigInteger $gasLimit = null) + { + $params = [ + "cobo_id" => $coboId, + "request_id" => $requestId, + ]; + + if ($fee) { + $params = array_merge($params, ["fee" => $fee->toString()]); + } + if ($gasPrice) { + $params = array_merge($params, ["gas_price" => $gasPrice->toString()]); + } + if ($gasLimit) { + $params = array_merge($params, ["gas_limit" => $gasLimit->toString()]); + } + + return $this->request("POST", "/v1/custody/mpc/drop_transaction/", $params); + } + + /*** + * speedup transaction + * string $coboId + * string $requestId + * BigInteger $fee + * BigInteger $gasPrice + * BigInteger $gasLimit + * @return mixed|string + */ + function speedupTransaction(string $coboId, string $requestId, BigInteger $fee = null, BigInteger $gasPrice = null, + BigInteger $gasLimit = null) + { + $params = [ + "cobo_id" => $coboId, + "request_id" => $requestId, + ]; + + if ($fee) { + $params = array_merge($params, ["fee" => $fee->toString()]); + } + if ($gasPrice) { + $params = array_merge($params, ["gas_price" => $gasPrice->toString()]); + } + if ($gasLimit) { + $params = array_merge($params, ["gas_limit" => $gasLimit->toString()]); + } + + return $this->request("POST", "/v1/custody/mpc/speedup_transaction/", $params); + } + + /*** + * transactions by requestIds + * string $requestIds + * int $status + * @return mixed|string + */ + function transactionsByRequestIds(string $requestIds, int $status = null) + { + $params = [ + "request_ids" => $requestIds, + ]; + + if ($status) { + $params = array_merge($params, ["status" => $status]); + } + + return $this->request("GET", "/v1/custody/mpc/transactions_by_request_ids/", $params); + } + + /*** + * transactions by coboIds + * string $coboIds + * int $status + * @return mixed|string + */ + function transactionsByCoboIds(string $coboIds, int $status = null) + { + $params = [ + "cobo_ids" => $coboIds, + ]; + + if ($status) { + $params = array_merge($params, ["status" => $status]); + } + + return $this->request("GET", "/v1/custody/mpc/transactions_by_cobo_ids/", $params); + } + + + /*** + * transactions by coboIds + * string $txHash + * int $transactionType + * @return mixed|string + */ + function transactionsByTxHash(string $txHash, int $transactionType = null) + { + $params = [ + "tx_hash" => $txHash, + ]; + + if ($transactionType) { + $params = array_merge($params, ["transaction_type" => $transactionType]); + } + + return $this->request("GET", "/v1/custody/mpc/transactions_by_tx_hash/", $params); + } + + /*** + * list transactions + * int $startTime + * int $endTime + * int $status + * string $order + * string $order_by + * int $transactionType + * string $coins + * string $fromAddress + * string $toAddress + * int $limit + * @return mixed|string + */ + function listTransactions(int $startTime = null, int $endTime = null, int $status = null, string $order = null, + string $order_by = null, int $transactionType = null, string $coins = null, string $fromAddress = null, + string $toAddress = null, int $limit = 50) + { + $params = [ + "limit" => $limit, + ]; + + if ($startTime) { + $params = array_merge($params, ["start_time" => $startTime]); + } + if ($endTime) { + $params = array_merge($params, ["end_time" => $endTime]); + } + if ($status) { + $params = array_merge($params, ["status" => $status]); + } + if ($order) { + $params = array_merge($params, ["order" => $order]); + } + if ($order_by) { + $params = array_merge($params, ["order_by" => $order_by]); + } + if ($transactionType) { + $params = array_merge($params, ["transaction_type" => $transactionType]); + } + if ($coins) { + $params = array_merge($params, ["coins" => $coins]); + } + if ($fromAddress) { + $params = array_merge($params, ["from_address" => $fromAddress]); + } + if ($toAddress) { + $params = array_merge($params, ["to_address" => $toAddress]); + } + + return $this->request("GET", "/v1/custody/mpc/list_transactions/", $params); + } + + /*** + * estimate fee + * string $coin + * BigInteger $amount + * string $address + * @return mixed|string + */ + function estimateFee(string $coin, BigInteger $amount, string $address) + { + $params = [ + "coin" => $coin, + "amount" => $amount->toString(), + "address" => $address, + ]; + return $this->request("GET", "/v1/custody/mpc/estimate_fee/", $params); + } + + /*** + * estimate fee + * int $requestType + * int $status + * string $address + * @return mixed|string + */ + function listTssNodeRequests(int $requestType = null, int $status = null) + { + $params = [ + "request_type" => $requestType, + ]; + + if ($status) { + $params = array_merge($params, ["status" => $status]); + } + + return $this->request("GET", "/v1/custody/mpc/list_tss_node_requests/", $params); + } +} \ No newline at end of file diff --git a/MPCClientTest.php b/MPCClientTest.php new file mode 100644 index 0000000..400aa7c --- /dev/null +++ b/MPCClientTest.php @@ -0,0 +1,154 @@ +data = Config::SANDBOX_DATA; + $signer = new LocalSigner($GLOBALS["MPCApiSecret"]); + $this->mpcClient = new MPCClient($signer, $env, false); + } + + public function testGetSupportedChains() + { + $res = $this->mpcClient->getSupportedChains(); + + $this->assertTrue($res->success); + } + + public function testGetSupportedCoins() + { + $chainCode = "GETH"; + $res = $this->mpcClient->getSupportedCoins($chainCode); + + $this->assertTrue($res->success); + } + + public function testIsValidAddress() + { + $coin = "GETH"; + $address = "0x3ede1e59a3f3a66de4260df7ba3029b515337e5c"; + $res = $this->mpcClient->isValidAddress($coin, $address); + + $this->assertTrue($res->success); + } + + public function testGetMainAddress() + { + $chainCode = "GETH"; + $res = $this->mpcClient->getMainAddress($chainCode); + + $this->assertTrue($res->success); + } + + public function testGenerateAddresses() + { + $chainCode = "GETH"; + $count = 2; + $res = $this->mpcClient->generateAddresses($chainCode, $count); + + $this->assertTrue($res->success); + } + + public function testGetListAddresses() + { + $chainCode = "GETH"; + $res = $this->mpcClient->listAddresses($chainCode); + + $this->assertTrue($res->success); + } + + public function testGetBalance() + { + $address = "0x3ede1e59a3f3a66de4260df7ba3029b515337e5c"; + $res = $this->mpcClient->getBalance($address); + + $this->assertTrue($res->success); + } + + public function testListBalances() + { + $pageIndex = 0; + $pageLength = 50; + $res = $this->mpcClient->listBalances($pageIndex, $pageLength); + + $this->assertTrue($res->success); + } + + public function testListSpendable() + { + $coin = "BTC"; + $res = $this->mpcClient->listSpendable($coin); + + $this->assertTrue($res->success); + } + + public function testCreateTransaction() + { + $coin = "GETH"; + $requestId = time(); + $fromAddr = "0x3ede1e59a3f3a66de4260df7ba3029b515337e5c"; + $toAddr = "0xEEACb7a5e53600c144C0b9839A834bb4b39E540c"; + $amount = new BigInteger("10"); + $res = $this->mpcClient->createTransaction($coin, $requestId, $amount, $fromAddr, $toAddr); + + $this->assertTrue($res->success); + } + + public function testTransactionsByRequestIds() + { + $requestIds = "1668678820274"; + $res = $this->mpcClient->transactionsByRequestIds($requestIds); + + $this->assertTrue($res->success); + } + + public function testTransactionsByCoboIds() + { + $coboIds = "20221219161653000350944000006087"; + $res = $this->mpcClient->transactionsByCoboIds($coboIds); + + $this->assertTrue($res->success); + } + + public function testListTransactions() + { + $res = $this->mpcClient->listTransactions(); + + $this->assertTrue($res->success); + } + + public function testEstimateFee() + { + $coin = "GETH"; + $amount = new BigInteger("10000000"); + $address = "0xEEACb7a5e53600c144C0b9839A834bb4b39E540c"; + $res = $this->mpcClient->estimateFee($coin, $amount, $address); + + $this->assertTrue($res->success); + } + + public function testListTssNodeRequests() + { + $res = $this->mpcClient->listTssNodeRequests(); + + $this->assertTrue($res->success); + } +} \ No newline at end of file