├── .styleci.yml ├── src ├── Exceptions │ ├── TokenException.php │ ├── ConfigException.php │ ├── OperationException.php │ ├── RequestException.php │ ├── ClientException.php │ └── ResponseException.php ├── Operations │ ├── Unfollow.php │ ├── Common │ │ ├── HasName.php │ │ ├── SerializesJson.php │ │ └── HasParameters.php │ ├── SubOperation.php │ ├── Operation.php │ ├── Reblog.php │ ├── Follow.php │ ├── CommentOptions.php │ ├── Vote.php │ ├── CustomJson.php │ └── Comment.php ├── Contracts │ └── Operations │ │ └── Operation.php ├── Client │ ├── Response.php │ ├── Broadcaster.php │ └── Client.php ├── Config │ └── Config.php ├── Auth │ ├── Token.php │ └── Manager.php ├── Http │ ├── HasHttpResponse.php │ └── Client.php └── Transactions │ └── Transaction.php ├── .gitignore ├── .travis.yml ├── docs ├── css │ └── extra.css ├── index.md ├── 01-configuration.md ├── 02-authentication.md ├── img │ └── sc-logo.svg └── 03-operations.md ├── tests ├── TestCase.php ├── Resources │ └── stub-transaction.json ├── Operations │ ├── OperationTest.php │ ├── ReblogTest.php │ ├── Common │ │ ├── HasNameTraitTest.php │ │ ├── SerializesJsonTest.php │ │ └── HasParametersTraitTest.php │ ├── VoteTest.php │ ├── CommentOptionsTest.php │ ├── FollowTest.php │ └── CommentTest.php ├── Auth │ ├── TokenTest.php │ └── ManagerTest.php ├── Config │ └── ConfigTest.php ├── Client │ ├── ResponseTest.php │ ├── ClientTest.php │ └── BroadcasterTest.php ├── Http │ ├── HasHttpResponseTest.php │ └── ClientTest.php ├── Transactions │ └── TransactionTest.php └── Exceptions │ └── ResponseExceptionTest.php ├── readme.md ├── mkdocs.yml ├── LICENSE ├── composer.json └── phpunit.xml.dist /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: recommended 2 | 3 | risky: false 4 | 5 | finder: 6 | name: 7 | - "*.php" 8 | path: 9 | - "src" -------------------------------------------------------------------------------- /src/Exceptions/TokenException.php: -------------------------------------------------------------------------------- 1 | name = $name; 27 | 28 | return $this; 29 | } 30 | 31 | /** 32 | * Operation name getter. 33 | * 34 | * @return null|string 35 | */ 36 | public function getName() : ?string 37 | { 38 | return $this->name; 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Resources/stub-transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref_block_num": 99999, 3 | "ref_block_prefix": 4094062943, 4 | "expired": true, 5 | "expiration": "2018-03-17T23:41:12", 6 | "operations": [ 7 | [ 8 | "vote", 9 | { 10 | "voter": "sparkesy43", 11 | "author": "gameon", 12 | "permlink": "nba-challenge-winner-s-for-03-15-2018-game-1-or-congratulations", 13 | "weight": 10000 14 | } 15 | ] 16 | ], 17 | "extensions": [], 18 | "signatures": [ 19 | "20756ad9e9b45b9e6a2b9e6f3cd7b0d1c46c636e554cbcee307070cb81eb60fc353da3687ff7d50fb53013dc82b3687784a98de6286c3aa8011fe870dc3a35e6e8" 20 | ], 21 | "id": "4b1d2f863b6e4b133ceb61e75c00534f95ebdd03", 22 | "block_num": 0, 23 | "trx_num": 0 24 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SteemConnect V2 SDK for PHP. 2 | 3 | [![Build Status](https://travis-ci.org/hernandev/sc2-sdk-php.svg?branch=master)](https://travis-ci.org/hernandev/sc2-sdk-php) 4 | [![Codecov](https://codecov.io/gh/hernandev/sc2-sdk-php/branch/master/graph/badge.svg)](https://codecov.io/gh/hernandev/sc2-sdk-php) 5 | [![Latest Stable Version](https://poser.pugx.org/hernandev/sc2-sdk-php/v/stable)](https://packagist.org/packages/hernandev/sc2-sdk-php) 6 | [![License](https://poser.pugx.org/hernandev/sc2-sdk-php/license)](https://packagist.org/packages/hernandev/sc2-sdk-php) 7 | 8 | Easily integrate STEEM blockchain into your PHP applications, though [SteemConnect](https://steemconnect.com). 9 | 10 | ## Documentation 11 | 12 | The complete documentation for this library can be read at: 13 | [https://hernandev.github.io/sc2-sdk-php](https://hernandev.github.io/sc2-sdk-php). 14 | 15 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: SteemConnect SDK for PHP 2 | theme: 3 | name: 'material' 4 | logo: 'img/sc-logo.svg' 5 | font: 6 | text: 'Roboto' 7 | code: 'Roboto Mono' 8 | repo_name: 'hernandev/sc2-sdk-php' 9 | repo_url: 'https://github.com/hernandev/sc2-sdk-php' 10 | 11 | extra_css: 12 | - 'css/extra.css' 13 | extra: 14 | social: 15 | - type: 'github' 16 | link: 'https://github.com/hernandev' 17 | - type: 'twitter' 18 | link: 'https://twitter.com/_hernandev' 19 | - type: 'linkedin' 20 | link: 'https://linkedin.com/in/hernandev' 21 | # Extensions 22 | markdown_extensions: 23 | - admonition 24 | - codehilite: 25 | linenums: true 26 | guess_lang: true 27 | - toc: 28 | permalink: true 29 | pages: 30 | - Home: index.md 31 | - Configuration: 01-configuration.md 32 | - Authentication: 02-authentication.md 33 | - Operations: 03-operations.md 34 | -------------------------------------------------------------------------------- /tests/Operations/OperationTest.php: -------------------------------------------------------------------------------- 1 | 'bar']); 21 | 22 | // assert the parameters were correctly set. 23 | $this->assertEquals($operation->getParameters(), ['foo' => 'bar']); 24 | 25 | // assert the magic getters will retrieve parameters. 26 | $this->assertEquals('bar', $operation->foo); 27 | // assert the magic getters will retrieve class attributes as well. 28 | $this->assertEquals('foo', $operation->name); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Client/Response.php: -------------------------------------------------------------------------------- 1 | responseBody || !is_array($this->responseBody)) { 27 | // then just return null cause there's no transaction to parse. 28 | return null; 29 | } 30 | 31 | // otherwise, factories a transaction from the response body itself and return. 32 | return Transaction::factory($this->responseBody); 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Diego Hernandes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hernandev/sc2-sdk-php", 3 | "description": "PHP SDK for SteemConnect V2", 4 | "type": "library", 5 | "keywords": ["steem", "steemconnect", "steem-connect", "sc2", "sdk"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Diego Hernandes", 10 | "email": "diego@hernandev.com" 11 | } 12 | ], 13 | "minimum-stability": "stable", 14 | "autoload": { 15 | "psr-4": { 16 | "SteemConnect\\": "src/" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "SteemConnect\\": "tests/" 22 | } 23 | }, 24 | "require": { 25 | "league/oauth2-client": "^2.3", 26 | "php": ">=7.1.0", 27 | "ext-curl": "*", 28 | "ext-json": "*", 29 | "illuminate/support": "^5.6", 30 | "symfony/http-foundation": "^4.0", 31 | "guzzlehttp/guzzle": "^6.3", 32 | "hernandev/oauth2-sc2": "^0.10.1", 33 | "nesbot/carbon": "^1.23" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "^7.0", 37 | "mockery/mockery": "^1.0", 38 | "symfony/var-dumper": "^4.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 22 | 23 | 24 | 25 | ./tests/ 26 | 27 | 28 | 29 | 30 | ./src 31 | 32 | ./vendor 33 | ./tests 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/Operations/ReblogTest.php: -------------------------------------------------------------------------------- 1 | account('testing-reblog-account'); 25 | $this->assertEquals('testing-reblog-account', $reblog->account); 26 | 27 | // assert set and get. 28 | $reblog->author('testing-author'); 29 | $this->assertEquals('testing-author', $reblog->author); 30 | 31 | // assert set and get. 32 | $reblog->permLink('some-post'); 33 | $this->assertEquals('some-post', $reblog->permlink); 34 | 35 | // empty instance. 36 | $reblog = new Reblog(); 37 | 38 | // test reblog method. 39 | $reblog->reblog('test-author', 'test-permlink'); 40 | $this->assertEquals('test-author', $reblog->author); 41 | $this->assertEquals('test-permlink', $reblog->permlink); 42 | } 43 | } -------------------------------------------------------------------------------- /tests/Operations/Common/HasNameTraitTest.php: -------------------------------------------------------------------------------- 1 | getDumbInstance(); 41 | 42 | // assert both getter and setter exists. 43 | $this->assertTrue(method_exists($dumb, 'getName')); 44 | $this->assertTrue(method_exists($dumb, 'setName')); 45 | 46 | // assert no default name exists. 47 | $this->assertNull($dumb->getName()); 48 | 49 | // call the setter and hold it's return. 50 | $setReturn = $dumb->setName('foo'); 51 | 52 | // assert the fluent return. 53 | $this->assertSame($dumb, $setReturn); 54 | 55 | // now assert the actual content was correctly set. 56 | $this->assertEquals('foo', $dumb->getName()); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Operations/SubOperation.php: -------------------------------------------------------------------------------- 1 | setName($name); 37 | 38 | // set sub operation parameters. 39 | $this->setParameters($parameters); 40 | } 41 | 42 | /** 43 | * Magic getter for operations. 44 | * 45 | * @param $name 46 | * 47 | * @return array|null|Operation|SubOperation|string 48 | */ 49 | public function __get($name) 50 | { 51 | if (property_exists($this, $name)) { 52 | return $this->{$name}; 53 | } 54 | 55 | return $this->getParameter($name); 56 | } 57 | } -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # SteemConnect PHP SDK 2 | 3 | This is the documentation for **[SteemConnect](https://steemconnect.com) [PHP SDK](https://github.com/hernandev/sc2-php-sdk)**, which is available at **[https://github.com/hernandev/sc2-sdk-php](https://github.com/hernandev/sc2-sdk-php)**. 4 | 5 | ## How to Read the Code Examples. 6 | 7 | This documentation was built with several code examples. 8 | 9 | For code highlighting and easy to understand reasons, any PHP code will start with a `app = $app; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Returns the current application name configured. 43 | * 44 | * @return null|string 45 | */ 46 | public function getApp() : ?string 47 | { 48 | return $this->app; 49 | } 50 | 51 | /** 52 | * Set the community using the SDK. 53 | * 54 | * @param string|null $community 55 | * 56 | * @return self 57 | */ 58 | public function setCommunity(string $community = null) : self 59 | { 60 | $this->community = $community; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Returns the community using the SDK. 67 | * 68 | * @return null|string 69 | */ 70 | public function getCommunity() : ?string 71 | { 72 | return $this->community; 73 | } 74 | } -------------------------------------------------------------------------------- /src/Operations/Operation.php: -------------------------------------------------------------------------------- 1 | name = $name; 45 | // set parameters directly, because of fluent interfaces. 46 | $this->parameters = $parameters; 47 | } 48 | 49 | /** 50 | * Magic getter for operations. 51 | * 52 | * @param $name 53 | * 54 | * @return array|null|Operation|SubOperation|string 55 | */ 56 | public function __get($name) 57 | { 58 | if (property_exists($this, $name)) { 59 | return $this->{$name}; 60 | } 61 | 62 | return $this->getParameter($name); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Auth/Token.php: -------------------------------------------------------------------------------- 1 | jsonSerialize(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Exceptions/ResponseException.php: -------------------------------------------------------------------------------- 1 | setHttpResponse($response); 29 | 30 | // parse the message and status code, then call parent constructor. 31 | parent::__construct($this->parseResponseMessage(), $this->responseStatusCode, $previous); 32 | } 33 | 34 | /** 35 | * Parses the error response message into a single string for the exception. 36 | * 37 | * @return string 38 | */ 39 | protected function parseResponseMessage() : string 40 | { 41 | // if the response is a json string. 42 | if ($this->responseIsJson()) { 43 | // collect the array body keys. 44 | $body = collect($this->responseBody); 45 | 46 | // parse the error name. 47 | $error = $body->get('error', null); 48 | // parse the error message, defaulting to HTTP. 49 | $message = $body->get('error_description', $this->responseStatusMessage); 50 | 51 | // return the formatted error message. 52 | return $error ? "{$error}: {$message}" : $message; 53 | } 54 | 55 | // in case the body is a string, just not an array / json. 56 | // return just the status message otherwise. 57 | return is_string($this->responseBody) ? strip_tags($this->responseBody) : $this->responseStatusMessage; 58 | } 59 | } -------------------------------------------------------------------------------- /tests/Operations/VoteTest.php: -------------------------------------------------------------------------------- 1 | voter('some-voter'); 24 | $this->assertEquals('some-voter', $vote->voter); 25 | 26 | // assert set and get. 27 | $vote->account('some-voter-account'); 28 | $this->assertEquals('some-voter-account', $vote->voter); 29 | 30 | // assert set and get. 31 | $vote->author('author'); 32 | $this->assertEquals('author', $vote->author); 33 | 34 | // assert set and get. 35 | $vote->permLink('some-post'); 36 | $this->assertEquals('some-post', $vote->permlink); 37 | 38 | // assert set and get. 39 | $vote->on('user', 'post-link'); 40 | $this->assertEquals('user', $vote->author); 41 | $this->assertEquals('post-link', $vote->permlink); 42 | 43 | // weight tests. 44 | $vote->weight(1); 45 | $this->assertEquals(10000, $vote->weight); 46 | 47 | // normal units 48 | $vote->weight(10000); 49 | $this->assertEquals(10000, $vote->weight); 50 | 51 | // percent alias. 52 | $vote->percent(0.5); 53 | $this->assertEquals(5000, $vote->weight); 54 | 55 | // test positive value on downvote. 56 | $vote->downVote(0.5); 57 | $this->assertEquals(-5000, $vote->weight); 58 | 59 | // test negative value on downvote. 60 | $vote->downVote(-5000); 61 | $this->assertEquals(-5000, $vote->weight); 62 | 63 | // test upvote. 64 | $vote->upVote(-5000); 65 | $this->assertEquals(5000, $vote->weight); 66 | 67 | // test upvote. 68 | $vote->upVote(0.7); 69 | $this->assertEquals(7000, $vote->weight); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Operations/Reblog.php: -------------------------------------------------------------------------------- 1 | getId()) { 34 | $this->setId('follow'); 35 | } 36 | } 37 | 38 | /** 39 | * Set the follower name. 40 | * 41 | * @param string $account 42 | * 43 | * @return self 44 | */ 45 | public function account(string $account) : self 46 | { 47 | $this->setRequiredPostingAuths([$account]); 48 | 49 | $this->getSubOperation()->setParameter('account', $account); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Set the follower name. 56 | * 57 | * @param string $author 58 | * 59 | * @return self 60 | */ 61 | public function author(string $author) : self 62 | { 63 | $this->getSubOperation()->setParameter('author', $author); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Set the follower name. 70 | * 71 | * @param string $permLink 72 | * 73 | * @return self 74 | */ 75 | public function permLink(string $permLink) : self 76 | { 77 | $this->getSubOperation()->setParameter('permlink', $permLink); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Set the original author username. 84 | * 85 | * @param string $author 86 | * @param string $permLink 87 | * 88 | * @return self 89 | */ 90 | public function reblog(string $author, string $permLink) : self 91 | { 92 | return $this->author($author)->permLink($permLink); 93 | } 94 | } -------------------------------------------------------------------------------- /tests/Operations/CommentOptionsTest.php: -------------------------------------------------------------------------------- 1 | author('foo-user'); 24 | $comment->permLink('foo-permlink'); 25 | 26 | // start the comment options. 27 | $commentOptions = new CommentOptions(); 28 | 29 | // set the comment operation pair. 30 | $commentOptions->of($comment); 31 | 32 | // assert the author and permlink were set from the comment. 33 | $this->assertEquals('foo-user', $commentOptions->author); 34 | $this->assertEquals('foo-permlink', $commentOptions->permlink); 35 | } 36 | 37 | /** 38 | * Test the setting of another options. 39 | */ 40 | public function test_options_setters() 41 | { 42 | // start a comment. 43 | $comment = new Comment(); 44 | 45 | // set the comment author and permlink. 46 | $comment->author('foo-user'); 47 | $comment->permLink('foo-permlink'); 48 | 49 | // start the comment options. 50 | $commentOptions = new CommentOptions(); 51 | 52 | // disable curation. 53 | $commentOptions->allowCurationRewards(false); 54 | // assert it was set as false. 55 | $this->assertFalse($commentOptions->allow_curation_rewards); 56 | 57 | // disable votes. 58 | $commentOptions->allowVotes(false); 59 | // assert it was set as false. 60 | $this->assertFalse($commentOptions->allow_votes); 61 | 62 | // set the max accepted payout. 63 | $commentOptions->maxAcceptedPayout(10); 64 | // assert the value formatting. 65 | $this->assertEquals('10.000 SBD', $commentOptions->max_accepted_payout); 66 | 67 | // set the SBD percent. 68 | $commentOptions->percentSteemDollars(9000); 69 | // assert the correct value. 70 | $this->assertEquals(9000, $commentOptions->percent_steem_dollars); 71 | } 72 | } -------------------------------------------------------------------------------- /src/Operations/Common/SerializesJson.php: -------------------------------------------------------------------------------- 1 | parameters; 31 | 32 | // collect and loop on json parameters. 33 | collect($this->jsonParameters)->each(function ($parameter) use (&$parameters) { 34 | // get the current value. 35 | $value = Arr::get($parameters, $parameter); 36 | 37 | // if the current value is indeed an array... 38 | if (is_array($value)) { 39 | // rewrite it's value with the JSON serialized version. 40 | Arr::set($parameters, $parameter, utf8_encode(json_encode($value))); 41 | } 42 | 43 | // set the array as json string. 44 | if (is_object($value) && method_exists($value, 'toArray')) { 45 | Arr::set($parameters, $parameter, utf8_encode(json_encode($value->toArray()))); 46 | } 47 | 48 | // set the value as it is, if string. 49 | if (is_string($value)) { 50 | Arr::set($parameters, $parameter, utf8_encode($value)); 51 | } 52 | }); 53 | 54 | // return the parameters. 55 | return $parameters; 56 | } 57 | 58 | /** 59 | * Array representation of the operation. 60 | * 61 | * @return array 62 | */ 63 | public function toArray() 64 | { 65 | // only call the getName if the method exists. 66 | if (method_exists($this, 'getName')) { 67 | return [ $this->getName(), $this->serializeJsonParameters() ]; 68 | } 69 | 70 | // return the raw serialization method otherwise. 71 | return $this->serializeJsonParameters(); 72 | } 73 | 74 | /** 75 | * JSON serialization method. 76 | * 77 | * {@inheritdoc} 78 | */ 79 | public function jsonSerialize() 80 | { 81 | return $this->toArray(); 82 | } 83 | } -------------------------------------------------------------------------------- /src/Auth/Manager.php: -------------------------------------------------------------------------------- 1 | config = $config; 50 | 51 | // setup provider. 52 | $this->provider = $provider; 53 | 54 | // set the access token on the manager instance. 55 | $this->accessToken = $token; 56 | } 57 | 58 | /** 59 | * Returns the provider authorization URL. 60 | * 61 | * @param array $options Additional options for the URL building. 62 | * 63 | * @return string 64 | */ 65 | public function getAuthorizationUrl(array $options = []): string 66 | { 67 | return $this->provider->getAuthorizationUrl($options); 68 | } 69 | 70 | /** 71 | * Parses the OAuth2 callback/return code and exchange for an access token. 72 | * 73 | * @param string|null $code 74 | * 75 | * @return Token 76 | */ 77 | public function parseReturn(string $code = null) 78 | { 79 | try { 80 | // get the original token object. 81 | $oAuthToken = $this->provider->parseReturn($code); 82 | } catch (\Exception $e) { 83 | // throw token exception if return parsing could not be successful. 84 | throw new TokenException('Error while exchanging access code with access token.'); 85 | } 86 | 87 | 88 | // return a new SDK token object. 89 | $this->accessToken = new Token($oAuthToken->jsonSerialize()); 90 | 91 | // return the access token parsed from response. 92 | return $this->accessToken; 93 | } 94 | } -------------------------------------------------------------------------------- /src/Operations/Follow.php: -------------------------------------------------------------------------------- 1 | getId()) { 34 | $this->setId('follow'); 35 | } 36 | } 37 | 38 | /** 39 | * Alias for the follower account. 40 | * 41 | * @param string $account 42 | * 43 | * @return Follow 44 | */ 45 | public function account(string $account): self 46 | { 47 | return $this->follower($account); 48 | } 49 | 50 | 51 | /** 52 | * Set the follower name. 53 | * 54 | * @param string $follower 55 | * 56 | * @return self 57 | */ 58 | public function follower(string $follower) : self 59 | { 60 | $this->setRequiredPostingAuths([$follower]); 61 | 62 | $this->getSubOperation()->setParameter('follower', $follower); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Set the user that will be followed. 69 | * 70 | * @param string $account 71 | * @param bool $following 72 | * 73 | * @return self 74 | */ 75 | public function following(string $account, bool $following = true) : self 76 | { 77 | // set the account to follow or unfollow. 78 | $this->getSubOperation()->setParameter('following', $account); 79 | 80 | // determine the what section based on the following flag, 81 | $what = $following ? ['blog'] : []; 82 | 83 | // set the what parameter. 84 | $this->getSubOperation()->setParameter('what', $what); 85 | 86 | // return it. 87 | return $this; 88 | } 89 | 90 | /** 91 | * Alias for follow. 92 | * 93 | * @param string $account 94 | * 95 | * @return self 96 | */ 97 | public function follow(string $account) : self 98 | { 99 | return $this->following($account, true); 100 | } 101 | 102 | /** 103 | * Alias for unfollow. 104 | * 105 | * @param string $account 106 | * 107 | * @return self 108 | */ 109 | public function unfollow(string $account) : self 110 | { 111 | return $this->following($account, false); 112 | } 113 | } -------------------------------------------------------------------------------- /tests/Auth/TokenTest.php: -------------------------------------------------------------------------------- 1 | validToken = [ 29 | 'username' => 'foo-username', 30 | 'access_token' => 'jwt.dummy.token', 31 | 'refresh_token' => 'jwt.dummy.refresh', 32 | 'expires' => Carbon::now()->addDays(1)->timestamp, 33 | 'resource_owner_id' => 'foo-username', 34 | ]; 35 | } 36 | 37 | /** 38 | * Token parsing. 39 | */ 40 | public function test_token_parsing() 41 | { 42 | $token = Token::fromArray($this->validToken); 43 | 44 | $this->assertInstanceOf(Token::class, $token); 45 | } 46 | 47 | /** 48 | * Empty token. 49 | */ 50 | public function test_empty_token_parsing_exception() 51 | { 52 | try { 53 | Token::fromArray([]); 54 | } catch (\Exception $e) { 55 | $this->assertInstanceOf(TokenException::class, $e); 56 | } 57 | } 58 | 59 | /** 60 | * Token JSON serialization tests. 61 | */ 62 | public function test_json_serialization() 63 | { 64 | $originalJson = json_encode($this->validToken); 65 | 66 | $token = Token::fromArray($this->validToken); 67 | 68 | $this->assertEquals($originalJson, json_encode($token->jsonSerialize())); 69 | 70 | $this->assertEquals($originalJson, json_encode($token->toArray())); 71 | } 72 | 73 | /** 74 | * Token JSON string parsing tests. 75 | */ 76 | public function test_token_parsing_from_json_string() 77 | { 78 | $jsonString = json_encode($this->validToken); 79 | 80 | $token = Token::fromJsonString($jsonString); 81 | 82 | $this->assertInstanceOf(Token::class, $token); 83 | } 84 | 85 | /** 86 | * Invalid token parsing tests. 87 | */ 88 | public function test_token_parsing_from_json_string_with_invalid_data() 89 | { 90 | $jsonString = json_encode(['foo' => 'bar']); 91 | 92 | try { 93 | $token = Token::fromJsonString($jsonString); 94 | } catch (\Exception $e) { 95 | $this->assertInstanceOf(TokenException::class, $e); 96 | } 97 | 98 | $jsonString = '{'; 99 | 100 | try { 101 | $token = Token::fromJsonString($jsonString); 102 | } catch (\Exception $e) { 103 | $this->assertInstanceOf(TokenException::class, $e); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/Http/HasHttpResponse.php: -------------------------------------------------------------------------------- 1 | responseInstance; 50 | } 51 | 52 | /** 53 | * Configure the http response on the current resource. 54 | * 55 | * @param ResponseInterface $response 56 | */ 57 | public function setHttpResponse(ResponseInterface $response) 58 | { 59 | // assign the http response instance itself. 60 | $this->responseInstance = $response; 61 | 62 | // assign the http response status code. 63 | $this->responseStatusCode = $response->getStatusCode(); 64 | 65 | // assign the http response status message. 66 | $this->responseStatusMessage = $response->getReasonPhrase(); 67 | 68 | // assign the http response headers. 69 | $this->responseHeaders = collect($response->getHeaders()); 70 | 71 | // assign the parsed http response body. 72 | $this->responseBody = $this->decodeResponseBody($response->getBody()); 73 | 74 | } 75 | 76 | /** 77 | * Decode the response body, parsing to array when json. 78 | * 79 | * @param StreamInterface $body 80 | * 81 | * @return mixed|string 82 | */ 83 | protected function decodeResponseBody(StreamInterface $body) 84 | { 85 | if ($this->responseIsJson()) { 86 | return json_decode((string) $body, true); 87 | } else { 88 | return (string) $body; 89 | } 90 | } 91 | 92 | /** 93 | * Detect the response as being a json response or not. 94 | * 95 | * @return bool 96 | */ 97 | protected function responseIsJson() : bool 98 | { 99 | $type = json_encode($this->responseHeaders->get('Content-Type')); 100 | 101 | return Str::contains($type, 'json'); 102 | } 103 | } -------------------------------------------------------------------------------- /tests/Operations/Common/SerializesJsonTest.php: -------------------------------------------------------------------------------- 1 | jsonParameters = ['foo', 'bar', 'baz']; 23 | } 24 | } 25 | 26 | /** 27 | * Class SerializesJsonTest. 28 | * 29 | * Tests for the SerializesJson trait. 30 | */ 31 | class SerializesJsonTest extends TestCase implements \JsonSerializable 32 | { 33 | // enable both traits. 34 | use HasParameters; 35 | use SerializesJson; 36 | 37 | /** 38 | * Test serialization (using the test class itself.) 39 | */ 40 | public function test_json_serialize_calls_array_transform() 41 | { 42 | // custom set name. 43 | $this->setName('foo'); 44 | 45 | // some fake data for testing. 46 | $fakeData = [ 'bar' => 'baz', 'obj' => collect(['a']) ]; 47 | 48 | // set some parameters 49 | $this->parameters = $fakeData; 50 | 51 | // set the name of the json parameters key. 52 | $this->jsonParameters = 'foo'; 53 | 54 | // expected result array format. 55 | $expectedArray = [ 56 | 'foo', $fakeData, 57 | ]; 58 | 59 | // encode the data. 60 | $expectedJson = json_encode($expectedArray); 61 | 62 | // encoded result 63 | $json = json_encode($this->toArray()); 64 | 65 | // assert both json versions are the same. 66 | $this->assertEquals($expectedJson, $json); 67 | 68 | // assert the array transformation. 69 | $this->assertEquals($expectedArray, $this->toArray()); 70 | } 71 | 72 | /** 73 | * Test for operations / instances that don't have name setters and getter. 74 | */ 75 | public function test_nameless_serialization() 76 | { 77 | // start a dump class instance. 78 | $dumb = new DumbJsonClass(); 79 | 80 | // set some parameters. 81 | $parameters = [ 82 | 'foo' => 'bar', 83 | 'bar' => collect(['baz']), 84 | 'baz' => ['a', 'b', 'c'] 85 | ]; 86 | 87 | // create a json version, with external json encoding. 88 | $parametersJson = json_encode([ 89 | 'foo' => 'bar', 90 | 'bar' => json_encode(collect(['baz'])), 91 | 'baz' => json_encode(['a', 'b', 'c']) 92 | ]); 93 | 94 | // set the parameters on the dump implementation. 95 | $dumb->setParameters($parameters); 96 | 97 | // finally call the json encode on the dump implementation. 98 | $jsonSerialized = json_encode($dumb); 99 | 100 | // assert the parameters match after serialization. 101 | $this->assertEquals($parametersJson, $jsonSerialized); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Operations/CommentOptions.php: -------------------------------------------------------------------------------- 1 | parameters, $parameters)); 35 | } 36 | 37 | /** 38 | * Set author and permlink from a comment instance. 39 | * 40 | * @param Comment $comment 41 | * 42 | * @return self 43 | */ 44 | public function of(Comment $comment) : self 45 | { 46 | return $this->author($comment->author)->permLink($comment->permlink); 47 | } 48 | 49 | /** 50 | * Author parameter setter. 51 | * 52 | * @param string $author 53 | * 54 | * @return $this 55 | */ 56 | public function author(string $author) : self 57 | { 58 | return $this->setParameter('author', $author); 59 | } 60 | 61 | /** 62 | * PermLink parameter setter. 63 | * 64 | * @param string $permLink 65 | * 66 | * @return $this 67 | */ 68 | public function permLink(string $permLink) : self 69 | { 70 | return $this->setParameter('permlink', $permLink); 71 | } 72 | 73 | /** 74 | * Set the max payout for the comment. 75 | * 76 | * @param float $maxPayout 77 | * 78 | * @return self 79 | */ 80 | public function maxAcceptedPayout(float $maxPayout) : self 81 | { 82 | // format the number with 3 decimal digits. 83 | $max = number_format($maxPayout, 3, '.', ''); 84 | 85 | return $this->setParameter('max_accepted_payout', "{$max} SBD"); 86 | } 87 | 88 | /** 89 | * @param $value 90 | * 91 | * @return self 92 | */ 93 | public function percentSteemDollars($value) : self 94 | { 95 | return $this->setParameter('percent_steem_dollars', $value); 96 | } 97 | 98 | /** 99 | * @param bool $allowVotes 100 | * 101 | * @return self 102 | */ 103 | public function allowVotes(bool $allowVotes = true) : self 104 | { 105 | return $this->setParameter('allow_votes', $allowVotes); 106 | } 107 | 108 | /** 109 | * @param bool $allowCuration 110 | * 111 | * @return self 112 | */ 113 | public function allowCurationRewards(bool $allowCuration = true) : self 114 | { 115 | return $this->setParameter('allow_curation_rewards', $allowCuration); 116 | } 117 | } -------------------------------------------------------------------------------- /tests/Config/ConfigTest.php: -------------------------------------------------------------------------------- 1 | clientId, $this->clientSecret); 23 | } 24 | 25 | /** 26 | * Test the construction and the inheritance of the config instance. 27 | */ 28 | public function test_construction() 29 | { 30 | // generate a local instance of the config class. 31 | // passing the default client and secret for testing. 32 | $config = new Config($this->clientId, $this->clientSecret); 33 | 34 | // assert the client id and secret were set correctly. 35 | $this->assertEquals($config->getClientId(), $this->clientId); 36 | $this->assertEquals($config->getClientSecret(), $this->clientSecret); 37 | 38 | // assert the instance if actually the SDK config. 39 | $this->assertInstanceOf(Config::class, $config); 40 | // assert the config class inherits the OAuth config class. 41 | $this->assertInstanceOf(OAuthConfig::class, $config); 42 | } 43 | 44 | /** 45 | * Test the default application name on the SDK. 46 | */ 47 | public function test_default_app_name() 48 | { 49 | // get a default config instance from factory. 50 | $config = $this->factoryConfig(); 51 | 52 | // assert the default application name. 53 | $this->assertEquals('sc2-php-sdk/1.0', $config->getApp()); 54 | } 55 | 56 | /** 57 | * Test the customizations on the application name/version. 58 | */ 59 | public function test_customization_of_app_name() 60 | { 61 | // factory a default configuration instance. 62 | $config = $this->factoryConfig(); 63 | 64 | // customize the application name. 65 | $setReturn = $config->setApp('testing/0.10.0'); 66 | 67 | // assert fluent return. 68 | $this->assertSame($config, $setReturn); 69 | 70 | // assert the customized application name was actually set on the instance 71 | $this->assertEquals('testing/0.10.0', $config->getApp()); 72 | } 73 | 74 | /** 75 | * Test the default community on the configuration. 76 | */ 77 | public function test_default_community_is_empty() 78 | { 79 | // factory config. 80 | $config = $this->factoryConfig(); 81 | 82 | // the default community should be null. 83 | $this->assertNull($config->getCommunity()); 84 | } 85 | 86 | /** 87 | * Test the custom community setting. 88 | */ 89 | public function test_customization_of_community() 90 | { 91 | // factory config. 92 | $config = $this->factoryConfig(); 93 | 94 | // customize the community. 95 | $setReturn = $config->setCommunity('some-community'); 96 | 97 | // assert the return is fluent. 98 | $this->assertSame($config, $setReturn); 99 | 100 | // the default community should be null. 101 | $this->assertEquals('some-community', $config->getCommunity()); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Operations/Vote.php: -------------------------------------------------------------------------------- 1 | parameters, $parameters)); 26 | } 27 | 28 | /** 29 | * Voter/account parameter setter. 30 | * 31 | * @param string $voter 32 | * 33 | * @return self 34 | */ 35 | public function voter(string $voter) : self 36 | { 37 | return $this->setParameter('voter', $voter); 38 | } 39 | 40 | /** 41 | * Alias for the voter setter. 42 | * 43 | * @param string $account 44 | * 45 | * @return self 46 | */ 47 | public function account(string $account) : self 48 | { 49 | return $this->voter($account); 50 | } 51 | 52 | /** 53 | * Author parameter setter. 54 | * 55 | * @param string $author 56 | * 57 | * @return self 58 | */ 59 | public function author(string $author) : self 60 | { 61 | return $this->setParameter('author', $author); 62 | } 63 | 64 | /** 65 | * PermLink parameter setter. 66 | * 67 | * @param string $permLink 68 | * 69 | * @return self 70 | */ 71 | public function permLink(string $permLink) : self 72 | { 73 | return $this->setParameter('permlink', $permLink); 74 | } 75 | 76 | /** 77 | * Simple vote author & permlink alias. 78 | * 79 | * @param string $author 80 | * @param string $permlink 81 | * 82 | * @return self 83 | */ 84 | public function on(string $author, string $permlink) : self 85 | { 86 | return $this->author($author)->permLink($permlink); 87 | } 88 | 89 | /** 90 | * Up vote alias. 91 | * 92 | * @param int|float $weight 93 | * 94 | * @return self 95 | */ 96 | public function upVote($weight = 1) : self 97 | { 98 | return $this->weight(abs($weight)); 99 | } 100 | 101 | /** 102 | * Down vote alias. 103 | * 104 | * @param int|float $weight 105 | * 106 | * @return self 107 | */ 108 | public function downVote($weight = -1) : self 109 | { 110 | return $this->weight(abs($weight) * -1); 111 | } 112 | 113 | /** 114 | * Weight parameter setter. 115 | * 116 | * Notice: This value should be a float between 0 and 1 if integers are disabled in configuration. 117 | * 118 | * @param float|int $weight For integers usage, pass 7500 for 75%, for decimal, pass 0.75 for 75%. 119 | * 120 | * @return self 121 | */ 122 | public function weight($weight) : self 123 | { 124 | // detect decimal vs big integer for vote weight. 125 | if (abs($weight) > 0 && abs($weight) <= 1) { 126 | $realWeight = $weight * 10000; 127 | } else { 128 | $realWeight = $weight; 129 | } 130 | 131 | return $this->setParameter('weight', $realWeight); 132 | } 133 | 134 | /** 135 | * Alias for weight setter. 136 | * 137 | * @param $percent 138 | * 139 | * @return self 140 | */ 141 | public function percent($percent) : self 142 | { 143 | return $this->weight($percent); 144 | } 145 | } -------------------------------------------------------------------------------- /tests/Auth/ManagerTest.php: -------------------------------------------------------------------------------- 1 | mockProvider(); 55 | // mock the authorization method to a given return. 56 | $provider->shouldReceive('getAuthorizationUrl')->andReturn('https://foo.bar/authorization'); 57 | 58 | // start a manager instance. 59 | $manager = new Manager($this->mockConfig(), $provider); 60 | 61 | // assert the url building matches. 62 | $this->assertEquals($manager->getAuthorizationUrl(), 'https://foo.bar/authorization'); 63 | } 64 | 65 | /** 66 | * Test token parsing from callback. 67 | */ 68 | public function test_parsing_return() 69 | { 70 | // start a mock oauth provider. 71 | $provider = $this->mockProvider(); 72 | 73 | // mock an access token. 74 | $mockToken = $this->mockAccessToken(); 75 | 76 | // mock the JSON serialization. 77 | $mockToken->shouldReceive('jsonSerialize')->andReturn([ "access_token" => "foo"]); 78 | 79 | // mock the parse return method. 80 | $provider->shouldReceive('parseReturn')->andReturn($mockToken); 81 | 82 | // start a manager instance. 83 | $manager = new Manager($this->mockConfig(), $provider); 84 | 85 | // parse the return from oauth. 86 | $token = $manager->parseReturn(); 87 | 88 | // assert the token was returned properly. 89 | $this->assertEquals("foo", $token->getToken()); 90 | } 91 | 92 | /** 93 | * Test the custom exception throw from parse error (token exception). 94 | */ 95 | public function test_parsing_return_error_custom_exception() 96 | { 97 | // start a mock oauth provider. 98 | $provider = $this->mockProvider(); 99 | 100 | // the provider should throw an exception when the 101 | // parse return method is called. 102 | $provider->shouldReceive('parseReturn')->andReturnUsing(function () { 103 | throw new \Exception(); 104 | }); 105 | 106 | // start a manager instance. 107 | $manager = new Manager($this->mockConfig(), $provider); 108 | 109 | // try the parse. 110 | try { 111 | // this will throw an exception. 112 | $manager->parseReturn(); 113 | } catch (\Exception $e) { 114 | // the exception must be of the TokenException type. 115 | $this->assertInstanceOf(TokenException::class, $e); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Operations/Common/HasParameters.php: -------------------------------------------------------------------------------- 1 | parameters = array_merge($this->parameters, $parameters); 33 | } else { 34 | $this->parameters = $parameters; 35 | } 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Get the parameters array as they are. 42 | * 43 | * @return array 44 | */ 45 | public function getParameters() : array 46 | { 47 | return $this->parameters; 48 | } 49 | 50 | /** 51 | * Fluent parameters setter. 52 | * 53 | * @param string $key 54 | * @param null $value 55 | * 56 | * @return $this 57 | */ 58 | public function setParameter(string $key, $value = null) : self 59 | { 60 | // set a parameter value. 61 | Arr::set($this->parameters, $key, $value); 62 | 63 | // fluent return. 64 | return $this; 65 | } 66 | 67 | /** 68 | * Parameters getter. 69 | * 70 | * @param string $key 71 | * 72 | * @return null|string|array|Operation|SubOperation 73 | */ 74 | public function getParameter(string $key) 75 | { 76 | return Arr::get($this->parameters, $key); 77 | } 78 | 79 | /** 80 | * Set a inner parameter. 81 | * 82 | * @param string $parameter 83 | * @param string $innerParameter 84 | * @param null $value 85 | * 86 | * @return self 87 | */ 88 | public function setInnerParameter(string $parameter, string $innerParameter, $value = null) : self 89 | { 90 | Arr::set($this->parameters, "{$parameter}.{$innerParameter}", $value); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Retrieve a inner parameter key. 97 | * 98 | * @param string $parameter 99 | * @param string $innerParameter 100 | * 101 | * @return mixed 102 | */ 103 | public function getInnerParameter(string $parameter, string $innerParameter) 104 | { 105 | return Arr::get($this->parameters, "{$parameter}.{$innerParameter}"); 106 | } 107 | 108 | /** 109 | * Unset a previously set parameter. 110 | * 111 | * @param string $key Parameter to remove. 112 | * 113 | * @return bool 114 | */ 115 | public function forgetParameter(string $key) 116 | { 117 | // forget the element on the parameters array. 118 | Arr::forget($this->parameters, $key); 119 | 120 | // return true if the parameter no longer exists. 121 | return !Arr::has($this->parameters, $key); 122 | } 123 | 124 | /** 125 | * Unset a previously set inner parameter. 126 | * 127 | * @param string $parameter Parameter key to remove. 128 | * @param string $innerParameter Inner parameter key to remove. 129 | * 130 | * @return bool 131 | */ 132 | public function forgetInnerParameter(string $parameter, string $innerParameter) 133 | { 134 | // forget the element on the parameters array. 135 | Arr::forget($this->parameters, "{$parameter}.{$innerParameter}"); 136 | 137 | // return the parameter key existence, to indicate operation success or failure. 138 | return !Arr::has($this->parameters, "{$parameter}.{$innerParameter}"); 139 | } 140 | } -------------------------------------------------------------------------------- /docs/01-configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Before heading into SDK usage, we need to configure it, with credentials, return url and scopes. 4 | 5 | ## Configuration Class. 6 | 7 | To start, we are going to create an instance of **`SteemConnect\Config\Config`**. 8 | 9 | ``` php 10 | setReturnUrl('https://your-steem-app/auth/callback'); 37 | ``` 38 | 39 | !!! note 40 | The URL being used on configuration must match the one configured on SteemConnect dashboard, otherwise the authorization flow will fail. 41 | 42 | ### Scopes. 43 | 44 | Other required configuration are which scopes your application requires. 45 | 46 | On the OAuth flow, a scope could be translated to which permissions your users will grant you. 47 | 48 | There are several scopes available. The list of scopes presented here may change with time, so, an up-to-date reference can fe found at **[SteemConnect wiki](https://github.com/steemit/steemconnect/wiki/OAuth-2#scopes)**. 49 | 50 | | Scope | Description 51 | | - | - 52 | | **login** | Verify Steem identity 53 | | **offline** | Allow long-lived token 54 | | **vote** | Upvote, downvote or unvote a post or comment 55 | | **comment** | Publish or edit a post or a comment 56 | | **comment_delete** | Delete a post or a comment 57 | | **comment_options** | Add options for a post or comment 58 | | **custom_json** | Follow, unfollow, ignore, reblog or any custom_json operation 59 | | **claim_reward_balance** | Claim reward for user 60 | 61 | !!! note 62 | If your application only needs to verify the user identity, the required scope **`login`** does not persist changes on the user account, so be sure to remember the user session to avoid the authorization dialog in every visit. 63 | 64 | To configure the scopes your application will require, just call the `setScopes` method on the config object, passing those scopes as an array: 65 | 66 | ``` php 67 | setScopes([ 73 | 'login', 74 | 'vote', 75 | 'comment' 76 | ]); 77 | 78 | ``` 79 | 80 | ### Application and Community Name. 81 | 82 | An optional but interesting feature, is to configure both the application and community names. Those are used to indicate 83 | what application was used to make the post, and some frontend will display that information. 84 | 85 | If you don't know what I'm talking about, here is an example: 86 | 87 | ![app name example](https://ipfs.io/ipfs/Qmcesk7EDVRr1t1kgD7P4AfsycZcka7gafvNpMnKdq3APi) 88 | 89 | The syntax for application name, as of right now, is **`lowercase-app-name/version`**, here's an example: 90 | 91 | ``` php 92 | setApp('coolapp/2.4'); 98 | ``` 99 | 100 | The same way, it's possible to set the community name: 101 | 102 | ``` php 103 | setCommunity('CoolApp'); 109 | ``` 110 | 111 | ### Custom SteemConnect Servers. 112 | 113 | If for some reason, you are using a custom install of SteemConnect (a development install, for example), you may 114 | change the base URL so all calls will use that domain. 115 | 116 | ``` php 117 | setBaseUrl('https://my-custom-steemconnect.com'); 123 | 124 | ``` -------------------------------------------------------------------------------- /tests/Operations/FollowTest.php: -------------------------------------------------------------------------------- 1 | account('some-account'); 25 | $this->assertEquals('some-account', $follow->follower); 26 | 27 | // set and test. 28 | $follow->follower('follower-account'); 29 | $this->assertEquals('follower-account', $follow->follower); 30 | 31 | // set and test. 32 | $follow->following('following', true); 33 | $this->assertEquals('following', $follow->following); 34 | $this->assertEquals(['blog'], $follow->what); 35 | 36 | // set and test. 37 | $follow->following('following', false); 38 | $this->assertEquals('following', $follow->following); 39 | $this->assertEquals([], $follow->what); 40 | 41 | // reset the follow instance. 42 | $follow = new Follow(); 43 | 44 | // follow alias test. 45 | $follow->follow('others'); 46 | $this->assertEquals('others', $follow->following); 47 | $this->assertEquals(['blog'], $follow->what); 48 | 49 | // unfollow alias test. 50 | $follow->unfollow('others'); 51 | $this->assertEquals('others', $follow->following); 52 | $this->assertEquals([], $follow->what); 53 | 54 | $follow->following('other', true); 55 | } 56 | 57 | /** 58 | * Test the getter on a sub operation property. 59 | */ 60 | public function test_defined_sub_operation_property() 61 | { 62 | // start a follow operation. 63 | $follow = new Follow(); 64 | 65 | // use the magic setter against a already defined property. 66 | $this->assertNotNull($follow->getSubOperation()->parameters); 67 | } 68 | 69 | /** 70 | * Required auths and posting auths testing. 71 | */ 72 | public function test_required_auths() 73 | { 74 | // start a follow operation. 75 | $follow = new Follow(); 76 | 77 | // required auths getter / setter tests. 78 | $this->assertEquals([], $follow->getRequiredAuths()); 79 | $this->assertEquals([], $follow->getRequiredPostingAuths()); 80 | 81 | // custom the auths. 82 | $follow->setRequiredAuths(['a', 'b']); 83 | $follow->setRequiredPostingAuths(['c', 'd']); 84 | 85 | // required auths getter tests. 86 | $this->assertEquals(['a', 'b'], $follow->getRequiredAuths()); 87 | $this->assertEquals(['c', 'd'], $follow->getRequiredPostingAuths()); 88 | } 89 | 90 | /** 91 | * Sub operation testing. 92 | */ 93 | public function test_sub_operations() 94 | { 95 | // start a follow operation. 96 | $follow = new Follow(); 97 | 98 | // get the sub operation (currently empty); 99 | $subOperation = $follow->getSubOperation(); 100 | // set a given parameter/ 101 | $subOperation->setParameter('foo', 'bar'); 102 | 103 | // set the sub operation back. 104 | $follow->setSubOperation($subOperation); 105 | 106 | // call the magic getter to extract the value. 107 | $this->assertEquals('bar', $follow->foo); 108 | } 109 | 110 | /** 111 | * Test direct parameters getter. 112 | */ 113 | public function test_magic_getter_against_local_property() 114 | { 115 | // start a follow operation. 116 | $follow = new Follow(); 117 | 118 | // test the getter and it's value. 119 | $this->assertEquals('follow', $follow->customOperation); 120 | } 121 | 122 | /** 123 | * Assert parsing JSON string on the constructor. 124 | */ 125 | public function test_json_string_parsing_on_constructor() 126 | { 127 | // dummy operation data. 128 | $operation = [ 129 | 'json' => json_encode(['foo' => 'bar']) 130 | ]; 131 | 132 | // start a follow instance, passing some data. 133 | $follow = new Follow($operation); 134 | 135 | // assert the value. 136 | $this->assertEquals('bar', $follow->foo); 137 | } 138 | } -------------------------------------------------------------------------------- /src/Operations/CustomJson.php: -------------------------------------------------------------------------------- 1 | [], 30 | 'required_posting_auths' => [], 31 | ]; 32 | 33 | /** 34 | * @var string List of parameters to treat as JSON. 35 | */ 36 | protected $jsonParameters = 'json'; 37 | 38 | /** 39 | * Vote operation constructor. 40 | * 41 | * @param array $parameters 42 | */ 43 | public function __construct(array $parameters = []) 44 | { 45 | // extract json string from custom_json. 46 | $json = array_get($parameters, $this->jsonParameters); 47 | 48 | // if there's a json sub operation data to be parsed. 49 | if ($json && Str::startsWith($json, '{')) { 50 | // decode it's data. 51 | $subOperationParameters = json_decode($json, true); 52 | // overwrite with a sub operation instance. 53 | Arr::set($parameters, $this->jsonParameters, new SubOperation($this->customOperation, $subOperationParameters)); 54 | } else { 55 | Arr::set($parameters, $this->jsonParameters, new SubOperation($this->customOperation)); 56 | } 57 | 58 | // cal parent constructor. 59 | parent::__construct('custom_json', array_merge($this->parameters, $parameters)); 60 | } 61 | 62 | /** 63 | * Set the required auths for the custom json operation. 64 | * 65 | * @param array $requiredAuths 66 | * 67 | * @return self 68 | */ 69 | public function setRequiredAuths(array $requiredAuths = []) : self 70 | { 71 | return $this->setParameter('required_auths', $requiredAuths); 72 | } 73 | 74 | /** 75 | * Returns the configured required auths for the operation. 76 | * 77 | * @return array 78 | */ 79 | public function getRequiredAuths() : array 80 | { 81 | return $this->getParameter('required_auths'); 82 | } 83 | 84 | /** 85 | * Set the required posting auths for the custom json operation. 86 | * 87 | * @param array $requiredPostingAuths 88 | * 89 | * @return self 90 | */ 91 | public function setRequiredPostingAuths(array $requiredPostingAuths = []) : self 92 | { 93 | return $this->setParameter('required_posting_auths', $requiredPostingAuths); 94 | } 95 | 96 | /** 97 | * Returns the configured required posting auths for the operation. 98 | * 99 | * @return array 100 | */ 101 | public function getRequiredPostingAuths() : array 102 | { 103 | return $this->getParameter('required_posting_auths'); 104 | } 105 | 106 | /** 107 | * Set the custom json operation id. 108 | * 109 | * @param string $id 110 | * 111 | * @return self 112 | */ 113 | public function setId(string $id) : self 114 | { 115 | return $this->setParameter('id', $id); 116 | } 117 | 118 | /** 119 | * Returns the custom json operation id. 120 | * 121 | * @return null|string 122 | */ 123 | public function getId() : ? string 124 | { 125 | return $this->getParameter('id'); 126 | } 127 | 128 | /** 129 | * Get the sub operation instance, if any. 130 | * 131 | * @return null|SubOperation 132 | */ 133 | public function getSubOperation() : ?SubOperation 134 | { 135 | return $this->getParameter($this->jsonParameters); 136 | } 137 | 138 | /** 139 | * Set the sub operation directly on the json attribute. 140 | * 141 | * @param SubOperation $subOperation 142 | * 143 | * @return self 144 | */ 145 | public function setSubOperation(SubOperation $subOperation) : self 146 | { 147 | $this->setParameter($this->jsonParameters, $subOperation); 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function __get($name) 156 | { 157 | $value = parent::__get($name); 158 | 159 | if ($value) { 160 | return $value; 161 | } 162 | 163 | return $this->getSubOperation()->{$name}; 164 | } 165 | } -------------------------------------------------------------------------------- /src/Client/Broadcaster.php: -------------------------------------------------------------------------------- 1 | config = $config; 47 | 48 | // assign token instance. 49 | $this->token = $token; 50 | 51 | // assign http client instance. 52 | $this->httpClient = $httpClient; 53 | } 54 | 55 | /** 56 | * Get the current configuration instance on the broadcaster. 57 | * 58 | * @return Config 59 | */ 60 | public function getConfig(): Config 61 | { 62 | return $this->config; 63 | } 64 | 65 | /** 66 | * Customize the configuration on the broadcaster instance. 67 | * 68 | * @param Config $config 69 | * 70 | * @return self 71 | */ 72 | public function setConfig(Config $config): self 73 | { 74 | $this->config = $config; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Get the current access token instance on the broadcaster. 81 | * 82 | * @return Token 83 | */ 84 | public function getToken(): Token 85 | { 86 | return $this->token; 87 | } 88 | 89 | /** 90 | * Customize the access token on the broadcaster. 91 | * 92 | * @param Token $token 93 | * 94 | * @return self 95 | */ 96 | public function setToken(Token $token): self 97 | { 98 | $this->token = $token; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Get the current HttpClient instance on the broadcaster. 105 | * 106 | * @return HttpClient 107 | */ 108 | public function getHttpClient(): HttpClient 109 | { 110 | return $this->httpClient; 111 | } 112 | 113 | /** 114 | * Customize the HttpClient instance on the broadcaster. 115 | * 116 | * @param HttpClient $httpClient 117 | * 118 | * @return self 119 | */ 120 | public function setHttpClient(HttpClient $httpClient): self 121 | { 122 | $this->httpClient = $httpClient; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Broadcasts an Operation through SteemConnect V2. 129 | * 130 | * @param array $operations List of operations to broadcast, usually, just one. 131 | * 132 | * @throws \Exception 133 | * 134 | * @return Response 135 | */ 136 | public function broadcast($operations) 137 | { 138 | // collect the operations list to broadcast. 139 | $operationsList = collect($operations)->toArray(); 140 | 141 | try { 142 | // call SteemConnect passing a list of operations to broadcast. 143 | $httpResponse = $this->getHttpClient() 144 | ->call('POST', 'api/broadcast', [ 145 | 'operations' => $operationsList 146 | ]); 147 | 148 | // creates a client response instance. 149 | $response = new Response(); 150 | // set the http response on the client response object. 151 | $response->setHttpResponse($httpResponse); 152 | 153 | // finally returns the response object. 154 | return $response; 155 | 156 | } catch (BadResponseException $e) { 157 | // throw a custom response exception otherwise. 158 | throw new ResponseException($e->getResponse()); 159 | } catch (HttpClientException $e) { 160 | // throw a client exception, passing the previous one. 161 | throw new ClientException("Error broadcasting the operation", 0, $e); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/Http/Client.php: -------------------------------------------------------------------------------- 1 | config = $config; 44 | 45 | // access token. 46 | $this->accessToken = $token; 47 | 48 | // get the http client (default one at constructor time.) 49 | $this->httpClient = $httpClient ? $httpClient : $this->getHttpClient(); 50 | } 51 | 52 | /** 53 | * Replaces the configuration object. 54 | * 55 | * @param Config $config Config instance to replace on the client. 56 | * 57 | * @return $this 58 | */ 59 | public function setConfig(Config $config) : self 60 | { 61 | $this->config = $config; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Retrieves the current configuration object. 68 | * 69 | * @return Config Configuration instance. 70 | */ 71 | public function getConfig() : ?Config 72 | { 73 | return $this->config; 74 | } 75 | 76 | /** 77 | * Custom Access Token setter. 78 | * 79 | * @param Token $token 80 | * 81 | * @return Client 82 | */ 83 | public function setAccessToken(Token $token) : self 84 | { 85 | $this->accessToken = $token; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Access Token getter. 92 | * 93 | * @return null|Token 94 | */ 95 | public function getAccessToken() : ?Token 96 | { 97 | return $this->accessToken; 98 | } 99 | 100 | /** 101 | * HttpClient instance. 102 | * 103 | * @param HttpClientInterface $httpClient 104 | */ 105 | public function setHttpClient(HttpClientInterface $httpClient) 106 | { 107 | $this->httpClient = $httpClient; 108 | } 109 | 110 | /** 111 | * Returns the custom or factories a new HttpClient for the API client. 112 | * 113 | * @return HttpClientInterface 114 | */ 115 | public function getHttpClient() : HttpClientInterface 116 | { 117 | // returns a new HTTP client, if none is set on the client instance. 118 | return $this->httpClient ? $this->httpClient : new HttpClient(); 119 | } 120 | 121 | /** 122 | * Returns a list of default HTTP headers to include in every request 123 | * 124 | * @return array 125 | */ 126 | protected function defaultHeaders() 127 | { 128 | return [ 129 | // accepted types of response. 130 | 'Accept' => 'application/json', 131 | // current request content type. 132 | 'Content-Type' => 'application/json', 133 | // authorization (OAuth token) header. 134 | 'Authorization' => "Bearer {$this->accessToken->getToken()}", 135 | ]; 136 | } 137 | 138 | /** 139 | * Execute a HTTP request. 140 | * 141 | * @param string $method HTTP method to call (most of the time, it's post.) 142 | * @param string $uri HTTP request URI. 143 | * @param array|null $body HTTP request body. 144 | * 145 | * @return mixed|\Psr\Http\Message\ResponseInterface 146 | * 147 | * @throws \GuzzleHttp\Exception\GuzzleException 148 | */ 149 | public function call(string $method, string $uri, array $body = null) 150 | { 151 | $request = new Request($method, $this->config->buildUrl($uri), $this->defaultHeaders(), $body ? json_encode($body) : null); 152 | 153 | $response = $this->httpClient->send($request); 154 | 155 | return $response; 156 | } 157 | } -------------------------------------------------------------------------------- /tests/Client/ResponseTest.php: -------------------------------------------------------------------------------- 1 | transactionJson = file_get_contents(__DIR__.'/../Resources/stub-transaction.json'); 42 | 43 | // decode the json stub transaction into array. 44 | $this->transactionData = json_decode($this->transactionJson, true); 45 | } 46 | 47 | /** 48 | * Mock a valid transaction http response. 49 | * 50 | * @return Mockery\MockInterface|ResponseInterface 51 | */ 52 | protected function mockHttpResponse() 53 | { 54 | // mock a response. 55 | $response = Mockery::mock(ResponseInterface::class); 56 | 57 | // status codes and reason phrase. 58 | $response->shouldReceive('getStatusCode')->andReturn(200); 59 | $response->shouldReceive('getReasonPhrase')->andReturn('OK'); 60 | 61 | // json headers. 62 | $response->shouldReceive('getHeaders')->andReturn(['Content-Type' => 'application/json']); 63 | 64 | // transaction json body. 65 | $body = Mockery::mock(StreamInterface::class); 66 | $body->shouldReceive('__toString')->andReturn($this->transactionJson); 67 | 68 | // response body stream. 69 | $response->shouldReceive('getBody')->andReturn($body); 70 | 71 | // response return. 72 | return $response; 73 | } 74 | 75 | /** 76 | * Mock a invalid, empty http response. 77 | * 78 | * @return Mockery\MockInterface|ResponseInterface 79 | */ 80 | protected function mockEmptyHttpResponse() 81 | { 82 | // mock a response. 83 | $response = Mockery::mock(ResponseInterface::class); 84 | 85 | // status codes and reason phrase. 86 | $response->shouldReceive('getStatusCode')->andReturn(200); 87 | $response->shouldReceive('getReasonPhrase')->andReturn('OK'); 88 | 89 | // json headers. 90 | $response->shouldReceive('getHeaders')->andReturn(['Content-Type' => 'application/json']); 91 | 92 | // transaction json body. 93 | $body = Mockery::mock(StreamInterface::class); 94 | $body->shouldReceive('__toString')->andReturn(""); 95 | 96 | // response body stream. 97 | $response->shouldReceive('getBody')->andReturn($body); 98 | 99 | // response return. 100 | return $response; 101 | } 102 | 103 | /** 104 | * Parse the transaction from the response. 105 | */ 106 | public function test_parsing_into_transaction() 107 | { 108 | // get the mock http response. 109 | $httpResponse = $this->mockHttpResponse(); 110 | 111 | // start a new response instance. 112 | $response = new Response(); 113 | 114 | // set the http response on the response instance. 115 | $response->setHttpResponse($httpResponse); 116 | 117 | // get the transaction from the http response. 118 | $transaction = $response->getTransaction(); 119 | 120 | // assert the transaction instance was returned. 121 | $this->assertInstanceOf(Transaction::class, $transaction); 122 | } 123 | 124 | /** 125 | * Test invalid responses. 126 | */ 127 | public function test_parsing_invalid_responses() 128 | { 129 | // get the mock http response. 130 | $httpResponse = $this->mockEmptyHttpResponse(); 131 | 132 | // start a new response instance. 133 | $response = new Response(); 134 | 135 | // set the http response on the response instance. 136 | $response->setHttpResponse($httpResponse); 137 | 138 | // get the transaction from the http response. 139 | // this return should be null. 140 | $transaction = $response->getTransaction(); 141 | 142 | // assert the return was null. 143 | $this->assertNull($transaction); 144 | } 145 | } -------------------------------------------------------------------------------- /tests/Operations/Common/HasParametersTraitTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(0, count($this->getParameters())); 24 | 25 | // assert the parameter does not previously exists. 26 | // meaning, it must return null. 27 | $this->assertNull($this->getParameter('foo')); 28 | 29 | // set a given parameter 30 | $setReturn = $this->setParameter('foo', 'bar'); 31 | 32 | // assert the fluent return. 33 | $this->assertSame($this, $setReturn); 34 | 35 | // assert the parameter was correctly set. 36 | $this->assertEquals('bar', $this->getParameter('foo')); 37 | } 38 | 39 | /** 40 | * Test batch setters and getters of parameters. 41 | */ 42 | public function test_batch_parameter_setter_and_getter() 43 | { 44 | // assert the number of parameters starts as zero. 45 | $this->assertEquals(0, count($this->getParameters())); 46 | 47 | // set a given number of parameters (2). 48 | $setReturn = $this->setParameters([ 49 | 'foo' => 'bar', 50 | 'bar' => 'baz', 51 | ]); 52 | 53 | // assert the fluent return. 54 | $this->assertSame($this, $setReturn); 55 | 56 | // assert that now there are two parameters. 57 | $this->assertEquals(2, count($this->getParameters())); 58 | 59 | // assert the parameters are the ones previously set. 60 | $this->assertEquals([ 61 | 'foo' => 'bar', 62 | 'bar' => 'baz', 63 | ], $this->getParameters()); 64 | } 65 | 66 | /** 67 | * Test batch parameter setter, with the merge option. 68 | */ 69 | public function test_batch_parameter_merge() 70 | { 71 | // set an initial parameter 72 | $this->setParameter('foo', 'bar'); 73 | 74 | // set additional parameters, this time merging. 75 | $this->setParameters(['bar' => 'baz'], true); 76 | 77 | // assert the parameters are the ones previously set. 78 | $this->assertEquals([ 79 | 'foo' => 'bar', 80 | 'bar' => 'baz', 81 | ], $this->getParameters()); 82 | } 83 | 84 | /** 85 | * Test for forgetting a given parameter by key. 86 | */ 87 | public function test_parameter_forget() 88 | { 89 | // set a given parameter. 90 | $this->setParameter('foo', 'bar'); 91 | 92 | // assert it was set on the first place. 93 | $this->assertEquals('bar', $this->getParameter('foo')); 94 | 95 | // forget the parameter previously set. 96 | $forgetReturn = $this->forgetParameter('foo'); 97 | 98 | // since the parameter was removed, true should be returned. 99 | $this->assertTrue($forgetReturn); 100 | 101 | // after removal, the return should be now null. 102 | $this->assertEquals(null, $this->getParameter('foo')); 103 | } 104 | 105 | /** 106 | * Test the inner parameter (used for nested parameter setting / getting). 107 | */ 108 | public function test_inner_parameter_getter_and_setter() 109 | { 110 | // get a inner value 111 | $innerBar = $this->getInnerParameter('foo', 'bar'); 112 | 113 | // assert the return is null, even when when the parent does not exists. 114 | $this->assertNull($innerBar); 115 | 116 | // set a inner parameter, on a non existing key. 117 | $innerSetReturn = $this->setInnerParameter('foo', 'bar', 'baz'); 118 | 119 | // assert the fluent return from setter. 120 | $this->assertSame($this, $innerSetReturn); 121 | 122 | // get a inner value 123 | $innerBar = $this->getInnerParameter('foo', 'bar'); 124 | 125 | // assert the value was set and get be retrieved just fine. 126 | $this->assertEquals('baz', $innerBar); 127 | } 128 | 129 | /** 130 | * Test a inner parameter forget. 131 | */ 132 | public function test_inner_parameter_forget() 133 | { 134 | // set a inner parameter. 135 | $this->setInnerParameter('foo', 'bar', 'baz'); 136 | 137 | // forget the inner parameter. 138 | $forgetInnerReturn = $this->forgetInnerParameter('foo', 'bar'); 139 | 140 | // forget should return true case the forget key no longer exists. 141 | $this->assertTrue($forgetInnerReturn); 142 | 143 | // assert the value is now null. 144 | $this->assertNull($this->getInnerParameter('foo', 'bar')); 145 | } 146 | } -------------------------------------------------------------------------------- /tests/Operations/CommentTest.php: -------------------------------------------------------------------------------- 1 | title($title); 31 | 32 | // assert a permlink was generated. 33 | $this->assertEquals($permLink, $comment->permlink); 34 | } 35 | 36 | /** 37 | * Test permlink is preserved when present and title is set. 38 | */ 39 | public function test_permlink_is_preserved() 40 | { 41 | // start comment. 42 | $comment = new Comment(); 43 | 44 | // set a custom permlink. 45 | $comment->permLink('some-custom-permlink'); 46 | 47 | // set comment title. 48 | $comment->title('foo bar, baz'); 49 | 50 | // now assert the title did not replaced the initial permlink. 51 | $this->assertEquals('some-custom-permlink', $comment->permlink); 52 | } 53 | 54 | /** 55 | * Test reply settings. 56 | */ 57 | public function test_reply_setters() 58 | { 59 | // start a comment. 60 | $comment = new Comment(); 61 | 62 | // call the reply alias. 63 | $comment->reply('some-user', 'post-permlink'); 64 | 65 | // assert the parent author and permlink. 66 | $this->assertEquals('some-user', $comment->parent_author); 67 | $this->assertEquals('post-permlink', $comment->parent_permlink); 68 | 69 | // start another comment. 70 | $comment = new Comment(); 71 | 72 | // parent author setter. 73 | $comment->parentAuthor('another-user'); 74 | $this->assertEquals('another-user', $comment->parent_author); 75 | 76 | // parent permlink setter. 77 | $comment->parentPermLink('another-post-permlink'); 78 | $this->assertEquals('another-post-permlink', $comment->parent_permlink); 79 | } 80 | 81 | /** 82 | * Test author and category. 83 | */ 84 | public function test_author_and_category() 85 | { 86 | // start a comment. 87 | $comment = new Comment(); 88 | 89 | // call the category alias. 90 | $comment->category('cool-topic'); 91 | 92 | // assert the parent permlink. 93 | $this->assertEquals('cool-topic', $comment->parent_permlink); 94 | // parent author should start as empty string. 95 | $this->assertEquals('', $comment->parent_author); 96 | 97 | // set the author. 98 | $comment->author('the-author'); 99 | // assert it's value. 100 | $this->assertEquals('the-author', $comment->author); 101 | } 102 | 103 | /** 104 | * Test tags. 105 | */ 106 | public function test_tags() 107 | { 108 | // start a comment. 109 | $comment = new Comment(); 110 | 111 | // set the tags on the comment. 112 | $comment->tags(['foo', 'bar', 'baz']); 113 | 114 | // assert the json meta value was set. 115 | $this->assertEquals(['foo', 'bar', 'baz'], $comment->tags); 116 | } 117 | 118 | /** 119 | * Customize application and community on a given comment. 120 | */ 121 | public function test_app_and_community() 122 | { 123 | // start a comment. 124 | $comment = new Comment(); 125 | 126 | // set the app name. 127 | $comment->app('foobar/9.0'); 128 | 129 | // set community. 130 | $comment->community('steemdev'); 131 | 132 | // assert the json meta value was set. 133 | $this->assertEquals('foobar/9.0', $comment->app); 134 | 135 | // assert the json meta value was set. 136 | $this->assertEquals('steemdev', $comment->community); 137 | } 138 | 139 | /** 140 | * Test direct setting json metadata. 141 | */ 142 | public function test_direct_json_meta_parsing() 143 | { 144 | // start a comment 145 | $comment = new Comment(); 146 | 147 | // set the json metadata. 148 | $comment->jsonMetadata(['foo' => 'bar', 'tags' => ['a', 'b']]); 149 | 150 | // assert the tags set from the direct array passing. 151 | $this->assertEquals(['a', 'b'], $comment->tags); 152 | } 153 | 154 | /** 155 | * Tests related to comment content (body). 156 | */ 157 | public function test_body_set_and_parsing() 158 | { 159 | // start the comment. 160 | $comment = new Comment(); 161 | 162 | // post body 163 | $comment->body('this is a body ok?'); 164 | 165 | // assert a slug was generated for the permlink from the content body. 166 | $this->assertEquals('this-is-a-body-ok', $comment->permlink); 167 | } 168 | } -------------------------------------------------------------------------------- /docs/02-authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | This library wraps all authentication functionality built on **[oauth2-sc2](https://github.com/hernandev/oauth2-sc2)**, if you are using the SDK, there's no need for separate authentication configuration, since all logic is wrapped on the SDK. 4 | 5 | ## Before Authentication. 6 | 7 | Before we head into the actual authentication, we must have a SDK client instance. 8 | 9 | The class that handle all SDK features is **`SteemConnect\Client\Client`** and for the examples on this section, we will create one instance and name the variable **`$sdk`**: 10 | 11 | Also, we are assuming, you have the **`$config`** variable from the configuration section available. 12 | 13 | ``` php 14 | auth()->getAuthorizationUrl(); 52 | ``` 53 | 54 | Now, you can actually redirect the user to that URL. Be free to use any method you want. 55 | 56 | ``` php 57 | auth()->parseReturn(); 96 | ``` 97 | 98 | The token returned on the callback / return page is an instance of **`SteemConnect\Auth\Token`**, which we will discuss on the next section. 99 | 100 | ## Storing and Using Access Tokens. 101 | 102 | Each access token is specific to a given user. Meaning when you need to the access token every time you need to broadcast an operation 103 | to the Steem blockchain. 104 | 105 | Since the tokens as an instance of **`SteemConnect\Auth\Token`**, it's easy to serialize and factory it's instance: 106 | 107 | ### Serialization for Storage: 108 | 109 | When you have a **`Token`** instance, and you need to store it, you can transform the token into a JSON string by doing: 110 | 111 | ``` php 112 | setToken($token); 150 | ``` -------------------------------------------------------------------------------- /tests/Http/HasHttpResponseTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('__toString')->andReturn('{"foo": "bar"}'); 31 | 32 | // return the mock stream. 33 | return $stream; 34 | } 35 | 36 | /** 37 | * Mock a Stream with Text/HTML contents. 38 | * 39 | * @return Mockery\MockInterface|StreamInterface 40 | */ 41 | protected function makeTextStreamMock() 42 | { 43 | // create a stream mock. 44 | $stream= Mockery::mock(StreamInterface::class); 45 | // return some html when serialized. 46 | $stream->shouldReceive('__toString')->andReturn('
foo - bar
'); 47 | 48 | // return the mock stream. 49 | return $stream; 50 | } 51 | 52 | /** 53 | * Generic response mock factory. 54 | * 55 | * @param int $statusCode 56 | * @param string $reasonPhrase 57 | * 58 | * @return Mockery\MockInterface|ResponseInterface 59 | */ 60 | protected function makeResponseMock(int $statusCode = 200, string $reasonPhrase = 'Ok.') 61 | { 62 | // get a new mock response. 63 | $response = Mockery::mock(ResponseInterface::class); 64 | 65 | // set both status code and status reason (phrase) on the mock. 66 | $response->shouldReceive('getStatusCode')->andReturn($statusCode); 67 | $response->shouldReceive('getReasonPhrase')->andReturn($reasonPhrase); 68 | 69 | // return the mock response. 70 | return $response; 71 | } 72 | 73 | /** 74 | * Mock a normal (200) response. 75 | * 76 | * @return Mockery\MockInterface|ResponseInterface 77 | */ 78 | protected function getNormalJsonResponse() 79 | { 80 | // start a mock with code 200. 81 | $response = $this->makeResponseMock(200, 'Ok'); 82 | 83 | // the response headers should match a JSON response headers. 84 | $response->shouldReceive('getHeaders')->andReturn([ 85 | 'Content-Type' => 'application/json' 86 | ]); 87 | 88 | // and the body should return a Stream with JSON contents. 89 | $response->shouldReceive('getBody')->andReturn($this->makeJsonStreamMock()); 90 | 91 | // return the mock response. 92 | return $response; 93 | } 94 | 95 | /** 96 | * Mock a normal (200) response. 97 | * 98 | * @return Mockery\MockInterface|ResponseInterface 99 | */ 100 | protected function getNormalNonJsonResponse() 101 | { 102 | // start a mock with code 200. 103 | $response = $this->makeResponseMock(200, 'Ok'); 104 | 105 | // instead of JSON, the headers should be anything else, in this case, HTML. 106 | $response->shouldReceive('getHeaders')->andReturn([ 107 | 'Content-Type' => 'text/html' 108 | ]); 109 | 110 | // the body stream should also return anything but JSON, in this case, some HTML tags. 111 | $response->shouldReceive('getBody')->andReturn($this->makeTextStreamMock()); 112 | 113 | // return the mock response. 114 | return $response; 115 | } 116 | 117 | /** 118 | * Test JSON response parsing. 119 | */ 120 | public function test_json_detection() 121 | { 122 | // set the HTTP response. 123 | $this->setHttpResponse($this->getNormalJsonResponse()); 124 | // assert the response is JSON (JSON detection). 125 | $this->assertTrue($this->responseIsJson()); 126 | } 127 | 128 | /** 129 | * Test NON-JSON (text/html and other) responses parsing. 130 | */ 131 | public function test_text_detection() 132 | { 133 | // set a non-json response/ 134 | $this->setHttpResponse($this->getNormalNonJsonResponse()); 135 | 136 | // assert the response is not JSON. 137 | $this->assertFalse($this->responseIsJson()); 138 | } 139 | 140 | /** 141 | * Tests for the internal http response getters and setters. 142 | */ 143 | public function test_http_response_getter_and_setter() 144 | { 145 | // assert the initial response content is empty 146 | $this->assertNull($this->getHttpResponse()); 147 | 148 | // get a response mock. 149 | $mockHttpResponse = $this->getNormalJsonResponse(); 150 | 151 | // set the mock http response. 152 | $this->setHttpResponse($mockHttpResponse); 153 | 154 | // assert the set was ok, by comparing the local instance with getter instance. 155 | $this->assertSame($mockHttpResponse, $this->getHttpResponse()); 156 | } 157 | } -------------------------------------------------------------------------------- /tests/Transactions/TransactionTest.php: -------------------------------------------------------------------------------- 1 | transactionJson = file_get_contents(__DIR__.'/../Resources/stub-transaction.json'); 46 | 47 | // decode the json stub transaction into array. 48 | $this->transactionData = json_decode($this->transactionJson, true); 49 | 50 | // make sure the transaction data is sorted by key. 51 | ksort($this->transactionData); 52 | 53 | // factory the transaction instance from the data. 54 | $this->transaction = Transaction::factory($this->transactionData); 55 | } 56 | 57 | /** 58 | * Test the transaction data parsing and it's serialization. 59 | */ 60 | public function test_transaction_getters() 61 | { 62 | // start a transaction instance with the stub data. 63 | $transaction = Transaction::factory($this->transactionData); 64 | 65 | // test transaction id. 66 | $this->assertEquals($this->transactionData['id'], $transaction->getId()); 67 | 68 | // test block number. 69 | $this->assertEquals($this->transactionData['block_num'], $transaction->getBlockNumber()); 70 | 71 | // test transaction number. 72 | $this->assertEquals($this->transactionData['trx_num'], $transaction->getTransactionNumber()); 73 | 74 | // test expiration status. 75 | $this->assertEquals($this->transactionData['expired'], $transaction->getExpired()); 76 | 77 | // ensure the expiration is a carbon instance. 78 | $this->assertInstanceOf(Carbon::class, $transaction->getExpiration()); 79 | 80 | // format the expiration for testing. 81 | $formattedExpiration = $transaction->getExpiration()->format('Y-m-d\TH:i:s'); 82 | // test if the expiration matches (after formatting). 83 | $this->assertEquals($this->transactionData['expiration'], $formattedExpiration); 84 | 85 | // test reference block number. 86 | $this->assertEquals($this->transactionData['ref_block_num'], $transaction->getReferenceBlockNumber()); 87 | 88 | // test reference block prefix. 89 | $this->assertEquals($this->transactionData['ref_block_prefix'], $transaction->getReferenceBlockPrefix()); 90 | 91 | // test the operations are a collection. 92 | $this->assertInstanceOf(Collection::class, $transaction->getOperations()); 93 | // test the operations (as array). 94 | $this->assertEquals($this->transactionData['operations'], $transaction->getOperations()->toArray()); 95 | 96 | // test signatures are a collection. 97 | $this->assertInstanceOf(Collection::class, $transaction->getSignatures()); 98 | // test the signatures (as array). 99 | $this->assertEquals($this->transactionData['signatures'], $transaction->getSignatures()->toArray()); 100 | 101 | // test extensions are a collection. 102 | $this->assertInstanceOf(Collection::class, $transaction->getExtensions()); 103 | // test the extensions (as array). 104 | $this->assertEquals($this->transactionData['extensions'], $transaction->getExtensions()->toArray()); 105 | 106 | // parse the transaction back to json. 107 | $parsedJson = json_encode($transaction->toArray()); 108 | $originalJson = json_encode([ 'result' => $this->transactionData ]); 109 | 110 | // assert the transaction have the same content after parsed. 111 | $this->assertEquals($parsedJson, $originalJson); 112 | } 113 | 114 | /** 115 | * Test parsing of transaction when errors happens. 116 | */ 117 | public function test_parsing_errors_no_matching_name() 118 | { 119 | // copy the transaction data to a local array. 120 | $data = $this->transactionData; 121 | 122 | // rename the operation to a custom name. 123 | $data['operations'][0][0] = 'some-other-name'; 124 | 125 | // factory the transaction. 126 | $transaction = Transaction::factory($data); 127 | 128 | // try getting the operations. 129 | $this->assertInstanceOf(Collection::class, $transaction->getOperations()); 130 | } 131 | 132 | /** 133 | * Test parsing of transaction when errors happens. 134 | */ 135 | public function test_parsing_errors_no_name_present() 136 | { 137 | // copy the transaction data to a local array. 138 | $data = $this->transactionData; 139 | 140 | // rename the operation to a custom name. 141 | $data['operations'][0][0] = null; 142 | 143 | // factory the transaction. 144 | $transaction = Transaction::factory($data); 145 | 146 | // try getting the operations. 147 | $this->assertInstanceOf(Collection::class, $transaction->getOperations()); 148 | } 149 | } -------------------------------------------------------------------------------- /tests/Http/ClientTest.php: -------------------------------------------------------------------------------- 1 | mockConfiguration()); 69 | 70 | // start an empty guzzle http client. 71 | $customGuzzle = new GuzzleClient(); 72 | 73 | // set the client. 74 | $client->setHttpClient($customGuzzle); 75 | 76 | // assert the instances are the same. 77 | $this->assertSame($customGuzzle, $client->getHttpClient()); 78 | } 79 | 80 | /** 81 | * Test the customization of the configuration instance on the Http client. 82 | */ 83 | public function test_custom_configuration() 84 | { 85 | // creates a client instance with default config. 86 | $client = new Client($this->mockConfiguration()); 87 | 88 | // get the default config, for later comparison. 89 | $defaultConfig = $client->getConfig(); 90 | 91 | // creates a new config, to customize on the client. 92 | $config = new Config('foo', 'bar'); 93 | 94 | // set the new config instance. 95 | $client->setConfig($config); 96 | 97 | // assert the config instance is the customized one. 98 | $this->assertSame($client->getConfig(), $config); 99 | 100 | // assert the default configuration is not the one on the client instance anymore. 101 | $this->assertNotSame($client->getConfig(), $defaultConfig); 102 | } 103 | 104 | /** 105 | * Test customization of the access token on the client instance. 106 | */ 107 | public function test_custom_access_token() 108 | { 109 | // creates a new client instance. 110 | $client = new Client($this->mockConfiguration()); 111 | 112 | // get a token mock. 113 | $token = $this->mockAccessToken(); 114 | 115 | // customize the access token on the client with the mock 116 | $setReturn = $client->setAccessToken($token); 117 | 118 | // assert the fluent return. 119 | $this->assertSame($client, $setReturn); 120 | 121 | // assert the custom token was set on the client instance. 122 | $this->assertSame($client->getAccessToken(), $token); 123 | } 124 | 125 | /** 126 | * Test all constructor customizations. 127 | */ 128 | public function test_constructor_with_optional_parameters() 129 | { 130 | // get a config mock. 131 | $config = $this->mockConfiguration(); 132 | // get an access token mock. 133 | $token = $this->mockAccessToken(); 134 | // get an internal http mock (guzzle) 135 | $internalHttpClient = $this->mockInternalHttpClient(); 136 | 137 | // create a new client instance, using custom token and internal http client. 138 | $client = new Client($config, $token, $internalHttpClient); 139 | 140 | // assert the configuration instance. 141 | $this->assertSame($config, $client->getConfig()); 142 | 143 | // assert the same access token instance. 144 | $this->assertSame($token, $client->getAccessToken()); 145 | 146 | // assert the same http client. 147 | $this->assertSame($internalHttpClient, $client->getHttpClient()); 148 | } 149 | 150 | /** 151 | * Simple test on the actual HTTP call. 152 | * 153 | * @throws 154 | */ 155 | public function test_headers_and_http_call() 156 | { 157 | // mock the configuration. 158 | $config = $this->mockConfiguration(); 159 | 160 | // the configuration must receive the build url method. 161 | $config->shouldReceive('buildUrl')->withArgs(['foo'])->andReturn('http://foo.bar/foo'); 162 | 163 | // mock the access token. 164 | $token = $this->mockAccessToken(); 165 | 166 | $token->shouldReceive('getToken')->andReturn('bar'); 167 | 168 | // mock the internal client. 169 | $internalClient = $this->mockInternalHttpClient(); 170 | 171 | $internalClient->shouldReceive('send')->andReturn(Mockery::mock(ResponseInterface::class)); 172 | 173 | // creates a client with the customized mocks 174 | $client = new Client($config, $token, $internalClient); 175 | 176 | // make a call. 177 | $response = $client->call('POST', 'foo'); 178 | 179 | // assert a response was returned. 180 | $this->assertInstanceOf(ResponseInterface::class, $response); 181 | } 182 | } -------------------------------------------------------------------------------- /docs/img/sc-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | logo-mono 26 | 27 | 28 | 29 | 53 | 55 | 57 | 58 | logo-mono 60 | 66 | 72 | 79 | 80 | -------------------------------------------------------------------------------- /src/Operations/Comment.php: -------------------------------------------------------------------------------- 1 | '', 30 | 'parent_permlink' => null, 31 | 'json_metadata' => [], 32 | ]; 33 | 34 | /** 35 | * @var array List of parameters that must be treated as JSON. 36 | */ 37 | protected $jsonParameters = [ 38 | 'json_metadata' 39 | ]; 40 | 41 | /** 42 | * Comment Operation constructor. 43 | * 44 | * @param array $parameters Parse parameters directly from constructor. 45 | */ 46 | public function __construct(array $parameters = []) 47 | { 48 | // call parent constructor. 49 | parent::__construct('comment', array_merge($this->parameters, $parameters)); 50 | 51 | } 52 | 53 | /** 54 | * Parent Author parameter setter. 55 | * 56 | * @param string $parentAuthor 57 | * 58 | * @return $this 59 | */ 60 | public function parentAuthor(string $parentAuthor) : self 61 | { 62 | return $this->setParameter('parent_author', $parentAuthor); 63 | } 64 | 65 | /** 66 | * Parent PermLink parameter setter. 67 | * 68 | * @param string $parentPermLink 69 | * 70 | * @return $this 71 | */ 72 | public function parentPermLink(string $parentPermLink) : self 73 | { 74 | return $this->setParameter('parent_permlink', $parentPermLink); 75 | } 76 | 77 | /** 78 | * Reply to post or comment. 79 | * 80 | * @param string $author 81 | * @param string $permLink 82 | * 83 | * @return self 84 | */ 85 | public function reply(string $author, string $permLink) : self 86 | { 87 | return $this->parentAuthor($author)->parentPermLink($permLink); 88 | } 89 | 90 | /** 91 | * Author parameter setter. 92 | * 93 | * @param string $author 94 | * 95 | * @return $this 96 | */ 97 | public function author(string $author) : self 98 | { 99 | return $this->setParameter('author', $author); 100 | } 101 | 102 | /** 103 | * PermLink parameter setter. 104 | * 105 | * @param string $permLink 106 | * 107 | * @return $this 108 | */ 109 | public function permLink(string $permLink) : self 110 | { 111 | return $this->setParameter('permlink', $permLink); 112 | } 113 | 114 | /** 115 | * Title parameter setter. 116 | * 117 | * @param string $title 118 | * 119 | * @return $this 120 | */ 121 | public function title(string $title) : self 122 | { 123 | if (!$this->getParameter('permlink')) { 124 | $this->setParameter('permlink', Str::slug($title)); 125 | } 126 | 127 | return $this->setParameter('title', $title); 128 | } 129 | 130 | /** 131 | * Body parameter setter. 132 | * 133 | * @param string $body 134 | * 135 | * @return $this 136 | */ 137 | public function body(string $body) : self 138 | { 139 | $randomTitle = Str::limit($body, 150); 140 | 141 | if (!$this->getParameter('title')) { 142 | $this->title($randomTitle); 143 | } 144 | 145 | return $this->setParameter('body', $body); 146 | } 147 | 148 | /** 149 | * Category on posts == parent_permlink. 150 | * 151 | * So this is an alias, the category functionality does not exactly exists. 152 | * 153 | * @param string|null $category 154 | * 155 | * @return self 156 | */ 157 | public function category(string $category = null) :self 158 | { 159 | return $this->parentPermLink($category); 160 | } 161 | 162 | /** 163 | * Set the tags metadata. 164 | * 165 | * @param array $tags List of tags to add. 166 | * 167 | * @return self 168 | */ 169 | public function tags(array $tags) : self 170 | { 171 | return $this->setParameter('json_metadata.tags', $tags); 172 | } 173 | 174 | 175 | /** 176 | * Set the community name. 177 | * 178 | * @param string|null $community 179 | * 180 | * @return Comment 181 | */ 182 | public function community(string $community = null) : self 183 | { 184 | return $this->setParameter('json_metadata.community', $community); 185 | } 186 | 187 | /** 188 | * Set the application name. 189 | * 190 | * @param string $app 191 | * 192 | * @return self 193 | */ 194 | public function app(string $app) : self 195 | { 196 | return $this->setParameter('json_metadata.app', $app); 197 | } 198 | 199 | /** 200 | * Metadata parameter setter. 201 | * 202 | * @param string|array $jsonMetadata 203 | * 204 | * @return $this 205 | */ 206 | public function jsonMetadata($jsonMetadata) : self 207 | { 208 | // convert into array, if json. 209 | $metadata = is_string($jsonMetadata) ? json_decode($jsonMetadata, true) : $jsonMetadata; 210 | 211 | // set the json metadata as string or array 212 | return $this->setParameter('json_metadata', $metadata); 213 | } 214 | 215 | /** 216 | * Custom getter implementation. 217 | * 218 | * @param $name 219 | * @return array|mixed|null|Operation|SubOperation|string 220 | */ 221 | public function __get($name) 222 | { 223 | $defaultGetValue = parent::__get($name); 224 | 225 | if ($defaultGetValue) { 226 | return $defaultGetValue; 227 | } 228 | 229 | return collect((array) $this->getParameter('json_metadata'))->get($name); 230 | } 231 | } -------------------------------------------------------------------------------- /tests/Exceptions/ResponseExceptionTest.php: -------------------------------------------------------------------------------- 1 | foo'; 37 | 38 | /** 39 | * Generates a mock response for testing the exception customized logic. 40 | * 41 | * @param int $statusCode 42 | * @param string $reasonPhrase 43 | * @param string $contentType 44 | * 45 | * @return ResponseInterface 46 | */ 47 | protected function makeResponseMock(int $statusCode, string $reasonPhrase, string $contentType) 48 | { 49 | // start a response mock. 50 | $response = Mockery::mock(ResponseInterface::class); 51 | 52 | // set the status code and reason phrase returns. 53 | $response->shouldReceive('getStatusCode')->andReturn($statusCode); 54 | $response->shouldReceive('getReasonPhrase')->andReturn($reasonPhrase); 55 | // set content type on the response headers using the desired value from parameter. 56 | $response->shouldReceive('getHeaders')->andReturn(['Content-Type' => $contentType]); 57 | 58 | // alias the variable to make it nice on the IDE> 59 | /** @var ResponseInterface $response */ 60 | return $response; 61 | } 62 | 63 | /** 64 | * Generates a mock of a body stream containing an error. 65 | * 66 | * @return StreamInterface 67 | */ 68 | protected function makeErrorStreamMock() 69 | { 70 | // mock a stream with error. 71 | $stream = Mockery::mock(StreamInterface::class); 72 | 73 | // error response body. 74 | $errorResponseBody = [ 75 | 'error' => $this->responseErrorMessage, 76 | ]; 77 | 78 | // set the string serialization of the stream. 79 | $stream->shouldReceive('__toString')->andReturn(json_encode($errorResponseBody)); 80 | 81 | /** @var StreamInterface $stream */ 82 | return $stream; 83 | } 84 | 85 | /** 86 | * Generates a mock of a body stream containing an error. 87 | * 88 | * This time, HTML version. 89 | * 90 | * @return StreamInterface 91 | */ 92 | protected function makeHtmlErrorStreamMock() 93 | { 94 | // mock a stream with error. 95 | $stream = Mockery::mock(StreamInterface::class); 96 | 97 | // set the string serialization of the stream. 98 | $stream->shouldReceive('__toString')->andReturn($this->responseErrorMessageHtml); 99 | 100 | /** @var StreamInterface $stream */ 101 | return $stream; 102 | } 103 | 104 | /** 105 | * Generates a mock HTTP error response. 106 | * 107 | * @return ResponseInterface 108 | */ 109 | protected function mockErrorResponse() 110 | { 111 | // generate a mock response, using the code and reason on the instance. 112 | /** @var Mockery\MockInterface $response */ 113 | $response = $this->makeResponseMock($this->statusCode, $this->reasonPhrase, 'application/json'); 114 | 115 | // assign the error stream on the response body before returning the response mock. 116 | $response->shouldReceive('getBody')->andReturn($this->makeErrorStreamMock()); 117 | 118 | /** @var ResponseInterface $response */ 119 | return $response; 120 | } 121 | 122 | /** 123 | * Generates a mock HTTP error response. 124 | * 125 | * This time, the mock response is not a json error but a generic html one. 126 | * 127 | * @return ResponseInterface 128 | */ 129 | protected function mockHtmlErrorResponse() 130 | { 131 | // generate a mock response, using the code and reason on the instance. 132 | /** @var Mockery\MockInterface $response */ 133 | $response = $this->makeResponseMock($this->statusCode, $this->reasonPhrase, 'text/html'); 134 | 135 | // assign the error stream on the response body before returning the response mock. 136 | $response->shouldReceive('getBody')->andReturn($this->makeHtmlErrorStreamMock()); 137 | 138 | /** @var ResponseInterface $response */ 139 | return $response; 140 | } 141 | 142 | /** 143 | * Test the exception construction and parsing 144 | */ 145 | public function test_exception_construction_and_error_parsing() 146 | { 147 | // generate the exception with the mock error response. 148 | $exception = new ResponseException($this->mockErrorResponse()); 149 | 150 | // assure it inherits RuntimeException. 151 | $this->assertInstanceOf(\RuntimeException::class, $exception); 152 | 153 | // assert the message on the exception was extracted correctly. 154 | $this->assertEquals($exception->getMessage(), "{$this->responseErrorMessage}: $this->reasonPhrase"); 155 | } 156 | 157 | /** 158 | * Test the exception construction and parsing for non JSON responses. 159 | */ 160 | public function test_exception_construction_and_error_parsing_on_non_json_responses() 161 | { 162 | // generate the exception with the mock error response. 163 | $exception = new ResponseException($this->mockHtmlErrorResponse()); 164 | 165 | // assure it inherits RuntimeException. 166 | $this->assertInstanceOf(\RuntimeException::class, $exception); 167 | 168 | // assert the message on the exception was extracted correctly. 169 | // the html errors will strip all html tags. 170 | $this->assertEquals($exception->getMessage(), "foo"); 171 | } 172 | } -------------------------------------------------------------------------------- /tests/Client/ClientTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getClientId')->andReturn('foo'); 33 | $config->shouldReceive('getClientSecret')->andReturn('bar'); 34 | $config->shouldReceive('getReturnUrl')->andReturn('https://foo.bar/callback'); 35 | 36 | // return the config mock. 37 | return $config; 38 | } 39 | 40 | /** 41 | * Mock the OAuth provider. 42 | * 43 | * @return Mockery\MockInterface|Provider 44 | */ 45 | protected function mockProvider() 46 | { 47 | // start the provider mock. 48 | $provider = Mockery::mock(Provider::class); 49 | 50 | // return the provider. 51 | return $provider; 52 | } 53 | 54 | /** 55 | * Mock an access token. 56 | * 57 | * @return Mockery\MockInterface|Token 58 | */ 59 | protected function mockToken() 60 | { 61 | // start a token mock. 62 | $token = Mockery::mock(Token::class); 63 | 64 | // when asked about, return a fake token string (access token). 65 | $token->shouldReceive('getToken')->andReturn('foo-bar-baz-token'); 66 | 67 | // return the token mock. 68 | return $token; 69 | } 70 | 71 | /** 72 | * Simple constructor test. 73 | */ 74 | public function test_construction() 75 | { 76 | // get a config mock. 77 | $config = $this->mockConfig(); 78 | 79 | // starts the client with the config mock. 80 | $client = new Client($config); 81 | 82 | // test the config instance matches. 83 | $this->assertSame($config, $client->getConfig()); 84 | 85 | // assets the constructor created an http client instance. 86 | $this->assertInstanceOf(HttpClient::class, $client->getHttpClient()); 87 | } 88 | 89 | /** 90 | * Test token getter and setter on the SDK client. 91 | */ 92 | public function test_token_getter_and_setter() 93 | { 94 | // start a client instance. 95 | $client = new Client($this->mockConfig()); 96 | 97 | // assert the token starts as null. 98 | $this->assertNull($client->getToken()); 99 | 100 | // get a mock access token. 101 | $token = $this->mockToken(); 102 | 103 | // set the custom token on the SDK client. 104 | $setReturn = $client->setToken($token); 105 | 106 | // assert the set token has a fluent return. 107 | $this->assertSame($client, $setReturn); 108 | 109 | // assert the instance was correctly set on the SDK client. 110 | $this->assertSame($token, $client->getToken()); 111 | } 112 | 113 | /** 114 | * Test token getter and setter on the SDK client. 115 | */ 116 | public function test_provider_getter_and_setter() 117 | { 118 | // start a client instance. 119 | $client = new Client($this->mockConfig()); 120 | 121 | // start a mock provider. 122 | $provider = $this->mockProvider(); 123 | 124 | // set the custom provider on the SDK client. 125 | $setReturn = $client->setOAuthProvider($provider); 126 | 127 | // assert the set provider has a fluent return. 128 | $this->assertSame($client, $setReturn); 129 | 130 | // assert the instance was correctly set on the SDK client. 131 | $this->assertSame($provider, $client->getOAuthProvider()); 132 | } 133 | 134 | /** 135 | * Test auth manager instantiation. 136 | */ 137 | public function test_auth_manager_instance() 138 | { 139 | // start a client instance. 140 | $client = new Client($this->mockConfig()); 141 | 142 | // assert the instance of Manager from auth method. 143 | $this->assertInstanceOf(Manager::class, $client->auth()); 144 | } 145 | 146 | /** 147 | * Test operation broadcasting from the client. 148 | */ 149 | public function test_broadcasting() 150 | { 151 | // start a broadcaster mock. 152 | /** @var Broadcaster|Mockery\MockInterface $broadcaster */ 153 | $broadcaster = Mockery::mock(Broadcaster::class); 154 | $broadcaster->shouldReceive('setHttpClient')->andReturn($broadcaster); 155 | $broadcaster->shouldReceive('setConfig')->andReturn($broadcaster); 156 | $broadcaster->shouldReceive('setToken')->andReturn($broadcaster); 157 | 158 | /** @var Operation|Mockery\MockInterface $operation */ 159 | $operation = Mockery::mock(Operation::class); 160 | 161 | /** @var Response|Mockery\MockInterface $response */ 162 | $response = Mockery::mock(Response::class); 163 | 164 | // main broadcast method mock. 165 | $broadcaster->shouldReceive('broadcast')->andReturn($response); 166 | 167 | // start a client with mock config. 168 | $client = new Client($this->mockConfig()); 169 | // pass the mock token into the client. 170 | $client->setToken($this->mockToken()); 171 | 172 | // customize the broadcaster instance 173 | $client->setBroadcaster($broadcaster); 174 | 175 | // call the broadcast method and get the response. 176 | $broadcastResponse = $client->broadcast($operation); 177 | 178 | // assert the return is the same. 179 | $this->assertSame($response, $broadcastResponse); 180 | } 181 | 182 | /** 183 | * Test the client refresh methods. 184 | */ 185 | public function test_refresh_methods() 186 | { 187 | // start a SDK client. 188 | $client = new Client($this->mockConfig()); 189 | // set a mock token. 190 | $client->setToken($this->mockToken()); 191 | 192 | // call the refresh method. 193 | $client->refreshBroadcaster(); 194 | 195 | // assert the broadcaster was created. 196 | $this->assertInstanceOf(Broadcaster::class, $client->getBroadcaster()); 197 | 198 | } 199 | } -------------------------------------------------------------------------------- /src/Client/Client.php: -------------------------------------------------------------------------------- 1 | setConfig($config); 56 | 57 | // setup provider instance. 58 | $this->setOAuthProvider($this->createProvider()); 59 | 60 | // setup http client instance. 61 | $this->setHttpClient($this->createHttpClient()); 62 | } 63 | 64 | /** 65 | * Set an already existing token into client instance. 66 | * 67 | * @param Token $token 68 | * 69 | * @return self 70 | */ 71 | public function setToken(Token $token): self 72 | { 73 | $this->token = $token; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Returns the instance token. 80 | * 81 | * @return Token Access token instance, if any. 82 | */ 83 | public function getToken(): ?Token 84 | { 85 | return $this->token; 86 | } 87 | 88 | /** 89 | * Override the configuration instance. 90 | * 91 | * @param Config $config 92 | * 93 | * @return self 94 | */ 95 | public function setConfig(Config $config): self 96 | { 97 | $this->config = $config; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Retrieves the current configuration instance set on the client. 104 | * 105 | * @return Config 106 | */ 107 | public function getConfig(): Config 108 | { 109 | return $this->config; 110 | } 111 | 112 | /** 113 | * Override the http client instance on the SDK. 114 | * 115 | * @param HttpClient $httpClient 116 | * 117 | * @return self 118 | */ 119 | public function setHttpClient(HttpClient $httpClient): self 120 | { 121 | $this->httpClient = $httpClient; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Retrieves the current Http client configured on the SDK. 128 | * 129 | * @return HttpClient 130 | */ 131 | public function getHttpClient(): HttpClient 132 | { 133 | return $this->httpClient; 134 | } 135 | 136 | /** 137 | * Refresh the HttpClient instance. 138 | * 139 | * This method is intended for cases where configuration is replaced after client instance has 140 | * been created. 141 | * 142 | * @return self 143 | */ 144 | public function refreshHttpClient(): self 145 | { 146 | // refresh the config and access token values. 147 | $this->httpClient->setConfig($this->config); 148 | $this->httpClient->setAccessToken($this->token); 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Set a custom OAuth2 provider instance. 155 | * 156 | * Most useful for testing and extending the provider than actual day-to-day usage. 157 | * 158 | * @param Provider $provider 159 | * 160 | * @return self 161 | */ 162 | public function setOAuthProvider(Provider $provider): self 163 | { 164 | $this->provider = $provider; 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Returns the current OAuth2 provider instance set on the client. 171 | * 172 | * @return null|Provider 173 | */ 174 | public function getOAuthProvider(): ?Provider 175 | { 176 | return $this->provider; 177 | } 178 | 179 | /** 180 | * Set a custom broadcaster instance. 181 | * 182 | * Most useful for testing purposes.. 183 | * 184 | * @param Broadcaster $broadcaster 185 | * 186 | * @return self 187 | */ 188 | public function setBroadcaster(Broadcaster $broadcaster): self 189 | { 190 | $this->broadcaster = $broadcaster; 191 | 192 | return $this; 193 | } 194 | 195 | /** 196 | * Returns the current Broadcaster instance on the client. 197 | * 198 | * @return null|Broadcaster 199 | */ 200 | public function getBroadcaster(): ?Broadcaster 201 | { 202 | return $this->broadcaster; 203 | } 204 | 205 | /** 206 | * Refresh the broadcaster instance. 207 | * 208 | * @return self 209 | */ 210 | public function refreshBroadcaster(): self 211 | { 212 | if (!$this->broadcaster) { 213 | $this->setBroadcaster($this->createBroadcaster()); 214 | } 215 | 216 | $this->broadcaster->setHttpClient($this->httpClient); 217 | $this->broadcaster->setConfig($this->config); 218 | $this->broadcaster->setToken($this->token); 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Refresh the provider instance. 225 | * 226 | * This method is intended for cases where configuration is replaced after client instance has 227 | * been created. 228 | * 229 | * @return self 230 | */ 231 | public function refreshProvider(): self 232 | { 233 | $this->setOAuthProvider($this->createProvider()); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Proxy for the authentication manager instance. 240 | * 241 | * @throws ClientException 242 | * 243 | * @return Manager 244 | */ 245 | public function auth() 246 | { 247 | // creates a fresh provider instance to reflect configuration data. 248 | $this->refreshProvider(); 249 | 250 | // returns a new manager, if possible. 251 | return new Manager($this->config, $this->provider, $this->token); 252 | } 253 | 254 | /** 255 | * Broadcast one or more operations to the Steem blockchain though SteemConnect. 256 | * 257 | * @param array ...$operations 258 | * 259 | * @return Response 260 | * 261 | * @throws 262 | */ 263 | public function broadcast(...$operations) 264 | { 265 | // refresh the oauth provider before doing anything. 266 | $this->refreshProvider(); 267 | // refresh the http client because the instances may have changed. 268 | $this->refreshHttpClient(); 269 | // refresh the broadcaster. 270 | $this->refreshBroadcaster(); 271 | 272 | // returns the broadcast operation result. 273 | // notice no try catch here because the broadcast will throw 274 | // a response or request exception internally and it should just 275 | // be forwarded. 276 | return $this->getBroadcaster()->broadcast($operations); 277 | } 278 | 279 | /** 280 | * Creates a new OAuth2 provider instance. 281 | * 282 | * @return Provider 283 | */ 284 | protected function createProvider() 285 | { 286 | return new Provider($this->config); 287 | } 288 | 289 | /** 290 | * Creates a new HttpClient instance. 291 | * 292 | * @return HttpClient 293 | */ 294 | protected function createHttpClient() 295 | { 296 | return new HttpClient($this->getConfig(), $this->getToken()); 297 | } 298 | 299 | /** 300 | * Creates a broadcaster instance. 301 | * 302 | * @return Broadcaster 303 | */ 304 | protected function createBroadcaster() 305 | { 306 | return new Broadcaster($this->config, $this->token, $this->httpClient); 307 | } 308 | } -------------------------------------------------------------------------------- /src/Transactions/Transaction.php: -------------------------------------------------------------------------------- 1 | Vote::class, 78 | 'comment' => Comment::class, 79 | 'custom_json' => CustomJson::class, 80 | ]; 81 | 82 | /** 83 | * Transaction constructor. 84 | * 85 | * @param array $transactionData 86 | */ 87 | public function __construct(array $transactionData = []) 88 | { 89 | // init operations as an empty collection. 90 | $this->operations = collect([]); 91 | // init extensions as an empty collection. 92 | $this->extensions = collect([]); 93 | // init signatures as an empty collection. 94 | $this->signatures = collect([]); 95 | } 96 | 97 | /** 98 | * Factors a transaction data into a transaction instance. 99 | * 100 | * @param array $transactionData 101 | * 102 | * @return self 103 | */ 104 | public static function factory(array $transactionData = []) 105 | { 106 | // extract the result key from transaction data. 107 | $data = array_has($transactionData, 'result') ? array_get($transactionData, 'result') : $transactionData; 108 | 109 | // create a transaction instance. 110 | $transaction = new self(); 111 | 112 | // set transaction id. 113 | $transaction->setId(array_get($data, 'id', null)); 114 | // set block number. 115 | $transaction->setBlockNumber(array_get($data, 'block_num', null)); 116 | // set transaction position on block. 117 | $transaction->setTransactionNumber(array_get($data, 'trx_num', null)); 118 | // set transaction expiration status. 119 | $transaction->setExpired(array_get($data, 'expired', null)); 120 | // set reference block number. 121 | $transaction->setReferenceBlockNumber(array_get($data, 'ref_block_num', null)); 122 | // set reference block prefix. 123 | $transaction->setReferenceBlockPrefix(array_get($data, 'ref_block_prefix', null)); 124 | // set expiration date. 125 | $transaction->setExpiration(array_get($data, 'expiration', null)); 126 | // set transaction operations. 127 | $transaction->setOperations(collect(array_get($data, 'operations', []))); 128 | // set transaction extensions. 129 | $transaction->setExtensions(collect(array_get($data, 'extensions', []))); 130 | // set transaction signatures. 131 | $transaction->setSignatures(collect(array_get($data, 'signatures', []))); 132 | 133 | // returns the transaction object. 134 | return $transaction; 135 | } 136 | 137 | /** 138 | * @return null|string 139 | */ 140 | public function getId(): ?string 141 | { 142 | return $this->id; 143 | } 144 | 145 | /** 146 | * @param null|string $id 147 | * 148 | * @return self 149 | */ 150 | public function setId(?string $id): self 151 | { 152 | $this->id = $id; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * @return int|null 159 | */ 160 | public function getBlockNumber(): ?int 161 | { 162 | return $this->blockNumber; 163 | } 164 | 165 | /** 166 | * @param int|null $blockNumber 167 | * 168 | * @return self 169 | */ 170 | public function setBlockNumber(?int $blockNumber): self 171 | { 172 | $this->blockNumber = $blockNumber; 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * @return int|null 179 | */ 180 | public function getTransactionNumber(): ?int 181 | { 182 | return $this->transactionNumber; 183 | } 184 | 185 | /** 186 | * @param int|null $transactionNumber 187 | * 188 | * @return self 189 | */ 190 | public function setTransactionNumber(?int $transactionNumber): self 191 | { 192 | $this->transactionNumber = $transactionNumber; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * @return bool|null 199 | */ 200 | public function getExpired(): ?bool 201 | { 202 | return $this->expired; 203 | } 204 | 205 | /** 206 | * @param bool|null $expired 207 | * 208 | * @return self 209 | */ 210 | public function setExpired(?bool $expired): self 211 | { 212 | $this->expired = $expired; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * @return int|null 219 | */ 220 | public function getReferenceBlockNumber(): ?int 221 | { 222 | return $this->referenceBlockNumber; 223 | } 224 | 225 | /** 226 | * @param int|null $referenceBlockNumber 227 | * 228 | * @return self 229 | */ 230 | public function setReferenceBlockNumber(?int $referenceBlockNumber): self 231 | { 232 | $this->referenceBlockNumber = $referenceBlockNumber; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * @return int|null 239 | */ 240 | public function getReferenceBlockPrefix(): ?int 241 | { 242 | return $this->referenceBlockPrefix; 243 | } 244 | 245 | /** 246 | * @param int|null $referenceBlockPrefix 247 | * 248 | * @return self 249 | */ 250 | public function setReferenceBlockPrefix(?int $referenceBlockPrefix): self 251 | { 252 | $this->referenceBlockPrefix = $referenceBlockPrefix; 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * @return Carbon|null 259 | */ 260 | public function getExpiration(): ?Carbon 261 | { 262 | return $this->expiration; 263 | } 264 | 265 | /** 266 | * @param string|Carbon|null $expiration 267 | * 268 | * @return self 269 | */ 270 | public function setExpiration($expiration): self 271 | { 272 | // carbon is smart enough to parse both strings and carbon instances. 273 | $this->expiration = $expiration ? Carbon::parse($expiration, 'UTC') : null; 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * @return Collection 280 | */ 281 | public function getOperations(): Collection 282 | { 283 | return $this->operations; 284 | } 285 | 286 | /** 287 | * @param Collection $operations 288 | * 289 | * @return self 290 | */ 291 | public function setOperations(Collection $operations): self 292 | { 293 | // parse the operations. 294 | $parsedOperations = $this->parseOperations($operations); 295 | 296 | // map the operations. 297 | $parsedOperations->map(function (Operation $operation) { 298 | $this->operations->push($operation); 299 | }); 300 | 301 | // fluent return. 302 | return $this; 303 | } 304 | 305 | /** 306 | * @return Collection 307 | */ 308 | public function getExtensions(): Collection 309 | { 310 | return $this->extensions; 311 | } 312 | 313 | /** 314 | * @param Collection $extensions 315 | * 316 | * @return self 317 | */ 318 | public function setExtensions(Collection $extensions): self 319 | { 320 | $this->extensions = $extensions; 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * @return Collection 327 | */ 328 | public function getSignatures(): Collection 329 | { 330 | return $this->signatures; 331 | } 332 | 333 | /** 334 | * @param Collection $signatures 335 | * 336 | * @return self 337 | */ 338 | public function setSignatures(Collection $signatures): self 339 | { 340 | $this->signatures = $signatures; 341 | 342 | return $this; 343 | } 344 | 345 | 346 | /** 347 | * This method parses a collection of operations as array into 348 | * they respective operation object instances, meaning working 349 | * with the objects should be a canonical representation 350 | * which can be serialized and de-serialized as needed. 351 | * 352 | * @param Collection $operationList 353 | * 354 | * @return Collection 355 | */ 356 | protected function parseOperations(Collection $operationList) : Collection 357 | { 358 | $operations = $operationList->map(function (array $operationData) { 359 | $operationName = array_get($operationData, 0, null); 360 | 361 | if (!$operationName) { 362 | return null; 363 | } 364 | 365 | $operationClass = array_get($this->operationClassMap, $operationName, null); 366 | 367 | if (!$operationClass || !class_exists($operationClass)) { 368 | return null; 369 | } 370 | 371 | return new $operationClass(array_get($operationData, 1)); 372 | }); 373 | 374 | return $operations->filter(); 375 | } 376 | 377 | 378 | /** 379 | * Array representation of a transaction. 380 | * 381 | * This method should return a transaction array that when converted to json, 382 | * reflects the original transaction json from response, meaning it will still 383 | * be valid for signature verifications. 384 | * 385 | * @return array 386 | */ 387 | public function toArray() 388 | { 389 | // create the transaction data array. 390 | $data = [ 391 | 'id' => $this->id, 392 | 'block_num' => $this->blockNumber, 393 | 'trx_num' => $this->transactionNumber, 394 | 'expired' => $this->expired, 395 | 'ref_block_num' => $this->referenceBlockNumber, 396 | 'ref_block_prefix' => $this->referenceBlockPrefix, 397 | 'expiration' => $this->expiration ? $this->expiration->format('Y-m-d\TH:i:s') : null, 398 | 'operations' => $this->operations->toArray(), 399 | 'extensions' => $this->extensions->toArray(), 400 | 'signatures' => $this->signatures->toArray(), 401 | ]; 402 | 403 | // sort the transaction data by its keys/ 404 | ksort($data); 405 | 406 | // return the result array 407 | return [ 'result' => $data ]; 408 | } 409 | } -------------------------------------------------------------------------------- /tests/Client/BroadcasterTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getStatusCode')->andReturn(200); 69 | $response->shouldReceive('getReasonPhrase')->andReturn('OK'); 70 | 71 | // json headers. 72 | $response->shouldReceive('getHeaders')->andReturn(['Content-Type' => 'application/json']); 73 | 74 | // body. 75 | $body = Mockery::mock(StreamInterface::class); 76 | $body->shouldReceive('__toString')->andReturn('{"foo":"bar"}'); 77 | 78 | $response->shouldReceive('getBody')->andReturn($body); 79 | 80 | return $response; 81 | } 82 | 83 | /** 84 | * Creates an error http response mock. 85 | * 86 | * @return Mockery\MockInterface|ResponseInterface 87 | */ 88 | protected function mockErrorHttpResponse() 89 | { 90 | // start a mock http response. 91 | $response = Mockery::mock(ResponseInterface::class); 92 | 93 | // status codes and reason phrase. 94 | $response->shouldReceive('getStatusCode')->andReturn(401); 95 | $response->shouldReceive('getReasonPhrase')->andReturn('Unauthorized'); 96 | 97 | // json headers. 98 | $response->shouldReceive('getHeaders')->andReturn(['Content-Type' => 'application/json']); 99 | 100 | // body mock. 101 | $body = Mockery::mock(StreamInterface::class); 102 | $body->shouldReceive('__toString')->andReturn('{"error":"YOUR ARE NOT AUTHORIZED"}'); 103 | 104 | // set the body mock to respond when the getBody method is called. 105 | $response->shouldReceive('getBody')->andReturn($body); 106 | 107 | // return the error response. 108 | return $response; 109 | } 110 | 111 | /** 112 | * Returns a broadcaster instance, with all dependencies mocked. 113 | * 114 | * @return Broadcaster 115 | */ 116 | protected function getInstanceWithMocks() 117 | { 118 | // get a mock config. 119 | $config = $this->mockConfig(); 120 | // get a mock token. 121 | $token = $this->mockToken(); 122 | // get a mock http client. 123 | $httpClient = $this->mockHttpClient(); 124 | 125 | // create a broadcaster instance. 126 | $broadcaster = new Broadcaster($config, $token, $httpClient); 127 | 128 | // returns the broadcaster. 129 | return $broadcaster; 130 | } 131 | 132 | /** 133 | * Creates a mock operation. 134 | * 135 | * @return Mockery\MockInterface|Operation 136 | */ 137 | protected function mockOperation() 138 | { 139 | // start a mock operation. 140 | $operation = Mockery::mock(Operation::class); 141 | 142 | // make it array compatible. 143 | $operation->shouldReceive('toArray')->andReturn(['foo' => 'bar']); 144 | 145 | // return the operation itself. 146 | return $operation; 147 | } 148 | 149 | /** 150 | * Test construction. 151 | */ 152 | public function test_construction() 153 | { 154 | // get a mock config. 155 | $config = $this->mockConfig(); 156 | // get a mock token. 157 | $token = $this->mockToken(); 158 | // get a mock http client. 159 | $httpClient = $this->mockHttpClient(); 160 | 161 | // create a broadcaster instance. 162 | $broadcaster = new Broadcaster($config, $token, $httpClient); 163 | 164 | // assert the config was correctly set by constructor. 165 | $this->assertSame($config, $broadcaster->getConfig()); 166 | // assert the token was correctly set by constructor. 167 | $this->assertSame($token, $broadcaster->getToken()); 168 | // assert the http client was correctly set by constructor. 169 | $this->assertSame($httpClient, $broadcaster->getHttpClient()); 170 | } 171 | 172 | /** 173 | * Test config getter and setter. 174 | */ 175 | public function test_config_getter_and_setter() 176 | { 177 | // create a new config mock. 178 | $config = $this->mockConfig(); 179 | 180 | // creates a new broadcaster instance. 181 | $broadcaster = $this->getInstanceWithMocks(); 182 | 183 | // assert the internal instance is not the same from the one 184 | // previously created. 185 | $this->assertNotSame($config, $broadcaster->getConfig()); 186 | 187 | // customize config. 188 | $setReturn = $broadcaster->setConfig($config); 189 | 190 | // assert the fluent return. 191 | $this->assertSame($broadcaster, $setReturn); 192 | 193 | // now, assert the instance was correctly set and returned. 194 | $this->assertSame($config, $broadcaster->getConfig()); 195 | } 196 | 197 | /** 198 | * Test access token getter and setter. 199 | */ 200 | public function test_access_token_getter_and_setter() 201 | { 202 | // create a new token mock. 203 | $token = $this->mockToken(); 204 | 205 | // creates a new broadcaster instance. 206 | $broadcaster = $this->getInstanceWithMocks(); 207 | 208 | // assert the internal instance is not the same. 209 | $this->assertNotSame($token, $broadcaster->getToken()); 210 | 211 | // customize token. 212 | $setReturn = $broadcaster->setToken($token); 213 | 214 | // assert the fluent return. 215 | $this->assertSame($broadcaster, $setReturn); 216 | 217 | // now, assert the instance was correctly set and returned. 218 | $this->assertSame($token, $broadcaster->getToken()); 219 | } 220 | 221 | /** 222 | * Test http client getter and setter. 223 | */ 224 | public function test_http_client_getter_and_setter() 225 | { 226 | // create a new http client mock. 227 | $httpClient = $this->mockHttpClient(); 228 | 229 | // creates a new broadcaster instance. 230 | $broadcaster = $this->getInstanceWithMocks(); 231 | 232 | // assert the internal instance is not the same. 233 | $this->assertNotSame($httpClient, $broadcaster->getHttpClient()); 234 | 235 | // customize http client. 236 | $setReturn = $broadcaster->setHttpClient($httpClient); 237 | 238 | // assert the fluent return. 239 | $this->assertSame($broadcaster, $setReturn); 240 | 241 | // now, assert the instance was correctly set and returned. 242 | $this->assertSame($httpClient, $broadcaster->getHttpClient()); 243 | } 244 | 245 | /** 246 | * Tests for the actual operation broadcasting. 247 | * 248 | * @throws 249 | */ 250 | public function test_broadcasting() 251 | { 252 | // start a broadcaster mock. 253 | $broadcaster = $this->getInstanceWithMocks(); 254 | 255 | // start a custom http client mock. 256 | $httpClient = $this->mockHttpClient(); 257 | 258 | // create a mock http response. 259 | $mockHttpResponse = $this->mockHttpResponse(); 260 | 261 | // when the http client receives a call, it should return the mock response. 262 | $httpClient->shouldReceive('call')->andReturn($mockHttpResponse); 263 | 264 | // customize the http client on the broadcaster. 265 | $broadcaster->setHttpClient($httpClient); 266 | 267 | // create a new, empty mock operation. 268 | $operation = $this->mockOperation(); 269 | 270 | // broadcast the operation, and get the response/ 271 | $response = $broadcaster->broadcast([$operation]); 272 | 273 | // assert the response is a SDK response instance. 274 | $this->assertInstanceOf(Response::class, $response); 275 | 276 | // the mock http response should be the same as the one previously 277 | // created for the mock http client. 278 | $this->assertSame($mockHttpResponse, $response->getHttpResponse()); 279 | } 280 | 281 | /** 282 | * Test for exceptions thrown by the broadcast method. 283 | * 284 | * @throws 285 | */ 286 | public function test_broadcast_exception_handling() 287 | { 288 | // start a broadcaster mock. 289 | $broadcaster = $this->getInstanceWithMocks(); 290 | 291 | // start a custom http client mock. 292 | $httpClient = $this->mockHttpClient(); 293 | 294 | // create a mock http response (error). 295 | $mockHttpResponse = $this->mockErrorHttpResponse(); 296 | 297 | // when the http client receives a call, it should return the mock response. 298 | $httpClient->shouldReceive('call')->andReturnUsing(function () use ($mockHttpResponse) { 299 | // generate a mock for the request. 300 | /** @var RequestInterface $mockRequest */ 301 | $mockRequest = Mockery::mock(RequestInterface::class); 302 | 303 | // throw a bad response exception. 304 | throw new BadResponseException('error', $mockRequest, $mockHttpResponse); 305 | }); 306 | 307 | // customize the http client on the broadcaster. 308 | $broadcaster->setHttpClient($httpClient); 309 | 310 | // create a new, empty mock operation. 311 | $operation = $this->mockOperation(); 312 | 313 | try { 314 | // broadcast the operation, that will throw an exception (bad response). 315 | $broadcaster->broadcast([$operation]); 316 | } catch (\Exception $e) { 317 | // assert that the response is actually, a SDK response exception. 318 | $this->assertInstanceOf(ResponseException::class, $e); 319 | /** @var $e ResponseException */ 320 | $this->assertSame($e->getHttpResponse(), $mockHttpResponse); 321 | } 322 | 323 | // now, let's try for generic errors. 324 | // start a custom http client mock. 325 | $httpClient = $this->mockHttpClient(); 326 | 327 | // when the call method gets called, it will throw a generic guzzle exception. 328 | $httpClient->shouldReceive('call')->andReturnUsing(function () { 329 | // throw a bad response exception. 330 | throw Mockery::mock(GuzzleException::class, \Exception::class); 331 | }); 332 | 333 | // set the custom http client. 334 | $broadcaster->setHttpClient($httpClient); 335 | 336 | try { 337 | // broadcast the operation, that will throw an exception (generic guzzle exception). 338 | $broadcaster->broadcast([$operation]); 339 | } catch (\Exception $e) { 340 | // assert it's a client (SDK) generic exception. 341 | $this->assertInstanceOf(ClientException::class, $e); 342 | } 343 | } 344 | } -------------------------------------------------------------------------------- /docs/03-operations.md: -------------------------------------------------------------------------------- 1 | # Broadcasting Operations 2 | 3 | Considering the user already authorized your application, and you have the SDK client instance configured with the 4 | user's access token, it's now time to broadcast operations. 5 | 6 | ## Broad-what, Ope-who? 7 | 8 | If you are lost on those terms, just understand the basics: 9 | 10 | An **operation**, is a given instruction transmitted to the Seem blockchain. The instruction could 11 | be an upvote, downvote, comment, etc. 12 | 13 | **Broadcast** is just a common name used to reference the act of signing and including the operation(s) 14 | into the Steem blockchain. 15 | 16 | Sometimes, a single operation will be broadcast, and, in other cases, like adding beneficiaries to a given 17 | comment, more than one operation will be broadcast at the same time. 18 | 19 | Finally, we have the **transaction** concept. A transaction is the result of a successful broadcast of 20 | a given number of operations. 21 | 22 | Meaning, a transaction is a record within a Steem blockchain block, and a transaction contains all the 23 | operations that were broadcast. 24 | 25 | ## A quick example. 26 | 27 | Before we head on a reference of all types of operations available, We need to understand 28 | the SDK flow for handling SteemConnect responses. 29 | 30 | Here is a quick example, where we will upvote a given post: 31 | 32 | ``` php 33 | voter('hernandev'); 45 | // let's vote on: 46 | // https://busy.org/@utopian-io/utopian-io-reborn-smarter-simpler-better 47 | $upvote->on('utopian-io', 'utopian-io-reborn-smarter-simpler-better'); 48 | // now, set the upvote at 50% 49 | $upvote->percent(50); 50 | 51 | // broadcast the operation to Steem through SteemConnect. 52 | $response = $sdk->broadcast($operation) 53 | 54 | // get the transaction from the broadcast response. 55 | $transaction = $response->getTransaction(); 56 | ``` 57 | 58 | The SDK api is really simple to understand, but, let's break down the concepts: 59 | 60 | First, on that example, we created a **`Vote`** operation instance, and populated that vote 61 | with the parameters we wanted. 62 | 63 | It's important to notice that, given Steem blockchain data structures, we need to set who is the account 64 | responsible for the operation, that's why the **`voter`** method was called. 65 | 66 | !!! warning 67 | Notice that, on the SDK calls, the **`@`** should not be used, only the account names. On the example, 68 | the user **`@hernandev`** is voting on a post by **`@utopian-io`**, both accounts must 69 | be referenced as **`hernandev`** and **`utopian-io`** only, without the **`@`**. 70 | 71 | Now, to what matters: 72 | 73 | The **`$response`** variable, returned from the **`broadcast()`** method on the SDK, is an instance of 74 | **`SteemConnect\Client\Response`**, this class is used to wrap the HTTP response from 75 | SteemConnect. 76 | 77 | In cases of errors, the broadcast method will not return a **`Response`** instance, instead, it will throw an exception. 78 | 79 | On success cases, the transaction, that is the result of the broadcast, can be accessed though the **`getTransaction()`** 80 | method on the **`Response`** instance. 81 | 82 | The transaction, from that method, is an instance of **`SteemConnect\Transactions\Transaction`**, and an be converted to an 83 | array, for storage purposes, or it's data can also be accessed using the Transaction getters. For a full list of the 84 | available methods, consult the 85 | [source code directly here](https://github.com/hernandev/sc2-sdk-php/blob/master/src/Transactions/Transaction.php). 86 | 87 | ## Available Operations. 88 | 89 | Here we list valid operation examples, that can be adapted on your applications. Notice that each operation has it's 90 | corresponding required scope, which the user must have previously allowed, on the authorization flow. 91 | 92 | ### Voting. 93 | 94 | Both upvote and downvote are the same operation, the difference is the percent given on the vote. In other words, a vote 95 | weight can vary between -100% and 100%. 96 | 97 | On the numerous Steem frontend applications, the downvote is displayed as `flagged posts`, which means the weight itself 98 | is rarely shown. 99 | 100 | For this library, the weight of a given vote can be passed as argument using any percent notation: 101 | 102 | - **Integer Notation** 103 | 104 | **`100`** represents **`100%`**, **`50`** represents **`50%`**, etc. 105 | 106 | - **Default Notation** 107 | 108 | Used by most Steem clients, and the internal format, where a **`100%`** vote is represented by the number **`10000`**. 109 | 110 | #### Upvote Example: 111 | 112 | ``` php 113 | voter('hernandev'); 124 | // let's vote on: 125 | // https://busy.org/@utopian-io/utopian-io-reborn-smarter-simpler-better 126 | $upvote->on('utopian-io', 'utopian-io-reborn-smarter-simpler-better'); 127 | // now, set the upvote at 90% 128 | $upvote->percent(90); 129 | 130 | // broadcast the operation to Steem through SteemConnect. 131 | $response = $sdk->broadcast($upvote); 132 | ``` 133 | 134 | #### Downvote Example: 135 | 136 | ``` php 137 | voter('hernandev'); 148 | // let's vote on: 149 | // https://busy.org/@utopian-io/utopian-io-reborn-smarter-simpler-better 150 | $upvote->on('utopian-io', 'utopian-io-reborn-smarter-simpler-better'); 151 | // now, set the upvote at -100% 152 | $upvote->percent(-100); 153 | 154 | // broadcast the operation to Steem through SteemConnect. 155 | $response = $sdk->broadcast($upvote); 156 | ``` 157 | 158 | If for some reason, you want to change the vote, all you need is to broadcast the new vote, Steem will consider the 159 | last vote as valid and the previous ones as invalid. 160 | 161 | ### Follow & UnFollow. 162 | 163 | Follow and Unfollow, are also, the same operation, meaning that the difference is only a internal flag 164 | inside it called **`what`**. 165 | 166 | The what parameter of a follow operation is an array, and the value `"blog"` indicated the follow status. 167 | 168 | #### Follow Example: 169 | 170 | On this example, the user `@hernandev` starts following the user `@utopian-io`. 171 | 172 | ``` php 173 | follower('hernandev'); 184 | // set who to follow. 185 | $follow->follow('utopian-io') 186 | 187 | // broadcast the operation to Steem through SteemConnect. 188 | $response = $sdk->broadcast($follow); 189 | ``` 190 | 191 | #### UnFollow Example: 192 | 193 | On this example, the user `@hernandev` stops following the user `@utopian-io`. 194 | 195 | This means we are reverting the previous follow operation. 196 | 197 | ``` php 198 | follower('hernandev'); 209 | // let's unfollow. 210 | $follow->unfollow('utopian-io') 211 | 212 | // broadcast the operation to Steem through SteemConnect. 213 | $response = $sdk->broadcast($follow); 214 | ``` 215 | 216 | ### Reblog. 217 | 218 | Reblog is a simple operation, all you need to do is: 219 | 220 | On the example, the user **`@hernandev`** is reblogging the post 221 | [https://steemit.com/@utopian-io/utopian-io-reborn-smarter-simpler-better](https://busy.org/@utopian-io/utopian-io-reborn-smarter-simpler-better) 222 | 223 | ``` php 224 | account('hernandev'); 235 | // reblog a given post. 236 | $reblog->reblog('utopian-io', 'utopian-io-reborn-smarter-simpler-better'); 237 | 238 | // broadcast the operation to Steem through SteemConnect. 239 | $response = $sdk->broadcast($reblog); 240 | ``` 241 | 242 | ### Post & Comment. 243 | 244 | On the Steem blockchain, a post is actually a comment. It means, that a Post is just a comment, without a parent. 245 | 246 | But, since there is no parent on a post, we need to set the parent permlink, that one will be used as the category 247 | for the post. 248 | 249 | The example will make it a little bit easier to understand: 250 | 251 | #### Post Example: 252 | 253 | ``` php 254 | author('hernandev'); 265 | $post->category('introduceyourself'); 266 | // set the post title. 267 | $post->title('Hello, this is Diego, but you can callme @hernandev'); 268 | // set the post body. 269 | $post->body('You may insert the post content here, markdown is advised'); 270 | // optionally, you may set tags on the post: 271 | $post->tags(['life', 'steem', 'steemdev']); 272 | 273 | // broadcast the operation to Steem through SteemConnect. 274 | $response = $sdk->broadcast($post); 275 | ``` 276 | 277 | Wait, what about the post URL? 278 | 279 | The post URL is automatically extract from the title, using an internal slug function. 280 | 281 | On the example, the title of the post was: 282 | 283 | `Hello, this is Diego, but you can callme @hernandev` 284 | 285 | The SDK will translate the title into a URL friendly slug: 286 | 287 | `hello-this-is-diego-but-you-can-callme-at-hernandev` 288 | 289 | But, if you want to customize the URL (which on Steem, is called `permlink`), you can do that by calling: 290 | 291 | ``` php 292 | permlink('this-is-a-custom-permlink-url-for-the-post'); 297 | ``` 298 | 299 | The permlink does not need to match the title, the only rule here is that one author may not use the same permlink twice, 300 | since that's the unique identifier for a post. 301 | 302 | #### Comment / Reply Example: 303 | 304 | To comment or reply on a given post, is also very simple: 305 | 306 | On the example, we are going to reply to the post we just created on the previous example. 307 | 308 | ``` php 309 | author('hernandev'); 320 | // set the parent post, you are replying to. 321 | $comment->reply('hernandev', 'hello-this-is-diego-but-you-can-callme-at-hernandev'); 322 | // set the post body. 323 | $post->body('You may insert the post content here, markdown is advised'); 324 | // optionally, you may set tags on the post: 325 | $post->tags(['life', 'steem', 'steemdev']); 326 | 327 | // broadcast the operation to Steem through SteemConnect. 328 | $response = $sdk->broadcast($post); 329 | ``` 330 | 331 | Just as the post, a reply will have the permlink automatically filled from the body content, if you want to customize 332 | the permlink, you can do the same you did for posts, by calling the `permlink()` method. 333 | 334 | ### Comment Options. 335 | 336 | One important thing about comments, is that there are special options, like beneficiares, 50% SBD or 100% SP, etc. 337 | 338 | Those special options are not a part of the comment operation itself. Instead those options must be set on a special 339 | operation called **`comment_options`**. 340 | 341 | !!! warning 342 | While is not required for a `comment` to have a `comment_options` operation, when they do, both operations **MUST** 343 | be broadcast at the same time, since they must be part of the same transaction. 344 | 345 | #### Comment With Comment Options Example: 346 | 347 | Here is an example, that creates a post, with options and broadcast the operations at the same time: 348 | 349 | ``` php 350 | author('hernandev'); 362 | // set the category. 363 | $post->category('testing'); 364 | // set the parent post, you are replying to. 365 | $post->title('This is an example comment'); 366 | // set the post body. 367 | $post->body('Hello dear Steemians...'); 368 | // optionally, you may set tags on the post: 369 | $post->tags(['life', 'steem', 'steemdev']); 370 | 371 | 372 | // create the comment options operation: 373 | $options = new CommentOptions(); 374 | // now, we set the post that will own the options. 375 | // this is where we link the two operations. 376 | $options->of($post); 377 | // you may disable votes. 378 | $options->allowVotes(false); 379 | // you may disable curation rewards. 380 | $options->allowCurationRewards(false); 381 | // don't wanna earn form your post, customize the max payout value. 382 | $options->maxAcceptedPayout(0); 383 | // set you only want 50% of the 50% SBD payout. 384 | // for a 100% SP payout, set this value as 0 (zero). 385 | $options->percentSteemDollars(5000); 386 | 387 | 388 | // now, broadcast both operations at once. 389 | $response = $sdk->broadcast($post, $options); 390 | ``` --------------------------------------------------------------------------------