├── CashAddress.php ├── README.md └── Test.php /CashAddress.php: -------------------------------------------------------------------------------- 1 | 101 | 102 | class CashAddressException extends \Exception { 103 | 104 | } 105 | 106 | class CashAddress { 107 | 108 | const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 109 | const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; 110 | const ALPHABET_MAP = 111 | [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 112 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 113 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 114 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, 115 | -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, 116 | 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, 117 | -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, 118 | 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1]; 119 | const BECH_ALPHABET = 120 | [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 121 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 122 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 123 | 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, 124 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 125 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 126 | -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 127 | 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1]; 128 | const EXPAND_PREFIX_UNPROCESSED = [2, 9, 20, 3, 15, 9, 14, 3, 1, 19, 8, 0]; 129 | const EXPAND_PREFIX_TESTNET_UNPROCESSED = [2, 3, 8, 20, 5, 19, 20, 0]; 130 | const EXPAND_PREFIX = 1058337025301; 131 | const EXPAND_PREFIX_TESTNET = 584719417569; 132 | const BASE16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, 133 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 134 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 135 | 13, 14, 15]; 136 | 137 | public function __construct() 138 | { 139 | if (PHP_INT_SIZE < 5) { 140 | 141 | // Requires x64 system and PHP! 142 | throw new CashAddressException('Run it on a x64 system (+ 64 bit PHP)'); 143 | } 144 | } 145 | /** 146 | * convertBits is the internal function to convert 256-based bytes 147 | * to base-32 grouped bit arrays and vice versa. 148 | * @param array $data Data whose bits to be re-grouped 149 | * @param integer $fromBits Bits per input group of the $data 150 | * @param integer $toBits Bits to be put to each output group 151 | * @param boolean $pad Whether to add extra zeroes 152 | * @return array $ret 153 | * @throws CashAddressException 154 | */ 155 | static private function convertBits(array $data, $fromBits, $toBits, $pad = true) 156 | { 157 | $acc = 0; 158 | $bits = 0; 159 | $ret = []; 160 | $maxv = (1 << $toBits) - 1; 161 | $maxacc = (1 << ($fromBits + $toBits - 1)) - 1; 162 | 163 | $datalen = sizeof($data); 164 | for ($i = 0; $i < $datalen; $i++) 165 | { 166 | $value = $data[$i]; 167 | 168 | if ($value < 0 || $value >> $fromBits !== 0) 169 | { 170 | throw new CashAddressException('Error!'); 171 | } 172 | 173 | $acc = (($acc << $fromBits) | $value) & $maxacc; 174 | $bits += $fromBits; 175 | 176 | while ($bits >= $toBits) 177 | { 178 | $bits -= $toBits; 179 | $ret[] = (($acc >> $bits) & $maxv); 180 | } 181 | } 182 | 183 | if ($pad) 184 | { 185 | if ($bits) 186 | { 187 | $ret[] = ($acc << $toBits - $bits) & $maxv; 188 | } 189 | } 190 | else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) 191 | { 192 | throw new CashAddressException('Error!'); 193 | } 194 | 195 | return $ret; 196 | } 197 | 198 | /** 199 | * polyMod is the internal function create BCH codes. 200 | * @param array $var 5-bit grouped data array whose polyMod to be calculated. 201 | * @param integer c Starting value, 1 if the prefix is appended to the array. 202 | * @return integer $polymodValue polymod result 203 | */ 204 | static private function polyMod($var, $c = 1) 205 | { 206 | $varlen = sizeof($var);; 207 | for ($i = 0; $i < $varlen; $i++) 208 | { 209 | $c0 = $c >> 35; 210 | $c = (($c & 0x07ffffffff) << 5) ^ 211 | ($var[$i]) ^ 212 | (-($c0 & 1) & 0x98f2bc8e61) ^ 213 | (-($c0 & 2) & 0x79b76d99e2) ^ 214 | (-($c0 & 4) & 0xf33e5fb3c4) ^ 215 | (-($c0 & 8) & 0xae2eabe2a8) ^ 216 | (-($c0 & 16) & 0x1e4f43e470); 217 | } 218 | 219 | return $c ^ 1; 220 | } 221 | 222 | /** 223 | * rebuildAddress is the internal function to recreate error 224 | * corrected addresses. 225 | * @param array $addressBytes 226 | * @return string $correctedAddress 227 | */ 228 | static private function rebuildAddress($addressBytes) 229 | { 230 | $ret = ''; 231 | $i = 0; 232 | 233 | while ($addressBytes[$i] !== 0) 234 | { 235 | // 96 = ord('a') & 0xe0 236 | $ret .= chr(96 + $addressBytes[$i]); 237 | $i++; 238 | } 239 | 240 | $ret .= ':'; 241 | $len = sizeof($addressBytes); 242 | for ($i++; $i < $len; $i++) 243 | { 244 | $ret .= self::CHARSET[$addressBytes[$i]]; 245 | } 246 | 247 | return $ret; 248 | } 249 | 250 | /** 251 | * old2new converts an address in old format to the new Cash Address format. 252 | * @param string $oldAddress (either Mainnet or Testnet) 253 | * @return string $newAddress Cash Address result 254 | * @throws CashAddressException 255 | */ 256 | static public function old2new($oldAddress) 257 | { 258 | $bytes = [0]; 259 | 260 | for ($x = 0; $x < strlen($oldAddress); $x++) 261 | { 262 | $carry = ord($oldAddress[$x]); 263 | if ($carry > 127 || ((($carry = self::ALPHABET_MAP[$carry]) === -1))) 264 | { 265 | throw new CashAddressException('Unexpected character in address!'); 266 | } 267 | 268 | $bytes_len = sizeof($bytes); 269 | for ($j = 0; $j < $bytes_len; $j++) 270 | { 271 | $carry += $bytes[$j] * 58; 272 | $bytes[$j] = $carry & 0xff; 273 | $carry >>= 8; 274 | } 275 | 276 | while ($carry !== 0) 277 | { 278 | array_push($bytes, $carry & 0xff); 279 | $carry >>= 8; 280 | } 281 | } 282 | 283 | for ($numZeros = 0; $numZeros < strlen($oldAddress) && $oldAddress[$numZeros] === '1'; $numZeros++) 284 | { 285 | array_push($bytes, 0); 286 | } 287 | 288 | // reverse array 289 | $answer = []; 290 | 291 | for ($i = sizeof($bytes) - 1; $i >= 0; $i--) 292 | { 293 | array_push($answer, $bytes[$i]); 294 | } 295 | 296 | $version = $answer[0]; 297 | $payload = array_slice($answer, 1, sizeof($answer) - 5); 298 | 299 | if (sizeof($payload) % 4 !== 0) 300 | { 301 | throw new CashAddressException('Unexpected address length!'); 302 | } 303 | 304 | // Assume the checksum of the old address is right 305 | // Here, the Cash Address conversion starts 306 | if ($version === 0x00) 307 | { 308 | // P2PKH 309 | $addressType = 0; 310 | $realNet = true; 311 | } 312 | else if ($version === 0x05) 313 | { 314 | // P2SH 315 | $addressType = 1; 316 | $realNet = true; 317 | } 318 | else if ($version === 0x6f) 319 | { 320 | // Testnet P2PKH 321 | $addressType = 0; 322 | $realNet = false; 323 | } 324 | else if ($version === 0xc4) 325 | { 326 | // Testnet P2SH 327 | $addressType = 1; 328 | $realNet = false; 329 | } 330 | else if ($version === 0x1c) 331 | { 332 | // BitPay P2PKH 333 | $addressType = 0; 334 | $realNet = true; 335 | } 336 | else if ($version === 0x28) 337 | { 338 | // BitPay P2SH 339 | $addressType = 1; 340 | $realNet = true; 341 | } 342 | else 343 | { 344 | throw new CashAddressException('Unknown address type!'); 345 | } 346 | 347 | $encodedSize = (sizeof($payload) - 20) / 4; 348 | 349 | $versionByte = ($addressType << 3) | $encodedSize; 350 | $data = array_merge([$versionByte], $payload); 351 | $payloadConverted = self::convertBits($data, 8, 5, true); 352 | $arr = array_merge($payloadConverted, [0, 0, 0, 0, 0, 0, 0, 0]); 353 | if ($realNet) { 354 | $expand_prefix = self::EXPAND_PREFIX; 355 | $ret = 'bitcoincash:'; 356 | } else { 357 | $expand_prefix = self::EXPAND_PREFIX_TESTNET; 358 | $ret = 'bchtest:'; 359 | } 360 | $mod = self::polymod($arr, $expand_prefix); 361 | $checksum = [0, 0, 0, 0, 0, 0, 0, 0]; 362 | 363 | for ($i = 0; $i < 8; $i++) 364 | { 365 | // Convert the 5-bit groups in mod to checksum values. 366 | // $checksum[$i] = ($mod >> 5*(7-$i)) & 0x1f; 367 | $checksum[$i] = ($mod >> (5 * (7 - $i))) & 0x1f; 368 | } 369 | 370 | $combined = array_merge($payloadConverted, $checksum); 371 | $combined_len = sizeof($combined); 372 | for ($i = 0; $i < $combined_len; $i++) 373 | { 374 | $ret .= self::CHARSET[$combined[$i]]; 375 | } 376 | 377 | return $ret; 378 | } 379 | 380 | /** 381 | * Decodes Cash Address. 382 | * @param string $inputNew New address to be decoded. 383 | * @param boolean $shouldFixErrors Whether to fix typing errors. 384 | * @param boolean &$isTestnetAddressResult Is pointer, set to whether it's 385 | * a testnet address. 386 | * @return array $decoded Returns decoded byte array if it can be decoded. 387 | * @return string $correctedAddress Returns the corrected address if there's 388 | * a typing error. 389 | * @throws CashAddressException 390 | */ 391 | static public function decodeNewAddr($inputNew, $shouldFixErrors, &$isTestnetAddressResult) { 392 | $inputNew = strtolower($inputNew); 393 | if (strpos($inputNew, ':') === false) { 394 | $afterPrefix = 0; 395 | $expand_prefix = self::EXPAND_PREFIX; 396 | $isTestnetAddressResult = false; 397 | } 398 | else if (substr($inputNew, 0, 12) === 'bitcoincash:') 399 | { 400 | $afterPrefix = 12; 401 | $expand_prefix = self::EXPAND_PREFIX; 402 | $isTestnetAddressResult = false; 403 | } 404 | else if (substr($inputNew, 0, 8) === 'bchtest:') 405 | { 406 | $afterPrefix = 8; 407 | $expand_prefix = self::EXPAND_PREFIX_TESTNET; 408 | $isTestnetAddressResult = true; 409 | } 410 | else 411 | { 412 | throw new CashAddressException('Unknown address type'); 413 | } 414 | 415 | $data = []; 416 | $len = strlen($inputNew); 417 | for (; $afterPrefix < $len; $afterPrefix++) 418 | { 419 | $i = ord($inputNew[$afterPrefix]); 420 | if ($i > 127 || (($i = self::BECH_ALPHABET[$i]) === -1)) 421 | { 422 | throw new CashAddressException('Unexpected character in address!'); 423 | } 424 | array_push($data, $i); 425 | } 426 | 427 | $checksum = self::polyMod($data, $expand_prefix); 428 | 429 | if ($checksum !== 0) 430 | { 431 | if ($expand_prefix === self::EXPAND_PREFIX_TESTNET) { 432 | $unexpand_prefix = self::EXPAND_PREFIX_TESTNET_UNPROCESSED; 433 | } else { 434 | $unexpand_prefix = self::EXPAND_PREFIX_UNPROCESSED; 435 | } 436 | // Checksum is wrong! 437 | // Try to fix up to two errors 438 | if ($shouldFixErrors) { 439 | $syndromes = Array(); 440 | $datalen = sizeof($data); 441 | for ($p = 0; $p < $datalen; $p++) 442 | { 443 | for ($e = 1; $e < 32; $e++) 444 | { 445 | $data[$p] ^= $e; 446 | $c = self::polyMod($data, $expand_prefix); 447 | if ($c === 0) 448 | { 449 | return self::rebuildAddress(array_merge($unexpand_prefix, $data)); 450 | } 451 | $syndromes[$c ^ $checksum] = $p * 32 + $e; 452 | $data[$p] ^= $e; 453 | } 454 | } 455 | 456 | foreach ($syndromes as $s0 => $pe) 457 | { 458 | if (array_key_exists($s0 ^ $checksum, $syndromes)) 459 | { 460 | $data[$pe >> 5] ^= $pe % 32; 461 | $data[$syndromes[$s0 ^ $checksum] >> 5] ^= $syndromes[$s0 ^ $checksum] % 32; 462 | return self::rebuildAddress(array_merge($unexpand_prefix, $data)); 463 | } 464 | } 465 | throw new CashAddressException('Can\'t correct typing errors!'); 466 | } 467 | } 468 | return $data; 469 | } 470 | 471 | /** 472 | * Corrects Cash Address typing errors. 473 | * @param string $inputNew Cash Address to be corrected. 474 | * @return string $correctedAddress Error corrected address, or the input itself 475 | * if there are no errors. 476 | * @throws CashAddressException 477 | */ 478 | static public function fixCashAddrErrors($inputNew) { 479 | try { 480 | $corrected = self::decodeNewAddr($inputNew, true, $isTestnet); 481 | if (gettype($corrected) === 'array') { 482 | return $inputNew; 483 | } else { 484 | return $corrected; 485 | } 486 | } 487 | catch(CashAddressException $e) { 488 | throw $e; 489 | } 490 | } 491 | 492 | 493 | /** 494 | * new2old converts an address in the Cash Address format to the old format. 495 | * @param string $inputNew Cash Address (either mainnet or testnet) 496 | * @param boolean $shouldFixErrors Whether to fix typing errors. 497 | * @return string $oldAddress Old style 1... or 3... address 498 | * @throws CashAddressException 499 | */ 500 | static public function new2old($inputNew, $shouldFixErrors) 501 | { 502 | try { 503 | $corrected = self::decodeNewAddr($inputNew, $shouldFixErrors, $isTestnet); 504 | if (gettype($corrected) === 'array') { 505 | $values = $corrected; 506 | } else { 507 | $values = self::decodeNewAddr($corrected, false, $isTestnet); 508 | } 509 | } 510 | catch(Exception $e) { 511 | throw new CashAddressException('Error'); 512 | } 513 | 514 | $values = self::convertBits(array_slice($values, 0, sizeof($values) - 8), 5, 8, false); 515 | $addressType = $values[0] >> 3; 516 | $addressHash = array_slice($values, 1, 21); 517 | 518 | // Encode Address 519 | if ($isTestnet) { 520 | if ($addressType) { 521 | $bytes = [0xc4]; 522 | } else { 523 | $bytes = [0x6f]; 524 | } 525 | } else { 526 | if ($addressType) { 527 | $bytes = [0x05]; 528 | } else { 529 | $bytes = [0x00]; 530 | } 531 | } 532 | $bytes = array_merge($bytes, $addressHash); 533 | $merged = array_merge($bytes, self::doubleSha256ByteArray($bytes)); 534 | $digits = [0]; 535 | $merged_len = sizeof($merged); 536 | for ($i = 0; $i < $merged_len; $i++) 537 | { 538 | $carry = $merged[$i]; 539 | $digits_len = sizeof($digits); 540 | for ($j = 0; $j < $digits_len; $j++) 541 | { 542 | $carry += $digits[$j] << 8; 543 | $digits[$j] = $carry % 58; 544 | $carry = intdiv($carry, 58); 545 | } 546 | 547 | while ($carry !== 0) 548 | { 549 | array_push($digits, $carry % 58); 550 | $carry = intdiv($carry, 58); 551 | } 552 | } 553 | 554 | // leading zero bytes 555 | for ($i = 0; $i < $merged_len && $merged[$i] === 0; $i++) 556 | { 557 | array_push($digits, 0); 558 | } 559 | 560 | // reverse 561 | $converted = ''; 562 | for ($i = sizeof($digits) - 1; $i >= 0; $i--) 563 | { 564 | if ($digits[$i] > strlen(self::ALPHABET)) 565 | { 566 | throw new CashAddressException('Error!'); 567 | } 568 | $converted .= self::ALPHABET[$digits[$i]]; 569 | } 570 | 571 | return $converted; 572 | } 573 | 574 | /** 575 | * internal function to calculate sha256 576 | * @param array $byteArray Byte array of data to be hashed 577 | * @return array $hashResult First four bytes of sha256 result 578 | */ 579 | private static function doubleSha256ByteArray($byteArray) { 580 | $stringToBeHashed = ''; 581 | $byteArrayLen = sizeof($byteArray); 582 | for ($i = 0; $i < $byteArrayLen; $i++) 583 | { 584 | $stringToBeHashed .= chr($byteArray[$i]); 585 | } 586 | $hash = hash('sha256', $stringToBeHashed); 587 | $hashArray = []; 588 | for ($i = 0; $i < 32; $i++) 589 | { 590 | array_push($hashArray, self::BASE16[ord($hash[2 * $i]) - 48] * 16 + self::BASE16[ord($hash[2 * $i + 1]) - 48]); 591 | } 592 | $stringToBeHashed = ''; 593 | for ($i = 0; $i < 32; $i++) 594 | { 595 | $stringToBeHashed .= chr($hashArray[$i]); 596 | } 597 | 598 | $hashArray = []; 599 | $hash = hash('sha256', $stringToBeHashed); 600 | for ($i = 0; $i < 4; $i++) 601 | { 602 | array_push($hashArray, self::BASE16[ord($hash[2 * $i]) - 48] * 16 + self::BASE16[ord($hash[2 * $i + 1]) - 48]); 603 | } 604 | return $hashArray; 605 | } 606 | } 607 | 608 | ?> 609 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CashAddressPHP 2 | 3 | Two functions to convert from the legacy Bitcoin Cash address format to the new one and vice versa. 4 | 5 | ### Example of usage: 6 | 7 | #### P2PK: 8 | 9 | old2new('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'): 'bitcoincash:qp3wjpa3tjlj042z2wv7hahsldgwhwy0rq9sywjpyy' 10 | 11 | new2old('bitcoincash:qp3wjpa3tjlj042z2wv7hahsldgwhwy0rq9sywjpyy'): '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa' 12 | 13 | #### P2PKH: 14 | 15 | old2new('12higDjoCCNXSA95xZMWUdPvXNmkAduhWv'): 'bitcoincash:qqf2hrw93r9f64u8mhn7k22knknrcw3r3s0mkt0zxa' 16 | 17 | new2old('bitcoincash:qqf2hrw93r9f64u8mhn7k22knknrcw3r3s0mkt0zxa'): '12higDjoCCNXSA95xZMWUdPvXNmkAduhWv' 18 | 19 | #### P2SH: 20 | 21 | old2new('342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey'): 'bitcoincash:pqv60krfqv3k3lglrcnwtee6ftgwgaykpccr8hujjz' 22 | 23 | new2old('bitcoincash:pqv60krfqv3k3lglrcnwtee6ftgwgaykpccr8hujjz'): '342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey' 24 | -------------------------------------------------------------------------------- /Test.php: -------------------------------------------------------------------------------- 1 |