2 require_once "bcbitwise.php";
3 require_once "Socket.php";
4 require_once "LoomRandom.php";
7 * Client to the webapp at http://loom.cc/
8 * See https://loom.cc/?function=help&topic=grid_tutorial&mode=advanced
13 var $url_prefix; // Who you gonna call?
14 var $bits128; // 128 1-bits = 2**128 - 1
15 var $socket; // our connection to the Loom server
16 var $random; // An instance of LoomRandom
18 function LoomClient($prefix = 'https://loom.cc/') {
19 if (substr($prefix, -1) != '/') $prefix .= '/';
20 $this->url_prefix
= $prefix;
21 $this->bits128
= bcsub(bcleftshift(1, 128), 1);
22 $this->socket
= FALSE;
23 $this->random
= new LoomRandom();
26 // This is all you really need to call
27 // The functions below are just syntactic sugar for calling get()
28 /* Save the old non-persistent connection version
29 function get($keys, &$url) {
30 $url = $this->url($this->url_prefix, $keys);
31 $kv = @file_get_contents($url);
32 return $this->parsekv($kv);
36 // The base of the Loom interface
37 // Send an HTTP GET, with args in $keys.
38 // Return whatever Loom returns.
39 function rawget($keys, &$url) {
40 return $this->rawGetOrPost($keys, 'get', $url);
43 function rawpost($keys, &$url) {
44 return $this->rawGetOrPost($keys, 'post', $url);
47 function rawGetOrPost($keys, $getOrPost, &$url) {
49 // Should make this less brittle
50 if (substr($this->url_prefix
, 0, 5) == 'https') {
51 $host = substr($this->url_prefix
, 8, -1);
54 $host = substr($this->url_prefix
, 7, -1);
57 $this->socket
= new Socket($host, $ssl);
58 $this->socket
->connect();
60 $uri = $this->url('', $keys);
61 $url = $this->url_prefix
. $uri;
63 if ($getOrPost == 'post') $this->socket
->post($uri);
64 else $this->socket
->get($uri);
65 return $this->socket
->body
;
68 // This is all you really need to call
69 // The functions below are just syntactic sugar for calling get()
70 function get($keys, &$url) {
71 $kv = $this->rawget($keys, $url);
72 return $this->parsekv($kv);
76 function buy($type, $location, $usage, &$url) {
77 return $this->get(array('function' => 'grid',
85 function sell($type, $location, $usage, &$url) {
86 return $this->get(array('function' => 'grid',
94 function issuer($type, $orig, $dest, &$url) {
95 return $this->get(array('function' => 'grid',
103 function touch($type, $location, &$url) {
104 return $this->get(array('function' => 'grid',
111 function look($type, $hash, &$url) {
112 return $this->get(array('function' => 'grid',
119 function move($type, $quantity, $origin, $destination, &$url) {
120 return $this->get(array('function' => 'grid',
125 'dest' => $destination),
129 // $locs and $types are lists of space-separated hex IDs.
130 // If $zeroes is true, will return 0 values.
131 function scan($locs, $types, $zeroes, &$url) {
132 $a = array('function' => 'grid',
136 if ($zeroes) $a['zeroes'] = '1';
137 return $this->get($a, $url);
140 // $locs is array(locname => id, ...)
141 // $types is array(typename => array('id' => id,
142 // 'name' => typename,
143 // 'min_precision' => min_precision,
144 // 'scale' => scale),
146 // Returns array(locname => array(typename => value, ...), ...)
147 // Returns FALSE if it gets an error from the loom server
148 function namedScan($locs, $types, $zeroes, &$url) {
151 foreach ($locs as $locname => $id) {
152 if ($locstring != '') $locstring .= ' ';
154 $loca[$id] = $locname;
159 foreach ($types as $typename => $attributes) {
160 $id = $attributes['id'];
161 if ($typestring != '') $typestring .= ' ';
163 $typea[$id] = $attributes;
166 $res = $this->scan($locstring, $typestring, $zeroes, $url);
169 foreach ($loca as $id => $locname) {
170 $vals = explode(' ', $res["loc/$id"]);
172 foreach ($vals as $val) {
173 $val = explode(':', $val);
176 $attributes = $typea[$id];
177 $typename = $attributes['name'];
178 $min_precision = $attributes['min_precision'];
179 $scale = $attributes['scale'];
180 $vala[$typename] = $this->applyScale($value, $min_precision, $scale);
182 $resa[$locname] = $vala;
187 function applyScale($value, $min_precision, $scale) {
188 if ($value < 0) $value = bcadd($value, 1);
189 if ($scale > 0) $value = bcdiv($value, bcpow(10, $scale, 0), $scale);
191 $dotpos = strpos($value, '.');
194 while (substr($value, -1) == '0') {
195 $value = substr($value, 0, strlen($value)-1);
197 if (substr($value, -1) == '.') {
198 $value = substr($value, 0, strlen($value)-1);
203 if ($min_precision > 0) {
206 $dotpos = strlen($value);
208 $places = strlen($value) - $dotpos;
209 if ($min_precision > $places) {
210 $value .= str_repeat("0", $min_precision - $places);
216 function unscale($value, $scale) {
217 if ($scale >= 0) return bcmul($value, bcpow(10, $scale, 0), 0);
221 function buy_archive($loc, $usage, &$url) {
222 return $this->get(array('function' => 'archive',
229 function sell_archive($loc, $usage, &$url) {
230 return $this->get(array('function' => 'archive',
237 function touch_archive($loc, &$url) {
238 return $this->get(array('function' => 'archive',
244 function look_archive($hash, &$url) {
245 return $this->get(array('function' => 'archive',
251 function write_archive($loc, $usage, $content, &$url) {
252 return $this->get(array('function' => 'archive',
256 'content' => $content),
260 // Vending machines aren't implemented yet at Loom.cc,
262 // Returns four different machines for first byte of $id mod 4 being 0, 1, 2, or 3
263 // Machine id of all 0 is invalid.
264 function query_vend($id, $key, &$url) {
265 $res = array('function' => 'queryvend',
267 'status' => 'success');
269 $res['status'] = 'error';
272 $val = 3 & hexdec(substr($id, 0, 1));
273 $assets = $this->knownAssets();
274 $dollars = $assets['Cartwheel USD'];
275 $goldgrams = $assets['Patrick GoldGrams'];
276 $tokens = $assets['usage tokens'];
278 $res['input'] = $this->unscale(30, $dollars['scale']) . ':' .
279 $dollars['id'] . ' ' .
280 $this->unscale(15, $tokens['scale']) . ':' . $tokens['id'];
281 $res['output'] = $this->unscale(1, $goldgrams['scale']) . ':' .
283 } else if ($val == 1) {
284 $res['input'] = $this->unscale(1, $goldgrams['scale']) . ':' .
286 $res['output'] = $this->unscale(30, $dollars['scale']) . ':' .
288 } else if ($val == 2) {
289 $res['input'] = $this->unscale(1, $dollars['scale']) . ':' .
291 $res['output'] = $this->unscale(100, $tokens['scale']) . ':' .
294 $res['input'] = $this->unscale(100, $tokens['scale']) . ':' .
296 $res['output'] = $this->unscale(1, $dollars['scale']) . ':' .
302 // From https://loom.cc//?function=view&hash=db53fd011eb53ee9c4dbb5a88198cb3e34e1778feeaf3b2a118e619425ecbf6a
303 // Returns an array mapping the id and name of each known asset to
304 // an array mapping 'id', 'scale', 'precision', and 'name' to that asset's
306 function knownAssets() {
307 $curs = array(array('id' => '00000000000000000000000000000000',
310 'name' => 'usage tokens'),
311 array('id' => 'a67b4f153e85d7cc6c846893dac1155c',
314 'name' => 'Cartwheel USD'),
315 array('id' => '1650f617c024d6441461b2538c6d9540',
318 'name' => 'GoldNowBanc GoldGrams'),
319 array('id' => 'c9c5ccc9957c3c8c0bec320c16da451a',
322 'name' => 'GoldNowBanc USD'),
323 array('id' => '40c28abac43d0001fd2aa5b705929852',
326 'name' => 'MetroPipe Tunneler Pro Tokens 1 Month'),
327 array('id' => 'd43b8563ea999c1f53279b60142be5fe',
330 'name' => 'Capulin Currency $'),
331 array('id' => 'f0d4c8bd09e9c8c276af9ddd7c514495',
334 'name' => 'Wontongold Grams'),
335 array('id' => '26ef701a952fe3d641a69bf859db71c2',
338 'name' => 'Patrick GoldGrams'));
340 foreach ($curs as $cur) {
341 $res[$cur['id']] = $cur;
342 $res[$cur['name']] = $cur;
347 function url($prefix, $keys) {
350 foreach($keys as $key => $value) {
351 $str .= $delim . $key . '=' . urlencode($value);
357 function parseFolder($location, $folder) {
360 $paren_pos = strpos($folder, "(");
361 if ($paren_pos ||
$folder[0] == '(') {
362 $kv = substr($folder, $paren_pos);
364 $keys = $client->parsekv($kv);
365 $keytypes = explode(' ', $keys['list_type']);
367 foreach ($keytypes as $keytype) {
368 $type = array('name' => $keys["type_name.$keytype"],
370 'min_precision' => blankToZero($keys["type_min_precision.$keytype"]),
371 'scale' => blankToZero($keys["type_scale.$keytype"]));
372 //$types[$keytype] = $type;
373 $types[$type['name']] = $type;
376 $res['types'] = $types;
377 $keylocs = explode(' ', $keys['list_loc']);
379 foreach ($keylocs as $keyloc) {
380 $name = $keys["loc_name.$keyloc"];
382 if ($keyloc == $location) {
383 $folder_name = $name;
385 $locs[$name] = $keyloc;
386 //$locs[$keyloc] = $name;
390 $res['locs'] = $locs;
391 $res['name'] = $folder_name;
392 $res['loc'] = $location;
398 // Convert our array() representation of the folder
399 // into the string to write into the archive
400 // Not used. We let Loom itself munge the folder string.
401 function folderArchiveString($folder) {
402 $res = "Content-type: loom/folder\n\n(\n";
405 foreach ($folder['types'] as $name => $type) {
407 if ($ids != '') $ids .= ' ';
409 $min_precision = $type['min_precision'];
410 $scale = $type['scale'];
411 $types .= ":type_name.$id\n=" . $this->quote_cstring($name) . "\n";
412 if ($min_precision != '0') {
413 $types .= ":type_min_precision.$id\n=$min_precision\n";
416 $types .= ":type_scale.$id\n=$scale\n";
419 $res .= ":list_type\n=$ids\n";
424 foreach ($folder['locs'] as $name => $location) {
425 if ($ids != '') $ids .= ' ';
427 $locations .= ":loc_name.$location\n=" . $this->quote_cstring($name) . "\n";
429 $res .= ":list_loc\n=$ids\n";
437 // Return the session associated with a folder location
438 // Create a new session if one doesn't already exist,
439 // buying the location to store it as necessary.
440 // Returns false if it can't buy the session location
441 function folderSession($folder_location) {
442 $loc = $this->leftPadHex(bcxorhex($folder_location, "1"), 32);
443 $res = $this->touch_archive($loc, $url);
444 if ($res['status'] == 'success') {
445 return $res['content'];
447 $session = $this->random
->random_id();
448 $res = $this->buy_archive($session, $folder_location, $url);
449 if ($res['status'] != 'success') return false;
450 // Probably don't need this, but you never know
451 $this->buy_archive($loc, $folder_location, $url);
452 $res = $this->write_archive($loc, $folder_location, $session, $url);
453 if ($res['status'] != 'success') return false;
457 // This doesn't yet test that it worked.
458 // I'll wait for Patrick to make a KV-returning version,
459 // instead of attempting to parse the returned HTML
460 function renameFolderLocation($session, $oldname, $newname) {
461 return $this->rawget(array('function' => 'folder_locations',
462 'session' => $session,
463 'old_name' => $oldname,
464 'new_name' => $newname,
469 // This doesn't yet test that it worked.
470 // I'll wait for Patrick to make a KV-returning version,
471 // instead of attempting to parse the returned HTML
472 function newFolderLocation($session, $newname, $newlocation) {
473 return $this->rawget(array('function' => 'folder_locations',
474 'session' => $session,
475 'add_location' => '1',
476 'loc' => $newlocation,
482 // Logout from Loom, destroying the old session
483 function logout($session) {
484 return $this->rawget(array('function' => 'folder',
486 'session' => $session),
490 function parsekv($kv, $recursive=FALSE) {
491 $lines = explode("\n", $kv);
496 foreach ($lines as $line) {
498 //echo "$line<br>\n";
499 if ($first && ($line != '(')) {
500 return $res; // Could throw exception in PHP 5
504 if (!$recursive ||
$stackptr == 0) return $res;
506 $res = $stack[--$stackptr];
507 $key = $stack[--$stackptr];
509 //echo "popped: $stackptr<pre>\n"; print_r($res); echo "</pre>\n";
511 if (substr($line, 0, 1) == ':') $key = substr($line, 1);
512 elseif (substr($line, 0, 1) == '=') {
513 $value = substr($line, 1);
514 if ($recursive && $value == "(") {
516 $stack[$stackptr++
] = $key;
517 $stack[$stackptr++
] = $res;
518 //echo "pushed: $stackptr<br>\n";
521 $value = $this->unquote_cstring($value);
529 function array2kv($array, $res="(\n") {
530 foreach ($array as $key => $value) {
532 if (is_array($value)) $res = $this->array2kv($value, $res . "=(\n");
533 else $res .= "=" . $this->quote_cstring($value) . "\n";
539 function quote_cstring($cstring) {
541 for ($i=0; $i<strlen($cstring); $i++
) {
542 $chr = substr($cstring, $i, 1);
543 if ($chr == "\n") $res .= '\n';
544 elseif ($chr == '"') $res .= '\"';
545 elseif ($chr == "\t") $res .= '\t';
546 elseif ($chr == "\\") $res .= "\\\\";
547 elseif ($chr < ' ' ||
$chr > '~') $res .= '\\'.sprintf('%03o', ord($chr));
553 function unquote_cstring($cstring) {
555 $len = strlen($cstring);
558 $chr = substr($cstring, $i, 1);
565 $chr = substr($cstring, $i, 1);
566 if ($chr == 'n') $res .= "\n";
567 elseif ($chr == '"') $res .= '"';
568 elseif ($chr == 't') $res .= "\t";
569 elseif ($chr == "\\") $res .= "\\";
570 elseif ($chr >= '0' and $chr <= '9') {
571 if ($len < ($i +
3)) {
572 $res .= substr($cstring, $i-1);
575 sscanf(substr($cstring, $i, 3), '%o', $n);
579 else $res .= "\\" . $chr;
589 // Return the sha256 hash of a string.
590 // The result is encoded as hex, and guaranteed to be 64 charaacters,
591 // with leading zeroes added, if necessary.
592 function sha256($str) {
593 if (function_exists('hash_init')) {
595 $ctx = hash_init('sha256');
596 hash_update($ctx, $str);
597 $hash = hash_final($ctx);
598 } else if (function_exists('mhash')) {
599 // Old PHP with mhash compiled in
600 $hash = bin2hex(mhash(MHASH_SHA256
, $str));
602 // Not a hash, really, but the best we can do
603 $hash = bin2hex($str);
604 if (strlen($hash) > 64) $hash = substr($hash, 1, 64);
606 return $this->leftPadHex($hash, 64);
609 // PHP has bin2hex($x). An easier to remember name for pack("H*", $x)
610 // Note that this does NOT get you a string that looks like a decimal number.
611 // It's raw bits, 8 bits per characetr.
612 function hex2bin($x) {
613 return pack("H*", $x);
616 // Loom changes an SHA256 hash to a location by xoring the two halves
617 // Input and output are both encoded as hex
618 // Won't work correctly
619 function hash2location($hash) {
620 $value = bchexdec($hash);
621 $bits128 = $this->bits128
;
622 $location = bcxor(bcrightshift($value, 128), bcand($value, $bits128));
623 return $this->leftPadHex(bcdechex($location), 32);
626 function leftPadHex($hex, $chars) {
627 if (strlen($hex) < $chars) {
628 $hex = str_repeat("0", $chars - strlen($hex)) . $hex;
633 function xorLocations($l1, $l2) {
634 return $this->leftPadHex(bcxorhex($l1, $l2), 32);
637 // Returns true if $location is occupied for $type, i.e. if touch() will succeed
638 function isLocationOccuppied($type, $location) {
639 $res = $this->touch($type, $location, $url);
640 return $res['status'] == 'success';
643 // Returns true if $location is vacant for $type,
644 // i.e. if touch() fails with vacant
645 function isLocationVacant($type, $location) {
646 $res = $this->touch($type, $location, $url);
647 return ($res['status'] == 'fail') && ($res['error_loc'] == 'vacant');
650 // Return a random vacant location.
651 // If can't find one after 10 tries, return false.
652 function randomVacantLocation($type) {
653 for ($i=0; $i<10; $i++
) {
654 $id = $this->random
->random_id();
655 if ($this->isLocationVacant($type, $id)) return $id;
660 // Test that an ID is a valid 32-character hex string
661 function isValidID($id) {
662 return (strlen($id) == 32) && preg_match('/[a-f0-9]{32}/', $id);
665 } // End of LoomClient class
668 /* Testing code. Uncomment to run.
669 $api = new LoomClient();
670 $values = $api->move('12345678123456781234567812345678',
672 '22345678123456781234567812345678',
673 '32345678123456781234567812345678',
679 /* ***** BEGIN LICENSE BLOCK *****
680 * Version: MPL 1.1/GPL 2.0/LGPL 2.1/Apache 2.0
682 * The contents of this file are subject to the Mozilla Public License Version
683 * 1.1 (the "License"); you may not use this file except in compliance with
684 * the License. You may obtain a copy of the License at
685 * http://www.mozilla.org/MPL/
687 * Software distributed under the License is distributed on an "AS IS" basis,
688 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
689 * for the specific language governing rights and limitations under the
692 * The Original Code is LoomClient PHP library
694 * The Initial Developer of the Original Code is
696 * Portions created by the Initial Developer are Copyright (C) 2008
697 * the Initial Developer. All Rights Reserved.
700 * Bill St. Clair <bill@billstclair.com>
702 * Alternatively, the contents of this file may be used under the
703 * terms of the GNU General Public License Version 2 or later (the
704 * "GPL"), the GNU Lesser General Public License Version 2.1 or later
705 * (the "LGPL"), or The Apache License Version 2.0 (the "AL"), in
706 * which case the provisions of the GPL, LGPL, or AL are applicable
707 * instead of those above. If you wish to allow use of your version of
708 * this file only under the terms of the GPL, the LGPL, or the AL, and
709 * not to allow others to use your version of this file under the
710 * terms of the MPL, indicate your decision by deleting the provisions
711 * above and replace them with the notice and other provisions
712 * required by the GPL or the LGPL. If you do not delete the
713 * provisions above, a recipient may use your version of this file
714 * under the terms of any one of the MPL, the GPL the LGPL, or the AL.
715 ****** END LICENSE BLOCK ***** */