Last active
November 1, 2019 10:45
-
-
Save tkthundr/86068664cf01dc2be9fe0efce96a3c18 to your computer and use it in GitHub Desktop.
Standalone PHP LNURL lib
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//this gist combines both of the following implementations into 1 standalone | |
//https://github.com/Bit-Wasp/bech32 | |
//https://github.com/tkijewski/php-lnurl | |
const TAG_WITHDRAW = 'withdrawRequest'; | |
const TAG_LOGIN = 'login'; | |
const TAG_CHANNEL = 'channelRequest'; | |
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; | |
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; | |
const CHARKEY_KEY = [ | |
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, | |
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, | |
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, | |
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, | |
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 | |
]; | |
/** | |
* Encode a url into lnurl bech32 format | |
* @param $url | |
* @return string | |
* @throws \BitWasp\Bech32\Exception\Bech32Exception | |
*/ | |
function encodeUrl($url) | |
{ | |
$arr = str_split($url); | |
array_walk($arr, function(&$value, &$key) { | |
$value = ord($value); | |
}); | |
$bits = convertBits($arr, count($arr), 8, 5, TRUE); | |
$encoded = encode('lnurl', $bits); | |
$encoded = strtoupper($encoded); | |
return $encoded; | |
} | |
/** | |
* Decode/parse an lnurl bech32 string into an array of elements. | |
* @param $lnurl | |
* @return mixed ['url'=>URL,'tag'=>TAG,'queryParam1'=>'value1',....] | |
* @throws LnurlException | |
* @throws \BitWasp\Bech32\Exception\Bech32Exception | |
*/ | |
function decodeUrl($lnurl) | |
{ | |
list ($hrp, $data) = \Bitwasp\Bech32\decodeRaw($lnurl); | |
if ($hrp != 'lnurl') | |
throw new LnurlException('Not a valid lnurl, hrp does not equal lnurl'); | |
$decoded = convertBits($data, count($data), 5, 8, false); | |
$url = ''; | |
foreach ($decoded as $char) { | |
$url .= chr($char); | |
} | |
parse_str(@parse_url($url)['query'], $queryParameters); | |
$arr = $queryParameters; | |
$arr['url'] = $url; | |
return $arr; | |
} | |
/** | |
* @param int[] $values | |
* @param int $numValues | |
* @return int | |
*/ | |
function polyMod(array $values, $numValues) | |
{ | |
$chk = 1; | |
for ($i = 0; $i < $numValues; $i++) { | |
$top = $chk >> 25; | |
$chk = ($chk & 0x1ffffff) << 5 ^ $values[$i]; | |
for ($j = 0; $j < 5; $j++) { | |
$value = (($top >> $j) & 1) ? GENERATOR[$j] : 0; | |
$chk ^= $value; | |
} | |
} | |
return $chk; | |
} | |
/** | |
* Expands the human readable part into a character array for checksumming. | |
* @param string $hrp | |
* @param int $hrpLen | |
* @return int[] | |
*/ | |
function hrpExpand($hrp, $hrpLen) | |
{ | |
$expand1 = []; | |
$expand2 = []; | |
for ($i = 0; $i < $hrpLen; $i++) { | |
$o = \ord($hrp[$i]); | |
$expand1[] = $o >> 5; | |
$expand2[] = $o & 31; | |
} | |
return \array_merge($expand1, [0], $expand2); | |
} | |
/** | |
* Converts words of $fromBits bits to $toBits bits in size. | |
* | |
* @param int[] $data - character array of data to convert | |
* @param int $inLen - number of elements in array | |
* @param int $fromBits - word (bit count) size of provided data | |
* @param int $toBits - requested word size (bit count) | |
* @param bool $pad - whether to pad (only when encoding) | |
* @return int[] | |
* @throws Bech32Exception | |
*/ | |
function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true) | |
{ | |
$acc = 0; | |
$bits = 0; | |
$ret = []; | |
$maxv = (1 << $toBits) - 1; | |
$maxacc = (1 << ($fromBits + $toBits - 1)) - 1; | |
for ($i = 0; $i < $inLen; $i++) { | |
$value = $data[$i]; | |
if ($value < 0 || $value >> $fromBits) { | |
throw new Bech32Exception('Invalid value for convert bits'); | |
} | |
$acc = (($acc << $fromBits) | $value) & $maxacc; | |
$bits += $fromBits; | |
while ($bits >= $toBits) { | |
$bits -= $toBits; | |
$ret[] = (($acc >> $bits) & $maxv); | |
} | |
} | |
if ($pad) { | |
if ($bits) { | |
$ret[] = ($acc << $toBits - $bits) & $maxv; | |
} | |
} else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) { | |
throw new Bech32Exception('Invalid data'); | |
} | |
return $ret; | |
} | |
/** | |
* @param string $hrp | |
* @param int[] $convertedDataChars | |
* @return int[] | |
*/ | |
function createChecksum($hrp, array $convertedDataChars) | |
{ | |
$values = \array_merge(hrpExpand($hrp, \strlen($hrp)), $convertedDataChars); | |
$polyMod = polyMod(\array_merge($values, [0, 0, 0, 0, 0, 0]), \count($values) + 6) ^ 1; | |
$results = []; | |
for ($i = 0; $i < 6; $i++) { | |
$results[$i] = ($polyMod >> 5 * (5 - $i)) & 31; | |
} | |
return $results; | |
} | |
/** | |
* Verifies the checksum given $hrp and $convertedDataChars. | |
* | |
* @param string $hrp | |
* @param int[] $convertedDataChars | |
* @return bool | |
*/ | |
function verifyChecksum($hrp, array $convertedDataChars) | |
{ | |
$expandHrp = hrpExpand($hrp, \strlen($hrp)); | |
$r = \array_merge($expandHrp, $convertedDataChars); | |
$poly = polyMod($r, \count($r)); | |
return $poly === 1; | |
} | |
/** | |
* @param string $hrp | |
* @param array $combinedDataChars | |
* @return string | |
*/ | |
function encode($hrp, array $combinedDataChars) | |
{ | |
$checksum = createChecksum($hrp, $combinedDataChars); | |
$characters = \array_merge($combinedDataChars, $checksum); | |
$encoded = []; | |
for ($i = 0, $n = count($characters); $i < $n; $i++) { | |
$encoded[$i] = CHARSET[$characters[$i]]; | |
} | |
return "{$hrp}1" . \implode('', $encoded); | |
} | |
/** | |
* @throws Bech32Exception | |
* @param string $sBech - the bech32 encoded string | |
* @return array - returns [$hrp, $dataChars] | |
*/ | |
function decodeRaw($sBech) | |
{ | |
$length = \strlen($sBech); | |
if ($length < 8) { | |
throw new Bech32Exception("Bech32 string is too short"); | |
} | |
$chars = array_values(unpack('C*', $sBech)); | |
$haveUpper = false; | |
$haveLower = false; | |
$positionOne = -1; | |
for ($i = 0; $i < $length; $i++) { | |
$x = $chars[$i]; | |
if ($x < 33 || $x > 126) { | |
throw new Bech32Exception('Out of range character in bech32 string'); | |
} | |
if ($x >= 0x61 && $x <= 0x7a) { | |
$haveLower = true; | |
} | |
if ($x >= 0x41 && $x <= 0x5a) { | |
$haveUpper = true; | |
$x = $chars[$i] = $x + 0x20; | |
} | |
// find location of last '1' character | |
if ($x === 0x31) { | |
$positionOne = $i; | |
} | |
} | |
if ($haveUpper && $haveLower) { | |
throw new Bech32Exception('Data contains mixture of higher/lower case characters'); | |
} | |
if ($positionOne === -1) { | |
throw new Bech32Exception("Missing separator character"); | |
} | |
if ($positionOne < 1) { | |
throw new Bech32Exception("Empty HRP"); | |
} | |
if (($positionOne + 7) > $length) { | |
throw new Bech32Exception('Too short checksum'); | |
} | |
$hrp = \pack("C*", ...\array_slice($chars, 0, $positionOne)); | |
$data = []; | |
for ($i = $positionOne + 1; $i < $length; $i++) { | |
$data[] = ($chars[$i] & 0x80) ? -1 : CHARKEY_KEY[$chars[$i]]; | |
} | |
if (!verifyChecksum($hrp, $data)) { | |
throw new Bech32Exception('Invalid bech32 checksum'); | |
} | |
return [$hrp, array_slice($data, 0, -6)]; | |
} | |
/** | |
* Validates a bech32 string and returns [$hrp, $dataChars] if | |
* the conversion was successful. An exception is thrown on invalid | |
* data. | |
* | |
* @param string $sBech - the bech32 encoded string | |
* @return array - returns [$hrp, $dataChars] | |
* @throws Bech32Exception | |
*/ | |
function decode($sBech) | |
{ | |
$length = strlen($sBech); | |
if ($length > 90) { | |
throw new Bech32Exception('Bech32 string cannot exceed 90 characters in length'); | |
} | |
return decodeRaw($sBech); | |
} | |
/** | |
* @param int $version | |
* @param string $program | |
* @throws Bech32Exception | |
*/ | |
function validateWitnessProgram($version, $program) | |
{ | |
if ($version < 0 || $version > 16) { | |
throw new Bech32Exception("Invalid witness version"); | |
} | |
$sizeProgram = strlen($program); | |
if ($version === 0) { | |
if ($sizeProgram !== 20 && $sizeProgram !== 32) { | |
throw new Bech32Exception("Invalid size for V0 witness program"); | |
} | |
} | |
if ($sizeProgram < 2 || $sizeProgram > 40) { | |
throw new Bech32Exception("Witness program size was out of valid range"); | |
} | |
} | |
/** | |
* @param string $hrp - human readable part | |
* @param int $version - segwit script version | |
* @param string $program - segwit witness program | |
* @return string - the encoded address | |
* @throws Bech32Exception | |
*/ | |
function encodeSegwit($hrp, $version, $program) | |
{ | |
$version = (int) $version; | |
validateWitnessProgram($version, $program); | |
$programChars = array_values(unpack('C*', $program)); | |
$programBits = convertBits($programChars, count($programChars), 8, 5, true); | |
$encodeData = array_merge([$version], $programBits); | |
return encode($hrp, $encodeData); | |
} | |
/** | |
* @param string $hrp - human readable part | |
* @param string $bech32 - Bech32 string to be decoded | |
* @return array - [$version, $program] | |
* @throws Bech32Exception | |
*/ | |
function decodeSegwit($hrp, $bech32) | |
{ | |
list ($hrpGot, $data) = decode($bech32); | |
if ($hrpGot !== $hrp) { | |
throw new Bech32Exception('Invalid prefix for address'); | |
} | |
$dataLen = count($data); | |
if ($dataLen === 0 || $dataLen > 65) { | |
throw new Bech32Exception("Invalid length for segwit address"); | |
} | |
$decoded = convertBits(array_slice($data, 1), count($data) - 1, 5, 8, false); | |
$program = pack("C*", ...$decoded); | |
validateWitnessProgram($data[0], $program); | |
return [$data[0], $program]; | |
} | |
class LnurlException extends \Exception | |
{ | |
} | |
class Bech32Exception extends \Exception | |
{ | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//USAGE: php test.php https://www.google.com | |
require 'lnurl.php'; | |
echo encodeUrl($argv[0]); | |
echo "\n"; | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment