├── demo ├── templates │ ├── page.footer.php │ ├── page.header.php │ ├── list.php │ └── details.php ├── images │ └── logo.png ├── .gitignore ├── composer.json ├── README.md ├── css │ ├── styles.css │ └── reset.css ├── storage.php ├── setup.sql ├── index.php ├── setup.example.php └── worker.php ├── .gitignore ├── src ├── GalacticHorizon │ ├── XDRInputInterface.php │ ├── XDROutputInterface.php │ ├── composer.json │ ├── MathSafety.php │ ├── AccountSigner.php │ ├── DecoratedSignature.php │ ├── ChangeTrustOperation.php │ ├── CreateAccountOperationResult.php │ ├── TimeBounds.php │ ├── AllowTrustOperationResult.php │ ├── ChangeTrustOperationResult.php │ ├── CreateAccountOperation.php │ ├── AllowTrustOperation.php │ ├── PaymentOperationResult.php │ ├── OfferEntry.php │ ├── PaymentOperation.php │ ├── Checksum.php │ ├── OperationResult.php │ ├── AccountBalance.php │ ├── Price.php │ ├── ManageBuyOfferOperation.php │ ├── ManageSellOfferOperation.php │ ├── Amount.php │ ├── Asset.php │ ├── TransactionResult.php │ ├── Memo.php │ ├── lib.php │ ├── AddressableKey.php │ ├── Operation.php │ ├── ManageOfferOperationResult.php │ ├── XDRDecoder.php │ ├── Keypair.php │ ├── index.php │ ├── XDRBuffer.php │ ├── XDREncoder.php │ ├── Transaction.php │ ├── Client.php │ └── Account.php ├── phpdoc.xml ├── Profiler.php ├── HelperFunctions.php ├── Samples.php ├── Time.php ├── DataInterface.php ├── Settings.php ├── Implementation │ ├── CrashGuardBot.php │ └── MysqlDataInterface.php └── Trade.php ├── composer.json ├── LICENSE └── README.md /demo/templates/page.footer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | /vendor/ 4 | .DS_Store 5 | *.swo 6 | *.swp 7 | -------------------------------------------------------------------------------- /demo/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unwindnl/GalacticBot_PHP/HEAD/demo/images/logo.png -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | composer.json 2 | .DS_Store 3 | *.swo 4 | *.swp 5 | composer.lock 6 | worker.json 7 | vendor/ 8 | setup.php 9 | -------------------------------------------------------------------------------- /src/GalacticHorizon/XDRInputInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ../docs/parsed/api 5 | 6 | 7 | ../docs/api 8 | 9 | 10 | . 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unwind/galacticbot/demo", 3 | "type": "project", 4 | "authors": [ 5 | { 6 | "name": "Fabian Kranen", 7 | "email": "fabian@unwind.nl" 8 | } 9 | ], 10 | "require": { 11 | "unwindnl/galacticbot": "@dev", 12 | "cocur/background-process": "^0.7.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Profiler.php: -------------------------------------------------------------------------------- 1 | weight = $json->weight; 14 | 15 | $o->signer = Account::createFromPublicKey($json->key); 16 | 17 | return $o; 18 | } 19 | 20 | public function __tostring() { 21 | $str = []; 22 | $str[] = "(" . get_class($this) . ")"; 23 | $str[] = " - Weight = " . $this->weight; 24 | $str[] = " - Signer = "; 25 | $str[] = IncreaseDepth($this->signer); 26 | return implode("\n", $str); 27 | } 28 | 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unwindnl/galacticbot", 3 | "description": "Trade Bot Library for the Stellar platform.", 4 | "keywords": [ 5 | "trade bot", 6 | "library", 7 | "stellar" 8 | ], 9 | "homepage": "https://galacticbot.com/", 10 | "type": "library", 11 | "license": "ISC", 12 | "authors": [ 13 | { 14 | "name": "Fabian Kranen", 15 | "email": "fabian@unwind.nl", 16 | "homepage": "http://unwind.nl/" 17 | }, 18 | { 19 | "name": "Robert Kooij", 20 | "email": "robert@unwind.nl", 21 | "homepage": "http://unwind.nl/" 22 | } 23 | ], 24 | "require": { 25 | "php": "^7.1", 26 | "zulucrypto/stellar-api": "^0.6.3" 27 | }, 28 | "require-dev": {}, 29 | "suggest": {}, 30 | "autoload": { 31 | "psr-4": { 32 | "GalacticBot\\": "src" 33 | } 34 | }, 35 | "autoload-dev": {}, 36 | "scripts": {} 37 | } 38 | -------------------------------------------------------------------------------- /src/GalacticHorizon/DecoratedSignature.php: -------------------------------------------------------------------------------- 1 | hint = $hint; 14 | $this->signature = $signature; 15 | } 16 | 17 | public function toXDRBuffer(XDRBuffer &$buffer) { 18 | $buffer->writeOpaqueFixed($this->hint, 4); 19 | $buffer->writeOpaqueVariable($this->signature); 20 | } 21 | 22 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 23 | $hint = $buffer->readOpaqueFixed(4); 24 | $signature = $buffer->readOpaqueVariable(); 25 | 26 | $o = new self($hint, $signature); 27 | return $o; 28 | } 29 | 30 | public function hasValue() { return true; } 31 | 32 | public function __tostring() { 33 | return "(Signature) Hint: " . Base32::encode($this->hint); 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | This is a simple example interface to the example EMA bot implementation. You can this on any Linux machine with PHP, MySQL and Apache. This demo is a fully functional bot which can trade on the Stellar platform and also do simulations to see what settings would work best. 2 | 3 | # Installation 4 | 5 | - Copy all the files from this folder to an empty project folder. 6 | - Run ```composer install``` from the newly created folder to install the dependencies. 7 | - Create a MySQL database and a user. 8 | - Rename setup.example.php to setup.php 9 | - Fill in your Stellar account secrets and database settings (please create a separate Stellar account for each of your bots - you don't want a bot to make a mistake with all your XLM holdings) 10 | - Start running the bots with ```php worker.php checkstart``` 11 | - Make the folder available with apache or another HTTP web server and browse to the folder in your browser to setup and start your bot(s) 12 | -------------------------------------------------------------------------------- /src/GalacticHorizon/ChangeTrustOperation.php: -------------------------------------------------------------------------------- 1 | asset = $asset; 14 | } 15 | 16 | public function setLimit(Amount $limit) { 17 | $this->limit = $limit; 18 | } 19 | 20 | protected function extendXDRBuffer(XDRBuffer &$buffer) { 21 | $this->asset->toXDRBuffer($buffer); 22 | $this->limit->toXDRBuffer($buffer); 23 | } 24 | 25 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 26 | $o = new self(); 27 | 28 | $o->asset = Asset::fromXDRBuffer($buffer); 29 | $o->limit = Amount::fromXDRBuffer($buffer); 30 | 31 | return $o; 32 | } 33 | 34 | public function toString($depth) { 35 | $str = []; 36 | $str[] = "(" . get_class($this) . ")"; 37 | $str[] = $depth . "- Asset = " . $this->asset; 38 | $str[] = $depth . "- Limit = " . $this->limit; 39 | 40 | return implode("\n", $str); 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /demo/templates/page.header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GalacticBot - Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/GalacticHorizon/CreateAccountOperationResult.php: -------------------------------------------------------------------------------- 1 | errorCode = $buffer->readInteger(); 21 | return $result; 22 | } 23 | 24 | public function __tostring() { 25 | $str = []; 26 | $str[] = "(CreateAccountOperationResult) Error code = " . $this->errorCode; 27 | 28 | return implode("\n", $str); 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /demo/templates/list.php: -------------------------------------------------------------------------------- 1 | 2 |

Bot Overview

3 | 4 |

5 | This will show all bots defined in setup.php with their current state and holdings.
6 | Click on the 'Details' button for more information about a specific bot. 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 |
Bot #NameTypeStateTrading StateBase HoldingsCounter Holdings
getSettings()->getID()?>getSettings()->getName()?>getSettings()->getType()))?>getStateInfo()["label"]?>getTradeStateInfo()["label"]?>getCurrentBaseAssetBudget()?>getCurrentCounterAssetBudget()?> 29 | getLastProcessingTime()) { 31 | echo $bot->getLastProcessingTime()->format("Y-m-d H:i:s"); 32 | } 33 | ?> 34 | Details
39 | 40 | -------------------------------------------------------------------------------- /src/GalacticHorizon/TimeBounds.php: -------------------------------------------------------------------------------- 1 | writeUnsignedInteger64($this->minTimestamp); 15 | $buffer->writeUnsignedInteger64($this->maxTimestamp); 16 | } 17 | 18 | public function __tostring() { 19 | $min = $this->minTimestamp ? \DateTime::createFromFormat("U", $this->minTimestamp) : null; 20 | $max = $this->maxTimestamp ? \DateTime::createFromFormat("U", $this->maxTimestamp) : null; 21 | 22 | $min = $min ? $min->format("Y-m-d H:i:s") : "null"; 23 | $max = $max ? $max->format("Y-m-d H:i:s") : "null"; 24 | 25 | return $min . " - " . $max; 26 | } 27 | 28 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 29 | $o = new self(); 30 | $o->minTimestamp = $buffer->readUnsignedInteger64(); 31 | $o->maxTimestamp = $buffer->readUnsignedInteger64(); 32 | return $o; 33 | } 34 | 35 | public function hasValue() { 36 | return $this->minTimestamp !== null || $this->maxTimestamp !== null; 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/GalacticHorizon/AllowTrustOperationResult.php: -------------------------------------------------------------------------------- 1 | errorCode; } 22 | 23 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 24 | $result = new self(); 25 | $result->errorCode = $buffer->readInteger(); 26 | 27 | return $result; 28 | } 29 | 30 | public function __tostring() { 31 | $str = []; 32 | $str[] = "(" . get_class($this) . ")"; 33 | $str[] = "- Error code = " . $this->errorCode; 34 | 35 | return implode("\n", $str); 36 | } 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/GalacticHorizon/ChangeTrustOperationResult.php: -------------------------------------------------------------------------------- 1 | errorCode; } 22 | 23 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 24 | $result = new self(); 25 | $result->errorCode = $buffer->readInteger(); 26 | 27 | return $result; 28 | } 29 | 30 | public function __tostring() { 31 | $str = []; 32 | $str[] = "(" . get_class($this) . ")"; 33 | $str[] = "- Error code = " . $this->errorCode; 34 | 35 | return implode("\n", $str); 36 | } 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/GalacticHorizon/CreateAccountOperation.php: -------------------------------------------------------------------------------- 1 | destinationAccount = $destinationAccount; 14 | } 15 | 16 | public function setStartingBalance(Amount $startingBalance) { 17 | $this->startingBalance = $startingBalance; 18 | } 19 | 20 | protected function extendXDRBuffer(XDRBuffer &$buffer) { 21 | $this->destinationAccount->toXDRBuffer($buffer); 22 | $this->startingBalance->toXDRBuffer($buffer); 23 | } 24 | 25 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 26 | $o = new self(); 27 | $o->destinationAccount = Account::fromXDRBuffer($buffer); 28 | $o->startingBalance = Amount::fromXDRBuffer($buffer); 29 | 30 | return $o; 31 | } 32 | 33 | public function toString($depth) { 34 | $str = []; 35 | $str[] = $depth . "- Destination account = " . $this->destinationAccount; 36 | $str[] = $depth . "- Starting balance = " . $this->startingBalance; 37 | 38 | return implode("\n", $str); 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /demo/css/styles.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | background: white; 4 | color: black; 5 | } 6 | 7 | html, body, textarea, td, th, input, select, button 8 | { 9 | font-family: "IBM Plex Sans", helvetica, arial, sans-serif; 10 | font-size: 14px; 11 | line-height: 20px; 12 | } 13 | 14 | textarea, input, select, button 15 | { 16 | color: #141414; 17 | } 18 | 19 | td, th { 20 | padding-right: 10px; 21 | } 22 | 23 | h1, h2 { 24 | color: black; 25 | font-weight: bold; 26 | font-size: 24px; 27 | font-family: "IBM Plex Sans",helvetica,arial,sans-serif; 28 | margin-top: 20px; 29 | margin-bottom: 20px; 30 | } 31 | 32 | p { 33 | margin-bottom: 20px; 34 | } 35 | 36 | td, th { 37 | padding-bottom: 10px; 38 | } 39 | 40 | th { 41 | font-weight: bold; 42 | text-align: left; 43 | } 44 | 45 | input { 46 | background: #eee !important; 47 | color: #000 !important; 48 | border: 1px solid #ddd !important; 49 | padding: 4px !important; 50 | margin: 2px !important; 51 | line-height: 12px; 52 | height: auto; 53 | } 54 | 55 | .inner { 56 | margin-left: 20px; 57 | margin-top: 20px; 58 | } 59 | 60 | a.button { 61 | padding: 5px; 62 | background: #777; 63 | color: white; 64 | border-radius: 5px; 65 | text-decoration: none; 66 | } 67 | 68 | a.button:hover { 69 | background: #33f; 70 | color: #fff; 71 | } 72 | 73 | -------------------------------------------------------------------------------- /demo/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: none; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/GalacticHorizon/AllowTrustOperation.php: -------------------------------------------------------------------------------- 1 | asset = $asset; 14 | } 15 | 16 | public function setAuthorized(bool $authorize) { 17 | $this->authorize = $authorize; 18 | } 19 | 20 | protected function extendXDRBuffer(XDRBuffer &$buffer) { 21 | $this->asset->getIssuer()->toXDRBuffer($buffer); 22 | $this->asset->toXDRBuffer($buffer, false); 23 | $buffer->writeUnsignedInteger($this->authorize ? 1 : 0); 24 | } 25 | 26 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 27 | $o = new self(); 28 | 29 | $issuer = Account::fromXDRBuffer($buffer); 30 | 31 | $o->asset = Asset::fromXDRBuffer($buffer, false); 32 | $o->asset->setIssuer($issuer); 33 | 34 | $o->authorize = $buffer->readUnsignedInteger() == 1; 35 | 36 | return $o; 37 | } 38 | 39 | public function toString($depth) { 40 | $str = []; 41 | $str[] = "(" . get_class($this) . ")"; 42 | $str[] = $depth . "- Asset = " . $this->asset; 43 | $str[] = $depth . "- Authorize = " . ($this->authorize ? "true" : "false"); 44 | 45 | return implode("\n", $str); 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /demo/storage.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 14 | $this->data = []; 15 | } 16 | 17 | function lock() 18 | { 19 | if (!is_file($this->filename)) 20 | @touch($this->filename); 21 | 22 | $this->handle = fopen($this->filename, "r+"); 23 | 24 | if (!$this->handle) 25 | return false; 26 | 27 | if (flock($this->handle, LOCK_EX | LOCK_NB)) 28 | { 29 | $this->data = (Array)@json_decode(fread($this->handle, filesize($this->filename))); 30 | 31 | if (!$this->data) 32 | $this->data = []; 33 | 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | 40 | function get($name) 41 | { 42 | if (isset($this->data[$name])) 43 | return $this->data[$name]; 44 | 45 | return null; 46 | } 47 | 48 | function set($name, $value) 49 | { 50 | $this->data[$name] = $value; 51 | } 52 | 53 | function unlock() 54 | { 55 | ftruncate($this->handle, 0); 56 | 57 | fseek($this->handle, 0, SEEK_SET); 58 | 59 | fputs($this->handle, json_encode($this->data)); 60 | 61 | flock($this->handle, LOCK_UN); 62 | 63 | fclose($this->handle); 64 | } 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/GalacticHorizon/PaymentOperationResult.php: -------------------------------------------------------------------------------- 1 | errorCode = $buffer->readInteger(); 25 | return $result; 26 | } 27 | 28 | public function __tostring() { 29 | $str = []; 30 | $str[] = "(PaymentResult) Error code = " . $this->errorCode; 31 | 32 | return implode("\n", $str); 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/GalacticHorizon/OfferEntry.php: -------------------------------------------------------------------------------- 1 | seller = Account::fromXDRBuffer($buffer); 22 | $o->offerID = $buffer->readUnsignedInteger64(); 23 | 24 | $o->sellingAsset = Asset::fromXDRBuffer($buffer); 25 | $o->buyingAsset = Asset::fromXDRBuffer($buffer); 26 | 27 | $o->sellingAmount = Amount::fromXDRBuffer($buffer); 28 | 29 | $o->price = Price::fromXDRBuffer($buffer); 30 | 31 | return $o; 32 | } 33 | 34 | public function getOfferID() { return $this->offerID; } 35 | 36 | public function __toString() { 37 | $str = []; 38 | $str[] = "(" . get_class($this) . ")"; 39 | $str[] = "- Seller = " . $this->seller; 40 | $str[] = "- Selling = " . $this->sellingAsset; 41 | $str[] = "- Buying = " . $this->buyingAsset; 42 | $str[] = "- Selling amount = " . $this->sellingAmount; 43 | $str[] = "- Price = " . $this->price; 44 | $str[] = "- OfferID = " . ($this->offerID === null ? "null" : $this->offerID); 45 | 46 | return implode("\n", $str); 47 | } 48 | 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/GalacticHorizon/PaymentOperation.php: -------------------------------------------------------------------------------- 1 | destinationAccount = $destinationAccount; 15 | } 16 | 17 | public function setAsset(Asset $asset) { 18 | $this->asset = $asset; 19 | } 20 | 21 | public function setAmount(Amount $amount) { 22 | $this->amount = $amount; 23 | } 24 | 25 | protected function extendXDRBuffer(XDRBuffer &$buffer) { 26 | $this->destinationAccount->toXDRBuffer($buffer); 27 | $this->asset->toXDRBuffer($buffer); 28 | $this->amount->toXDRBuffer($buffer); 29 | } 30 | 31 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 32 | $o = new self(); 33 | $o->destinationAccount = Account::fromXDRBuffer($buffer); 34 | $o->asset = Asset::fromXDRBuffer($buffer); 35 | $o->amount = Amount::fromXDRBuffer($buffer); 36 | 37 | return $o; 38 | } 39 | 40 | public function toString($depth) { 41 | $str = []; 42 | $str[] = $depth . "- Destination account = " . $this->destinationAccount; 43 | $str[] = $depth . "- Asset = " . $this->asset; 44 | $str[] = $depth . "- Amount = " . $this->amount; 45 | 46 | return implode("\n", $str); 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Checksum.php: -------------------------------------------------------------------------------- 1 | > (7 - $i) & 1) == 1); 52 | $c15 = (($crc >> 15 & 1) == 1); 53 | $crc <<= 1; 54 | if ($c15 ^ $bit) $crc ^= $polynomial; 55 | } 56 | } 57 | 58 | return $crc & 0xffff; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/GalacticHorizon/OperationResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 12 | 13 | if ($errorCode != 0) { 14 | return FailedOperationResult::fromErrorCode($errorCode); 15 | } 16 | 17 | $type = $buffer->readInteger(); 18 | 19 | switch($type) { 20 | case Operation::TYPE_CREATE_ACCOUNT: return CreateAccountOperationResult::fromXDRBuffer($buffer); break; 21 | case Operation::TYPE_PAYMENT: return PaymentOperationResult::fromXDRBuffer($buffer); break; 22 | case Operation::TYPE_MANAGE_BUY_OFFER: return ManageOfferOperationResult::fromXDRBuffer($buffer); break; 23 | case Operation::TYPE_MANAGE_SELL_OFFER: return ManageOfferOperationResult::fromXDRBuffer($buffer); break; 24 | case Operation::TYPE_ALLOW_TRUST: return AllowTrustOperationResult::fromXDRBuffer($buffer); break; 25 | case Operation::TYPE_CHANGE_TRUST: return ChangeTrustOperationResult::fromXDRBuffer($buffer); break; 26 | 27 | default: 28 | throw \GalacticHorizon\Exception::create( 29 | \GalacticHorizon\Exception::TYPE_UNIMPLEMENTED_FEATURE, 30 | "OperationResult type with code '{$type}' isn't implemented yet in " . __FILE__ . "." 31 | ); 32 | break; 33 | } 34 | 35 | return NULL; 36 | } 37 | 38 | } 39 | 40 | class FailedOperationResult extends OperationResult { 41 | const BAD_AUTH = -1; // too few signatures or wrong network 42 | const NO_ACCOUNT = -2; // source account doesn't exist 43 | 44 | private $errorCode; 45 | 46 | static function fromResultCode($resultCode) { 47 | $o = new self(); 48 | $o->errorCode = $errorCode; 49 | return $o; 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/HelperFunctions.php: -------------------------------------------------------------------------------- 1 | = 0.01) 15 | return 1; 16 | else if ($delta <= -0.01) 17 | return -1; 18 | else 19 | return 0; 20 | } 21 | 22 | /* 23 | * Determines the differences between each entry in an array 24 | * @return Array 25 | */ 26 | function array_deltas(Array $ar) { 27 | $lastV = null; 28 | $deltas = []; 29 | 30 | foreach($ar AS $i => $v) { 31 | if ($i > 0) { 32 | $deltas[] = $v - $lastV; 33 | } 34 | 35 | $lastV = $v; 36 | } 37 | 38 | return $deltas; 39 | } 40 | 41 | /* 42 | * Determines the average of all values in an array 43 | * @return float 44 | */ 45 | function array_average($a) { 46 | if (count($a) == 0) 47 | return null; 48 | 49 | return array_sum($a) / count($a); 50 | } 51 | 52 | /* 53 | * Makes sure all values in an array are scaled to a 0..1 range 54 | * @return Array 55 | */ 56 | function array_normalize(Array $ar) { 57 | $max = null; 58 | 59 | foreach($ar as $v) { 60 | $v = abs($v); 61 | 62 | if ($max == null) { 63 | $max = $v; 64 | } else { 65 | $max = max($max, $v); 66 | } 67 | } 68 | 69 | if ($max > 0) 70 | foreach($ar as $i => $v) 71 | $ar[$i] = $v / $max; 72 | 73 | return $ar; 74 | } 75 | 76 | /* 77 | * Tries to determine the direction the prediction is going 78 | * @return float 79 | */ 80 | function forecast_direction(Array $real, Array $predicted, $windowSize = 60, $windowSizePredicted = 15) { 81 | $real = array_splice($real, -$windowSize); 82 | $predicted = array_splice($predicted, 0, $windowSizePredicted); 83 | $samples = array_merge($real, $predicted); 84 | 85 | return array_direction($samples); 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/GalacticHorizon/AccountBalance.php: -------------------------------------------------------------------------------- 1 | balance; } 18 | 19 | public function getLimit() { return $this->limit; } 20 | 21 | public function getBuyingLiabilities() { return $this->buyingLiabilities; } 22 | 23 | public function getSellingLiabilities() { return $this->sellingLiabilities; } 24 | 25 | public function getLastModifiedLedger() { return $this->lastModifiedLedger; } 26 | 27 | public function getAsset() { return $this->asset; } 28 | 29 | public static function fromJSON(object $json) { 30 | $o = new self(); 31 | 32 | $o->balance = Amount::createFromFloat($json->balance); 33 | $o->limit = isset($json->limit) ? Amount::createFromFloat($json->limit) : null; 34 | 35 | $o->lastModifiedLedger = isset($json->last_modified_ledger) ? $json->last_modified_ledger : null; 36 | 37 | $o->buyingLiabilities = Amount::createFromFloat($json->buying_liabilities); 38 | $o->sellingLiabilities = Amount::createFromFloat($json->selling_liabilities); 39 | 40 | if ($json->asset_type == "native") { 41 | $o->asset = Asset::createNative(); 42 | } else { 43 | $o->asset = Asset::createFromCodeAndIssuer($json->asset_code, Account::createFromPublicKey($json->asset_issuer)); 44 | } 45 | 46 | return $o; 47 | } 48 | 49 | public function __tostring() { 50 | $str = []; 51 | $str[] = "(" . get_class($this) . ")"; 52 | $str[] = " - Asset = " . $this->asset; 53 | $str[] = " - Balance = " . $this->balance; 54 | $str[] = " - Limit = " . $this->limit; 55 | $str[] = " - Buying liabilities = " . $this->buyingLiabilities; 56 | $str[] = " - Selling liabilities = " . $this->sellingLiabilities; 57 | $str[] = " - Last modified ledger = " . $this->lastModifiedLedger; 58 | return implode("\n", $str); 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Price.php: -------------------------------------------------------------------------------- 1 | numerator = $numerator; 15 | $this->denominator = $denominator; 16 | } 17 | 18 | static public function createFromFloat(float $value) { 19 | $price = self::float2Rat($value); 20 | 21 | $o = new self($price[0], $price[1]); 22 | return $o; 23 | } 24 | 25 | public function isValid() { 26 | return !(is_nan($this->numerator) || is_nan($this->denominator) || is_infinite($this->numerator) || is_infinite($this->denominator)); 27 | } 28 | 29 | public function toFloat() { 30 | return $this->numerator / $this->denominator; 31 | } 32 | 33 | public function getNumerator() { 34 | return $this->numerator; 35 | } 36 | 37 | public function getDenominator() { 38 | return $this->denominator; 39 | } 40 | 41 | public function toXDRBuffer(XDRBuffer &$buffer) { 42 | $buffer->writeUnsignedInteger($this->numerator); 43 | $buffer->writeUnsignedInteger($this->denominator); 44 | } 45 | 46 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 47 | $o = new self($buffer->readUnsignedInteger(), $buffer->readUnsignedInteger()); 48 | return $o; 49 | } 50 | 51 | public function hasValue() { return true; } 52 | 53 | public function __toString() { 54 | return "(Price) {$this->numerator} / {$this->denominator} (" . $this->toFloat() . ")"; 55 | } 56 | 57 | // Source: http://jonisalonen.com/2012/converting-decimal-numbers-to-ratios/ 58 | static function float2Rat($n, $tolerance = 1.e-6) { 59 | if ($n <= 0) 60 | return [0, 0]; 61 | 62 | $h1=1; $h2=0; 63 | $k1=0; $k2=1; 64 | $b = 1/$n; 65 | 66 | do { 67 | $b = 1/$b; 68 | $a = floor($b); 69 | $aux = $h1; $h1 = $a*$h1+$h2; $h2 = $aux; 70 | $aux = $k1; $k1 = $a*$k1+$k2; $k2 = $aux; 71 | $b = $b-$a; 72 | } while (abs($n-$h1/$k1) > $n*$tolerance); 73 | 74 | return [$h1, $k1]; 75 | } 76 | 77 | } 78 | 79 | -------------------------------------------------------------------------------- /demo/setup.sql: -------------------------------------------------------------------------------- 1 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 2 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 3 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 4 | /*!40101 SET NAMES utf8 */; 5 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 6 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 7 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 8 | 9 | CREATE TABLE `BotData` ( 10 | `botID` varchar(255) NOT NULL, 11 | `name` varchar(64) NOT NULL, 12 | `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', 13 | `value` text, 14 | PRIMARY KEY (`botID`,`name`,`date`), 15 | KEY `search` (`botID`,`name`), 16 | KEY `forBot` (`botID`) 17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 18 | 19 | CREATE TABLE `BotTrade` ( 20 | `ID` int(11) unsigned NOT NULL AUTO_INCREMENT, 21 | `botID` varchar(255) NOT NULL, 22 | `previousBotTradeID` int(11) unsigned DEFAULT NULL, 23 | `type` varchar(64) DEFAULT NULL, 24 | `state` varchar(32) NOT NULL, 25 | `offerID` varchar(255) DEFAULT NULL, 26 | `transactionEnvelopeXdr` text, 27 | `claimedOffers` text, 28 | `sellAmount` double DEFAULT NULL, 29 | `spentAmount` double DEFAULT NULL, 30 | `amountRemaining` double DEFAULT NULL, 31 | `boughtAmount` double DEFAULT NULL, 32 | `price` double DEFAULT NULL, 33 | `priceN` double DEFAULT NULL, 34 | `priceD` double DEFAULT NULL, 35 | `paidPrice` double DEFAULT NULL, 36 | `fee` double DEFAULT NULL, 37 | `stateData` double DEFAULT NULL, 38 | `fillPercentage` double DEFAULT NULL, 39 | `createdAt` datetime NOT NULL, 40 | `processedAt` datetime NOT NULL, 41 | `updatedAt` datetime DEFAULT NULL, 42 | PRIMARY KEY (`ID`), 43 | KEY `search` (`botID`,`processedAt`), 44 | KEY `forBot` (`botID`) 45 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 46 | 47 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 48 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 49 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 50 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 51 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 52 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 53 | -------------------------------------------------------------------------------- /src/GalacticHorizon/ManageBuyOfferOperation.php: -------------------------------------------------------------------------------- 1 | sellingAsset = $sellingAsset; 20 | } 21 | 22 | public function getSellingAsset() { return $this->sellingAsset; } 23 | 24 | public function setBuyingAsset(Asset $buyingAsset) { 25 | $this->buyingAsset = $buyingAsset; 26 | } 27 | 28 | public function getBuyingAsset() { return $this->buyingAsset; } 29 | 30 | public function getBuyAmount() { return $this->buyAmount; } 31 | 32 | public function setBuyAmount(Amount $buyAmount) { 33 | $this->buyAmount = $buyAmount; 34 | } 35 | 36 | public function getPrice() { return $this->price; } 37 | 38 | public function setPrice(Price $price) { 39 | $this->price = $price; 40 | } 41 | 42 | public function setOfferID(string $offerID = null) { 43 | $this->offerID = $offerID; 44 | } 45 | 46 | protected function extendXDRBuffer(XDRBuffer &$buffer) { 47 | $this->sellingAsset->toXDRBuffer($buffer); 48 | $this->buyingAsset->toXDRBuffer($buffer); 49 | 50 | $this->buyAmount->toXDRBuffer($buffer); 51 | 52 | $this->price->toXDRBuffer($buffer); 53 | 54 | $buffer->writeUnsignedInteger64($this->offerID); 55 | } 56 | 57 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 58 | $o = new self(); 59 | 60 | $o->sellingAsset = Asset::fromXDRBuffer($buffer); 61 | $o->buyingAsset = Asset::fromXDRBuffer($buffer); 62 | 63 | $o->buyAmount = Amount::fromXDRBuffer($buffer); 64 | 65 | $o->price = Price::fromXDRBuffer($buffer); 66 | 67 | $o->offerID = $buffer->readUnsignedInteger64(); 68 | 69 | return $o; 70 | } 71 | 72 | public function toString($depth) { 73 | $str = []; 74 | $str[] = "(" . get_class($this) . ")"; 75 | $str[] = $depth . "- Selling = " . $this->sellingAsset; 76 | $str[] = $depth . "- Buying = " . $this->buyingAsset; 77 | $str[] = $depth . "- Buying amount = " . $this->buyAmount; 78 | $str[] = $depth . "- Price = " . $this->price; 79 | $str[] = $depth . "- OfferID = " . ($this->offerID === null ? "null" : $this->offerID); 80 | 81 | return implode("\n", $str); 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/GalacticHorizon/ManageSellOfferOperation.php: -------------------------------------------------------------------------------- 1 | sellingAsset = $sellingAsset; 20 | } 21 | 22 | public function getSellingAsset() { return $this->sellingAsset; } 23 | 24 | public function setBuyingAsset(Asset $buyingAsset) { 25 | $this->buyingAsset = $buyingAsset; 26 | } 27 | 28 | public function getBuyingAsset() { return $this->buyingAsset; } 29 | 30 | public function getSellAmount() { return $this->sellAmount; } 31 | 32 | public function setSellAmount(Amount $sellAmount) { 33 | $this->sellAmount = $sellAmount; 34 | } 35 | 36 | public function getPrice() { return $this->price; } 37 | 38 | public function setPrice(Price $price) { 39 | $this->price = $price; 40 | } 41 | 42 | public function setOfferID(string $offerID = null) { 43 | $this->offerID = $offerID; 44 | } 45 | 46 | protected function extendXDRBuffer(XDRBuffer &$buffer) { 47 | $this->sellingAsset->toXDRBuffer($buffer); 48 | $this->buyingAsset->toXDRBuffer($buffer); 49 | 50 | $this->sellAmount->toXDRBuffer($buffer); 51 | 52 | $this->price->toXDRBuffer($buffer); 53 | 54 | $buffer->writeUnsignedInteger64($this->offerID); 55 | } 56 | 57 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 58 | $o = new self(); 59 | 60 | $o->sellingAsset = Asset::fromXDRBuffer($buffer); 61 | $o->buyingAsset = Asset::fromXDRBuffer($buffer); 62 | 63 | $o->sellAmount = Amount::fromXDRBuffer($buffer); 64 | 65 | $o->price = Price::fromXDRBuffer($buffer); 66 | 67 | $o->offerID = $buffer->readUnsignedInteger64(); 68 | 69 | return $o; 70 | } 71 | 72 | public function toString($depth) { 73 | $str = []; 74 | $str[] = "(" . get_class($this) . ")"; 75 | $str[] = $depth . "- Selling = " . $this->sellingAsset; 76 | $str[] = $depth . "- Buying = " . $this->buyingAsset; 77 | $str[] = $depth . "- Selling amount = " . $this->sellAmount; 78 | $str[] = $depth . "- Price = " . $this->price; 79 | $str[] = $depth . "- OfferID = " . ($this->offerID === null ? "null" : $this->offerID); 80 | 81 | return implode("\n", $str); 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Amount.php: -------------------------------------------------------------------------------- 1 | value = new BigInteger(0); 16 | } 17 | 18 | static public function createFromString(string $value) { 19 | $scale = new BigInteger(static::STROOP_SCALE); 20 | 21 | $o = new self(); 22 | $o->value = new BigInteger($value); 23 | $o->value->divide($scale); 24 | 25 | return $o; 26 | } 27 | 28 | static public function createFromFloat(float $value) { 29 | $value = (float)number_format($value, 7, '.', ''); 30 | $parts = explode('.', $value); 31 | $unscaledAmount = new BigInteger(0); 32 | 33 | // Everything to the left of the decimal point 34 | if ($parts[0]) { 35 | $unscaledAmountLeft = (new BigInteger($parts[0]))->multiply(new BigInteger(static::STROOP_SCALE)); 36 | $unscaledAmount = $unscaledAmount->add($unscaledAmountLeft); 37 | } 38 | 39 | // Add everything to the right of the decimal point 40 | if (count($parts) == 2 && str_replace('0', '', $parts[1]) != '') { 41 | // Should be a total of 7 decimal digits to the right of the decimal 42 | $unscaledAmountRight = str_pad($parts[1], 7, '0',STR_PAD_RIGHT); 43 | $unscaledAmount = $unscaledAmount->add(new BigInteger($unscaledAmountRight)); 44 | } 45 | 46 | $scale = new BigInteger(static::STROOP_SCALE); 47 | 48 | $o = new self(); 49 | $o->value = $unscaledAmount; 50 | 51 | return $o; 52 | } 53 | 54 | public function toBigInteger() { 55 | return $this->value; 56 | } 57 | 58 | public function toString() { 59 | return $this->value->toString(); 60 | } 61 | 62 | public function toFloat() { 63 | $scale = new BigInteger(static::STROOP_SCALE); 64 | 65 | list($quotient, $remainder) = $this->value->divide($scale); 66 | 67 | $number = intval($quotient->toString()) + (intval($remainder->toString()) / intval($scale->toString())); 68 | 69 | return (float)number_format($number, 7, '.', ''); 70 | } 71 | 72 | public function toXDRBuffer(XDRBuffer &$buffer) { 73 | $buffer->writeSignedBigInteger64($this->value); 74 | } 75 | 76 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 77 | $o = new self(); 78 | $o->value = new BigInteger($buffer->readUnsignedInteger64($buffer)); 79 | return $o; 80 | } 81 | 82 | public function hasValue() { return true; } 83 | 84 | public function __tostring() { 85 | return "(Amount) " . $this->toFloat(); 86 | } 87 | 88 | } 89 | 90 | -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | All actions are disabled for this live demo."; 39 | } 40 | } 41 | else if ($action == "update-settings") 42 | { 43 | // Quick and dirty way of updating settings without any checks on the values! 44 | foreach($_POST AS $k => $v) 45 | { 46 | if ($bot->getDataInterface()->isSetting($k)) 47 | { 48 | $v = (float)$v; 49 | 50 | $bot->getDataInterface()->setSetting($k, $v); 51 | } 52 | } 53 | 54 | echo "Settings updated."; 55 | } 56 | else if ($action == "start") 57 | { 58 | echo "Action performed."; 59 | $bot->start(); 60 | } 61 | else if ($action == "pause") 62 | { 63 | echo "Action performed."; 64 | $bot->pause(); 65 | } 66 | else if ($action == "stop") 67 | { 68 | echo "Action performed."; 69 | $bot->stop(); 70 | } 71 | else if ($action == "simulation-reset") 72 | { 73 | echo "Action performed."; 74 | $bot->simulationReset(); 75 | } 76 | else if ($action) 77 | { 78 | echo "Unknown action '$action'."; 79 | } 80 | 81 | include_once "templates/details.php"; 82 | } 83 | else 84 | { 85 | echo "No bot found with this ID."; 86 | } 87 | } 88 | else 89 | { 90 | // Show a list of all bots (from the $bots array which were setup in setup.php) 91 | include_once "templates/list.php"; 92 | } 93 | 94 | // Page footer, and we're done! 95 | include_once "templates/page.footer.php"; 96 | 97 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Asset.php: -------------------------------------------------------------------------------- 1 | type = $type; 17 | $this->code = $code; 18 | $this->issuer = $issuer; 19 | } 20 | 21 | public function setIssuer(Account $issuer) { 22 | $this->issuer = $issuer; 23 | } 24 | 25 | public function getIssuer() { return $this->issuer; } 26 | 27 | public function getCode() { return $this->code; } 28 | 29 | public function getType() { return $this->type; } 30 | 31 | public function isNative() { return $this->type == self::TYPE_NATIVE; } 32 | 33 | public static function createNative() { 34 | return new self(self::TYPE_NATIVE); 35 | } 36 | 37 | public static function createFromCodeAndIssuer(string $code, Account $issuer) { 38 | return new self( 39 | strlen($code) <= 4 ? self::TYPE_ALPHANUM_4 : self::TYPE_ALPHANUM_12, 40 | $code, 41 | $issuer 42 | ); 43 | } 44 | 45 | public function toXDRBuffer(XDRBuffer &$buffer, $includeIssuer = true) { 46 | $buffer->writeUnsignedInteger($this->type); 47 | 48 | if ($this->type == self::TYPE_NATIVE) { 49 | // no additional content for native types 50 | } else if ($this->type == self::TYPE_ALPHANUM_4) { 51 | $buffer->writeOpaqueFixed($this->code, 4, true); 52 | 53 | if ($includeIssuer) 54 | $this->issuer->toXDRBuffer($buffer); 55 | } elseif ($this->type == self::TYPE_ALPHANUM_12) { 56 | $buffer->writeOpaqueFixed($this->code, 12, true); 57 | 58 | if ($includeIssuer) 59 | $this->issuer->toXDRBuffer($buffer); 60 | } 61 | } 62 | 63 | public static function fromXDRBuffer(XDRBuffer &$buffer, $includeIssuer = true) { 64 | $type = $buffer->readUnsignedInteger($buffer); 65 | 66 | $o = new self($type); 67 | 68 | switch($o->type) { 69 | case self::TYPE_NATIVE: 70 | break; 71 | case self::TYPE_ALPHANUM_4: 72 | $o->code = $buffer->readOpaqueFixed(4, true); 73 | 74 | if ($includeIssuer) 75 | $o->issuer = Account::fromXDRBuffer($buffer); 76 | break; 77 | case self::TYPE_ALPHANUM_12: 78 | $o->code = $buffer->readOpaqueFixed(12, true); 79 | 80 | if ($includeIssuer) 81 | $o->issuer = Account::fromXDRBuffer($buffer); 82 | break; 83 | } 84 | 85 | return $o; 86 | } 87 | 88 | public function hasValue() { return true; } 89 | 90 | public function __tostring() { 91 | if ($this->type == self::TYPE_NATIVE) 92 | return "(Asset) XLM"; 93 | else 94 | return "(Asset) " . $this->code . " (Issuer: " . ($this->issuer && $this->issuer->getKeypair() ? $this->issuer->getKeypair()->getPublicKey() : "null") .")"; 95 | } 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/GalacticHorizon/TransactionResult.php: -------------------------------------------------------------------------------- 1 | hash = $hash; 35 | } 36 | 37 | public function setLedger($ledger) { 38 | $this->ledger = $ledger; 39 | } 40 | 41 | public function setEnvelopeXDRString($envelopeXDRString) { 42 | $this->envelopeXDRString = $envelopeXDRString; 43 | } 44 | 45 | public function getHash() { return $this->hash; } 46 | 47 | public function getErrorCode() { return $this->errorCode; } 48 | 49 | public function getFeeCharged() { return $this->feeCharged; } 50 | 51 | public function getResultCount() { return count($this->results); } 52 | public function getResult(int $index) { return $this->results[$index]; } 53 | 54 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 55 | $result = new self(); 56 | 57 | $result->feeCharged = Amount::fromXDRBuffer($buffer); 58 | $result->errorCode = $buffer->readInteger(); 59 | 60 | if ($result->errorCode == self::TX_SUCCESS || $result->errorCode == self::TX_FAILED) { 61 | $resultCount = $buffer->readUnsignedInteger(); 62 | 63 | for($i=0; $i<$resultCount; $i++) 64 | $result->results[] = OperationResult::fromXDRBuffer($buffer); 65 | } 66 | 67 | return $result; 68 | } 69 | 70 | public function __tostring() { 71 | $str = []; 72 | $str[] = "(" . get_class($this) . ")"; 73 | $str[] = "- Hash = " . $this->hash; 74 | $str[] = "- Free charged = " . $this->feeCharged->toString(); 75 | $str[] = "- Error code = " . $this->errorCode; 76 | $str[] = "* Results (" . count($this->results) . ")"; 77 | 78 | foreach($this->results AS $result) 79 | $str[] = IncreaseDepth($result); 80 | 81 | return implode("\n", $str); 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /demo/setup.example.php: -------------------------------------------------------------------------------- 1 | 1, 21 | 22 | // Type of bot (SETTING_TYPE_LIVE or SETTING_TYPE_SIMULATION) 23 | "type" => GalacticBot\Bot::SETTING_TYPE_LIVE, 24 | "API" => GalacticBot\StellarAPI::getPublicAPI(), 25 | 26 | // Name so we can tell bots apart in the webinterface 27 | "name" => "Live Bot", 28 | 29 | // Source or base asset, this will normally be the native (XLM) asset 30 | "baseAsset" => ZuluCrypto\StellarSdk\XdrModel\Asset::newNativeAsset(), 31 | 32 | // Amount of base asset the bot can't 'touch' 33 | "baseAssetReservationAmount" => 0, 34 | 35 | // Asset we want to trade with 36 | "counterAsset" => ZuluCrypto\StellarSdk\XdrModel\Asset::newCustomAsset("MOBI", "GA6HCMBLTZS5VYYBCATRBRZ3BZJMAFUDKYYF6AH6MVCMGWMRDNSWJPIH"), 37 | 38 | // The Stellar account secret - in this case this is on the PUBLIC network 39 | // as this is a live bot 40 | "accountSecret" => PASTE_YOUR_PUBLIC_ACCOUNT_SECRET_HERE 41 | ) 42 | ); 43 | 44 | // This is the configuration of the simulation bot 45 | // Please see the notes above for the live bot on how to configure this 46 | $simulationSettings = new GalacticBot\Settings( 47 | new GalacticBot\Implementation\MysqlDataInterface( 48 | _DATABASE_HOST, 49 | _DATABASE_USER, 50 | _DATABASE_PASS, 51 | _DATABASE_NAME 52 | ), 53 | Array( 54 | "ID" => 2, 55 | "type" => GalacticBot\Bot::SETTING_TYPE_SIMULATION, 56 | "API" => GalacticBot\StellarAPI::getTestNetAPI(), 57 | 58 | "name" => "Simulation Bot", 59 | 60 | "baseAsset" => ZuluCrypto\StellarSdk\XdrModel\Asset::newNativeAsset(), 61 | 62 | "counterAsset" => ZuluCrypto\StellarSdk\XdrModel\Asset::newCustomAsset("MOBI", "GA6HCMBLTZS5VYYBCATRBRZ3BZJMAFUDKYYF6AH6MVCMGWMRDNSWJPIH"), 63 | 64 | "accountSecret" => PASTE_YOUR_TESTNET_ACCOUNT_SECRET_HERE 65 | ) 66 | ); 67 | 68 | // We'll create an list for all the bots we are working with 69 | $bots = []; 70 | 71 | // Add the live bot (ID: #1) to the bot list 72 | $bots[$liveSettings->getID()] = new GalacticBot\Implementation\EMABot($liveSettings); 73 | 74 | // Add the simulation bot (ID: #2) to the bot list 75 | $bots[$simulationSettings->getID()] = new GalacticBot\Implementation\EMABot($simulationSettings); 76 | 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## Galactic Bot Licence 2 | 3 | ISC License 4 | 5 | Copyright (c) 2018 Unwind Creative Technology 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 10 | 11 | ### stellar-api licence 12 | https://github.com/zulucrypto/stellar-api 13 | 14 | MIT License 15 | 16 | Copyright (c) 2017 ZuluCrypto 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. 35 | 36 | ### background-process licence 37 | https://github.com/cocur/background-process/ 38 | 39 | The MIT License (MIT) 40 | 41 | Copyright (c) 2013 Florian Eckerstorfer 42 | 43 | Permission is hereby granted, free of charge, to any person obtaining a copy 44 | of this software and associated documentation files (the "Software"), to deal 45 | in the Software without restriction, including without limitation the rights 46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 47 | copies of the Software, and to permit persons to whom the Software is 48 | furnished to do so, subject to the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be included in 51 | all copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 56 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 59 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Samples.php: -------------------------------------------------------------------------------- 1 | maxLength = $maxLength; 22 | $this->samples = $samples; 23 | } 24 | 25 | /* 26 | * Adds an sample to the buffer and caps the buffer if it's getting too long 27 | */ 28 | function add($value) { 29 | $this->samples[] = $value; 30 | 31 | $this->samples = array_slice($this->samples, -$this->maxLength); 32 | } 33 | 34 | /* 35 | * Returns the maximum length of this buffer 36 | */ 37 | function getMaxLength() 38 | { 39 | return $this->maxLength; 40 | } 41 | 42 | /* 43 | * Returns the current length of this buffer 44 | */ 45 | function getLength() 46 | { 47 | return count($this->samples); 48 | } 49 | 50 | /* 51 | * Changes the maximum length of this buffer 52 | */ 53 | function setMaxLength($maxLength) 54 | { 55 | $this->maxLength = $maxLength; 56 | } 57 | 58 | /* 59 | * Returns the sample buffer array 60 | * @return Array 61 | */ 62 | function getArray() 63 | { 64 | return $this->samples; 65 | } 66 | 67 | /* 68 | * Clears the sample buffer 69 | */ 70 | function clear() 71 | { 72 | $this->samples = []; 73 | } 74 | 75 | /* 76 | * Fills the sample buffer with a specific value 77 | */ 78 | function fillWithValue($value) 79 | { 80 | $this->samples = []; 81 | 82 | for($i=0; $i<$this->maxLength; $i++) 83 | { 84 | $this->samples[] = $value; 85 | } 86 | } 87 | 88 | /* 89 | * Checks to see if the buffer is full or not 90 | * @return bool 91 | */ 92 | function getIsBufferFull() 93 | { 94 | return count($this->samples) == $this->maxLength; 95 | } 96 | 97 | /* 98 | * Returns the average value of all values in the buffer 99 | * @return float or false when no data is present 100 | */ 101 | function getAverage() { 102 | if (!count($this->samples)) 103 | return false; 104 | 105 | $sum = 0; 106 | 107 | foreach($this->samples AS $s) 108 | $sum += $s; 109 | 110 | return $sum / count($this->samples); 111 | } 112 | 113 | /** 114 | * Exponential moving average (EMA) - Source: https://github.com/markrogoyski/math-php/blob/master/src/Statistics/Average.php 115 | * 116 | * The start of the EPA is seeded with the first data point. 117 | * Then each day after that: 118 | * EMAtoday = α⋅xtoday + (1-α)EMAyesterday 119 | * 120 | * where 121 | * α: coefficient that represents the degree of weighting decrease, a constant smoothing factor between 0 and 1. 122 | * 123 | * @return Array of exponential moving averages 124 | */ 125 | function getExponentialMovingAverage() { 126 | $m = count($this->samples); 127 | 128 | if ($m == 0) 129 | return null; 130 | 131 | $n = count($this->samples)/2; 132 | $α = 2 / ($n + 1); 133 | $EMA = []; 134 | 135 | // Start off by seeding with the first data point 136 | $EMA[] = $this->samples[0]; 137 | 138 | // Each day after: EMAtoday = α⋅xtoday + (1-α)EMAyesterday 139 | for ($i = 1; $i < $m; $i++) { 140 | $EMA[] = ($α * $this->samples[$i]) + ((1 - $α) * $EMA[$i - 1]); 141 | } 142 | return array_pop($EMA); 143 | } 144 | 145 | } 146 | 147 | -------------------------------------------------------------------------------- /demo/worker.php: -------------------------------------------------------------------------------- 1 | lock()) 60 | exitWithError("Cannot lock 'worker.json'. Maybe it's already used by another process?", false); 61 | 62 | foreach($bots AS $bot) 63 | { 64 | $botID = $bot->getSettings()->getID(); 65 | 66 | $PID = $storage->get("PID_" . $botID); 67 | 68 | $process = null; 69 | 70 | if ($PID) 71 | { 72 | $process = BackgroundProcess::createFromPID($PID); 73 | 74 | if ($process->isRunning()) 75 | { 76 | echo " - Bot #{$botID} (PID: {$PID}) is running\n"; 77 | 78 | if ($shouldStop) 79 | { 80 | echo " - Stopping bot\n"; 81 | 82 | $process->stop(); 83 | } 84 | } 85 | else 86 | { 87 | echo " - Bot #{$botID} (PID: {$PID}) has stopped running\n"; 88 | 89 | $process = null; 90 | 91 | $storage->set("PID_" . $botID, null); 92 | } 93 | } 94 | 95 | if (!$process && !$shouldStop) 96 | { 97 | $command = $_SERVER["_"] . " " . $scriptName . " run " . $botID . " 2>/dev/null"; 98 | 99 | $process = new BackgroundProcess($command); 100 | $process->run(); 101 | 102 | $PID = $process->getPid(); 103 | 104 | echo " - Started bot #{$botID} (PID: {$PID})\n"; 105 | 106 | $storage->set("PID_" . $botID, $PID); 107 | } 108 | } 109 | 110 | $storage->unlock(); 111 | break; 112 | 113 | case "run": 114 | $ID = array_shift($argv); 115 | 116 | // Todo: also check from here if bot is running 117 | 118 | echo "Starting bot #{$ID}\n"; 119 | 120 | if (!isset($bots[$ID])) 121 | exitWithError("Unknown bot with ID #{$ID}."); 122 | 123 | $bots[$ID]->work(); 124 | break; 125 | 126 | default: 127 | exitWithError("Unknown action '$action'."); 128 | break; 129 | } 130 | 131 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Memo.php: -------------------------------------------------------------------------------- 1 | type = $type; 25 | $this->value = $value; 26 | } 27 | 28 | public function validate() { 29 | if ($this->type == static::MEMO_TYPE_NONE) 30 | return; 31 | 32 | if ($this->type == static::MEMO_TYPE_TEXT) { 33 | // Verify length does not exceed max 34 | if (strlen($this->value) > static::VALUE_TEXT_MAX_SIZE) { 35 | throw new \ErrorException(sprintf('memo text is greater than the maximum of %s bytes', static::VALUE_TEXT_MAX_SIZE)); 36 | } 37 | } else if ($this->type == static::MEMO_TYPE_ID) { 38 | if ($this->value < 0) 39 | throw new \ErrorException('value cannot be negative'); 40 | 41 | if ($this->value > PHP_INT_MAX) 42 | throw new \ErrorException(sprintf('value cannot be larger than %s', PHP_INT_MAX)); 43 | } else if ($this->type == static::MEMO_TYPE_HASH || $this->type == static::MEMO_TYPE_RETURN) { 44 | if (strlen($this->value) != 32) 45 | throw new \InvalidArgumentException(sprintf('hash values must be 32 bytes, got %s bytes', strlen($this->value))); 46 | } 47 | } 48 | 49 | public function __tostring() { 50 | if ($this->type == static::MEMO_TYPE_NONE) { 51 | return "(none)"; 52 | } else if ($this->type == static::MEMO_TYPE_TEXT) { 53 | return "(string) " . $this->value; 54 | } else if ($this->type == static::MEMO_TYPE_ID) { 55 | return "(ID) " . $this->value; 56 | } else if ($this->type == static::MEMO_TYPE_HASH) { 57 | return "(hash) " . $this->value; 58 | } else if ($this->type == static::MEMO_TYPE_RETURN) { 59 | return "(return) " . $this->value; 60 | } 61 | } 62 | 63 | public function hasValue() { return true; } 64 | 65 | public function toXDRBuffer(XDRBuffer &$buffer) { 66 | $this->validate(); 67 | 68 | $buffer->writeUnsignedInteger($this->type); 69 | 70 | if ($this->type == static::MEMO_TYPE_NONE) { 71 | } else if ($this->type == static::MEMO_TYPE_TEXT) { 72 | $buffer->writeString($this->value, static::VALUE_TEXT_MAX_SIZE); 73 | } else if ($this->type == static::MEMO_TYPE_ID) { 74 | $buffer->writeUnsignedInteger64($this->value); 75 | } else if ($this->type == static::MEMO_TYPE_HASH) { 76 | $buffer->writeOpaqueFixed($this->value, 32); 77 | } else if ($this->type == static::MEMO_TYPE_RETURN) { 78 | $buffer->writeOpaqueFixed($this->value, 32); 79 | } 80 | } 81 | 82 | public static function fromXDRBuffer(XDRBuffer &$xdr) { 83 | $type = $xdr->readUnsignedInteger(); 84 | $value = null; 85 | 86 | if ($type == static::MEMO_TYPE_TEXT) { 87 | $value = $xdr->readString(static::VALUE_TEXT_MAX_SIZE); 88 | } else if ($type == static::MEMO_TYPE_ID) { 89 | $value = $xdr->readBigInteger()->toString(); 90 | } else if ($type == static::MEMO_TYPE_HASH || $type == static::MEMO_TYPE_RETURN) { 91 | $value = $xdr->readOpaqueFixed(32); 92 | } 93 | 94 | $memo = new Memo($type, $value); 95 | return $memo; 96 | } 97 | 98 | public static function fromText($string) { 99 | return new self(self::MEMO_TYPE_TEXT, $string); 100 | } 101 | 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/GalacticHorizon/lib.php: -------------------------------------------------------------------------------- 1 | httpResponseBody; } 80 | 81 | static function create($type, $message, \GuzzleHttp\Exception\BadResponseException $guzzleException = null) { 82 | $e = new self(); 83 | $e->type = $type; 84 | 85 | switch($type) { 86 | case self::TYPE_SERVER_ERROR: $e->title = "Cannot communicate with the Stellar network."; break; 87 | case self::TYPE_REQUEST_ERROR: $e->title = "The Stellar network did not accept our request."; break; 88 | 89 | case self::TYPE_UNIMPLEMENTED_FEATURE: $e->title = "An unimplemented feature was requested."; break; 90 | } 91 | 92 | $e->message = $message; 93 | 94 | if ($guzzleException) { 95 | $e->httpResponseCode = $guzzleException->getResponse()->getStatusCode(); 96 | $e->httpResponseBody = (string)$guzzleException->getResponse()->getBody(true); 97 | 98 | $e->httpRequestURL = $guzzleException->getRequest()->getUri(); 99 | } 100 | 101 | return $e; 102 | } 103 | 104 | public function __tostring() { 105 | $str = []; 106 | $str[] = "(Exception)"; 107 | $str[] = " - Type = " . $this->type; 108 | $str[] = " - Title = " . $this->title; 109 | $str[] = " - Message = " . $this->message; 110 | $str[] = " - Request URL = " . $this->httpRequestURL; 111 | $str[] = " - Response code = " . $this->httpResponseCode; 112 | 113 | return implode("\n", $str); 114 | } 115 | 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/GalacticHorizon/AddressableKey.php: -------------------------------------------------------------------------------- 1 | sourceAccount = $sourceAccount ? $sourceAccount : new Account(); 25 | } 26 | 27 | public function setSourceAccount(Account $source) { 28 | $this->sourceAccount = $source; 29 | } 30 | 31 | public function hasValue() { return true; } 32 | 33 | final public function toXDRBuffer(XDRBuffer &$buffer) { 34 | // Source Account 35 | $buffer->writeOptional($this->sourceAccount); 36 | 37 | // Type 38 | $buffer->writeUnsignedInteger($this->getType()); 39 | 40 | $this->extendXDRBuffer($buffer); 41 | } 42 | 43 | abstract public function getType(); 44 | 45 | abstract protected function extendXDRBuffer(XDRBuffer &$buffer); 46 | 47 | abstract public function toString($depth); 48 | 49 | public function getTypeName() { 50 | switch($this->getType()) { 51 | case self::TYPE_CREATE_ACCOUNT: return "TYPE_CREATE_ACCOUNT"; 52 | case self::TYPE_PAYMENT: return "TYPE_PAYMENT"; 53 | case self::TYPE_PATH_PAYMENT: return "TYPE_PATH_PAYMENT"; 54 | case self::TYPE_MANAGE_SELL_OFFER: return "TYPE_MANAGE_SELL_OFFER"; 55 | case self::TYPE_CREATE_PASSIVE_OFFER: return "TYPE_CREATE_PASSIVE_OFFER"; 56 | case self::TYPE_SET_OPTIONS: return "TYPE_SET_OPTIONS"; 57 | case self::TYPE_CHANGE_TRUST: return "TYPE_CHANGE_TRUST"; 58 | case self::TYPE_ALLOW_TRUST: return "TYPE_ALLOW_TRUST"; 59 | case self::TYPE_ACCOUNT_MERGE: return "TYPE_ACCOUNT_MERGE"; 60 | case self::TYPE_INFLATION: return "TYPE_INFLATION"; 61 | case self::TYPE_MANAGE_DATA: return "TYPE_MANAGE_DATA"; 62 | case self::TYPE_BUMP_SEQUENCE: return "TYPE_BUMP_SEQUENCE"; 63 | case self::TYPE_MANAGE_BUY_OFFER: return "TYPE_MANAGE_BUY_OFFER"; 64 | } 65 | 66 | return "Unknown (" . $this->getType() . ")"; 67 | } 68 | 69 | public function __tostring() { 70 | $str = []; 71 | $str[] = "* (Operation)"; 72 | $str[] = " - Source account = " . $this->sourceAccount; 73 | $str[] = " - Type = " . $this->getTypeName(); 74 | $str[] = $this->toString(" "); 75 | 76 | return implode("\n", $str); 77 | } 78 | 79 | } 80 | 81 | class OperationFactory extends Operation { 82 | 83 | public function getType() { return null; } 84 | protected function extendXDRBuffer(XDRBuffer &$buffer) { } 85 | public function toString($depth) {} 86 | 87 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 88 | $sourceAccount = null; 89 | 90 | if ($buffer->readBoolean()) 91 | $sourceAccount = Account::fromXDRBuffer($buffer); 92 | 93 | $type = $buffer->readUnsignedInteger(); 94 | 95 | switch($type) { 96 | case self::TYPE_PAYMENT: $operation = PaymentOperation::fromXDRBuffer($buffer); break; 97 | case self::TYPE_MANAGE_SELL_OFFER: $operation = ManageOfferOperation::fromXDRBuffer($buffer); break; 98 | case self::TYPE_ALLOW_TRUST: $operation = AllowTrustOperation::fromXDRBuffer($buffer); break; 99 | case self::TYPE_CHANGE_TRUST: $operation = ChangeTrustOperation::fromXDRBuffer($buffer); break; 100 | case self::TYPE_MANAGE_BUY_OFFER: $operation = ManageOfferOperation::fromXDRBuffer($buffer); break; 101 | 102 | default: 103 | throw \GalacticHorizon\Exception::create( 104 | \GalacticHorizon\Exception::TYPE_UNIMPLEMENTED_FEATURE, 105 | "Operation type with code '{$type}' isn't implemented yet in " . __FILE__ . "." 106 | ); 107 | break; 108 | } 109 | 110 | if ($sourceAccount) 111 | $operation->setSourceAccount($sourceAccount); 112 | 113 | return $operation; 114 | } 115 | 116 | } 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/Time.php: -------------------------------------------------------------------------------- 1 | dateTime = clone $other->dateTime; 17 | } 18 | 19 | function setDate(\DateTime $dateTime, $includeSeconds = false) 20 | { 21 | $this->dateTime = clone $dateTime; 22 | $this->dateTime->setTime($this->dateTime->format("H"), $this->dateTime->format("i"), $includeSeconds ? $this->dateTime->format("s") : 0); 23 | } 24 | 25 | function add($count, $units = "minutes") { 26 | if (!$count) 27 | return; 28 | 29 | $this->dateTime->modify("+{$count} {$units}"); 30 | } 31 | 32 | function subtract($count, $units = "minutes") { 33 | if (!$count) 34 | return; 35 | 36 | $this->dateTime->modify("-{$count} {$units}"); 37 | } 38 | 39 | function subtractWeeks($weeks) 40 | { 41 | if (!$weeks) 42 | return; 43 | 44 | $this->dateTime->modify("-{$weeks} weeks"); 45 | } 46 | 47 | function toString() 48 | { 49 | return $this->dateTime->format("Y-m-d H:i:s"); 50 | } 51 | 52 | function getTimestamp() 53 | { 54 | return $this->dateTime->format("U"); 55 | } 56 | 57 | function isBefore(Time $other) 58 | { 59 | return $this->dateTime < $other->dateTime; 60 | } 61 | 62 | function isEqual(Time $other) 63 | { 64 | return $this->dateTime == $other->dateTime; 65 | } 66 | 67 | function isAfter(Time $other) 68 | { 69 | return $this->dateTime > $other->dateTime; 70 | } 71 | 72 | function isNow() { 73 | $now = self::now(); 74 | return $this->dateTime->format("Y-m-d H:i:00") == $now->dateTime->format("Y-m-d H:i:00"); 75 | } 76 | 77 | function getDateTime() { 78 | return $this->dateTime; 79 | } 80 | 81 | function getAgeInMinutes(Time $now = null) { 82 | return $this->getAgeInSeconds($now) / 60; 83 | } 84 | 85 | function getAgeInSeconds(Time $now = null) { 86 | if (!$now) 87 | $now = self::now(); 88 | 89 | return (float)$now->dateTime->format("U") - (float)$this->dateTime->format("U"); 90 | } 91 | 92 | function getSeconds() { 93 | return (float)$this->dateTime->format("s"); 94 | } 95 | 96 | static function getDurationDescription(Time $from, Time $to) { 97 | // var_dump("from = ", $from->toString()); 98 | // var_dump("to = ", $to->toString()); 99 | 100 | $minutes = $from->getAgeInMinutes($to); 101 | $hours = floor($minutes / (60)); 102 | $minutes -= $hours * 60; 103 | $days = floor($hours / 24); 104 | $hours -= $days * 24; 105 | 106 | if ($days > 0 && $days <= 1) { 107 | if ($days == 1) 108 | $days = "1 day"; 109 | else 110 | $days = $days . " days"; 111 | 112 | if ($hours == 1) 113 | return $days . " and 1 hour"; 114 | else 115 | return $days . " and {$hours} hours"; 116 | } else if ($hours > 0 && $hours <= 24) { 117 | if ($hours == 1) 118 | $hours = "1 hour"; 119 | else 120 | $hours = $hours . " hours"; 121 | 122 | if ($minutes == 1) 123 | return $hours . " and 1 minute"; 124 | else 125 | return $hours . " and {$minutes} minutes"; 126 | } else if ($minutes == 0) 127 | return null; 128 | else if ($minutes == 1) 129 | return "1 minute"; 130 | else if ($minutes <= 59) 131 | return $minutes . " minutes"; 132 | else 133 | 134 | return $days == 1 ? "1 day" : $days . " days"; 135 | } 136 | 137 | static function fromString($string) 138 | { 139 | $o = new self(); 140 | $o->dateTime = new \DateTime($string, new \DateTimeZone("UTC")); 141 | return $o; 142 | } 143 | 144 | static function fromDateTime(\DateTime $date) 145 | { 146 | $o = new self(); 147 | $o->setDate($date); 148 | return $o; 149 | } 150 | 151 | static function now($includeSeconds = false) 152 | { 153 | $o = new self(); 154 | $o->setDate(new \DateTime(null, new \DateTimeZone("UTC")), $includeSeconds); 155 | return $o; 156 | } 157 | 158 | static function fromTimestamp($stamp) 159 | { 160 | $d = new \DateTime(null, new \DateTimeZone("UTC")); 161 | $d->setTimestamp($stamp); 162 | 163 | $o = new self(); 164 | $o->setDate($d); 165 | return $o; 166 | } 167 | 168 | static function getRange(Time $from, Time $to) 169 | { 170 | $date = new self($from); 171 | $list = []; 172 | 173 | while(!$date->isAfter($to)) 174 | { 175 | $list[] = new self($date); 176 | 177 | $date->add(1); 178 | } 179 | 180 | return $list; 181 | } 182 | 183 | } 184 | 185 | -------------------------------------------------------------------------------- /src/GalacticHorizon/ManageOfferOperationResult.php: -------------------------------------------------------------------------------- 1 | seller = Account::fromXDRBuffer($buffer); 22 | $result->offerID = $buffer->readUnsignedInteger64(); 23 | 24 | $result->assetSold = Asset::fromXDRBuffer($buffer); 25 | $result->amountSold = Amount::fromXDRBuffer($buffer); 26 | 27 | $result->assetBought = Asset::fromXDRBuffer($buffer); 28 | $result->amountBought = Amount::fromXDRBuffer($buffer); 29 | 30 | return $result; 31 | } 32 | 33 | public function __tostring() { 34 | $str = []; 35 | $str[] = "(" . get_class($this) . ")"; 36 | $str[] = "- Seller = " . $this->seller; 37 | $str[] = "- OfferID = " . $this->offerID; 38 | $str[] = "- Asset sold = " . $this->assetSold; 39 | $str[] = "- Amount sold = " . $this->amountSold; 40 | $str[] = "- Asset bought = " . $this->assetBought; 41 | $str[] = "- Amount bought = " . $this->amountBought; 42 | 43 | return implode("\n", $str); 44 | } 45 | 46 | } 47 | 48 | 49 | class ManageOfferOperationResult extends OperationResult implements XDRInputInterface { 50 | 51 | // codes considered as "success" for the operation 52 | const MANAGE_BUY_OFFER_SUCCESS = 0; 53 | 54 | // codes considered as "failure" for the operation 55 | const MANAGE_BUY_OFFER_MALFORMED = -1; // generated offer would be invalid 56 | const MANAGE_BUY_OFFER_SELL_NO_TRUST = -2; // no trust line for what we're selling 57 | const MANAGE_BUY_OFFER_BUY_NO_TRUST = -3; // no trust line for what we're buying 58 | const MANAGE_BUY_OFFER_SELL_NOT_AUTHORIZED = -4; // not authorized to sell 59 | const MANAGE_BUY_OFFER_BUY_NOT_AUTHORIZED = -5; // not authorized to buy 60 | const MANAGE_BUY_OFFER_LINE_FULL = -6; // can't receive more of what it's buying 61 | const MANAGE_BUY_OFFER_UNDERFUNDED = -7; // doesn't hold what it's trying to sell 62 | const MANAGE_BUY_OFFER_CROSS_SELF = -8; // would cross an offer from the same user 63 | const MANAGE_BUY_OFFER_SELL_NO_ISSUER = -9; // no issuer for what we're selling 64 | const MANAGE_BUY_OFFER_BUY_NO_ISSUER = -10; // no issuer for what we're buying 65 | 66 | // update errors 67 | const MANAGE_BUY_OFFER_NOT_FOUND = -11; // offerID does not match an existing offer 68 | 69 | const MANAGE_BUY_OFFER_LOW_RESERVE = -12; // not enough funds to create a new Offer 70 | 71 | const EFFECT_MANAGE_OFFER_CREATED = 0; 72 | const EFFECT_MANAGE_OFFER_UPDATED = 1; 73 | const EFFECT_MANAGE_OFFER_DELETED = 2; 74 | 75 | private $errorCode; 76 | 77 | private $claimedOffers = []; 78 | 79 | private $effect = null; 80 | 81 | private $offer = null; 82 | 83 | public function getClaimedOfferCount() { return count($this->claimedOffers); } 84 | public function getClaimedOffer(int $index) { return $this->claimedOffers[$index]; } 85 | 86 | public function getOffer() { return $this->offer; } 87 | 88 | public function getErrorCode() { return $this->errorCode; } 89 | 90 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 91 | $result = new self(); 92 | $result->errorCode = $buffer->readInteger(); 93 | 94 | if ($result->errorCode == self::MANAGE_BUY_OFFER_SUCCESS) { 95 | $claimedOfferCount = $buffer->readUnsignedInteger(); 96 | 97 | for($i=0; $i<$claimedOfferCount; $i++) 98 | $result->claimedOffers[] = ClaimOfferAtom::fromXDRBuffer($buffer); 99 | 100 | $result->effect = $buffer->readUnsignedInteger(); 101 | 102 | switch($result->effect) { 103 | case self::EFFECT_MANAGE_OFFER_CREATED: 104 | case self::EFFECT_MANAGE_OFFER_UPDATED: 105 | $result->offer = OfferEntry::fromXDRBuffer($buffer); 106 | break; 107 | } 108 | } 109 | 110 | return $result; 111 | } 112 | 113 | public function __tostring() { 114 | $str = []; 115 | $str[] = "(" . get_class($this) . ")"; 116 | $str[] = "- Error code = " . $this->errorCode; 117 | $str[] = "* Claimed offers (" . count($this->claimedOffers) . ") = "; 118 | 119 | foreach($this->claimedOffers AS $offer) 120 | $str[] = IncreaseDepth((string)$offer); 121 | 122 | $str[] = "- Effect = " . ($this->effect === null ? "null" : $this->effect); 123 | $str[] = "* Offer\n" . IncreaseDepth((string)$this->offer); 124 | 125 | return implode("\n", $str); 126 | } 127 | 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/GalacticHorizon/XDRDecoder.php: -------------------------------------------------------------------------------- 1 | GalacticBot 2 | 3 | GalacticBot is a PHP 7.x library for creating trade bots on the Stellar platform. 4 | 5 | With GalacticBot you can run bot(s) with our default trading algorithm or create and test your own custom trading algorithm(s). 6 | 7 | Bots can be run locally on your machine or on a server. 8 | 9 | # Features 10 | 11 | - An abstract bot class which you can extend to do implement your own trading logic 12 | - Bots can be run both the Stellar public net as well as on the Stellar testnet for testing 13 | - Bots can be run in real time or as an simulation to test out bots and settings 14 | - Exponential Moving Average (EMA) bot implemention which is currently being live tested (see below for links) 15 | - Demo project to get a bot up and running in minutes 16 | - Demo project also includes a script to manage bot processes 17 | 18 | # Requirements 19 | 20 | - PHP version 7.1 21 | - No extra PHP modules are required 22 | - MySQL is needed for running the demo EMA bot, but you could create an implementation for any other type of database 23 | 24 | # Warning 25 | 26 | Please note that this library is still under development. We advice to use this software only on the Stellar testnet as this is experimental code created for R&D into blockchain technology. 27 | 28 | # Installation 29 | 30 | This package is available on [Composer](https://packagist.org/packages/unwindnl/galacticbot). 31 | 32 | Use ```composer require unwindnl/galacticbot``` to add this library to your PHP project. 33 | 34 | # Demo 35 | 36 | This project contains a demo project of how to setup and run a bot with a minimal web interface to interact with the bot. Please see the demo/README.md [demo/README.md](demo/README.md) folder for more information. 37 | 38 | A live demo is available on: https://www.galacticbot.com/libdemo/. 39 | 40 | We also created an example of a custom (graphical) view for the live bot on: https://www.galacticbot.com/demo/. 41 | 42 | # Data(base) abstraction 43 | 44 | The bot does not interact with a database directly but uses the ```DataInterface``` interface for describing how an implemention should look like. 45 | 46 | Please see the example ```MysqlDataInterface``` implementation of how to implement your own if you would want to interface with another database. This example implementation is not optimized and could slow down with more data in the database. So its best to clear our older data or optimize/create your own implementation. 47 | 48 | # Implementing your own trading algorithm 49 | 50 | Create a class that extends the GalacticBot\Bot class. 51 | 52 | Add a list of custom trading states your bot can have, for example: 53 | 54 | ```php 55 | const TRADE_STATE_BUY_WAIT = "BUY_WAIT"; 56 | const TRADE_STATE_SELL_WAIT = "SELL_WAIT"; 57 | ``` 58 | 59 | Implement the following abstract methods: 60 | 61 | ## initialize() 62 | 63 | Here you could load for example data from a database you going to need for your algorithm. This is only called once. 64 | 65 | ## getTradeStateLabel($forState) 66 | 67 | This is were you return a more descriptive text for each of your custom states you defined earlier. 68 | 69 | ## process(\GalacticBot\Time $time, $sample) 70 | 71 | A very minimal example: 72 | 73 | ```php 74 | protected function process(\GalacticBot\Time $time, $sample) 75 | { 76 | // Get current trade state 77 | $tradeState = $this->data->get("tradeState"); 78 | 79 | // Get the last added trade 80 | $lastTrade = $this->data->getLastTrade(); 81 | 82 | // If we have a trade and it isn't completed yet, then update it to get the latest state 83 | if ($lastTrade && !$lastTrade->getIsFilledCompletely()) 84 | { 85 | // Get the latest state from the Stellar network 86 | $lastTrade->updateFromAPIForBot($this->settings->getAPI(), $this); 87 | 88 | // If it isn't done, we'll have to return and wait for it to complete 89 | // You could also cancel it or change it if you want 90 | if (!$lastTrade->getIsFilledCompletely()) 91 | return; 92 | } 93 | 94 | if ($tradeState == self::TRADE_STATE_NONE || $tradeState == self::TRADE_STATE_BUY_WAIT) 95 | if (time to buy / do something with $sample) 96 | { 97 | $this->buy(); 98 | $tradeState = self::TRADE_STATE_SELL_WAIT; 99 | } 100 | } 101 | else if ($tradeState == self::TRADE_STATE_SELL_WAIT) 102 | { 103 | if (time to sell / do something with $sample) 104 | { 105 | $this->sell(); 106 | $tradeState = self::TRADE_STATE_BUY_WAIT; 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | # Open issues 113 | 114 | - You need to setup a trustline for the assets you want to trade with your Stellar account. The library will do this for you in the future but for now you will have to do this yourself (with for example StellarTerm). 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/DataInterface.php: -------------------------------------------------------------------------------- 1 | setPublicKey($base32String); 60 | return $keypair; 61 | } 62 | 63 | /* 64 | public static function newFromMnemonic($mnemonic, $passphrase = '', $index = 0) 65 | { 66 | $bip39 = new Bip39(); 67 | $seedBytes = $bip39->mnemonicToSeedBytesWithErrorChecking($mnemonic, $passphrase); 68 | 69 | $masterNode = HdNode::newMasterNode($seedBytes); 70 | 71 | $accountNode = $masterNode->derivePath(sprintf("m/44'/148'/%s'", $index)); 72 | 73 | return static::newFromRawSeed($accountNode->getPrivateKeyBytes()); 74 | } 75 | */ 76 | 77 | public function __construct($seedString = null) { 78 | if ($seedString) 79 | $this->setSeed($seedString); 80 | } 81 | 82 | public function signDecorated($value) { 83 | $this->requirePrivateKey(); 84 | 85 | return new DecoratedSignature( 86 | $this->getHint(), 87 | $this->sign($value) 88 | ); 89 | } 90 | 91 | public function sign($value) { 92 | $this->requirePrivateKey(); 93 | 94 | return Ed25519::sign_detached($value, $this->getEd25519SecretKey()); 95 | } 96 | 97 | /* 98 | public function verifySignature($signature, $message) 99 | return Ed25519::verify_detached($signature, $message, $this->publicKey); 100 | } 101 | */ 102 | 103 | public function setPublicKey($base32String) { 104 | // Clear out all private key fields 105 | $this->privateKey = null; 106 | 107 | $this->publicKey = AddressableKey::getRawBytesFromBase32AccountId($base32String); 108 | $this->publicKeyString = $base32String; 109 | } 110 | 111 | public function setSeed($base32SeedString) { 112 | $this->seed = $base32SeedString; 113 | $this->privateKey = AddressableKey::getRawBytesFromBase32Seed($base32SeedString); 114 | $this->publicKeyString = AddressableKey::addressFromRawSeed($this->privateKey); 115 | $this->publicKey = AddressableKey::getRawBytesFromBase32AccountId($this->publicKeyString); 116 | } 117 | 118 | public function getHint() { 119 | return substr($this->publicKey, -4); 120 | } 121 | 122 | public function getPublicKeyChecksum() { 123 | $checksumBytes = substr($this->getPublicKeyBytes(), -2); 124 | 125 | $unpacked = unpack('v', $checksumBytes); 126 | 127 | return array_shift($unpacked); 128 | } 129 | 130 | public function getSecret() { 131 | $this->requirePrivateKey(); 132 | 133 | return $this->seed; 134 | } 135 | 136 | /* 137 | public function getPrivateKeyBytes() 138 | { 139 | $this->requirePrivateKey(); 140 | 141 | return AddressableKey::getRawBytesFromBase32Seed($this->seed); 142 | } 143 | 144 | public function getPublicKeyBytes() 145 | { 146 | return $this->publicKey; 147 | } 148 | 149 | public function getAccountId() 150 | { 151 | return $this->publicKeyString; 152 | } 153 | */ 154 | 155 | public function getPublicKey() { 156 | return $this->publicKeyString; 157 | } 158 | 159 | protected function requirePrivateKey() { 160 | if (!$this->privateKey) { 161 | $exception = \GalacticHorizon\Exception::create( 162 | \GalacticHorizon\Exception::TYPE_INVALID_PARAMETERS, 163 | "Private key is required to perform this operation.", 164 | $e 165 | ); 166 | } 167 | } 168 | 169 | protected function getEd25519SecretKey() { 170 | $this->requirePrivateKey(); 171 | 172 | $pk = ''; 173 | $sk = ''; 174 | 175 | Ed25519::seed_keypair($pk, $sk, $this->privateKey); 176 | 177 | return $sk; 178 | } 179 | 180 | } 181 | 182 | } 183 | 184 | -------------------------------------------------------------------------------- /src/GalacticHorizon/index.php: -------------------------------------------------------------------------------- 1 | getSecret() . "\n - Public: " . $keypair->getPublicKey() . "\n"; 33 | 34 | $account = \GalacticHorizon\Account::createFromKeyPair($keypair); 35 | 36 | $client->fundTestAccount($account); 37 | 38 | 39 | /////// --------------- PAYMENT TRANSACTION 40 | 41 | $keypair = \GalacticHorizon\Keypair::newFromSecretKey("SCPKNPC3GTATMX2V7ILFKPVRIVTIUIVJAW4YLFWVKOTUKTBI272MVQZ4"); 42 | var_dump("From: Priv: " . $keypair->getSecret()); 43 | var_dump("From: Publ: " . $keypair->getPublicKey()); 44 | $sourceAccount = \GalacticHorizon\Account::createFromKeyPair($keypair); 45 | 46 | $keypair = \GalacticHorizon\Keypair::newFromSecretKey("SAG37SM52EIHDOOX7K5SJ2MFIF2SOP2BBA5727QAVN6PIL35KZA44VYO"); 47 | var_dump("To: Priv: " . $keypair->getSecret()); 48 | var_dump("To: Publ:" . $keypair->getPublicKey()); 49 | $destinationAccount = \GalacticHorizon\Account::createFromKeyPair($keypair); 50 | 51 | try { 52 | $payment = new PaymentOperation(); 53 | $payment->setDestinationAccount($destinationAccount); 54 | $payment->setAsset(Asset::native()); 55 | $payment->setAmount(Amount::fromFloat(100.5)); 56 | 57 | $transaction = new Transaction($sourceAccount); 58 | $transaction->addOperation($payment); 59 | //$transaction->setMemo(Memo::fromText("Text hierzo")); 60 | $transaction->sign([$sourceAccount]); 61 | 62 | $result = $transaction->submit(); 63 | 64 | var_dump("transaction result = ", $result); 65 | } catch (\GalacticHorizon\Exception $e) { 66 | echo $e . "\n"; 67 | } 68 | 69 | */ 70 | 71 | 72 | /* 73 | $xdr = "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAEAAAAA9dCK5ZLJf6GcamYAYxpGpCCDW87noAHzmMphaxtqOxQAAAAABN5RVgAAAAAAAAAAASOckQAAAAFVU0QAAAAAAOimGoYeYK9g+Adz4GNG5ccsvlncrdo3YI1Y70JRHZ/cAAAAAAAhkcAAAAACAAAAAA=="; 74 | 75 | $buffer = XDRBuffer::fromBase32String($xdr); 76 | 77 | $r = TransactionResult::fromXDRBuffer($buffer); 78 | 79 | echo("\n\nr = " . (string)$r . "\n\n"); 80 | 81 | if ($r->getResult(0)->getClaimedOfferCount() > 0) { 82 | for($i=0; $i<$r->getResult(0)->getClaimedOfferCount(); $i++) { 83 | echo $r->getResult(0)->getClaimedOffer($i); 84 | } 85 | } 86 | 87 | exit(); 88 | */ 89 | $client = Client::createPublicClient(); 90 | 91 | exit(); 92 | 93 | $liveAccount = Account::createFromKeyPair(Keypair::createFromSecretKey("SD4FNGX5MXZ5DPBGV4F7HYLXAEZGW2OQ2DQUMUCOSJCWWYXVE7XGCCKQ")); 94 | 95 | $sellingAsset = Asset::createNative(); 96 | $buyingAsset = Asset::createFromCodeAndIssuer("USD", Account::createFromPublicKey("GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX")); 97 | 98 | /* 99 | 100 | $automaticlyFixTrustLineWithAmount = Amount::createFromFloat(200); 101 | 102 | $manageOffer = new ManageOfferOperation(); 103 | $manageOffer->setSellingAsset($sellingAsset); 104 | $manageOffer->setBuyingAsset($buyingAsset); 105 | $manageOffer->setAmount(Amount::createFromFloat(0.22)); 106 | $manageOffer->setPrice(Price::createFromFloat(0.2129193)); 107 | $manageOffer->setOfferID(null); 108 | 109 | try { 110 | $transaction = new Transaction($liveAccount); 111 | $transaction->addOperation($manageOffer); 112 | //$transaction->setMemo(Memo::fromText("Text hierzo")); 113 | $transaction->sign([$liveAccount]); 114 | 115 | $transactionResult = $transaction->submit($automaticlyFixTrustLineWithAmount); 116 | 117 | if ($transactionResult->getErrorCode() == TransactionResult::TX_SUCCESS) { 118 | echo "result =\n"; 119 | echo $transactionResult->getResult(0) . "\n"; 120 | } else { 121 | echo "Niet goed nie.\n"; 122 | } 123 | 124 | } catch (\GalacticHorizon\Exception $e) { 125 | echo $e . "\n"; 126 | 127 | echo $e->getHttpResponseBody() . "\n"; 128 | } 129 | */ 130 | 131 | 132 | /* 133 | $operation = new \GalacticHorizon\ChangeTrustOperation(); 134 | $operation->setAsset($buyingAsset); 135 | $operation->setLimit(Amount::createFromFloat(0)); 136 | 137 | try { 138 | $transaction = new Transaction($liveAccount); 139 | $transaction->addOperation($operation); 140 | 141 | $transaction->sign([$liveAccount]); 142 | 143 | $transactionResult = $transaction->submit(); 144 | 145 | if ($transactionResult->getErrorCode() == TransactionResult::TX_SUCCESS) { 146 | echo "JAH, goed gegaan ...\n"; 147 | echo (string)$transactionResult . "\n"; 148 | } else { 149 | echo "Niet goed nie.\n"; 150 | } 151 | 152 | } catch (\GalacticHorizon\Exception $e) { 153 | echo $e . "\n"; 154 | 155 | echo $e->getHttpResponseBody() . "\n"; 156 | } 157 | */ 158 | -------------------------------------------------------------------------------- /src/Settings.php: -------------------------------------------------------------------------------- 1 | dataInterface = $dataInterface; 55 | 56 | $this->ID = self::getFromArray($options, "ID"); 57 | $this->type = self::getFromArray($options, "type"); 58 | $this->name = self::getFromArray($options, "name"); 59 | 60 | $this->baseAsset = self::getFromArray($options, "baseAsset"); 61 | $this->baseAssetReservationAmount = self::getOptionalFromArray($options, "baseAssetReservationAmount"); 62 | 63 | $this->onTestNet = (bool)self::getFromArray($options, "onTestNet"); 64 | $this->accountSecret = self::getFromArray($options, "accountSecret"); 65 | 66 | $this->counterAsset = self::getFromArray($options, "counterAsset"); 67 | 68 | foreach($options AS $k => $v) { 69 | throw new \Exception("Unknown option '$k'."); 70 | } 71 | } 72 | 73 | /** 74 | * Parses the customizable settings from a database. 75 | */ 76 | public function loadFromDataInterface($defaults) 77 | { 78 | foreach($defaults AS $name => $defaultValue) 79 | { 80 | $this->settings[$name] = $this->dataInterface->getSetting($name, $defaultValue); 81 | } 82 | } 83 | 84 | /** 85 | * Returns the Bot identifier, usually a number but if your DataInterface supports it it could be a string. 86 | * 87 | * @return Number 88 | */ 89 | public function getID() { return $this->ID; } 90 | 91 | /** 92 | * Returns the Bot name 93 | * 94 | * @return String 95 | */ 96 | public function getName() { return $this->name; } 97 | 98 | /** 99 | * Returns the Bot type (Bot::SETTING_TYPE_SIMULATION or Bot::SETTING_TYPE_LIVE) 100 | * 101 | * @return String 102 | */ 103 | public function getType() { return $this->type; } 104 | 105 | /** 106 | * Returns the base asset - which usually is XLM native. 107 | * @return ZuluCrypto\StellarSdk\XdrModel\Asset 108 | */ 109 | public function getBaseAsset() { return $this->baseAsset; } 110 | 111 | /** 112 | * Returns the base asset reservation - the bot won't do anything with this amount. 113 | * @return float 114 | */ 115 | public function getBaseAssetReservationAmount() { return $this->baseAssetReservationAmount; } 116 | 117 | /** 118 | * Returns the counter asset - for example MOBI 119 | * @return ZuluCrypto\StellarSdk\XdrModel\Asset 120 | */ 121 | public function getCounterAsset() { return $this->counterAsset; } 122 | 123 | /** 124 | * Instance of the DataInterface implemtated class 125 | * @return DataInterface 126 | */ 127 | public function getDataInterface() 128 | { 129 | return $this->dataInterface; 130 | } 131 | 132 | public function getIsOnTestNet() { 133 | return $this->onTestNet ? true : false; 134 | } 135 | 136 | /** 137 | * Returns a specific setting which has to be defined first in BotClassName::$settingDefaults 138 | */ 139 | public function get($name) 140 | { 141 | return isset($this->settings[$name]) ? $this->settings[$name] : null; 142 | } 143 | 144 | /** 145 | * Stellar account secret 146 | * @return String 147 | */ 148 | public function getAccountKeypair() 149 | { 150 | if (!$this->stellarAccountKeypair) 151 | $this->stellarAccountKeypair = \GalacticHorizon\Account::createFromSecretKey($this->accountSecret); 152 | 153 | return $this->stellarAccountKeypair; 154 | } 155 | 156 | /** 157 | * Stellar account public key 158 | * @return String 159 | */ 160 | public function getAccountPublicKey() 161 | { 162 | return $this->getAccountKeypair()->getPublicKey(); 163 | } 164 | 165 | /** 166 | * Gets a setting from the settings array and removes it from the array afterwards 167 | */ 168 | private function getOptionalFromArray(Array &$settings, $name, $defaultValue = null) 169 | { 170 | $value = isset($settings[$name]) ? $settings[$name] : $defaultValue; 171 | 172 | unset($settings[$name]); 173 | 174 | return $value; 175 | } 176 | 177 | /** 178 | * Gets a setting from the settings array and removes it from the array afterwards, will fail if the settings doesn't exist 179 | */ 180 | private function getFromArray(Array &$settings, $name) 181 | { 182 | $value = $settings[$name]; 183 | 184 | unset($settings[$name]); 185 | 186 | return $value; 187 | } 188 | 189 | } 190 | 191 | -------------------------------------------------------------------------------- /demo/templates/details.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Bot #getSettings()->getID()?> "getSettings()->getName()?>" Details

4 | 5 |

6 | Back to the overview 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | 46 |
ModegetSettings()->getType()))?>
StategetStateInfo()["label"]?>
Trading StategetTradeStateInfo()["label"]?>
Current base asset holdingsgetCurrentBaseAssetBudget()?>
Current base asset profitgetProfitPercentage()?>%
Current counter asset holdingsgetCurrentCounterAssetBudget()?>
Last processed time 38 | getLastProcessingTime()) { 40 | echo $bot->getLastProcessingTime()->format("Y-m-d H:i:s"); 41 | } 42 | ?> 43 |
47 | 48 | Refresh 49 | 50 | 51 | getSettings()->getType() == GalacticBot\Bot::SETTING_TYPE_LIVE) { ?> 52 |

Live Bot Actions

53 | 54 |

55 | Please remember to at call 'php worker.php checkstart' from this installation after starting a bot. 56 |

57 | 58 |

59 | Start 60 | Pause 61 | Stop 62 |

63 | getSettings()->getType() == GalacticBot\Bot::SETTING_TYPE_SIMULATION) { ?> 64 |

Simulation Bot Actions

65 | 66 |

67 | Please remember to at call 'php worker.php checkstart' from this installation after starting a bot. 68 |

69 | 70 |

71 | Start 72 | Stop 73 | Reset 74 |

75 | 76 | 77 |

Log

78 |

79 | Last 50 offers & trades done by this bot. 80 |

81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | getDataInterface()->getTrades(50, true) AS $trade) { ?> 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
DateTypeStateAmountPrice
getProcessedAt()->format("Y-m-d H:i:s")?>getType()))?>getStateInfo()["label"]?>getSellAmount()?>getPrice()?>
99 |

Settings

100 | 101 |

102 | All settings have prefilled default values. 103 |
104 | Change any of the values and click on 'Save settings' to change the values in the database for this bot. 105 |

106 | 107 |
108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 |
Buy delay" min="0" max="30" /> minutes
Minimum hold" min="0" max="60" /> minutes
Prognosis window" min="10" max="60" /> minutes
Minimum profit before selling" min="0" max="100" step="0.01" /> %
Shortterm (buy)" min="0" max="120" /> minutes
Shortterm (sale)" min="0" max="120" /> minutes
Midterm" min="0" max="3600" /> minutes
Longterm" min="0" max="3600" /> minutes
166 |
167 |
168 | 169 | -------------------------------------------------------------------------------- /src/Implementation/CrashGuardBot.php: -------------------------------------------------------------------------------- 1 | 120, 28 | "currentPriceSampleCount" => 120, 29 | 30 | "maximumLossPercentage" => 1, 31 | 32 | "minimumRisePercentage" => 2, 33 | "minimumRiseTimeMinutes" => 5, 34 | ); 35 | 36 | protected function initialize() 37 | { 38 | $this->paidPriceEMASamples = $this->data->getS("paidPriceEMASamples", $this->settings->get("paidPriceSampleCount")); 39 | $this->currentPriceEMASamples = $this->data->getS("currentPriceEMASamples", $this->settings->get("currentPriceSampleCount")); 40 | 41 | $this->outOfDipDate = $this->data->get("outOfDipDate", null); 42 | 43 | if ($this->outOfDipDate) 44 | $this->outOfDipDate = \GalacticBot\Time::fromString($this->outOfDipDate); 45 | } 46 | 47 | public function getTradeStateLabel($forState) { 48 | $counter = $this->settings->getCounterAsset()->getAssetCode(); 49 | 50 | if (!$this->data->get("lastProcessingTime")) 51 | return null; 52 | 53 | $label = null; 54 | 55 | switch($forState) 56 | { 57 | case self::TRADE_STATE_BUFFERING: $label = "Waiting for enough data"; break; 58 | case self::TRADE_STATE_NONE: $label = "Gaining profit (" . number_format($this->data->get("differencePercentage"), 2) . "%), waiting for a dip"; break; 59 | 60 | case self::TRADE_STATE_WAIT_BUY_ORDER: $label = "Waiting for buy order to complete"; break; 61 | 62 | case self::TRADE_STATE_TRACK_DIP: $label = "Tracking current dip (" . number_format($this->data->get("differencePercentage"), 2) . "%), stay below loss %"; break; 63 | 64 | case self::TRADE_STATE_WAIT_SELL_ORDER: $label = "Waiting for sell order to complete"; break; 65 | 66 | case self::TRADE_STATE_WAIT_FOR_RISE: $label = "Waiting for rise (" . number_format($this->data->get("differencePercentage"), 2) . "%)"; break; 67 | case self::TRADE_STATE_WAIT_FOR_RISE_HOLD: $label = "Waiting for rise to hold long enough"; break; 68 | } 69 | 70 | return $label; 71 | } 72 | 73 | protected function process(\GalacticBot\Time $time, $sample) 74 | { 75 | $state = $this->data->get("state"); 76 | $tradeState = $this->data->get("tradeState"); 77 | 78 | $lastTrade = $this->data->getLastTrade(); 79 | $lastCompletedTrade = $this->data->getLastCompletedTrade(); 80 | 81 | $currentPrice = $sample ? 1/$sample : null; 82 | 83 | $this->data->logVerbose("- state = {$state}, tradeState = {$tradeState}"); 84 | 85 | if ($sample === null) 86 | { 87 | $this->data->logWarning("No sample data received, not processing this timeframe."); 88 | $this->data->save(); 89 | exit(); 90 | } 91 | else 92 | { 93 | switch($tradeState) 94 | { 95 | case self::TRADE_STATE_WAIT_BUY_ORDER: 96 | if ($lastTrade->getIsFilledCompletely()) 97 | { 98 | $tradeState = self::TRADE_STATE_NONE; 99 | 100 | $this->paidPriceEMASamples->fillWithValue($lastTrade->getPaidPrice()); 101 | } 102 | else 103 | { 104 | // TODO: adjust price 105 | } 106 | break; 107 | 108 | case self::TRADE_STATE_WAIT_SELL_ORDER: 109 | if ($lastTrade->getIsFilledCompletely()) 110 | { 111 | $tradeState = self::TRADE_STATE_WAIT_FOR_RISE; 112 | 113 | $this->currentPriceEMASamples->fillWithValue($currentPrice); 114 | } 115 | else 116 | { 117 | // TODO: adjust price 118 | } 119 | break; 120 | } 121 | 122 | $this->paidPriceEMAValue = $this->paidPriceEMASamples->getExponentialMovingAverage(); 123 | 124 | $differencePercentage = 0; 125 | $paidPrice = null; 126 | 127 | if ($lastCompletedTrade) 128 | { 129 | $paidPrice = $lastCompletedTrade->getPrice(); 130 | 131 | if ($lastCompletedTrade->getType() == \GalacticBot\Trade::TYPE_BUY) 132 | { 133 | // Get closer to the current price when the current price is higher than we (averaged) paid 134 | if ($currentPrice >= $this->paidPriceEMAValue) // $this->paidPriceEMAValue) 135 | { 136 | $this->paidPriceEMASamples->add($currentPrice); 137 | $this->paidPriceEMAValue = $this->paidPriceEMASamples->getExponentialMovingAverage(); 138 | } 139 | 140 | $this->data->setT($time, "paidPriceEMAValue", $this->paidPriceEMAValue); 141 | $differencePercentage = 100 * (($currentPrice / $this->paidPriceEMAValue) - 1); 142 | } 143 | else 144 | { 145 | if ($currentPrice <= $this->currentPriceEMAValue) 146 | { 147 | $this->currentPriceEMASamples->add($currentPrice); 148 | $this->data->setS("currentPriceEMASamples", $this->currentPriceEMASamples); 149 | $this->currentPriceEMAValue = $this->currentPriceEMASamples->getExponentialMovingAverage(); 150 | } 151 | 152 | $this->data->setT($time, "currentPriceEMAValue", $this->currentPriceEMAValue); 153 | $differencePercentage = 100 * (($currentPrice / $this->currentPriceEMAValue) - 1); 154 | } 155 | } 156 | 157 | switch($tradeState) 158 | { 159 | case "": 160 | case self::TRADE_STATE_NONE: 161 | case self::TRADE_STATE_TRACK_DIP: 162 | $this->outOfDipDate = null; 163 | $tradeState = self::TRADE_STATE_NONE; 164 | 165 | if ($lastTrade) 166 | { 167 | if ($differencePercentage <= -$this->settings->get("maximumLossPercentage")) 168 | { 169 | if ($this->sell($time)) 170 | { 171 | $tradeState = self::TRADE_STATE_WAIT_SELL_ORDER; 172 | } 173 | } 174 | else if ($differencePercentage < 0) 175 | { 176 | $tradeState = self::TRADE_STATE_TRACK_DIP; 177 | } 178 | } 179 | else if (!$lastTrade) 180 | { 181 | // We need to but the counter asset 182 | if ($this->buy($time)) 183 | { 184 | $tradeState = self::TRADE_STATE_WAIT_BUY_ORDER; 185 | } 186 | } 187 | break; 188 | 189 | case self::TRADE_STATE_WAIT_FOR_RISE: 190 | case self::TRADE_STATE_WAIT_FOR_RISE_HOLD: 191 | if ($differencePercentage >= $this->settings->get("minimumRisePercentage")) 192 | { 193 | if (!$this->outOfDipDate) 194 | { 195 | $this->outOfDipDate = new \GalacticBot\Time($time); 196 | 197 | $this->data->logVerbose("We're out of the dip. Now wait for it to hold long enough to buy in again."); 198 | } 199 | else if ($this->outOfDipDate->getAgeInMinutes($time) >= $this->settings->get("minimumRiseTimeMinutes")) 200 | { 201 | $this->data->logVerbose("The rise is holding long enough, we can buy in again."); 202 | 203 | if ($this->buy($time)) 204 | { 205 | $this->outOfDipDate = null; 206 | $tradeState = self::TRADE_STATE_WAIT_BUY_ORDER; 207 | } 208 | } 209 | else 210 | { 211 | } 212 | } 213 | else if ($tradeState == self::TRADE_STATE_WAIT_FOR_RISE_HOLD) 214 | { 215 | $this->outOfDipDate = null; 216 | $tradeState = self::TRADE_STATE_WAIT_FOR_RISE; 217 | } 218 | break; 219 | 220 | default: 221 | exit("Unhandled tradeState: {$tradeState}"); 222 | break; 223 | } 224 | } 225 | 226 | $this->data->set("state", $state); 227 | $this->data->set("tradeState", $tradeState); 228 | $this->data->set("differencePercentage", $differencePercentage); 229 | 230 | $this->data->set("outOfDipDate", $this->outOfDipDate ? $this->outOfDipDate->toString() : null); 231 | 232 | $this->data->logVerbose("[DONE] - tradeState = {$tradeState}"); 233 | } 234 | 235 | } 236 | -------------------------------------------------------------------------------- /src/GalacticHorizon/XDRBuffer.php: -------------------------------------------------------------------------------- 1 | xdrBytes = $xdrBytes; 29 | $this->position = 0; 30 | } 31 | 32 | static function fromBase32String($string) { 33 | return new self(base64_decode($string)); 34 | } 35 | 36 | public function getRawBytes() { 37 | return $this->xdrBytes; 38 | } 39 | 40 | public function writeUnsignedInteger($value) { 41 | $this->xdrBytes .= XDREncoder::unsignedInteger($value); 42 | } 43 | 44 | public function writeUnsignedBigInteger64($value) { 45 | $this->xdrBytes .= XDREncoder::unsignedBigInteger64($value); 46 | } 47 | 48 | public function writeSignedBigInteger64($value) { 49 | $this->xdrBytes .= XDREncoder::signedBigInteger64($value); 50 | } 51 | 52 | public function writeUnsignedInteger64($value) { 53 | $this->xdrBytes .= XDREncoder::unsignedInteger64($value); 54 | } 55 | 56 | public function writeString($value, $maximumLength = null) { 57 | $this->xdrBytes .= XDREncoder::string($value, $maximumLength); 58 | } 59 | 60 | public function writeOptional(XDROutputInterface $value) { 61 | if ($value !== null && $value->hasValue()) { 62 | $this->xdrBytes .= XDREncoder::boolean(true); 63 | $value->toXDRBuffer($this); 64 | } else { 65 | $this->xdrBytes .= XDREncoder::boolean(false); 66 | } 67 | } 68 | 69 | public function writeOpaqueFixed($value, $expectedLength = null, $padUnexpectedLength = false) { 70 | $this->xdrBytes .= XDREncoder::opaqueFixed($value, $expectedLength, $padUnexpectedLength); 71 | } 72 | 73 | public function writeOpaqueVariable($value) { 74 | $this->xdrBytes .= XDREncoder::opaqueVariable($value); 75 | } 76 | 77 | public function toBase64String() { 78 | return base64_encode($this->xdrBytes); 79 | } 80 | 81 | public function writeHash($data) { 82 | $this->xdrBytes .= hash('sha256', $data, true); 83 | } 84 | 85 | public function readHash() { 86 | $dataSize = 32; 87 | $this->assertBytesRemaining($dataSize); 88 | 89 | $data = substr($this->xdrBytes, $this->position, $dataSize); 90 | $this->position += $dataSize; 91 | 92 | return $data; 93 | } 94 | 95 | /** 96 | * @return int 97 | * @throws \ErrorException 98 | */ 99 | public function readUnsignedInteger() 100 | { 101 | $dataSize = 4; 102 | $this->assertBytesRemaining($dataSize); 103 | 104 | $data = XDRDecoder::unsignedInteger(substr($this->xdrBytes, $this->position, $dataSize)); 105 | $this->position += $dataSize; 106 | 107 | return $data; 108 | } 109 | 110 | /** 111 | * @return int 112 | * @throws \ErrorException 113 | */ 114 | public function readUnsignedInteger64() 115 | { 116 | $dataSize = 8; 117 | $this->assertBytesRemaining($dataSize); 118 | 119 | $data = XDRDecoder::unsignedInteger64(substr($this->xdrBytes, $this->position, $dataSize)); 120 | $this->position += $dataSize; 121 | 122 | return $data; 123 | } 124 | 125 | /** 126 | * @return BigInteger 127 | * @throws \ErrorException 128 | */ 129 | public function readBigInteger() 130 | { 131 | $dataSize = 8; 132 | $this->assertBytesRemaining($dataSize); 133 | 134 | $bigInteger = new BigInteger(substr($this->xdrBytes, $this->position, $dataSize), 256); 135 | $this->position += $dataSize; 136 | 137 | return $bigInteger; 138 | } 139 | 140 | /** 141 | * @return int 142 | * @throws \ErrorException 143 | */ 144 | public function readInteger() 145 | { 146 | $dataSize = 4; 147 | $this->assertBytesRemaining($dataSize); 148 | 149 | $data = XDRDecoder::signedInteger(substr($this->xdrBytes, $this->position, $dataSize)); 150 | $this->position += $dataSize; 151 | 152 | return $data; 153 | } 154 | 155 | /** 156 | * @return int 157 | * @throws \ErrorException 158 | */ 159 | public function readInteger64() 160 | { 161 | $dataSize = 8; 162 | $this->assertBytesRemaining($dataSize); 163 | 164 | $data = XDRDecoder::signedInteger64(substr($this->xdrBytes, $this->position, $dataSize)); 165 | $this->position += $dataSize; 166 | 167 | return $data; 168 | } 169 | 170 | /** 171 | * @param $length 172 | * @return bool|string 173 | * @throws \ErrorException 174 | */ 175 | public function readOpaqueFixed($length) 176 | { 177 | $this->assertBytesRemaining($length); 178 | 179 | $data = XDRDecoder::opaqueFixed(substr($this->xdrBytes, $this->position), $length); 180 | $this->position += $length; 181 | 182 | return $data; 183 | } 184 | 185 | /** 186 | * @param $length 187 | * @return string 188 | * @throws \ErrorException 189 | */ 190 | public function readOpaqueFixedString($length) 191 | { 192 | $this->assertBytesRemaining($length); 193 | 194 | $data = XDRDecoder::opaqueFixedString(substr($this->xdrBytes, $this->position), $length); 195 | $this->position += $length; 196 | 197 | return $data; 198 | } 199 | 200 | /** 201 | * @return bool|string 202 | * @throws \ErrorException 203 | */ 204 | public function readOpaqueVariable($maxLength = null) 205 | { 206 | $length = $this->readUnsignedInteger(); 207 | $paddedLength = $this->roundTo4($length); 208 | 209 | if ($maxLength !== null && $length > $maxLength) { 210 | throw new \InvalidArgumentException(sprintf('length of %s exceeds max length of %s', $length, $maxLength)); 211 | } 212 | 213 | $this->assertBytesRemaining($paddedLength); 214 | 215 | $data = XDRDecoder::opaqueFixed(substr($this->xdrBytes, $this->position), $length); 216 | $this->position += $paddedLength; 217 | 218 | return $data; 219 | } 220 | 221 | /** 222 | * @param null $maxLength 223 | * @return bool|string 224 | * @throws \ErrorException 225 | */ 226 | public function readString($maxLength = null) 227 | { 228 | $strLen = $this->readUnsignedInteger(); 229 | $paddedLength = $this->roundTo4($strLen); 230 | if ($strLen > $maxLength) throw new \InvalidArgumentException(sprintf('maxLength of %s exceeded (string is %s bytes)', $maxLength, $strLen)); 231 | 232 | $this->assertBytesRemaining($paddedLength); 233 | 234 | $data = XDRDecoder::opaqueFixed(substr($this->xdrBytes, $this->position), $strLen); 235 | $this->position += $paddedLength; 236 | 237 | return $data; 238 | } 239 | 240 | /** 241 | * @return bool 242 | * @throws \ErrorException 243 | */ 244 | public function readBoolean() 245 | { 246 | $dataSize = 4; 247 | $this->assertBytesRemaining($dataSize); 248 | 249 | $data = XDRDecoder::boolean(substr($this->xdrBytes, $this->position, $dataSize)); 250 | $this->position += $dataSize; 251 | 252 | return $data; 253 | } 254 | 255 | /** 256 | * @param $numBytes 257 | * @throws \ErrorException 258 | */ 259 | protected function assertBytesRemaining($numBytes) 260 | { 261 | if ($this->position + $numBytes > strlen($this->xdrBytes)) { 262 | throw new \ErrorException('Unexpected end of XDR data'); 263 | } 264 | } 265 | 266 | public function getBytesRemaining() { 267 | return strlen($this->xdrBytes) - $this->position; 268 | } 269 | 270 | /** 271 | * rounds $number up to the nearest value that's a multiple of 4 272 | * 273 | * @param $number 274 | * @return int 275 | */ 276 | protected function roundTo4($number) 277 | { 278 | $remainder = $number % 4; 279 | if (!$remainder) return $number; 280 | 281 | return $number + (4 - $remainder); 282 | } 283 | } 284 | 285 | -------------------------------------------------------------------------------- /src/GalacticHorizon/XDREncoder.php: -------------------------------------------------------------------------------- 1 | $expectedLength) throw new \InvalidArgumentException(sprintf('Unexpected length for value. Has length %s, expected %s', strlen($value), $expectedLength)); 25 | if ($expectedLength && !$padUnexpectedLength && strlen($value) != $expectedLength) throw new \InvalidArgumentException(sprintf('Unexpected length for value. Has length %s, expected %s', strlen($value), $expectedLength)); 26 | 27 | if ($expectedLength && strlen($value) != $expectedLength) { 28 | $value = self::applyPadding($value, $expectedLength); 29 | } 30 | 31 | return self::applyPadding($value); 32 | } 33 | 34 | /** 35 | * Variable-length opaque data 36 | * 37 | * Maximum length is 2^32 - 1 38 | * 39 | * @param $value 40 | * @return string 41 | */ 42 | public static function opaqueVariable($value) 43 | { 44 | $maxLength = pow(2, 32) - 1; 45 | if (strlen($value) > $maxLength) throw new \InvalidArgumentException(sprintf('Value of length %s is greater than the maximum allowed length of %s', strlen($value), $maxLength)); 46 | 47 | $bytes = ''; 48 | 49 | $bytes .= self::unsignedInteger(strlen($value)); 50 | $bytes .= self::applyPadding($value); 51 | 52 | return $bytes; 53 | } 54 | 55 | public static function signedInteger($value) 56 | { 57 | // pack() does not support a signed 32-byte int, so work around this with 58 | // custom encoding 59 | return (self::nativeIsBigEndian()) ? pack('l', $value) : strrev(pack('l', $value)); 60 | } 61 | 62 | public static function unsignedInteger($value) 63 | { 64 | // unsigned 32-bit big-endian 65 | return pack('N', $value); 66 | } 67 | 68 | public static function signedInteger64($value) 69 | { 70 | // pack() does not support a signed 64-byte int, so work around this with 71 | // custom encoding 72 | return (self::nativeIsBigEndian()) ? pack('q', $value) : strrev(pack('q', $value)); 73 | } 74 | 75 | /** 76 | * Converts $value to a signed 8-byte big endian int64 77 | * 78 | * @param BigInteger $value 79 | * @return string 80 | */ 81 | public static function signedBigInteger64(BigInteger $value) 82 | { 83 | $xdrBytes = ''; 84 | $bigIntBytes = $value->toBytes(true); 85 | $bigIntBits = $value->toBits(true); 86 | 87 | // Special case: MAX_UINT_64 will look like 00ffffffffffffffff and have an 88 | // extra preceeding byte we need to get rid of 89 | if (strlen($bigIntBytes) === 9 && substr($value->toHex(true), 0, 2) === '00') { 90 | $bigIntBytes = substr($bigIntBytes, 1); 91 | } 92 | 93 | $paddingChar = chr(0); 94 | // If the number is negative, pad with 0xFF 95 | if (substr($bigIntBits, 0, 1) == 1) { 96 | $paddingChar = chr(255); 97 | } 98 | 99 | $paddingBytes = 8 - strlen($bigIntBytes); 100 | while ($paddingBytes > 0) { 101 | $xdrBytes .= $paddingChar; 102 | $paddingBytes--; 103 | } 104 | 105 | $xdrBytes .= $bigIntBytes; 106 | 107 | return XdrEncoder::opaqueFixed($xdrBytes, 8); 108 | } 109 | 110 | /** 111 | * Converts $value to an unsigned 8-byte big endian uint64 112 | * 113 | * @param BigInteger $value 114 | * @return string 115 | */ 116 | public static function unsignedBigInteger64(BigInteger $value) 117 | { 118 | $xdrBytes = ''; 119 | $bigIntBytes = $value->toBytes(true); 120 | 121 | // Special case: MAX_UINT_64 will look like 00ffffffffffffffff and have an 122 | // extra preceeding byte we need to get rid of 123 | if (strlen($bigIntBytes) === 9 && substr($value->toHex(true), 0, 2) === '00') { 124 | $bigIntBytes = substr($bigIntBytes, 1); 125 | } 126 | 127 | $paddingChar = chr(0); 128 | 129 | $paddingBytes = 8 - strlen($bigIntBytes); 130 | while ($paddingBytes > 0) { 131 | $xdrBytes .= $paddingChar; 132 | $paddingBytes--; 133 | } 134 | 135 | $xdrBytes .= $bigIntBytes; 136 | 137 | return XdrEncoder::opaqueFixed($xdrBytes, 8); 138 | } 139 | 140 | /** 141 | * Use this to write raw bytes representing a 64-bit integer 142 | * 143 | * This value will be padded up to 8 bytes 144 | * 145 | * @param $value 146 | * @return string 147 | */ 148 | public static function integer64RawBytes($value) 149 | { 150 | // Some libraries will give a 4-byte value here but it must be encoded 151 | // as 8 152 | return self::applyPadding($value, 8, false); 153 | } 154 | 155 | public static function unsignedInteger64($value) 156 | { 157 | if ($value > PHP_INT_MAX) throw new \InvalidArgumentException('value is greater than PHP_INT_MAX'); 158 | 159 | // unsigned 64-bit big-endian 160 | return pack('J', $value); 161 | } 162 | 163 | public static function signedHyper($value) 164 | { 165 | return self::signedInteger64($value); 166 | } 167 | 168 | public static function unsignedHyper($value) 169 | { 170 | return self::unsignedInteger64($value); 171 | } 172 | 173 | public static function unsignedInteger256($value) 174 | { 175 | return self::opaqueFixed($value, (256/8)); 176 | } 177 | 178 | public static function boolean($value) 179 | { 180 | // Equivalent to 1 or 0 uint32 181 | return ($value) ? self::unsignedInteger(1) : self::unsignedInteger(0); 182 | } 183 | 184 | /** 185 | * @param $value 186 | * @param null $maximumLength 187 | * @return string 188 | */ 189 | public static function string($value, $maximumLength = null) 190 | { 191 | if ($maximumLength === null) $maximumLength = pow(2, 32) - 1; 192 | 193 | if (strlen($value) > $maximumLength) throw new \InvalidArgumentException('string exceeds maximum length'); 194 | 195 | $bytes = self::unsignedInteger(strlen($value)); 196 | $bytes .= $value; 197 | 198 | // Pad with null bytes to get a multiple of 4 bytes 199 | $remainder = (strlen($value) % 4); 200 | if ($remainder) { 201 | while ($remainder < 4) { 202 | $bytes .= "\0"; 203 | $remainder++; 204 | } 205 | } 206 | 207 | return $bytes; 208 | } 209 | 210 | /** 211 | * @param $value 212 | * @return string 213 | */ 214 | public static function optionalUnsignedInteger($value) 215 | { 216 | $bytes = ''; 217 | 218 | if ($value !== null) { 219 | $bytes .= self::boolean(true); 220 | $bytes .= static::unsignedInteger($value); 221 | } 222 | else { 223 | $bytes .= self::boolean(false); 224 | } 225 | 226 | return $bytes; 227 | } 228 | 229 | /** 230 | * @param $value 231 | * @return string 232 | */ 233 | public static function optionalString($value, $maximumLength) 234 | { 235 | $bytes = ''; 236 | 237 | if ($value !== null) { 238 | $bytes .= self::boolean(true); 239 | $bytes .= static::string($value, $maximumLength); 240 | } 241 | else { 242 | $bytes .= self::boolean(false); 243 | } 244 | 245 | return $bytes; 246 | } 247 | 248 | /** 249 | * Ensures $value's length is a multiple of $targetLength bytes 250 | * 251 | * The default value for XDR is 4 252 | * 253 | * @param $value 254 | * @param $targetLength - desired length after padding is applied 255 | * @param $rightPadding - pad on the right of the value, false to pad to the left 256 | * @return string 257 | */ 258 | private static function applyPadding($value, $targetLength = 4, $rightPadding = true) 259 | { 260 | // No padding necessary if it's a multiple of 4 bytes 261 | if (strlen($value) % $targetLength === 0) return $value; 262 | 263 | $numPaddingChars = $targetLength - (strlen($value) % $targetLength); 264 | 265 | if ($rightPadding) { 266 | return $value . str_repeat(chr(0), $numPaddingChars); 267 | } 268 | else { 269 | return str_repeat(chr(0), $numPaddingChars) . $value; 270 | } 271 | } 272 | 273 | /** 274 | * @return bool 275 | */ 276 | private static function nativeIsBigEndian() 277 | { 278 | if (null === self::$nativeIsBigEndian) { 279 | self::$nativeIsBigEndian = pack('L', 1) === pack('N', 1); 280 | } 281 | 282 | return self::$nativeIsBigEndian; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Transaction.php: -------------------------------------------------------------------------------- 1 | sourceAccount = $sourceAccount; 29 | $this->timeBounds = new TimeBounds(); 30 | $this->memo = new Memo(); 31 | } 32 | 33 | public function addOperation(Operation $operation) { 34 | if (count($this->operations) == self::MAX_OPS_PER_TX) 35 | throw new \ErrorException("Maximum number of operations per transaction exceeded."); 36 | 37 | $this->operations[] = $operation; 38 | } 39 | 40 | public function setMemo(Memo $memo) { 41 | $this->memo = $memo; 42 | } 43 | 44 | public function getFee() { 45 | // Todo: load base fee from network but not really relevant now as it's a fixed fee of 100 stroops 46 | if ($this->fee === null) 47 | return 100 * count($this->operations); 48 | 49 | return $this->fee; 50 | } 51 | 52 | private function operationsToXDRBuffer(XDRBuffer &$buffer) { 53 | $buffer->writeUnsignedInteger(count($this->operations)); 54 | 55 | foreach($this->operations as $operation) 56 | $operation->toXDRBuffer($buffer); 57 | } 58 | 59 | private function signaturesToXDRBuffer(XDRBuffer &$buffer) { 60 | $buffer->writeUnsignedInteger(count($this->signatures)); 61 | 62 | foreach($this->signatures AS $signature) 63 | $signature->toXDRBuffer($buffer); 64 | } 65 | 66 | public static function fromXDRBuffer(XDRBuffer &$buffer) { 67 | $sourceAccount = Account::fromXDRBuffer($buffer); 68 | 69 | $transaction = new self($sourceAccount); 70 | 71 | $transaction->fee = $buffer->readUnsignedInteger(); 72 | 73 | $transaction->sequenceNumber = $buffer->readBigInteger(); 74 | 75 | if ($buffer->readBoolean()) 76 | $transaction->timeBounds = TimeBounds::fromXDRBuffer($buffer); 77 | 78 | $transaction->memo = Memo::fromXDRBuffer($buffer); 79 | 80 | $operationsCount = $buffer->readUnsignedInteger(); 81 | 82 | for($o=0; $o<$operationsCount; $o++) { 83 | $transaction->operations[] = OperationFactory::fromXDRBuffer($buffer); 84 | } 85 | 86 | $transactionExt = $buffer->readUnsignedInteger(); 87 | 88 | $signatureCount = $buffer->readUnsignedInteger(); 89 | 90 | if ($signatureCount > 0) { 91 | for($i=0; $i<$signatureCount; $i++) 92 | $transaction->signatures[] = DecoratedSignature::fromXDRBuffer($buffer); 93 | 94 | //throw new \ErrorException("TODO: Validate signatures."); 95 | } 96 | 97 | return $transaction; 98 | } 99 | 100 | public function hasValue() { return true; } 101 | 102 | public function toXDRBuffer(XDRBuffer &$buffer, $includeSignatures = true) { 103 | $this->sequenceNumber = $this->sourceAccount->getNextSequenceNumber(); 104 | 105 | // Account ID (36 bytes) 106 | $this->sourceAccount->toXDRBuffer($buffer); 107 | 108 | // Fee (4 bytes) 109 | $buffer->writeUnsignedInteger($this->getFee()); 110 | 111 | // Sequence number (8 bytes) 112 | $buffer->writeUnsignedBigInteger64($this->sequenceNumber); 113 | 114 | // Time Bounds are optional 115 | $buffer->writeOptional($this->timeBounds); 116 | 117 | // Memo (4 bytes if empty, 36 bytes maximum) 118 | $this->memo->toXDRBuffer($buffer); 119 | 120 | // Operations 121 | $this->operationsToXDRBuffer($buffer); 122 | 123 | // TransactionExt (union reserved for future use) 124 | $buffer->writeUnsignedInteger(0); 125 | 126 | if ($includeSignatures) 127 | $this->signaturesToXDRBuffer($buffer); 128 | } 129 | 130 | public function generateTransactionEnvelopeXDRBufferForTransaction(Transaction $transaction, $includeSignatures = true) { 131 | $xdrBuffer = new XDRBuffer(); 132 | $xdrBuffer->writeHash(Client::getInstance()->getNetworkPassphrase()); 133 | $xdrBuffer->writeUnsignedInteger(static::ENVELOPE_TYPE_TX); 134 | $transaction->toXDRBuffer($xdrBuffer, $includeSignatures); 135 | return $xdrBuffer; 136 | } 137 | 138 | public function clearSignatures() { 139 | $this->signatures = []; 140 | } 141 | 142 | public function sign(Array $signeeAccounts) { 143 | $this->signees = $signeeAccounts; 144 | 145 | $xdrBuffer = $this->generateTransactionEnvelopeXDRBufferForTransaction($this, false); 146 | 147 | $transactionHash = hash('sha256', $xdrBuffer->getRawBytes(), true); 148 | 149 | foreach($signeeAccounts AS $account) 150 | $this->signatures[] = $account->getKeypair()->signDecorated($transactionHash); 151 | } 152 | 153 | public function getOperationByType($type) { 154 | foreach($this->operations AS $operation) 155 | if ($operation->getType() == $type) 156 | return $operation; 157 | 158 | return null; 159 | } 160 | 161 | public function submit(Amount $automaticlyFixTrustLineWithAmount = null) { 162 | $xdrBuffer = new XDRBuffer(); 163 | $this->toXDRBuffer($xdrBuffer); 164 | 165 | $envelopeXDR = $xdrBuffer->toBase64String(); 166 | 167 | /* 168 | echo "\n --- envelopeXDR = $envelopeXDR\n\n"; 169 | 170 | echo " nu weer terug lezen:\n"; 171 | 172 | $bytes = base64_decode($envelopeXDR); 173 | 174 | $buffer = new XDRBuffer($bytes); 175 | 176 | $transaction = Transaction::fromXDRBuffer($buffer); 177 | 178 | echo $transaction . "\n"; 179 | exit(); 180 | */ 181 | 182 | $transactionResult = new TransactionResult(); 183 | 184 | try { 185 | Client::getInstance()->post( 186 | "transactions/", 187 | Array( 188 | "tx" => $envelopeXDR 189 | ), 190 | function($data) use (&$transactionResult) { 191 | //echo "XDR = \n" . $data->result_xdr . "\n\n"; 192 | 193 | $resultXDRBuffer = XDRBuffer::fromBase32String($data->result_xdr); 194 | 195 | $transactionResult = TransactionResult::fromXDRBuffer($resultXDRBuffer); 196 | $transactionResult->setHash($data->hash); 197 | $transactionResult->setLedger($data->ledger); 198 | $transactionResult->setEnvelopeXDRString($data->envelope_xdr); 199 | } 200 | ); 201 | } catch(\GalacticHorizon\Exception $e) { 202 | $json = @json_decode($e->getHttpResponseBody()); 203 | 204 | if ($json && isset($json->extras)) { 205 | if (isset($json->extras->result_xdr)) { 206 | $resultXDRBuffer = XDRBuffer::fromBase32String($json->extras->result_xdr); 207 | 208 | $transactionResult = TransactionResult::fromXDRBuffer($resultXDRBuffer); 209 | $transactionResult->setEnvelopeXDRString($envelopeXDR); 210 | 211 | //echo " response = \n" . (string)$transactionResult . "\n\n"; 212 | 213 | if ($automaticlyFixTrustLineWithAmount !== null) { 214 | $asset = null; 215 | 216 | if ($transactionResult->getResultCount() > 0) { 217 | if ($transactionResult->getResult(0)->getErrorCode() == ManageOfferOperationResult::MANAGE_BUY_OFFER_BUY_NO_TRUST) { 218 | $manageOfferOperation = $this->getOperationByType(Operation::TYPE_MANAGE_OFFER); 219 | 220 | if ($manageOfferOperation) 221 | $asset = $manageOfferOperation->getBuyingAsset(); 222 | } else if ($transactionResult->getResult(0)->getErrorCode() == ManageOfferOperationResult::MANAGE_BUY_OFFER_SELL_NO_TRUST) { 223 | $manageOfferOperation = $this->getOperationByType(Operation::TYPE_MANAGE_OFFER); 224 | 225 | if ($manageOfferOperation) 226 | $asset = $manageOfferOperation->getSellingAsset(); 227 | } 228 | } 229 | 230 | if ($asset) { 231 | if ($this->createTrustLine($asset, $automaticlyFixTrustLineWithAmount)) { 232 | 233 | // Resign 234 | $this->clearSignatures(); 235 | $this->sign($this->signees); 236 | 237 | // Try to submit again but do not try again after that 238 | return $this->submit(null); 239 | } 240 | } 241 | } 242 | } 243 | } 244 | 245 | if ($transactionResult == null) { 246 | // Rethrow the exception, can't do anything here with this response 247 | throw $e; 248 | } 249 | } 250 | 251 | return $transactionResult; 252 | } 253 | 254 | public function createTrustLine(Asset $asset, Amount $limit) { 255 | $operation = new \GalacticHorizon\ChangeTrustOperation(); 256 | $operation->setAsset($asset); 257 | $operation->setLimit($limit); 258 | 259 | $transaction = new self($this->sourceAccount); 260 | $transaction->addOperation($operation); 261 | 262 | $transaction->sign($this->signees); 263 | 264 | $transactionResult = $transaction->submit(); 265 | 266 | if ($transactionResult->getErrorCode() == TransactionResult::TX_SUCCESS) { 267 | return true; 268 | } 269 | 270 | return false; 271 | } 272 | 273 | public function __tostring() { 274 | $str = []; 275 | $str[] = "(" . get_class($this) . ")"; 276 | $str[] = " - Account = " . $this->sourceAccount; 277 | $str[] = " - Fee = " . $this->getFee(); 278 | $str[] = " - Sequence number = " . $this->sequenceNumber->toString(); 279 | $str[] = " - Time bounds = " . $this->timeBounds; 280 | $str[] = " - Memo = " . $this->memo; 281 | $str[] = " - Operations (count: " . count($this->operations) . "):"; 282 | 283 | foreach($this->operations AS $operation) 284 | $str[] = IncreaseDepth((string)$operation); 285 | 286 | $str[] = " - Signatures (count: " . count($this->signatures) . "):"; 287 | 288 | foreach($this->signatures AS $signature) 289 | $str[] = IncreaseDepth("* " . (string)$signature); 290 | 291 | return implode("\n", $str); 292 | } 293 | 294 | } 295 | 296 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Client.php: -------------------------------------------------------------------------------- 1 | URL = $URL; 23 | $this->isTestNet = $isTestNet; 24 | $this->networkPassphrase = $isTestNet ? self::NETWORK_PASSPHRASE_TEST : self::NETWORK_PASSPHRASE_PUBLIC; 25 | $this->httpClient = new \GuzzleHttp\Client(); 26 | } 27 | 28 | static function getInstance() { 29 | return self::$instance; 30 | } 31 | 32 | static function createTestNetClient() { 33 | self::$instance = new self("https://horizon-testnet.stellar.org/", true); 34 | 35 | return self::$instance; 36 | } 37 | 38 | static function createPublicClient() { 39 | self::$instance = new self("https://horizon.stellar.org/", false); 40 | 41 | return self::$instance; 42 | } 43 | 44 | static function createTemporaryPublicClient() { 45 | return new self("https://horizon.stellar.org/", false); 46 | } 47 | 48 | public function getNetworkPassphrase() { 49 | return $this->networkPassphrase; 50 | } 51 | 52 | public function fundTestAccount(Account $account) { 53 | $response = $this->httpClient->request("GET", sprintf("https://friendbot.stellar.org/?addr=%s", $account->getKeypair()->getPublicKey())); 54 | 55 | if ($response->getStatusCode() == 200) { 56 | $body = (string)$response->getBody(); 57 | 58 | var_dump( json_decode($body) ); 59 | } 60 | } 61 | 62 | public function get($URL, Array $data, callable $callback) { 63 | if ($data) 64 | $URL .= '?' . http_build_query($data); 65 | 66 | return $this->request("GET", $URL, [], $callback); 67 | } 68 | 69 | public function post($URL, Array $data, callable $callback) { 70 | return $this->request("POST", $URL, $data, $callback); 71 | } 72 | 73 | public function request($type, $URL, Array $data, callable $callback) { 74 | $result = false; 75 | 76 | try { 77 | $response = $this->httpClient->request( 78 | $type, 79 | sprintf("%s%s", $this->URL, $URL), 80 | [ 81 | "form_params" => $data 82 | ] 83 | ); 84 | 85 | if ($response->getStatusCode() == 200) { 86 | $body = (string)$response->getBody(); 87 | 88 | $callback(json_decode($body)); 89 | 90 | $result = true; 91 | } 92 | } catch(\GuzzleHttp\Exception\ServerException $e) { 93 | throw \GalacticHorizon\Exception::create( 94 | \GalacticHorizon\Exception::TYPE_SERVER_ERROR, 95 | null, 96 | $e 97 | ); 98 | } catch(\GuzzleHttp\Exception\ClientException $e) { 99 | if ($e->getResponse()->getStatusCode() == 404) 100 | { 101 | } 102 | else 103 | { 104 | throw \GalacticHorizon\Exception::create( 105 | \GalacticHorizon\Exception::TYPE_REQUEST_ERROR, 106 | null, 107 | $e 108 | ); 109 | } 110 | } 111 | 112 | return $result; 113 | } 114 | 115 | public function callback($curl, $inData) 116 | { 117 | //echo "[INDATA] " . $inData; 118 | 119 | $this->inBuffer .= $inData; 120 | 121 | while(strlen($this->inBuffer) > 0) { 122 | $ch = $this->inBuffer[0]; 123 | $this->inBuffer = substr($this->inBuffer, 1); 124 | 125 | $this->lineBuffer .= $ch; 126 | 127 | if ($ch == "\n") { 128 | $line = $this->lineBuffer; 129 | $this->lineBuffer = ""; 130 | 131 | if (!$line) 132 | continue; 133 | 134 | //echo "[LINE] " . $line . "\n"; 135 | 136 | // Ignore "data: hello" handshake 137 | if (strpos($line, "data: \"hello\"") === 0) 138 | continue; 139 | 140 | if (strpos($line, "retry: ") === 0) 141 | $this->retryAfterTime = (int)substr($line, strlen('retry: ')); 142 | 143 | if (strpos($line, "data: \"byebye\"") === 0) { 144 | //$openConnection = false; 145 | continue; 146 | } 147 | 148 | if (strpos($line, "id: ") === 0) 149 | { 150 | $this->cursor = trim(substr($line, strlen("id: "))); 151 | //var_dump("cursor changed to: ", $this->cursor); 152 | continue; 153 | } 154 | 155 | // Ignore lines that don't start with "data: " 156 | $sentinel = "data: "; 157 | 158 | if (strpos($line, $sentinel) !== 0) 159 | continue; 160 | 161 | // Remove sentinel prefix 162 | $json = trim(substr($line, strlen($sentinel))); 163 | $decoded = self::arrayToObject(json_decode($json, true)); 164 | 165 | //var_dump("json = ", $json); 166 | 167 | if (is_object($decoded)) { 168 | $callback = $this->streamCallback; 169 | 170 | try 171 | { 172 | $callback($this->cursor, $decoded); 173 | } 174 | catch(Exception $e) 175 | { 176 | var_dump("Exception while running callback:", $e); 177 | } 178 | } 179 | } 180 | } 181 | 182 | return strlen($inData); 183 | } 184 | 185 | public function stream($URL, Array $data, callable $callback, $automaticlyReconnect = true) 186 | { 187 | $this->retryAfterTime = 1000; 188 | $this->streamCallback = $callback; 189 | $this->cursor = $data["cursor"]; 190 | $this->inBuffer = ""; 191 | $this->lineBuffer = ""; 192 | 193 | // . "?cursor=102837532999315457-0" 194 | 195 | while(1) 196 | { 197 | try 198 | { 199 | $data["cursor"] = $this->cursor; 200 | 201 | $curl = curl_init(); 202 | curl_setopt_array( 203 | $curl, array( 204 | CURLOPT_URL => $this->URL . $URL . "?" . http_build_query($data), 205 | CURLOPT_HEADER => 0, 206 | CURLOPT_WRITEFUNCTION => array($this, "callback"), 207 | CURLOPT_HTTPHEADER => [ 208 | 'Accept: text/event-stream', 209 | ], 210 | //CURLOPT_POST => 1, 211 | //CURLOPT_POSTFIELDS => $data 212 | ) 213 | ); 214 | 215 | //curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 216 | var_dump($this->URL . $URL . "?" . http_build_query($data), $data); 217 | 218 | curl_exec($curl); 219 | 220 | if (curl_errno($curl)){ 221 | var_dump(" Error: ", curl_error($curl)); 222 | } 223 | 224 | curl_close($curl); 225 | } 226 | catch (Exception $e) 227 | { 228 | var_dump("Exception = ", $e); 229 | } 230 | 231 | sleep($this->retryAfterTime/1000); 232 | } 233 | 234 | /* 235 | exit(); 236 | 237 | while(1) { 238 | $response = $this->httpClient->get( 239 | sprintf("%s%s", $this->URL, $URL), 240 | [ 241 | "stream" => true, 242 | "read_timeout" => null, 243 | "headers" => [ 244 | 'Accept' => 'text/event-stream', 245 | ], 246 | "form_params" => $data 247 | ] 248 | ); 249 | 250 | // var_dump($URL, $data); 251 | // exit(); 252 | 253 | $body = $response->getBody(); 254 | 255 | //while(!$body->eof()) { 256 | while(1) { 257 | $line = ''; 258 | $char = null; 259 | 260 | echo "."; 261 | 262 | //while ($char != "\n") { 263 | 264 | $char = $body->read(1); 265 | 266 | while(strlen($char) > 0) { 267 | $line .= $char; 268 | $char = $body->read(1); 269 | echo ","; 270 | } 271 | 272 | if (!$line) { 273 | sleep(1); 274 | continue; 275 | } 276 | 277 | echo "[INCOMMING]: " . $line . "\n"; 278 | 279 | 280 | 281 | } 282 | 283 | if ($automaticlyReconnect) { 284 | var_dump("waiting: ", $retryAfterTime); 285 | 286 | sleep($retryAfterTime / 1000); 287 | } else { 288 | return false; 289 | } 290 | } 291 | } catch(\GuzzleHttp\Exception\ServerException $e) { 292 | throw \GalacticHorizon\Exception::create( 293 | \GalacticHorizon\Exception::TYPE_SERVER_ERROR, 294 | null, 295 | $e 296 | ); 297 | } catch(\GuzzleHttp\Exception\ClientException $e) { 298 | throw \GalacticHorizon\Exception::create( 299 | \GalacticHorizon\Exception::TYPE_REQUEST_ERROR, 300 | null, 301 | $e 302 | ); 303 | } 304 | */ 305 | } 306 | 307 | static function arrayToObject($data) { 308 | if (is_array($data)) { 309 | $numericIndices = false; 310 | 311 | foreach($data AS $i => $v) 312 | if (is_numeric($i)) 313 | $numericIndices = true; 314 | 315 | if (!$numericIndices) { 316 | $data = (Object)$data; 317 | } 318 | } 319 | 320 | if (is_array($data)) 321 | foreach($data AS $k => $v) 322 | $data[$k] = self::arrayToObject($v); 323 | else if (is_object($data)) 324 | foreach($data AS $k => $v) 325 | $data->$k = self::arrayToObject($v); 326 | 327 | return $data; 328 | } 329 | 330 | static function setAssetParametersAs(Asset $asset, Array &$arguments, $asType) { 331 | if ($asset->getType() == Asset::TYPE_NATIVE) { 332 | $arguments[$asType . "_asset_type"] = "native"; 333 | } else { 334 | if ($asset->getType() == Asset::TYPE_ALPHANUM_4) 335 | $arguments[$asType . "_asset_type"] = "credit_alphanum4"; 336 | else 337 | $arguments[$asType . "_asset_type"] = "credit_alphanum12"; 338 | 339 | $arguments[$asType . "_asset_code"] = $asset->getCode(); 340 | $arguments[$asType . "_asset_issuer"] = $asset->getIssuer()->getKeypair()->getPublicKey(); 341 | } 342 | } 343 | 344 | public function getOrderbookForAssetPair(Asset $sellingAsset, Asset $buyingAsset, $limit = null) { 345 | $params = []; 346 | 347 | self::setAssetParametersAs($sellingAsset, $params, "selling"); 348 | self::setAssetParametersAs($buyingAsset, $params, "buying"); 349 | 350 | if ($limit) 351 | $params['limit'] = $limit; 352 | 353 | $response = null; 354 | 355 | $this->get( 356 | "order_book/", 357 | $params, 358 | function($data) use (&$response) { 359 | $response = $data; 360 | } 361 | ); 362 | 363 | return $response; 364 | } 365 | 366 | public function getTradeAggregations(Asset $baseAsset, Asset $counterAsset, \DateTime $start, \DateTime $end, $resolution, $order = null) { 367 | $start = $start->format("U") * 1000; 368 | $end = $end->format("U") * 1000; 369 | 370 | $limit = ($end - $start) / ($resolution / 1000); 371 | $limit = round($limit); 372 | 373 | // Limit to 100 which seems to be the max 374 | $limit = min(100, $limit); 375 | 376 | $params = []; 377 | 378 | self::setAssetParametersAs($baseAsset, $params, "base"); 379 | self::setAssetParametersAs($counterAsset, $params, "counter"); 380 | 381 | $params['start_time'] = $start; 382 | $params['end_time'] = $end; 383 | $params['resolution'] = $resolution; 384 | 385 | if ($limit) 386 | $params['limit'] = $limit; 387 | 388 | if ($order) 389 | $params['order'] = $order; 390 | 391 | $response = null; 392 | 393 | $this->get( 394 | "trade_aggregations/", 395 | $params, 396 | function($data) use (&$response) { 397 | $response = $data->_embedded->records; 398 | } 399 | ); 400 | 401 | return $response; 402 | } 403 | 404 | } 405 | 406 | -------------------------------------------------------------------------------- /src/GalacticHorizon/Account.php: -------------------------------------------------------------------------------- 1 | sequence = new BigInteger(0); 31 | $this->keypair = $keypair; 32 | $this->accountIDBytes = $this->keypair ? self::getRawBytesFromBase32AccountId($this->keypair->getPublicKey()) : null; 33 | } 34 | 35 | public static function createFromKeyPair(Keypair $keypair) { 36 | return new self($keypair); 37 | } 38 | 39 | public static function createFromSecretKey(string $publicKey) { 40 | return new self(Keypair::createFromSecretKey($publicKey)); 41 | } 42 | 43 | public static function createFromPublicKey(string $publicKey) { 44 | return new self(Keypair::createFromPublicKey($publicKey)); 45 | } 46 | 47 | private static function getRawBytesFromBase32AccountId($base32AccountId) { 48 | $decoded = Base32::decode($base32AccountId); 49 | 50 | // Unpack version byte 51 | $unpacked = unpack('Cversion', substr($decoded, 0, 1)); 52 | $version = $unpacked['version']; 53 | $payload = substr($decoded, 1, strlen($decoded) - 3); 54 | $checksum = substr($decoded, -2); 55 | 56 | // Verify version 57 | if ($version != self::VERSION_BYTE_ACCOUNT_ID) { 58 | $msg = 'Invalid account ID version.'; 59 | 60 | if ($version == self::VERSION_BYTE_SEED) 61 | $msg .= ' Got a private key and expected a public account ID'; 62 | 63 | throw new \InvalidArgumentException($msg); 64 | } 65 | 66 | // Verify checksum of version + payload 67 | if (!Checksum::verify($checksum, substr($decoded, 0, -2))) 68 | throw new \InvalidArgumentException('Invalid checksum'); 69 | 70 | return $payload; 71 | } 72 | 73 | public function getMinimumBalance() { 74 | // Base reserve 75 | $minimumBalance = 1; 76 | 77 | foreach($this->balances AS $balance) 78 | $minimumBalance += 0.5; 79 | 80 | $signers = count($this->signers)-1; 81 | 82 | $minimumBalance += 0.5 * $signers; 83 | 84 | foreach($this->data AS $dataEntry) 85 | $minimumBalance += 0.5; 86 | 87 | return $minimumBalance; 88 | } 89 | 90 | public function getBalances() { 91 | return $this->balances; 92 | } 93 | 94 | public function getSigners() { 95 | return $this->signers; 96 | } 97 | 98 | public function getData() { 99 | return $this->data; 100 | } 101 | 102 | public function getKeypair() { 103 | return $this->keypair; 104 | } 105 | 106 | public function getPublicKey() { 107 | return $this->keypair->getPublicKey(); 108 | } 109 | 110 | public function fetch() { 111 | $result = false; 112 | 113 | Client::getInstance()->get( 114 | sprintf("accounts/%s", $this->keypair->getPublicKey()), 115 | [], 116 | function($data) use (&$result) { 117 | $this->sequence = new BigInteger($data->sequence); 118 | $this->balances = []; 119 | 120 | foreach($data->balances AS $balance) 121 | $this->balances[] = AccountBalance::fromJSON($balance); 122 | 123 | $this->signers = []; 124 | 125 | foreach($data->signers AS $signer) 126 | $this->signers[] = AccountSigner::fromJSON($signer); 127 | 128 | $this->data = (Array)$data->data; 129 | 130 | if (isset($data->low_threshold)) { 131 | $this->thresholds = (object)Array( 132 | "low" => $data->low_threshold, 133 | "medium" => $data->med_threshold, 134 | "high" => $data->high_threshold 135 | ); 136 | } else { 137 | $this->thresholds = null; 138 | } 139 | 140 | $result = true; 141 | } 142 | ); 143 | 144 | return $result; 145 | } 146 | 147 | public function getNextSequenceNumber() { 148 | $this->fetch(); 149 | 150 | $this->sequence = $this->sequence->add(new BigInteger(1)); 151 | 152 | return $this->sequence; 153 | } 154 | 155 | public function hasValue() { return $this->keypair !== null; } 156 | 157 | public function toXDRBuffer(XDRBuffer &$buffer) { 158 | $buffer->writeUnsignedInteger(self::KEY_TYPE_ED25519); 159 | $buffer->writeOpaqueFixed($this->accountIDBytes); 160 | } 161 | 162 | static public function fromXDRBuffer(XDRBuffer &$buffer) { 163 | $keyType = $buffer->readUnsignedInteger(); 164 | $key = $buffer->readOpaqueFixed(32); 165 | 166 | $accountID = AddressableKey::addressFromRawBytes($key); 167 | 168 | $keypair = Keypair::createFromPublicKey($accountID); 169 | 170 | $o = new self($keypair); 171 | return $o; 172 | } 173 | 174 | public function getTrades($cursor = "now", $order = "asc") { 175 | $trades = null; 176 | 177 | Client::getInstance()->get( 178 | sprintf("accounts/%s/trades", $this->keypair->getPublicKey()), 179 | [ 180 | "cursor" => $cursor, 181 | "order" => $order 182 | ], 183 | function($data) use (&$trades) { 184 | $trades = []; 185 | 186 | foreach($data->_embedded->records AS $record) { 187 | $trades[] = Trade::createFromJSON($record); 188 | } 189 | } 190 | ); 191 | 192 | return $trades; 193 | } 194 | 195 | public function getTradesStreaming($cursor, \Closure $callback, $order = "asc") { 196 | $trades = null; 197 | 198 | Client::getInstance()->stream( 199 | sprintf("accounts/%s/trades", $this->keypair->getPublicKey()), 200 | [ 201 | "cursor" => $cursor, 202 | "order" => $order, 203 | ], 204 | function($ID, $data) use (&$callback) { 205 | $callback($ID, Trade::createFromJSON($data)); 206 | } 207 | ); 208 | 209 | return $trades; 210 | } 211 | 212 | public function getOffers($cursor = "now", $order = "asc") { 213 | $offers = null; 214 | 215 | Client::getInstance()->get( 216 | sprintf("accounts/%s/offers", $this->keypair->getPublicKey()), 217 | [ 218 | "cursor" => $cursor, 219 | "order" => $order 220 | ], 221 | function($data) use (&$offers) { 222 | $offers = []; 223 | 224 | foreach($data->_embedded->records AS $record) { 225 | $offers[] = Offer::createFromJSON($record); 226 | } 227 | } 228 | ); 229 | 230 | return $offers; 231 | } 232 | 233 | public function getOffersStreaming($cursor, \Closure $callback) { 234 | $trades = null; 235 | 236 | Client::getInstance()->stream( 237 | sprintf("accounts/%s/offers", $this->keypair->getPublicKey()), 238 | [ 239 | "cursor" => $cursor 240 | ], 241 | function($data) use (&$callback) { 242 | $callback(Offer::createFromJSON($data)); 243 | } 244 | ); 245 | 246 | return $trades; 247 | } 248 | 249 | public function __tostring() { 250 | $str = []; 251 | $str[] = "(" . get_class($this) . ") " . ($this->keypair === null ? "null" : $this->keypair->getPublicKey()); 252 | $str[] = " - Balances (count: " . count($this->balances) . "):"; 253 | 254 | foreach($this->balances AS $balance) 255 | $str[] = IncreaseDepth((string)$balance); 256 | 257 | $str[] = " - Signers (count: " . count($this->signers) . "):"; 258 | 259 | foreach($this->signers AS $signer) 260 | $str[] = IncreaseDepth((string)$signer); 261 | 262 | $str[] = " - Data entries (count: " . count($this->data) . "):"; 263 | 264 | foreach($this->data AS $k => $v) 265 | $str[] = "{$k} = {$v}"; 266 | 267 | return implode("\n", $str); 268 | } 269 | 270 | } 271 | 272 | class Offer 273 | { 274 | 275 | private $ID; 276 | 277 | private $seller, $sellingAsset; 278 | 279 | private $buyingAsset; 280 | 281 | private $buyingAmount; 282 | 283 | private $price; 284 | 285 | public function getID() { return $this->ID; } 286 | 287 | static function createFromJSON(object $data) { 288 | $o = new self(); 289 | $o->ID = $data->id; 290 | 291 | $o->seller = Account::createFromPublicKey($data->seller); 292 | 293 | if ($data->selling->asset_type == "native") 294 | $o->sellingAsset = Asset::createNative(); 295 | else 296 | $o->sellingAsset = Asset::createFromCodeAndIssuer($data->selling->asset_code, Account::createFromPublicKey($data->selling->asset_issuer)); 297 | 298 | if ($data->buying->asset_type == "native") 299 | $o->buyingAsset = Asset::createNative(); 300 | else 301 | $o->buyingAsset = Asset::createFromCodeAndIssuer($data->buying->asset_code, Account::createFromPublicKey($data->buying->asset_issuer)); 302 | 303 | $o->buyingAmount = Amount::createFromFloat($data->amount); 304 | 305 | $o->price = Price::createFromFloat($data->price); 306 | 307 | return $o; 308 | } 309 | 310 | } 311 | 312 | class Trade 313 | { 314 | 315 | private $ID, $offerID, $operationID; 316 | 317 | private $ledgerCloseTime; 318 | 319 | private $baseAccount, $baseAmount, $baseAsset; 320 | 321 | private $counterOfferID, $counterAccount, $counterAmount, $counterAsset; 322 | 323 | private $baseIsSeller; 324 | 325 | private $price; 326 | 327 | public function getID() { return $this->ID; } 328 | public function getOfferID() { return $this->offerID; } 329 | public function getCounterOfferID() { return $this->counterOfferID; } 330 | 331 | public function getLedgerCloseTime() { return $this->ledgerCloseTime; } 332 | 333 | public function getBaseAccount() { return $this->baseAccount; } 334 | public function getBaseAmount() { return $this->baseAmount; } 335 | public function getBaseAsset() { return $this->baseAsset; } 336 | public function getBaseIsSeller() { return $this->baseIsSeller; } 337 | 338 | public function getCounterAccount() { return $this->counterAccount; } 339 | public function getCounterAsset() { return $this->counterAsset; } 340 | public function getCounterAmount() { return $this->counterAmount; } 341 | 342 | public function getPrice() { return $this->price; } 343 | 344 | public function getOperationID() { return $this->operationID; } 345 | 346 | static function createFromJSON(object $data) { 347 | $o = new self(); 348 | $o->ID = $data->id; 349 | $o->offerID = $data->offer_id; 350 | 351 | $date = date_parse($data->ledger_close_time); 352 | 353 | $o->ledgerCloseTime = new \DateTime($date["year"] . "-" . $date["month"] . "-" . $date["day"] . " " . $date["hour"] . ":" . $date["minute"] . ":" . $date["second"], null); 354 | 355 | $o->baseAccount = Account::createFromPublicKey($data->base_account); 356 | $o->baseAmount = Amount::createFromFloat($data->base_amount); 357 | 358 | if ($data->base_asset_type == "native") 359 | $o->baseAsset = Asset::createNative(); 360 | else 361 | $o->baseAsset = Asset::createFromCodeAndIssuer($data->base_asset_code, Account::createFromPublicKey($data->base_asset_issuer)); 362 | 363 | $o->counterOfferID = $data->counter_offer_id; 364 | $o->counterAccount = Account::createFromPublicKey($data->counter_account); 365 | $o->counterAmount = Amount::createFromFloat($data->counter_amount); 366 | 367 | if ($data->counter_asset_type == "native") 368 | $o->counterAsset = Asset::createNative(); 369 | else 370 | $o->counterAsset = Asset::createFromCodeAndIssuer($data->counter_asset_code, Account::createFromPublicKey($data->counter_asset_issuer)); 371 | 372 | $o->baseIsSeller = $data->base_is_seller ? true : false; 373 | 374 | $o->price = new Price($data->price->n, $data->price->d); 375 | 376 | $parts = explode("/", $data->_links->operation->href); 377 | $o->operationID = array_pop($parts); 378 | 379 | return $o; 380 | 381 | /* 382 | ["base_account"]=> 383 | string(56) "" 384 | [""]=> 385 | string(9) "9.8728614" 386 | [""]=> 387 | string(6) "native" 388 | [""]=> 389 | string(19) "4710550934311165953" 390 | [""]=> 391 | string(56) "GCILEORWFS6PKGCXUVC73TKTPPHXADNQFLVGKVWLAW4FJJTCXNE6DGB7" 392 | [""]=> 393 | string(9) "0.2070182" 394 | [""]=> 395 | string(16) "credit_alphanum4" 396 | [""]=> 397 | string(3) "SLT" 398 | [""]=> 399 | string(56) "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP" 400 | [""]=> 401 | bool(true) 402 | ["price"]=> 403 | object(stdClass)#1237 (2) { 404 | ["n"]=> 405 | int(152) 406 | ["d"]=> 407 | int(7249) 408 | */ 409 | } 410 | 411 | } 412 | 413 | -------------------------------------------------------------------------------- /src/Trade.php: -------------------------------------------------------------------------------- 1 | state == self::STATE_CREATED; 36 | } 37 | 38 | function setData(Array $data) 39 | { 40 | $vars = get_object_vars($this); 41 | 42 | foreach($vars AS $k => $v) 43 | $this->$k = $data[$k]; 44 | } 45 | 46 | function getData() 47 | { 48 | $data = []; 49 | $vars = get_object_vars($this); 50 | 51 | foreach($vars AS $k => $v) 52 | $data[$k] = $this->$k; 53 | 54 | return $data; 55 | } 56 | 57 | function getID() { return $this->ID; } 58 | function setID($ID) { $this->ID = $ID; } 59 | 60 | function getState() { return $this->state; } 61 | 62 | function getStateInfo() { 63 | $state = $this->getState(); 64 | $label = "Unknown state ($state)"; 65 | 66 | switch($state) 67 | { 68 | case self::STATE_CREATED: $label = "Created"; break; 69 | case self::STATE_REPLACED: $label = "Replaced"; break; 70 | case self::STATE_CANCELLED: $label = "Cancelled"; break; 71 | case self::STATE_FILLED: $label = "Filled"; break; 72 | } 73 | 74 | return Array( 75 | "state" => $state, 76 | "label" => $label 77 | ); 78 | } 79 | 80 | function setState($state) { $this->state = $state; } 81 | 82 | function setProcessedAt(\DateTime $date) { $this->processedAt = $date->format("Y-m-d H:i:s"); } 83 | 84 | function setPreviousBotTradeID($previousBotTradeID) { $this->previousBotTradeID = $previousBotTradeID; } 85 | function getPreviousBotTradeID() { return $this->previousBotTradeID; } 86 | 87 | function getOfferID() { return $this->offerID; } 88 | 89 | function getType() { return $this->type; } 90 | 91 | function getPrice() { return $this->price; } 92 | function getSellAmount() { return $this->sellAmount; } 93 | function getPaidPrice() { return $this->paidPrice; } 94 | function getBoughtAmount() { return $this->boughtAmount; } 95 | function getSpentAmount() { return $this->spentAmount; } 96 | function getAmountRemaining() { return $this->amountRemaining; } 97 | 98 | function getCreatedAt() { global $_BASETIMEZONE; return new \DateTime($this->createdAt, $_BASETIMEZONE); } 99 | function getUpdatedAt() { global $_BASETIMEZONE; return new \DateTime($this->updatedAt, $_BASETIMEZONE); } 100 | function getProcessedAt() { return new \DateTime($this->processedAt); } 101 | 102 | function getFillPercentage() { return $this->fillPercentage; } 103 | 104 | function getIsFilledCompletely() 105 | { 106 | return $this->state == self::STATE_FILLED; 107 | } 108 | 109 | function getAgeInMinutes(Time $time) 110 | { 111 | $processedAt = Time::fromDateTime($this->getProcessedAt()); 112 | return $processedAt->getAgeInMinutes($time); 113 | } 114 | 115 | function simulate($type, Bot $bot, Time $processingTime, \ZuluCrypto\StellarSdk\XdrModel\Asset $sellingAsset, $sellingAmount, \ZuluCrypto\StellarSdk\XdrModel\Asset $buyingAsset) 116 | { 117 | $price = $bot->getDataInterface()->getAssetValueForTime($processingTime); 118 | 119 | $this->type = $type; 120 | $this->offerID = "SIM_" . time(); 121 | 122 | $this->amountRemaining = 0; 123 | $this->price = 1/$price; 124 | $this->paidPrice = $this->price; 125 | $this->fillPercentage = 100; 126 | $this->state = self::STATE_FILLED; 127 | 128 | $this->processedAt = $processingTime->toString(); 129 | 130 | $claimedOffers = Array(); 131 | 132 | if ($type == self::TYPE_BUY) 133 | { 134 | $this->sellAmount = $sellingAmount; 135 | $this->fee = 0.00001; 136 | $this->spentAmount = $sellingAmount + $this->fee; 137 | 138 | $claimedOffers[] = Array( 139 | "offerID" => $this->offerID, 140 | 141 | "sellingAssetType" => $buyingAsset->getType(), 142 | "sellingAssetCode" => $buyingAsset->getCode(), 143 | 144 | "sellingAmount" => $this->sellAmount * $price, 145 | 146 | "buyingAssetType" => $sellingAsset->getType(), 147 | "buyingAssetCode" => $sellingAsset->getCode(), 148 | ); 149 | } 150 | else 151 | { 152 | $this->sellAmount = $sellingAmount; 153 | $this->fee = 0.00001; 154 | $this->spentAmount = $sellingAmount + $this->fee; 155 | 156 | $claimedOffers[] = Array( 157 | "offerID" => $this->offerID, 158 | 159 | "sellingAssetType" => $sellingAsset->getType(), 160 | "sellingAssetCode" => $sellingAsset->getCode(), 161 | 162 | "sellingAmount" => $sellingAmount * (1/$price), 163 | 164 | "buyingAssetType" => $buyingAsset->getType(), 165 | "buyingAssetCode" => $buyingAsset->getCode(), 166 | ); 167 | } 168 | 169 | $this->boughtAmount = $claimedOffers[0]["sellingAmount"]; 170 | 171 | $this->claimedOffers = json_encode($claimedOffers); 172 | } 173 | 174 | function clearClaimedOffers() 175 | { 176 | $this->claimedOffers = null; 177 | } 178 | 179 | function addCompletedHorizonTradeForBot(\GalacticHorizon\Trade $trade, $bot) 180 | { 181 | if ($this->state == self::STATE_FILLED) 182 | return; 183 | 184 | $claimedOffers = @json_decode($this->claimedOffers); 185 | 186 | if (!$claimedOffers) 187 | $claimedOffers = []; 188 | else 189 | $claimedOffers = (Array)$claimedOffers; 190 | 191 | if (1) 192 | { 193 | $baseIsBot = $trade->getBaseAccount()->getPublicKey() == $bot->getSettings()->getAccountPublicKey(); 194 | $otherAccountID = $baseIsBot ? $trade->getCounterAccount()->getPublicKey() : $trade->getBaseAccount()->getPublicKey(); 195 | $uniqueID = $trade->getOfferID() . "_" . $trade->getCounterOfferID() . "_" . $otherAccountID . "_" . number_format($trade->getCounterAmount()->toFloat(), 7, '.', ''); 196 | 197 | $info = Array( 198 | "offerID" => $trade->getOfferID(), 199 | "counterOfferID" => $trade->getCounterOfferID(), 200 | ); 201 | 202 | /* 203 | if ( 204 | $trade 205 | ) 206 | { 207 | $info["sellingAssetType"] = $trade->getBaseAsset()->getType(); 208 | $info["sellingAssetCode"] = $trade->getBaseAsset()->getCode(); 209 | 210 | $info["counterAssetType"] = $trade->getCounterAsset()->getType(); 211 | $info["counterAssetCode"] = $trade->getCounterAsset()->getCode(); 212 | 213 | $info["price"] = $trade->getPrice()->toFloat(); 214 | 215 | $info["sellingAmount"] = $trade->getCounterAmount()->toFloat(); 216 | } 217 | else 218 | { 219 | $info["counterAssetType"] = $trade->getBaseAsset()->getType(); 220 | $info["counterAssetCode"] = $trade->getBaseAsset()->getCode(); 221 | 222 | $info["sellingAssetType"] = $trade->getCounterAsset()->getType(); 223 | $info["sellingAssetCode"] = $trade->getCounterAsset()->getCode(); 224 | 225 | $info["price"] = 1/$trade->getPrice()->toFloat(); 226 | 227 | $info["sellingAmount"] = $trade->getCounterAmount()->toFloat(); 228 | } 229 | */ 230 | 231 | $trade_sellingAssset = null; 232 | $trade_sellingAsssetType = null; 233 | $trade_buyingAsset = null; 234 | $trade_buyingAssetType = null; 235 | 236 | if ($trade->getBaseIsSeller()) 237 | { 238 | $trade_sellingAssset = $trade->getBaseAsset()->getCode(); 239 | $trade_sellingAsssetType = $trade->getBaseAsset()->getType(); 240 | 241 | $trade_buyingAsset = $trade->getCounterAsset()->getCode(); 242 | $trade_buyingAssetType = $trade->getCounterAsset()->getType(); 243 | } 244 | else 245 | { 246 | $trade_sellingAssset = $trade->getCounterAsset()->getCode(); 247 | $trade_sellingAsssetType = $trade->getCounterAsset()->getType(); 248 | 249 | $trade_buyingAsset = $trade->getBaseAsset()->getCode(); 250 | $trade_buyingAssetType = $trade->getBaseAsset()->getType(); 251 | } 252 | 253 | $botOffer_sellingAssset = null; 254 | $botOffer_sellingAsssetType = null; 255 | $botOffer_buyingAsset = null; 256 | $botOffer_buyingAssetType = null; 257 | 258 | if ($this->getType() == \GalacticBot\Trade::TYPE_BUY) 259 | { 260 | $botOffer_buyingAsset = $bot->getSettings()->getCounterAsset()->getCode(); 261 | $botOffer_buyingAssetType = $bot->getSettings()->getCounterAsset()->getType(); 262 | 263 | $botOffer_sellingAssset = $bot->getSettings()->getBaseAsset()->getCode(); 264 | $botOffer_sellingAsssetType = $bot->getSettings()->getBaseAsset()->getType(); 265 | } 266 | else 267 | { 268 | $botOffer_buyingAsset = $bot->getSettings()->getBaseAsset()->getCode(); 269 | $botOffer_buyingAssetType = $bot->getSettings()->getBaseAsset()->getType(); 270 | 271 | $botOffer_sellingAssset = $bot->getSettings()->getCounterAsset()->getCode(); 272 | $botOffer_sellingAsssetType = $bot->getSettings()->getCounterAsset()->getType(); 273 | } 274 | 275 | /* 276 | var_dump("trade_sellingAssset = {$trade_sellingAssset} {$trade_sellingAsssetType}"); 277 | var_dump("trade_buyingAsset = {$trade_buyingAsset} {$trade_buyingAssetType}"); 278 | 279 | var_dump("botOffer_sellingAssset = {$botOffer_sellingAssset} {$botOffer_sellingAsssetType}"); 280 | var_dump("botOffer_buyingAsset = {$botOffer_buyingAsset} {$botOffer_buyingAssetType}"); 281 | */ 282 | 283 | $info["buyingAssetType"] = $botOffer_buyingAsset; 284 | $info["buyingAssetCode"] = $botOffer_buyingAssetType; 285 | 286 | $info["sellingAssetType"] = $botOffer_sellingAssset; 287 | $info["sellingAssetCode"] = $botOffer_sellingAsssetType; 288 | 289 | if ($trade->getBaseIsSeller()) 290 | { 291 | $info["price"] = $trade->getPrice()->toFloat(); 292 | $info["buyingAmount"] = $info["price"] * $trade->getBaseAmount()->toFloat(); 293 | $info["sellingAmount"] = $trade->getBaseAmount()->toFloat(); 294 | } 295 | else 296 | { 297 | $info["price"] = $trade->getPrice()->toFloat(); 298 | $info["sellingAmount"] = $info["price"] * $trade->getBaseAmount()->toFloat(); 299 | $info["buyingAmount"] = $trade->getBaseAmount()->toFloat(); 300 | } 301 | 302 | $info["sellingAmount"] = number_format($info["sellingAmount"], 7, '.', ''); 303 | $info["buyingAmount"] = number_format($info["buyingAmount"], 7, '.', ''); 304 | 305 | if ( 306 | $botOffer_buyingAsset != $trade_buyingAsset 307 | && $botOffer_buyingAssetType != $trade_buyingAssetType 308 | ) 309 | { 310 | $temp = $info["buyingAmount"]; 311 | $info["buyingAmount"] = $info["sellingAmount"]; 312 | $info["sellingAmount"] = $temp; 313 | } 314 | 315 | $claimedOffers[$uniqueID] = (object)$info; 316 | } 317 | 318 | $this->claimedOffers = json_encode($claimedOffers); 319 | 320 | $this->amountRemaining = $this->sellAmount; 321 | $this->boughtAmount = 0; 322 | $paidPrices = []; 323 | 324 | var_dump("trade = ", $trade); 325 | var_dump("getCounterAmount = ", $trade->getCounterAmount()->toString()/10000000); 326 | var_dump("claimedOffers = ", $claimedOffers); 327 | 328 | foreach($claimedOffers AS $offer) 329 | { 330 | $this->amountRemaining -= $offer->sellingAmount; 331 | $this->boughtAmount += $offer->sellingAmount * $offer->price; 332 | 333 | $paidPrices[] = 1/$offer->price; 334 | } 335 | 336 | $this->amountRemaining = number_format($this->amountRemaining, 7, '.', ''); 337 | $this->boughtAmount = number_format($this->boughtAmount, 7, '.', ''); 338 | 339 | var_dump("amountRemaining = ", $this->amountRemaining); 340 | var_dump("sellAmount = ", $this->sellAmount); 341 | 342 | $percentage = 100 * (1-($this->amountRemaining / $this->sellAmount)); 343 | 344 | $this->fillPercentage = round($percentage * 100) / 100; // Two decimals 345 | 346 | var_dump("fillPercentage = ", $this->fillPercentage); 347 | 348 | if ($this->fillPercentage > 100) { 349 | exit("\n\n -- niet goed niet \n\n"); 350 | } 351 | 352 | // Not really correct to just take an average of all paid prices 353 | $this->paidPrice = array_average($paidPrices); 354 | 355 | if ($this->fillPercentage >= 99.99) { 356 | // Make sure the offer is closed 357 | $bot->cancel(\GalacticBot\Time::now(true), $this, null); 358 | 359 | $this->state = self::STATE_FILLED; 360 | } 361 | 362 | // exit("daaahaaaag"); 363 | 364 | $bot->getDataInterface()->saveTrade($this); 365 | } 366 | 367 | function getTradeInfo($trade, $fromTransactionResult = false) { 368 | $o = (object)Array( 369 | "offerID" => $this->offerID, 370 | ); 371 | 372 | if ($fromTransactionResult) { 373 | $o->sellingAssetType = $trade->getAssetSold()->getType(); 374 | $o->sellingAssetCode = $trade->getAssetSold()->getCode(); 375 | 376 | $o->buyingAssetType = $trade->getAssetBought()->getType(); 377 | $o->buyingAssetCode = $trade->getAssetBought()->getCode(); 378 | 379 | $o->sellingAmount = $this->type == self::TYPE_BUY ? $trade->getAmountSold()->getScaledValue() : $trade->getAmountBought()->getScaledValue(); 380 | } else { 381 | $o->sellingAssetType = $trade->getCounterAsset()->getType(); 382 | $o->sellingAssetCode = $trade->getCounterAsset()->getCode(); 383 | 384 | $o->buyingAssetType = $trade->getBaseAsset()->getType(); 385 | $o->buyingAssetCode = $trade->getBaseAsset()->getCode(); 386 | 387 | $o->sellingAmount = $this->type == self::TYPE_BUY ? $trade->getCounterAmount() : $trade->getBaseAmount(); 388 | 389 | $o->price = $trade->getPrice(); 390 | } 391 | 392 | return $o; 393 | } 394 | 395 | static function fromGalacticHorizonOperationResponseAndResultForBot( 396 | $operation, 397 | $type, 398 | \GalacticHorizon\TransactionResult $response, 399 | \GalacticHorizon\ManageOfferOperationResult $result, 400 | $transactionEnvelopeXdrString, 401 | $paidFee, 402 | \GalacticBot\Bot $bot 403 | ) { 404 | global $_BASETIMEZONE; 405 | 406 | $now = new \DateTime(null, $_BASETIMEZONE); 407 | 408 | $o = new self(); 409 | $o->state = self::STATE_CREATED; 410 | $o->transactionEnvelopeXdr = $transactionEnvelopeXdrString; 411 | $o->ID = null; 412 | $o->createdAt = $now->format("Y-m-d H:i:s"); 413 | $o->offerID = $result->getOffer() ? $result->getOffer()->getOfferID() : null; 414 | $o->hash = $response ? $response->getHash() : null; 415 | 416 | $o->claimedOffers = json_encode([]); 417 | 418 | $o->type = $type; 419 | 420 | if ($type == self::TYPE_BUY) 421 | { 422 | $o->priceD = $operation->getPrice()->getNumerator(); 423 | $o->priceN = $operation->getPrice()->getDenominator(); 424 | $o->price = $o->priceN / $o->priceD; 425 | } 426 | else 427 | { 428 | $o->priceD = $operation->getPrice()->getDenominator(); 429 | $o->priceN = $operation->getPrice()->getNumerator(); 430 | $o->price = $o->priceN / $o->priceD; 431 | } 432 | 433 | $o->sellAmount = $operation->getSellAmount()->toFloat(); 434 | 435 | $o->fee = $paidFee; 436 | 437 | $o->spentAmount = $o->fee; 438 | 439 | $o->fillPercentage = 0; 440 | 441 | return $o; 442 | } 443 | 444 | } 445 | 446 | -------------------------------------------------------------------------------- /src/Implementation/MysqlDataInterface.php: -------------------------------------------------------------------------------- 1 | connect_errno) { 34 | throw Exception("Mysql error #{self::$mysqli->connect_errno} {self::$mysqli->connect_error}"); 35 | } 36 | } 37 | 38 | static function escape_string($str) 39 | { 40 | return self::$mysqli->real_escape_string($str); 41 | } 42 | 43 | function isSetting($name) 44 | { 45 | $value = $this->get("setting_" . $name); 46 | 47 | return $value !== null; 48 | } 49 | 50 | function getSetting($name, $defaultValue = null) 51 | { 52 | $value = $this->get("setting_" . $name); 53 | 54 | if ($value === null) 55 | { 56 | $this->setSetting($name, $defaultValue); 57 | 58 | $value = $defaultValue; 59 | } 60 | 61 | return $value; 62 | } 63 | 64 | function setSetting($name, $value) 65 | { 66 | $this->directSet("setting_" . $name, $value); 67 | } 68 | 69 | function getBot() 70 | { 71 | return $this->bot; 72 | } 73 | 74 | private function excludeLastTradeFromOffers(Array $offers) 75 | { 76 | $lastTrade = $this->getLastTrade(); 77 | 78 | $lastTradePrice = 0; 79 | $lastTradeAmount = 0; 80 | 81 | if ($lastTrade) 82 | { 83 | $lastTradePrice = number_format(1/$lastTrade->getPrice(), 7); 84 | $lastTradeAmount = number_format($lastTrade->getSellAmount(), 7); 85 | } 86 | 87 | foreach($offers AS $bid) 88 | { 89 | $price = number_format($bid->price, 7); 90 | $amount = $bid->amount; 91 | 92 | /* 93 | -- lets not do this, this way we come closer to the price people want to pay or have 94 | if ($price == $lastTradePrice) 95 | { 96 | $amount -= (float)$lastTradeAmount; 97 | } 98 | */ 99 | 100 | if ($amount > 0) 101 | { 102 | // Still have left when our offer is excluded 103 | // We can assume this is valid offer from someone else 104 | return $bid->price; 105 | } 106 | } 107 | 108 | return null; 109 | } 110 | 111 | /* 112 | * Figure out what the price for an asset is on a specific time. 113 | * 114 | * TODO: This isn't the best place for this method, shouldn't this be a task of the Bot implementation? 115 | */ 116 | function getAssetValueForTime(\GalacticBot\Time $time) 117 | { 118 | $baseAsset = $this->bot->getSettings()->getBaseAsset(); 119 | $counterAsset = $this->bot->getSettings()->getCounterAsset(); 120 | 121 | $sample = $this->getT($time, "value"); 122 | 123 | if ($sample !== null) 124 | return $sample; 125 | 126 | if ($time->isNow()) { 127 | $orderbook = Client::createTemporaryPublicClient()->getOrderbookForAssetPair($baseAsset, $counterAsset, 10); 128 | 129 | $samples = new \GalacticBot\Samples(2); 130 | 131 | if ($orderbook && isset($orderbook->asks)) 132 | { 133 | $price = $this->excludeLastTradeFromOffers($orderbook->asks); 134 | 135 | if ($price !== null) 136 | $samples->add($price); 137 | } 138 | 139 | if ($orderbook && isset($orderbook->bids)) 140 | { 141 | $price = $this->excludeLastTradeFromOffers($orderbook->bids); 142 | 143 | if ($price !== null) 144 | $samples->add($price); 145 | } 146 | 147 | if ($samples->getLength() > 0) { 148 | $price = (float)number_format($samples->getAverage(), 7, '.', ''); 149 | 150 | $this->setT($time, "value", $price); 151 | 152 | return $price; 153 | } 154 | else 155 | { 156 | $price = $this->getLatestT($time, "value"); 157 | 158 | if ($price) 159 | $this->setT($time, "value", $price); 160 | 161 | return $price; 162 | } 163 | 164 | return null; 165 | } 166 | 167 | // Fetch older data which we didn't have collected 168 | $start = new \GalacticBot\Time($time); 169 | 170 | $end = new \GalacticBot\Time($time); 171 | $end->add(120); 172 | 173 | $now = \GalacticBot\Time::now(); 174 | 175 | if ($end->isAfter($now)) 176 | { 177 | $end = $now; 178 | $end->subtract(1); 179 | } 180 | 181 | $list = \GalacticHorizon\Client::getInstance()->getTradeAggregations($baseAsset, $counterAsset, $start->getDateTime(), $end->getDateTime(), \GalacticHorizon\Client::INTERVAL_MINUTE); 182 | 183 | if ($list) 184 | { 185 | $lastDate = new \GalacticBot\Time($start); 186 | 187 | foreach($list AS $i => $record) { 188 | $date = \GalacticBot\Time::fromTimestamp($record->timestamp / 1000); 189 | 190 | $isLastRecord = $i == count($list); 191 | 192 | $range = \GalacticBot\Time::getRange($lastDate, $isLastRecord ? $end : $date); 193 | 194 | foreach($range AS $rangeDate) { 195 | $price = (float)number_format(($record->low+$record->high)/2, 7, '.', ''); 196 | $this->setT($rangeDate, "value", $price); 197 | } 198 | 199 | $lastDate = $date; 200 | } 201 | } 202 | 203 | $price = $this->getT($time, "value"); 204 | 205 | if (!$price) { 206 | // This happens when we're requesting the price of an asset 207 | // without any transactions in the requested timefrime 208 | // We'll have to get the last known price as that is still valid 209 | $sql = " 210 | SELECT value 211 | FROM `{$this->tableNames_botData}` 212 | WHERE botID = " . $this->escapeSQLValue($this->bot->getSettings()->getID()) . " 213 | AND name = 'value' 214 | AND date <= " . $this->escapeSQLValue($time->getDateTime()->format("Y-m-d H:i:s")) . " 215 | AND date IS NOT NULL 216 | AND date <> '0000-00-00 00:00:00' 217 | ORDER BY 218 | date DESC 219 | LIMIT 1 220 | "; 221 | 222 | if (!$result = $this->query($sql)) 223 | { 224 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 225 | } 226 | 227 | $row = $result->fetch_assoc(); 228 | 229 | if ($row) { 230 | $time = clone $start; 231 | 232 | $price = $row["value"]; 233 | 234 | while($time->isBefore($end)) 235 | { 236 | $this->setT($time, "value", $price); 237 | $time->add(1); 238 | } 239 | } 240 | } 241 | 242 | $price = $price ? (float)number_format($price, 7, '.', '') : null; 243 | 244 | return $price; 245 | } 246 | 247 | function clearAllExceptSampleDataAndSettings() 248 | { 249 | // Make sure to clean our cache 250 | $this->save(); 251 | 252 | $sql = " 253 | DELETE FROM 254 | `{$this->tableNames_botData}` 255 | WHERE botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 256 | AND name <> 'value' 257 | AND name NOT LIKE 'setting_%' 258 | "; 259 | 260 | if (!$result = $this->query($sql)) 261 | { 262 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 263 | } 264 | 265 | $sql = " 266 | DELETE FROM 267 | `{$this->tableNames_botTrade}` 268 | WHERE botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 269 | "; 270 | 271 | if (!$result = $this->query($sql)) 272 | { 273 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 274 | } 275 | 276 | $this->sampleBuffers = []; 277 | 278 | // Reload (empty) data from database 279 | $this->saveAndReload(); 280 | } 281 | 282 | function getLastTrade() 283 | { 284 | $sql = " 285 | SELECT * 286 | FROM `{$this->tableNames_botTrade}` 287 | WHERE state NOT IN ('" . \GalacticBot\Trade::STATE_CANCELLED . "', '" . \GalacticBot\Trade::STATE_REPLACED . "') 288 | AND botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 289 | ORDER BY 290 | processedAt DESC 291 | LIMIT 1 292 | "; 293 | 294 | if (!$result = $this->query($sql)) 295 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 296 | 297 | $row = $result->fetch_assoc(); 298 | 299 | if ($row && count($row)) { 300 | $trade = new \GalacticBot\Trade(); 301 | $trade->setData($row); 302 | return $trade; 303 | } 304 | } 305 | 306 | function getFirstCompletedTrade() 307 | { 308 | $sql = " 309 | SELECT * 310 | FROM `{$this->tableNames_botTrade}` 311 | WHERE state = '" . \GalacticBot\Trade::STATE_FILLED . "' 312 | AND botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 313 | ORDER BY 314 | processedAt ASC 315 | LIMIT 1 316 | "; 317 | 318 | if (!$result = $this->query($sql)) 319 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 320 | 321 | $row = $result->fetch_assoc(); 322 | 323 | if ($row && count($row)) { 324 | $trade = new \GalacticBot\Trade(); 325 | $trade->setData($row); 326 | return $trade; 327 | } 328 | 329 | return null; 330 | } 331 | 332 | function getLastCompletedTrade() 333 | { 334 | $last = $this->getLastTrade(); 335 | 336 | if ($last && $last->getIsFilledCompletely()) 337 | return $last; 338 | 339 | $sql = " 340 | # hier 341 | SELECT * 342 | FROM `{$this->tableNames_botTrade}` 343 | WHERE state = '" . \GalacticBot\Trade::STATE_FILLED . "' 344 | AND botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 345 | ORDER BY 346 | processedAt DESC 347 | LIMIT 1 348 | "; 349 | 350 | if (!$result = $this->query($sql)) 351 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 352 | 353 | $row = $result->fetch_assoc(); 354 | 355 | if ($row && count($row)) { 356 | $trade = new \GalacticBot\Trade(); 357 | $trade->setData($row); 358 | return $trade; 359 | } 360 | 361 | return null; 362 | } 363 | 364 | function getTrades($limit, $orderDesc) 365 | { 366 | $list = []; 367 | 368 | $limit = (int)$limit; 369 | $order = "processedAt " . ($orderDesc ? "DESC" : "ASC"); 370 | 371 | $sql = " 372 | SELECT * 373 | FROM `{$this->tableNames_botTrade}` 374 | WHERE botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 375 | ORDER BY 376 | $order 377 | LIMIT 378 | $limit 379 | "; 380 | 381 | if (!$result = $this->query($sql)) 382 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 383 | 384 | while(($row = $result->fetch_assoc())) { 385 | $trade = new \GalacticBot\Trade(); 386 | $trade->setData($row); 387 | 388 | $list[] = $trade; 389 | } 390 | 391 | return $list; 392 | } 393 | 394 | function getTradeInTimeRange(\GalacticBot\Time $begin, \GalacticBot\Time $end) 395 | { 396 | $list = []; 397 | 398 | $sql = " 399 | SELECT * 400 | FROM `{$this->tableNames_botTrade}` 401 | WHERE botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 402 | AND processedAt >= '" . self::escape_string($begin->toString()) . "' 403 | AND processedAt < '" . self::escape_string($end->toString()) . "' 404 | ORDER BY 405 | processedAt ASC 406 | "; 407 | 408 | if (!$result = $this->query($sql)) 409 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 410 | 411 | while(($row = $result->fetch_assoc())) { 412 | $trade = new \GalacticBot\Trade(); 413 | $trade->setData($row); 414 | 415 | $list[] = $trade; 416 | } 417 | 418 | return $list; 419 | } 420 | 421 | function getTradeByID($ID) 422 | { 423 | if (!$ID) 424 | return null; 425 | 426 | $sql = " 427 | SELECT * 428 | FROM `{$this->tableNames_botTrade}` 429 | WHERE ID = " . $this->escapeSQLValue($ID) . " 430 | AND botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 431 | "; 432 | 433 | if (!$result = $this->query($sql)) 434 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 435 | 436 | $row = $result->fetch_assoc(); 437 | 438 | if ($row && count($row)) { 439 | $trade = new \GalacticBot\Trade(); 440 | $trade->setData($row); 441 | return $trade; 442 | } 443 | 444 | return null; 445 | } 446 | 447 | function saveTrade(\GalacticBot\Trade $trade) 448 | { 449 | $set = []; 450 | $data = $trade->getData(); 451 | 452 | foreach($data AS $k => $v) 453 | { 454 | $set[] = "$k = " . $this->escapeSQLValue($v); 455 | } 456 | 457 | $set[] = "updatedAt = UTC_TIMESTAMP()"; 458 | 459 | $set = implode(",\n", $set); 460 | 461 | $sql = " 462 | UPDATE `{$this->tableNames_botTrade}` 463 | SET $set 464 | WHERE ID = " . $this->escapeSQLValue($trade->getID()) . " 465 | "; 466 | 467 | if (!$result = $this->query($sql)) 468 | { 469 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 470 | } 471 | } 472 | 473 | function addTrade(\GalacticBot\Trade $trade) 474 | { 475 | $names = []; 476 | $values = []; 477 | 478 | $names[] = "botID"; 479 | $values[] = $this->escapeSQLValue($this->bot->getSettings()->getID()); 480 | 481 | $data = $trade->getData(); 482 | 483 | foreach($data AS $k => $v) 484 | { 485 | $names[] = $k; 486 | $values[] = $this->escapeSQLValue($v); 487 | } 488 | 489 | $names = implode(",\n", $names); 490 | $values = implode(",\n", $values); 491 | 492 | $sql = " 493 | INSERT INTO 494 | `{$this->tableNames_botTrade}` 495 | ( 496 | $names 497 | ) 498 | VALUES 499 | ( 500 | $values 501 | ) 502 | "; 503 | 504 | if (!$result = $this->query($sql)) 505 | { 506 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 507 | } 508 | 509 | $trade->setID(self::$mysqli->insert_id); 510 | } 511 | 512 | function escapeSQLValue($value) 513 | { 514 | if ($value === NULL) 515 | return "NULL"; 516 | 517 | return "'" . self::escape_string($value) . "'"; 518 | } 519 | 520 | function get($name, $defaultValue = null) 521 | { 522 | return isset($this->data[$name]) ? $this->data[$name] : $defaultValue; 523 | } 524 | 525 | function set($name, $value) 526 | { 527 | if (!isset($this->data[$name]) || $this->data[$name] != $value) 528 | { 529 | $this->data[$name] = $value; 530 | $this->changedData[$name] = $name; 531 | } 532 | } 533 | 534 | function directSet($name, $value) 535 | { 536 | $this->data[$name] = $value; 537 | 538 | $sql = " 539 | REPLACE INTO `{$this->tableNames_botData}` 540 | ( 541 | botID, 542 | name, 543 | date, 544 | value 545 | ) 546 | VALUES 547 | ( 548 | '" . self::escape_string($this->bot->getSettings()->getID()) . "', 549 | '" . self::escape_string($name) . "', 550 | '0000-00-00 00:00:00', 551 | '" . self::escape_string($value) . "' 552 | ) 553 | "; 554 | 555 | $this->query($sql); 556 | } 557 | 558 | function getT(\GalacticBot\Time $time, $name) 559 | { 560 | $sql = " 561 | SELECT value 562 | FROM `{$this->tableNames_botData}` 563 | WHERE 564 | botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 565 | AND name = '" . self::escape_string($name) . "' 566 | AND date = '" . self::escape_string($time->toString()) . "' 567 | "; 568 | 569 | if (!$result = $this->query($sql)) 570 | { 571 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 572 | } 573 | 574 | $row = $result->fetch_assoc(); 575 | 576 | if ($row && count($row)) 577 | return $row["value"]; 578 | 579 | return null; 580 | } 581 | 582 | function getLatestT(\GalacticBot\Time $time, $name) 583 | { 584 | $sql = " 585 | SELECT value 586 | FROM `{$this->tableNames_botData}` 587 | WHERE 588 | botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 589 | AND name = '" . self::escape_string($name) . "' 590 | AND date <= '" . self::escape_string($time->toString()) . "' 591 | ORDER BY 592 | date DESC 593 | LIMIT 1 594 | "; 595 | 596 | if (!$result = $this->query($sql)) 597 | { 598 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 599 | } 600 | 601 | $row = $result->fetch_assoc(); 602 | 603 | if ($row && count($row)) 604 | return $row["value"]; 605 | 606 | return null; 607 | } 608 | 609 | function getFirstT(\GalacticBot\Time $time, $name) 610 | { 611 | $sql = " 612 | SELECT value 613 | FROM `{$this->tableNames_botData}` 614 | WHERE 615 | botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 616 | AND name = '" . self::escape_string($name) . "' 617 | AND date <= '" . self::escape_string($time->toString()) . "' 618 | ORDER BY 619 | date ASC 620 | LIMIT 1 621 | "; 622 | 623 | if (!$result = $this->query($sql)) 624 | { 625 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 626 | } 627 | 628 | $row = $result->fetch_assoc(); 629 | 630 | if ($row && count($row)) 631 | return $row["value"]; 632 | 633 | return null; 634 | } 635 | 636 | function setT(\GalacticBot\Time $time, $name, $value) 637 | { 638 | $sql = " 639 | REPLACE INTO `{$this->tableNames_botData}` 640 | ( 641 | botID, 642 | name, 643 | date, 644 | value 645 | ) 646 | VALUES 647 | ( 648 | '" . self::escape_string($this->bot->getSettings()->getID()) . "', 649 | '" . self::escape_string($name) . "', 650 | '" . self::escape_string($time->toString()) . "', 651 | '" . self::escape_string($value) . "' 652 | ) 653 | "; 654 | 655 | //echo $sql; 656 | 657 | if (!$result = $this->query($sql)) 658 | { 659 | throw new \Exception("Mysql error #{self::$mysqli->errno}: {self::$mysqli->error}."); 660 | } 661 | } 662 | 663 | function getS($name, $maxLength) 664 | { 665 | if (!isset($this->sampleBuffers[$name])) 666 | $this->sampleBuffers[$name] = new \GalacticBot\Samples($maxLength); 667 | else 668 | $this->sampleBuffers[$name]->setMaxLength($maxLength); 669 | 670 | return $this->sampleBuffers[$name]; 671 | } 672 | 673 | function setS($name, \GalacticBot\Samples $buffer) 674 | { 675 | $this->sampleBuffers[$name] = $buffer; 676 | } 677 | 678 | function loadForBot(\GalacticBot\Bot $bot, $force = false) 679 | { 680 | if ($this->bot && $this->bot->getSettings()->getID() == $bot->getSettings()->getID() && !$force) 681 | return; 682 | 683 | $this->data = []; 684 | $this->changedData = []; 685 | $this->sampleBuffers = []; 686 | $this->bot = $bot; 687 | 688 | $sql = " 689 | SELECT name, 690 | value 691 | FROM `{$this->tableNames_botData}` 692 | WHERE botID = '" . self::escape_string($this->bot->getSettings()->getID()) . "' 693 | AND date = '0000-00-00 00:00:00' 694 | "; 695 | 696 | if (!$result = $this->query($sql)) 697 | { 698 | throw new \Exception("Mysql error #" . self::$mysqli->errno . ": " . self::$mysqli->error); 699 | } 700 | 701 | while($row = $result->fetch_assoc()) 702 | { 703 | if (preg_match("/^SB_(.+?)$/", $row["name"], $matches)) 704 | { 705 | $data = json_decode($row["value"]); 706 | 707 | if (is_object($data)) 708 | $this->sampleBuffers[$matches[1]] = new \GalacticBot\Samples($data->maxLength, $data->samples); 709 | } 710 | else 711 | { 712 | $this->data[$row["name"]] = $row["value"]; 713 | } 714 | } 715 | 716 | $this->bot->getSettings()->loadFromDataInterface($this->bot->getSettingDefaults()); 717 | } 718 | 719 | function saveAndReload() 720 | { 721 | $this->save(); 722 | 723 | $this->loadForBot($this->bot, true); 724 | } 725 | 726 | function save($includingBuffers = true) 727 | { 728 | foreach($this->changedData AS $k => $v) 729 | { 730 | $v = $this->data[$k]; 731 | 732 | $sql = " 733 | REPLACE INTO `{$this->tableNames_botData}` 734 | ( 735 | botID, 736 | name, 737 | date, 738 | value 739 | ) 740 | VALUES 741 | ( 742 | '" . self::escape_string($this->bot->getSettings()->getID()) . "', 743 | '" . self::escape_string($k) . "', 744 | '0000-00-00 00:00:00', 745 | '" . self::escape_string($v) . "' 746 | ) 747 | "; 748 | 749 | //echo " --- updating '$k' to '$v'\n"; 750 | $this->query($sql); 751 | } 752 | 753 | $this->changedData = []; 754 | 755 | if ($includingBuffers) 756 | { 757 | foreach($this->sampleBuffers AS $k => $v) 758 | { 759 | $jv = (object)[]; 760 | $jv->maxLength = $v->getMaxLength(); 761 | $jv->samples = $v->getArray(); 762 | 763 | $sql = " 764 | REPLACE INTO `{$this->tableNames_botData}` 765 | ( 766 | botID, 767 | name, 768 | date, 769 | value 770 | ) 771 | VALUES 772 | ( 773 | '" . self::escape_string($this->bot->getSettings()->getID()) . "', 774 | 'SB_" . self::escape_string($k) . "', 775 | '0000-00-00 00:00:00', 776 | '" . self::escape_string(json_encode($jv)) . "' 777 | ) 778 | "; 779 | 780 | $this->query($sql); 781 | } 782 | } 783 | } 784 | 785 | function query($sql) 786 | { 787 | // $start = microtime(true); 788 | 789 | //echo "[QUERY - MysqlDataInterface] " . $sql . "\n"; 790 | 791 | $res = self::$mysqli->query($sql); 792 | 793 | // $stop = microtime(true); 794 | 795 | // $delta = $stop - $start; 796 | 797 | // echo " --- TIME: $delta\n"; 798 | 799 | // if ($delta > 0.2) 800 | // exit(); 801 | 802 | return $res; 803 | } 804 | 805 | function logVerbose($what) { 806 | echo "[VERBOSE] " . date("Y-m-d H:i:s") . " [Bot #" . $this->bot->getSettings()->getID() . " - " . $this->bot->getSettings()->getName() . "]: {$what}\n"; 807 | } 808 | 809 | function logWarning($what) { 810 | echo "[WARNING] " . date("Y-m-d H:i:s") . " [Bot #" . $this->bot->getSettings()->getID() . " - " . $this->bot->getSettings()->getName() . "]: {$what}\n"; 811 | } 812 | 813 | function logError($what) { 814 | echo "[ERROR] " . date("Y-m-d H:i:s") . " [Bot #" . $this->bot->getSettings()->getID() . " - " . $this->bot->getSettings()->getName() . "]: {$what}\n"; 815 | } 816 | 817 | } 818 | 819 | --------------------------------------------------------------------------------