<?php

namespace ZuluCrypto\StellarSdk\Transaction;

use phpseclib\Math\BigInteger;
use ZuluCrypto\StellarSdk\Horizon\Api\PostTransactionResponse;
use ZuluCrypto\StellarSdk\Horizon\ApiClient;
use ZuluCrypto\StellarSdk\Horizon\Exception\HorizonException;
use ZuluCrypto\StellarSdk\Horizon\Exception\PostTransactionException;
use ZuluCrypto\StellarSdk\Keypair;
use ZuluCrypto\StellarSdk\Model\StellarAmount;
use ZuluCrypto\StellarSdk\Server;
use ZuluCrypto\StellarSdk\Signing\PrivateKeySigner;
use ZuluCrypto\StellarSdk\Signing\SigningInterface;
use ZuluCrypto\StellarSdk\Util\MathSafety;
use ZuluCrypto\StellarSdk\Xdr\Iface\XdrEncodableInterface;
use ZuluCrypto\StellarSdk\Xdr\Type\VariableArray;
use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;
use ZuluCrypto\StellarSdk\Xdr\XdrEncoder;
use ZuluCrypto\StellarSdk\XdrModel\AccountId;
use ZuluCrypto\StellarSdk\XdrModel\Asset;
use ZuluCrypto\StellarSdk\XdrModel\DecoratedSignature;
use ZuluCrypto\StellarSdk\XdrModel\Memo;
use ZuluCrypto\StellarSdk\XdrModel\Operation\AccountMergeOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\AllowTrustOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\BumpSequenceOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\ChangeTrustOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\CreateAccountOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\ManageDataOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\Operation;
use ZuluCrypto\StellarSdk\XdrModel\Operation\PaymentOp;
use ZuluCrypto\StellarSdk\XdrModel\Operation\SetOptionsOp;
use ZuluCrypto\StellarSdk\XdrModel\Signer;
use ZuluCrypto\StellarSdk\XdrModel\SignerKey;
use ZuluCrypto\StellarSdk\XdrModel\TimeBounds;
use ZuluCrypto\StellarSdk\XdrModel\TransactionEnvelope;

/**
 * todo: rename to Transaction
 * Helper class to build a transaction on the Stellar network
 *
 * References:
 *  Debugging / testing:
 *      https://www.stellar.org/laboratory/
 *
 *  Retrieve fee information from:
 *      https://www.stellar.org/developers/horizon/reference/endpoints/ledgers-single.html
 *      https://www.stellar.org/developers/horizon/reference/resources/ledger.html
 *
 * Notes:
 *  - Per-operation fee is 100 stroops (0.00001 XLM)
 *  - Base reserve is 10 XLM
 *      - Minimum balance for an account is base reserve * 2
 *      - Each additional trustline, offer, signer, and data entry requires another 10 XLM
 *
 *
 * Format of a transaction:
 *  Source Address (AddressId)
 *      type
 *      address
 *  Fee (Uint32)
 *  Next sequence number (SequenceNumber - uint64)
 *      ...
 *  Time bounds (TimeBounds)
 *  Memo (Memo)
 *  Operations (Operation[])
 *  ext (TransactionExt) - extra? currently is a union with no arms
 */
class TransactionBuilder implements XdrEncodableInterface
{
    /**
     * Base-32 account ID
     *
     * @var AccountId
     */
    private $accountId;

    /**
     * @var TimeBounds
     */
    private $timeBounds;

    /**
     * @var Memo
     */
    private $memo;

    /**
     * @var VariableArray[]
     */
    private $operations;

    /**
     * Horizon API client, used for retrieving sequence numbers and validating
     * transaction
     *
     * @var ApiClient
     */
    private $apiClient;

    /**
     * @var SigningInterface
     */
    protected $signingProvider;

    /**
     * @var DecoratedSignature[]
     */
    protected $signatures;

    /**
     * If null, this is retrieved from the network
     *
     * @var BigInteger
     */
    protected $sequenceNumber;

    /**
     * TransactionBuilder constructor.
     *
     * @param $sourceAccountId
     * @return TransactionBuilder
     */
    public function __construct($sourceAccountId)
    {
        $this->accountId = new AccountId($sourceAccountId);

        $this->timeBounds = new TimeBounds();
        $this->memo = new Memo(Memo::MEMO_TYPE_NONE);
        $this->operations = new VariableArray();

        $this->signatures = [];

        return $this;
    }

    /**
     * Uses $signer to add a new DecoratedSignature to this TransactionBuilder
     *
     * @param SigningInterface $signer
     * @return DecoratedSignature
     */
    public function signWith(SigningInterface $signer)
    {
        $decoratedSignature = $signer->signTransaction($this);

        $this->signatures[] = $decoratedSignature;

        return $decoratedSignature;
    }

    /**
     * @param Keypair $keypair
     * @return DecoratedSignature
     */
    public function getSignatureForKeypair(Keypair $keypair)
    {
        $signer = new PrivateKeySigner($keypair);

        return $this->signWith($signer);
    }

    /**
     * @return TransactionEnvelope
     */
    public function getTransactionEnvelope()
    {
        $txEnvelope = new TransactionEnvelope($this);

        foreach ($this->signatures as $signature) {
            $txEnvelope->addDecoratedSignature($signature);
        }

        return $txEnvelope;
    }

    /**
     * @param $secretKeyString
     * @return TransactionEnvelope
     */
    public function sign($secretKeyString = null)
    {
        if ($secretKeyString instanceof Keypair) {
            $secretKeyString = $secretKeyString->getSecret();
        }

        // If $secretKeyString is null, check for a SigningProvider
        if (!$secretKeyString) {
            // No secret key and no signing provider: could be a pre-authorized
            // transaction. Return empty envelope with no signatures
            if (!$this->signingProvider) {
                return new TransactionEnvelope($this);
            }

            $this->signWith($this->signingProvider);

            return $this->getTransactionEnvelope();
        }
        else {
            return (new TransactionEnvelope($this))->sign($secretKeyString);
        }
    }

    public function hash()
    {
        return $this->apiClient->hash($this);
    }

    public function getHashAsString()
    {
        return $this->apiClient->getHashAsString($this);
    }

    /**
     * @param $secretKeyString string|Keypair
     * @return PostTransactionResponse
     * @throws PostTransactionException
     * @throws \ErrorException
     */
    public function submit($secretKeyString = null)
    {
        if ($secretKeyString instanceof Keypair) {
            $secretKeyString = $secretKeyString->getSecret();
        }

        return $this->apiClient->submitTransaction($this, $secretKeyString);
    }

    public function getFee()
    {
        // todo: load base fee from network
        return 100 * $this->operations->count();
    }

    /**
     * @param string|Keypair          $destination
     * @param number|BigInteger       $amount int representing lumens or BigInteger representing stroops
     * @param null                    $sourceAccountId
     * @return TransactionBuilder
     */
    public function addLumenPayment($destination, $amount, $sourceAccountId = null)
    {
        return $this->addOperation(PaymentOp::newNativePayment($destination, $amount, $sourceAccountId));
    }

    /**
     * @param string            $newAccountId
     * @param number|BigInteger $amount int representing lumens or BigInteger representing stroops
     * @param string            $sourceAccountId
     * @return TransactionBuilder
     */
    public function addCreateAccountOp($newAccountId, $amount, $sourceAccountId = null)
    {
        return $this->addOperation(new CreateAccountOp(new AccountId($newAccountId), $amount, $sourceAccountId));
    }

    /**
     * @param Asset               $asset
     * @param number|BigInteger   $amount number representing lumens or BigInteger representing stroops
     * @param string|Keypair      $destinationAccountId
     * $param null|string|Keypair $sourceAccountId
     * @return TransactionBuilder
     */
    public function addCustomAssetPaymentOp(Asset $asset, $amount, $destinationAccountId, $sourceAccountId = null)
    {
        return $this->addOperation(
            PaymentOp::newCustomPayment($destinationAccountId, $amount, $asset->getAssetCode(), $asset->getIssuer()->getAccountIdString(), $sourceAccountId)
        );
    }

    /**
     * @param Asset $asset
     * @param int   $amount defaults to maximum if null
     * @param null  $sourceAccountId
     * @return TransactionBuilder
     */
    public function addChangeTrustOp(Asset $asset, $amount = null, $sourceAccountId = null)
    {
        if ($amount === null) {
            $amount = StellarAmount::newMaximum();
        }

        return $this->addOperation(new ChangeTrustOp($asset, $amount, $sourceAccountId));
    }

    /**
     * This is called by asset issuers to authorize a trustline established by
     * a client account
     *
     * @param Asset $asset
     * @param       $trustorId
     * @param null  $sourceAccountId
     * @return TransactionBuilder
     */
    public function authorizeTrustline(Asset $asset, $trustorId, $sourceAccountId = null)
    {
        if ($trustorId instanceof Keypair) {
            $trustorId = $trustorId->getPublicKey();
        }

        $op = new AllowTrustOp($asset, new AccountId($trustorId), $sourceAccountId);
        $op->setIsAuthorized(true);

        return $this->addOperation($op);
    }

    /**
     * This is called by asset issuers to revoke a trustline established by
     * a client account
     *
     * @param Asset $asset
     * @param       $trustorId
     * @param null  $sourceAccountId
     * @return TransactionBuilder
     */
    public function revokeTrustline(Asset $asset, $trustorId, $sourceAccountId = null)
    {
        if ($trustorId instanceof Keypair) {
            $trustorId = $trustorId->getPublicKey();
        }

        $op = new AllowTrustOp($asset, new AccountId($trustorId), $sourceAccountId);
        $op->setIsAuthorized(false);

        return $this->addOperation($op);
    }

    /**
     * Adds an operation to merge the balance of the source account to $destinationAccountId
     * @param      $destinationAccountId
     * @param null $sourceAccountId
     * @return TransactionBuilder
     */
    public function addMergeOperation($destinationAccountId, $sourceAccountId = null)
    {
        if ($destinationAccountId instanceof Keypair) {
            $destinationAccountId = $destinationAccountId->getPublicKey();
        }

        return $this->addOperation(new AccountMergeOp($destinationAccountId, $sourceAccountId));
    }

    /**
     * @param      $key
     * @param      $value
     * @param null $sourceAccountId
     * @return TransactionBuilder
     */
    public function setAccountData($key, $value = null, $sourceAccountId = null)
    {
        return $this->addOperation(new ManageDataOp($key, $value, $sourceAccountId));
    }

    /**
     * @param      $key
     * @param null $sourceAccountId
     * @return TransactionBuilder
     */
    public function clearAccountData($key, $sourceAccountId = null)
    {
        return $this->addOperation(new ManageDataOp($key, null, $sourceAccountId));
    }

    /**
     * @param BigInteger $bumpTo
     * @param null       $sourceAccountId
     * @return TransactionBuilder
     */
    public function bumpSequenceTo(BigInteger $bumpTo, $sourceAccountId = null)
    {
        return $this->addOperation(new BumpSequenceOp($bumpTo, $sourceAccountId));
    }

    /**
     * @return string
     */
    public function toXdr()
    {
        $bytes = '';

        if (!$this->sequenceNumber) {
            $this->sequenceNumber = $this->generateSequenceNumber();
        }

        // Account ID (36 bytes)
        $bytes .= $this->accountId->toXdr();
        // Fee (4 bytes)
        $bytes .= XdrEncoder::unsignedInteger($this->getFee());
        // Sequence number (8 bytes)
        $bytes .= XdrEncoder::unsignedBigInteger64($this->sequenceNumber);

        // Time Bounds are optional
        if ($this->timeBounds->isEmpty()) {
            $bytes .= XdrEncoder::boolean(false);
        }
        else {
            $bytes .= XdrEncoder::boolean(true);
            $bytes .= $this->timeBounds->toXdr();
        }

        // Memo (4 bytes if empty, 36 bytes maximum)
        $bytes .= $this->memo->toXdr();

        // Operations
        $bytes .= $this->operations->toXdr();

        // TransactionExt (union reserved for future use)
        $bytes .= XdrEncoder::unsignedInteger(0);

        return $bytes;
    }

    /**
     * @param XdrBuffer $xdr
     * @param Server    $server
     * @return TransactionBuilder
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr, Server $server)
    {
        return Transaction::fromXdr($xdr)->toTransactionBuilder($server);
    }

    /**
     * @param $operation
     * @return TransactionBuilder
     */
    public function addOperation($operation)
    {
        $this->operations->append($operation);

        return $this;
    }

    /**
     * @param $memo
     * @return $this
     */
    public function setTextMemo($memo)
    {
        $this->memo = new Memo(Memo::MEMO_TYPE_TEXT, $memo);

        return $this;
    }

    /**
     * @param $memo
     * @return $this
     */
    public function setIdMemo($memo)
    {
        $this->memo = new Memo(Memo::MEMO_TYPE_ID, $memo);

        return $this;
    }

    /**
     * Note: this should be called with the raw sha256 hash
     *
     * For example:
     *  $builder->setHashMemo(hash('sha256', 'example thing being hashed', true));
     *
     * @param $memo 32-byte sha256 hash
     * @return $this
     */
    public function setHashMemo($memo)
    {
        $this->memo = new Memo(Memo::MEMO_TYPE_HASH, $memo);

        return $this;
    }

    /**
     * Note: this should be called with the raw sha256 hash
     *
     * For example:
     *  $builder->setReturnMemo(hash('sha256', 'example thing being hashed', true));
     *
     * @param $memo 32-byte sha256 hash
     * @return $this
     */
    public function setReturnMemo($memo)
    {
        $this->memo = new Memo(Memo::MEMO_TYPE_RETURN, $memo);

        return $this;
    }

    /**
     * @param \DateTime $lowerTimebound
     * @return $this
     */
    public function setLowerTimebound(\DateTime $lowerTimebound)
    {
        $this->timeBounds->setMinTime($lowerTimebound);

        return $this;
    }

    /**
     * @param \DateTime $upperTimebound
     * @return $this
     */
    public function setUpperTimebound(\DateTime $upperTimebound)
    {
        $this->timeBounds->setMaxTime($upperTimebound);

        return $this;
    }

    protected function generateSequenceNumber()
    {
        $this->ensureApiClient();

        $account = $this->apiClient
            ->getAccount($this->accountId->getAccountIdString());

        if (!$account) {
            throw new \ErrorException(sprintf('Account not found: %s', $this->accountId->getAccountIdString()));
        }

        return $account->getSequenceAsBigInteger()->add(new BigInteger(1));
    }

    protected function ensureApiClient()
    {
        if (!$this->apiClient) throw new \ErrorException("An API client is required, call setApiClient before using this method");
    }

    /**
     * @return ApiClient
     */
    public function getApiClient()
    {
        return $this->apiClient;
    }

    /**
     * @param ApiClient $apiClient
     * @return TransactionBuilder
     */
    public function setApiClient($apiClient)
    {
        $this->apiClient = $apiClient;

        return $this;
    }

    /**
     * @return SigningInterface
     */
    public function getSigningProvider()
    {
        return $this->signingProvider;
    }

    /**
     * @param SigningInterface $signingProvider
     */
    public function setSigningProvider($signingProvider)
    {
        $this->signingProvider = $signingProvider;

        return $this;
    }

    /**
     * @return BigInteger
     */
    public function getSequenceNumber()
    {
        return $this->sequenceNumber;
    }

    /**
     * @param BigInteger $sequenceNumber
     */
    public function setSequenceNumber($sequenceNumber)
    {
        if (!is_a($sequenceNumber, 'phpseclib\Math\BigInteger')) {
            $sequenceNumber = new BigInteger($sequenceNumber);
        }

        $this->sequenceNumber = $sequenceNumber;

        return $this;
    }

    /**
     * @return Memo
     */
    public function getMemo()
    {
        return $this->memo;
    }

    /**
     * @param Memo $memo
     */
    public function setMemo($memo)
    {
        $this->memo = $memo;

        return $this;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class CreateAccountResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L364
    const MALFORMED             = 'create_account_malformed';
    const UNDERFUNDED           = 'create_account_underfunded'; // source account does not have enough funds
    const LOW_RESERVE           = 'create_account_low_reserve'; // would create an account below the minimum reserve
    const ALREADY_EXIST         = 'create_account_already_exists';

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new CreateAccountResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::MALFORMED,
            '-2' => static::UNDERFUNDED,
            '-3' => static::LOW_RESERVE,
            '-4' => static::ALREADY_EXIST,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        return $model;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class AllowTrustResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L562
    const MALFORMED                 = 'allow_trust_malformed';
    const NO_TRUST_LINE             = 'allow_trust_no_trust_line';
    const TRUST_NOT_REQUIRED        = 'allow_trust_trust_not_required';
    const CANT_REVOKE               = 'allow_trust_trust_cant_revoke';
    const SELF_NOT_ALLOWED          = 'allow_trust_trust_self_not_allowed';

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new AllowTrustResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::MALFORMED,
            '-2' => static::NO_TRUST_LINE,
            '-3' => static::TRUST_NOT_REQUIRED,
            '-4' => static::CANT_REVOKE,
            '-5' => static::SELF_NOT_ALLOWED,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        return $model;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel\Operation;

use ZuluCrypto\StellarSdk\Keypair;
use ZuluCrypto\StellarSdk\Xdr\Iface\XdrEncodableInterface;
use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;
use ZuluCrypto\StellarSdk\Xdr\XdrEncoder;
use ZuluCrypto\StellarSdk\XdrModel\AccountId;

/**
 * Known operation types:
 *
    xdr.enum("OperationType", {
        createAccount: 0,
        payment: 1,
        pathPayment: 2,
        manageOffer: 3,
        createPassiveOffer: 4,
        setOption: 5,
        changeTrust: 6,
        allowTrust: 7,
        accountMerge: 8,
        inflation: 9,
        manageDatum: 10,
    });
 *
 * See: https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-transaction.x
 *
 */
abstract class Operation implements XdrEncodableInterface
{
    const TYPE_CREATE_ACCOUNT       = 0;
    const TYPE_PAYMENT              = 1;
    const TYPE_PATH_PAYMENT         = 2;
    const TYPE_MANAGE_OFFER         = 3;
    const TYPE_CREATE_PASSIVE_OFFER = 4;
    const TYPE_SET_OPTIONS          = 5;
    const TYPE_CHANGE_TRUST         = 6;
    const TYPE_ALLOW_TRUST          = 7;
    const TYPE_ACCOUNT_MERGE        = 8;
    const TYPE_INFLATION            = 9;
    const TYPE_MANAGE_DATA          = 10;
    const TYPE_BUMP_SEQUENCE        = 11;

    /**
     * @var AccountId
     */
    protected $sourceAccount;

    /**
     * Type constants are defined by each subclass
     *
     * A full list can be found at: https://www.stellar.org/developers/guides/concepts/list-of-operations.html
     *
     * @var int
     */
    protected $type;

    /**
     * Operation constructor.
     *
     * @param $type int operation type constant
     * @param $sourceAccountId AccountId if null this will default to the source for the transaction
     * @return Operation
     */
    public function __construct($type, $sourceAccountId = null)
    {
        if ($sourceAccountId instanceof Keypair) {
            $sourceAccountId = new AccountId($sourceAccountId->getPublicKey());
        }
        if (is_string($sourceAccountId)) {
            $sourceAccountId = new AccountId($sourceAccountId);
        }

        $this->sourceAccount = $sourceAccountId;
        $this->type = $type;

        return $this;
    }

    /**
     * Child classes MUST call this method to get the header for the operation
     * and then append their body
     *
     * @return string
     */
    public function toXdr()
    {
        $bytes = '';

        // Source Account
        $bytes .= XdrEncoder::optional($this->sourceAccount);

        // Type
        $bytes .= XdrEncoder::unsignedInteger($this->type);

        return $bytes;
    }

    /**
     * @param XdrBuffer $xdr
     * @return Operation
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        /** @var Operation $model */
        $model = null;
        $hasSourceAccount = $xdr->readBoolean();

        $sourceAccount = null;
        if ($hasSourceAccount) {
            $sourceAccount = AccountId::fromXdr($xdr);
        }

        $type = $xdr->readUnsignedInteger();

        switch ($type) {
            case Operation::TYPE_CREATE_ACCOUNT:
                $model = CreateAccountOp::fromXdr($xdr);
                break;
            case Operation::TYPE_PAYMENT:
                $model = PaymentOp::fromXdr($xdr);
                break;
            case Operation::TYPE_PATH_PAYMENT:
                $model = PathPaymentOp::fromXdr($xdr);
                break;
            case Operation::TYPE_MANAGE_OFFER:
                $model = ManageOfferOp::fromXdr($xdr);
                break;
            case Operation::TYPE_CREATE_PASSIVE_OFFER:
                $model = CreatePassiveOfferOp::fromXdr($xdr);
                break;
            case Operation::TYPE_SET_OPTIONS:
                $model = SetOptionsOp::fromXdr($xdr);
                break;
            case Operation::TYPE_CHANGE_TRUST:
                $model = ChangeTrustOp::fromXdr($xdr);
                break;
            case Operation::TYPE_ALLOW_TRUST:
                $model = AllowTrustOp::fromXdr($xdr);
                break;
            case Operation::TYPE_ACCOUNT_MERGE:
                $model = AccountMergeOp::fromXdr($xdr);
                break;
            case Operation::TYPE_INFLATION:
                $model = new InflationOp(); // no additional XDR to parse
                break;
            case Operation::TYPE_MANAGE_DATA:
                $model = ManageDataOp::fromXdr($xdr);
                break;
            case Operation::TYPE_BUMP_SEQUENCE:
                $model = BumpSequenceOp::fromXdr($xdr);
                break;
            default:
                throw new \InvalidArgumentException(sprintf('unrecognized operation type %s', $type));
        }

        $model->sourceAccount = $sourceAccount;

        return $model;
    }

    /**
     * @return AccountId
     */
    public function getSourceAccount()
    {
        return $this->sourceAccount;
    }

    /**
     * @param AccountId $sourceAccount
     */
    public function setSourceAccount(AccountId $sourceAccount)
    {
        $this->sourceAccount = $sourceAccount;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Xdr;

use phpseclib\Math\BigInteger;
use ZuluCrypto\StellarSdk\Xdr\Iface\XdrEncodableInterface;

/**
 * See: https://tools.ietf.org/html/rfc4506
 *
 * - Data is stored in big endian
 */
class XdrEncoder
{
    /**
     * NOTE: do not access this directly, it's used for caching whether the current
     * platform is big endian.
     *
     * Instead, call self::nativeIsBigEndian()
     *
     * @deprecated use nativeIsBigEndian() instead
     * @var bool
     */
    private static $nativeIsBigEndian;

    /**
     * @param       $value
     * @param null  $expectedLength in bytes
     * @param false $padUnexpectedLength If true, an unexpected length is padded instead of throwing an exception
     * @return string
     */
    public static function opaqueFixed($value, $expectedLength = null, $padUnexpectedLength = false)
    {
        // Length greater than expected length is always an error
        if ($expectedLength && strlen($value) > $expectedLength) throw new \InvalidArgumentException(sprintf('Unexpected length for value. Has length %s, expected %s', strlen($value), $expectedLength));
        if ($expectedLength && !$padUnexpectedLength && strlen($value) != $expectedLength) throw new \InvalidArgumentException(sprintf('Unexpected length for value. Has length %s, expected %s', strlen($value), $expectedLength));

        if ($expectedLength && strlen($value) != $expectedLength) {
            $value = self::applyPadding($value, $expectedLength);
        }

        return self::applyPadding($value);
    }

    /**
     * Variable-length opaque data
     *
     * Maximum length is 2^32 - 1
     *
     * @param $value
     * @return string
     */
    public static function opaqueVariable($value)
    {
        $maxLength = pow(2, 32) - 1;
        if (strlen($value) > $maxLength) throw new \InvalidArgumentException(sprintf('Value of length %s is greater than the maximum allowed length of %s', strlen($value), $maxLength));

        $bytes = '';

        $bytes .= self::unsignedInteger(strlen($value));
        $bytes .= self::applyPadding($value);

        return $bytes;
    }

    public static function signedInteger($value)
    {
        // pack() does not support a signed 32-byte int, so work around this with
        // custom encoding
        return (self::nativeIsBigEndian()) ? pack('l', $value) : strrev(pack('l', $value));
    }

    public static function unsignedInteger($value)
    {
        // unsigned 32-bit big-endian
        return pack('N', $value);
    }

    public static function signedInteger64($value)
    {
        // pack() does not support a signed 64-byte int, so work around this with
        // custom encoding
        return (self::nativeIsBigEndian()) ? pack('q', $value) : strrev(pack('q', $value));
    }

    /**
     * Converts $value to a signed 8-byte big endian int64
     *
     * @param BigInteger $value
     * @return string
     */
    public static function signedBigInteger64(BigInteger $value)
    {
        $xdrBytes = '';
        $bigIntBytes = $value->toBytes(true);
        $bigIntBits = $value->toBits(true);

        // Special case: MAX_UINT_64 will look like 00ffffffffffffffff and have an
        // extra preceeding byte we need to get rid of
        if (strlen($bigIntBytes) === 9 && substr($value->toHex(true), 0, 2) === '00') {
            $bigIntBytes = substr($bigIntBytes, 1);
        }

        $paddingChar = chr(0);
        // If the number is negative, pad with 0xFF
        if (substr($bigIntBits, 0, 1) == 1) {
            $paddingChar = chr(255);
        }

        $paddingBytes = 8 - strlen($bigIntBytes);
        while ($paddingBytes > 0) {
            $xdrBytes .= $paddingChar;
            $paddingBytes--;
        }

        $xdrBytes .= $bigIntBytes;

        return XdrEncoder::opaqueFixed($xdrBytes, 8);
    }

    /**
     * Converts $value to an unsigned 8-byte big endian uint64
     *
     * @param BigInteger $value
     * @return string
     */
    public static function unsignedBigInteger64(BigInteger $value)
    {
        $xdrBytes = '';
        $bigIntBytes = $value->toBytes(true);

        // Special case: MAX_UINT_64 will look like 00ffffffffffffffff and have an
        // extra preceeding byte we need to get rid of
        if (strlen($bigIntBytes) === 9 && substr($value->toHex(true), 0, 2) === '00') {
            $bigIntBytes = substr($bigIntBytes, 1);
        }

        $paddingChar = chr(0);

        $paddingBytes = 8 - strlen($bigIntBytes);
        while ($paddingBytes > 0) {
            $xdrBytes .= $paddingChar;
            $paddingBytes--;
        }

        $xdrBytes .= $bigIntBytes;

        return XdrEncoder::opaqueFixed($xdrBytes, 8);
    }

    /**
     * Use this to write raw bytes representing a 64-bit integer
     *
     * This value will be padded up to 8 bytes
     *
     * @param $value
     * @return string
     */
    public static function integer64RawBytes($value)
    {
        // Some libraries will give a 4-byte value here but it must be encoded
        // as 8
        return self::applyPadding($value, 8, false);
    }

    public static function unsignedInteger64($value)
    {
        if ($value > PHP_INT_MAX) throw new \InvalidArgumentException('value is greater than PHP_INT_MAX');

        // unsigned 64-bit big-endian
        return pack('J', $value);
    }

    public static function signedHyper($value)
    {
        return self::signedInteger64($value);
    }

    public static function unsignedHyper($value)
    {
        return self::unsignedInteger64($value);
    }

    public static function unsignedInteger256($value)
    {
        return self::opaqueFixed($value, (256/8));
    }

    public static function boolean($value)
    {
        // Equivalent to 1 or 0 uint32
        return ($value) ? self::unsignedInteger(1) : self::unsignedInteger(0);
    }

    /**
     * @param      $value
     * @param null $maximumLength
     * @return string
     */
    public static function string($value, $maximumLength = null)
    {
        if ($maximumLength === null) $maximumLength = pow(2, 32) - 1;

        if (strlen($value) > $maximumLength) throw new \InvalidArgumentException('string exceeds maximum length');

        $bytes = self::unsignedInteger(strlen($value));
        $bytes .= $value;

        // Pad with null bytes to get a multiple of 4 bytes
        $remainder = (strlen($value) % 4);
        if ($remainder) {
            while ($remainder < 4) {
                $bytes .= "\0";
                $remainder++;
            }
        }

        return $bytes;
    }

    /**
     * Encodes an optional data value as XDR.
     *
     * Any non-null $value will be encoded and returned
     *
     * @param XdrEncodableInterface $value
     * @return string
     */
    public static function optional(XdrEncodableInterface $value = null)
    {
        $bytes = '';

        if ($value !== null) {
            $bytes .= self::boolean(true);
            $bytes .= $value->toXdr();
        }
        else {
            $bytes .= self::boolean(false);
        }

        return $bytes;
    }

    /**
     * @param $value
     * @return string
     */
    public static function optionalUnsignedInteger($value)
    {
        $bytes = '';

        if ($value !== null) {
            $bytes .= self::boolean(true);
            $bytes .= static::unsignedInteger($value);
        }
        else {
            $bytes .= self::boolean(false);
        }

        return $bytes;
    }

    /**
     * @param $value
     * @return string
     */
    public static function optionalString($value, $maximumLength)
    {
        $bytes = '';

        if ($value !== null) {
            $bytes .= self::boolean(true);
            $bytes .= static::string($value, $maximumLength);
        }
        else {
            $bytes .= self::boolean(false);
        }

        return $bytes;
    }

    /**
     * Ensures $value's length is a multiple of $targetLength bytes
     *
     * The default value for XDR is 4
     *
     * @param $value
     * @param $targetLength - desired length after padding is applied
     * @param $rightPadding - pad on the right of the value, false to pad to the left
     * @return string
     */
    private static function applyPadding($value, $targetLength = 4, $rightPadding = true)
    {
        // No padding necessary if it's a multiple of 4 bytes
        if (strlen($value) % $targetLength === 0) return $value;

        $numPaddingChars = $targetLength - (strlen($value) % $targetLength);

        if ($rightPadding) {
            return $value . str_repeat(chr(0), $numPaddingChars);
        }
        else {
            return str_repeat(chr(0), $numPaddingChars) . $value;
        }
    }

    /**
     * @return bool
     */
    private static function nativeIsBigEndian()
    {
        if (null === self::$nativeIsBigEndian) {
            self::$nativeIsBigEndian = pack('L', 1) === pack('N', 1);
        }

        return self::$nativeIsBigEndian;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class ManageOfferResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L457
    const MALFORMED             = 'manage_offer_malformed';
    const SELL_NO_TRUST         = 'manage_offer_sell_no_trust';         // no trustline for what's being sold
    const BUY_NO_TRUST          = 'manage_offer_buy_no_trust';
    const SELL_NOT_AUTHORIZED   = 'manage_offer_sell_not_authorized';
    const BUY_NOT_AUTHORIZED    = 'manage_offer_buy_not_authorized';
    const LINE_FULL             = 'manage_offer_line_full';
    const UNDERFUNDED           = 'manage_offer_underfunded';
    const CROSS_SELF            = 'manage_offer_cross_self';
    const SELL_NO_ISSUER        = 'manage_offer_sell_no_issuer';
    const BUY_NO_ISSUER         = 'manage_offer_buy_no_issuer';
    const NOT_FOUND             = 'manage_offer_not_found';             // offer ID doesn't match an existing offer
    const LOW_RESERVE           = 'manage_offer_low_reserve';           // not enough funds to create a new offer

    // Constants for checking ManageOfferEffect
    const EFFECT_CREATED        = 0;
    const EFFECT_UPDATED        = 1;
    const EFFECT_DELETED        = 2;

    /**
     * @var ClaimOfferAtom[]
     */
    protected $claimedOffers = array();

    /**
     * This will be present if the ManageOfferOp resulted in an offer getting
     * created or updated
     *
     * @var OfferEntry
     */
    protected $offer;

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new ManageOfferResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::MALFORMED,
            '-2' => static::SELL_NO_TRUST,
            '-3' => static::BUY_NO_TRUST,
            '-4' => static::SELL_NOT_AUTHORIZED,
            '-5' => static::BUY_NOT_AUTHORIZED,
            '-6' => static::LINE_FULL,
            '-7' => static::UNDERFUNDED,
            '-8' => static::CROSS_SELF,
            '-9' => static::SELL_NO_ISSUER,
            '-10' => static::BUY_NO_ISSUER,
            '-11' => static::NOT_FOUND,
            '-12' => static::LOW_RESERVE,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
            // Return immediately if there was an error since there won't be any additional data
            return $model;
        }

        // Populate claimed offers
        $numOffersClaimed = $xdr->readInteger();
        for ($i=0; $i < $numOffersClaimed; $i++) {
            $model->claimedOffers[] = ClaimOfferAtom::fromXdr($xdr);
        }

        $effect = $xdr->readInteger();
        if ($effect === static::EFFECT_CREATED || $effect === static::EFFECT_UPDATED) {
            $model->offer = OfferEntry::fromXdr($xdr);
        }

        return $model;
    }

    /**
     * @return ClaimOfferAtom[]
     */
    public function getClaimedOffers()
    {
        return $this->claimedOffers;
    }

    /**
     * @param ClaimOfferAtom[] $claimedOffers
     */
    public function setClaimedOffers($claimedOffers)
    {
        $this->claimedOffers = $claimedOffers;
    }

    /**
     * @return OfferEntry
     */
    public function getOffer()
    {
        return $this->offer;
    }

    /**
     * @param OfferEntry $offer
     */
    public function setOffer($offer)
    {
        $this->offer = $offer;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\History;

/**
 * State documentation available at:
 *  https://github.com/stellar/stellar-core/blob/master/docs/history.md#history-archive-state-has-files
 *
 * Overview of the bucket format available at:
 *  https://github.com/stellar/stellar-core/blob/master/src/bucket/BucketList.h
 *
 */
class BucketLevel
{
    const HASH_EMPTY = '0000000000000000000000000000000000000000000000000000000000000000';

    // Future bucket that hasn't been populated yet
    const STATE_PENDING         = 0;
    //
    const STATE_MERGED          = 1;
    const STATE_MERGE_PENDING   = 2;

    /**
     *
     * @var int
     */
    protected $level;

    /**
     * Current bucket for this level
     *
     * @var string sha256
     */
    protected $curr;

    /**
     * "snap" bucket for this level
     *
     * @var string sha256
     */
    protected $snap;

    /**
     * Information about the future bucket for this level. An array with the
     * following keys:
     *  state: see STATE_ constants
     *  output: todo
     *
     * @var string sha256
     */
    protected $next;

    public static function fromRaw($raw, $level)
    {
        $object = new BucketLevel($level);

        // Only "next" buckets have a state
        $object->curr = $raw['curr'];
        $object->snap = $raw['snap'];
        $object->next = $raw['next'];

        return $object;
    }

    public function __construct($level)
    {
        $this->level = $level;
    }

    /**
     * Returns an array of unique sha256 hashes in this bucket level
     *
     * @return array
     */
    public function getUniqueBucketHashes()
    {
        $hashes = [];

        if ($this->curr != self::HASH_EMPTY) $hashes[] = $this->curr;
        if ($this->snap != self::HASH_EMPTY) $hashes[] = $this->snap;

        return array_values(array_unique($hashes));
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class SetOptionsResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L513
    const LOW_RESERVE               = 'set_options_low_reserve';
    const TOO_MANY_SIGNERS          = 'set_options_too_many_signers';
    const BAD_FLAGS                 = 'set_options_bad_flags';
    const INVALID_INFLATION         = 'set_options_invalid_inflation';  // inflation account does not exist
    const CANT_CHANGE               = 'set_options_cant_change';        // option can no longer be changed
    const UNKNOWN_FLAG              = 'set_options_unknown_flag';
    const THRESHOLD_OUT_OF_RANGE    = 'set_options_threshold_out_of_range'; // bad value for weight or threshold
    const BAD_SIGNER                = 'set_options_bad_signer';         // signer cannot be master key
    const INVALID_HOME_DOMAIN       = 'set_options_invalid_home_domain';// malformed home domain

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new SetOptionsResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::LOW_RESERVE,
            '-2' => static::TOO_MANY_SIGNERS,
            '-3' => static::BAD_FLAGS,
            '-4' => static::INVALID_INFLATION,
            '-5' => static::CANT_CHANGE,
            '-6' => static::UNKNOWN_FLAG,
            '-7' => static::THRESHOLD_OUT_OF_RANGE,
            '-8' => static::BAD_SIGNER,
            '-9' => static::INVALID_HOME_DOMAIN,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        return $model;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Util;

class Debug
{
    /**
     * Returns a printable hex string of the binary $data
     *
     * Adapted from: https://stackoverflow.com/a/4225813/2056308
     *
     * - Modified to return a string instead of echo it
     *
     * @param        $data
     * @param string $newline
     * @return string
     */
    public static function hexDump($data, $newline = "\n")
    {
        $output = '';
        static $from = '';
        static $to = '';

        static $width = 16; # number of bytes per line

        static $pad = '.'; # padding for non-visible characters

        if ($from==='')
        {
            for ($i=0; $i<=0xFF; $i++)
            {
                $from .= chr($i);
                $to .= ($i >= 0x20 && $i <= 0x7E) ? chr($i) : $pad;
            }
        }

        $hex = str_split(bin2hex($data), $width*2);
        $chars = str_split(strtr($data, $from, $to), $width);

        $offset = 0;
        foreach ($hex as $i => $line)
        {
            $output .= sprintf('%6X',$offset).' : '.implode(' ', str_split($line,2)) . ' [' . $chars[$i] . ']' . $newline;
            $offset += $width;
        }

        return $output;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel\Operation;

use ZuluCrypto\StellarSdk\Keypair;
use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;
use ZuluCrypto\StellarSdk\XdrModel\AccountId;

class AccountMergeOp extends Operation
{
    /**
     * @var AccountId
     */
    protected $destination;

    public function __construct($destinationAccountId, $sourceAccountId = null)
    {
        parent::__construct(Operation::TYPE_ACCOUNT_MERGE, $sourceAccountId);

        if ($destinationAccountId instanceof Keypair) {
            $destinationAccountId = $destinationAccountId->getPublicKey();
        }

        $this->destination = new AccountId($destinationAccountId);
    }

    /**
     * @return string
     */
    public function toXdr()
    {
        $bytes = parent::toXdr();

        // Destination account
        $bytes .= $this->destination->toXdr();

        return $bytes;
    }

    /**
     * NOTE: This only parses the XDR that's specific to this operation and cannot
     * load a full Operation
     *
     * @deprecated Do not call this directly, instead call Operation::fromXdr()
     * @param XdrBuffer $xdr
     * @return AccountMergeOp
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $destination = AccountId::fromXdr($xdr);

        return new AccountMergeOp($destination->getAccountIdString());
    }

    /**
     * @return AccountId
     */
    public function getDestination()
    {
        return $this->destination;
    }

    /**
     * @param AccountId $destination
     */
    public function setDestination($destination)
    {
        $this->destination = $destination;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Horizon\Api;

class HorizonResponse
{
    /**
     * Raw json-decoded response body
     *
     * @var array
     */
    protected $rawData;

    public function __construct($jsonEncodedData)
    {
        $this->rawData = @json_decode($jsonEncodedData, true);

        if (null === $this->rawData && json_last_error() != JSON_ERROR_NONE) {
            throw new \InvalidArgumentException(sprintf("Error in json_decode: %s", json_last_error_msg()));
        }
    }

    /**
     * @return array
     */
    public function getRawData()
    {
        return $this->rawData;
    }

    /**
     * Returns the value of $fieldName or null if $fieldName is not in the response
     *
     * @param $fieldName
     * @return mixed|null
     */
    public function getField($fieldName)
    {
        if (!isset($this->rawData[$fieldName])) return null;

        return $this->rawData[$fieldName];
    }

    /**
     * Throws an exception if $fieldName is not present in the response
     *
     * @param $fieldName
     * @throws \InvalidArgumentException
     * @return mixed|null
     */
    public function mustGetField($fieldName)
    {
        if (!isset($this->rawData[$fieldName])) throw new \InvalidArgumentException(sprintf("Field '%s' not present in response", $fieldName));

        return $this->rawData[$fieldName];
    }

    public function getRecords($limit = 100)
    {
        // todo: support paging

        $records = [];
        foreach ($this->rawData['_embedded']['records'] as $rawRecord) {
            $record = $rawRecord;
            unset($record['_links']);

            $records[] = $record;
        }

        return $records;
    }
}


<?php

namespace ZuluCrypto\StellarSdk;

use phpseclib\Math\BigInteger;
use Prophecy\Exception\InvalidArgumentException;
use ZuluCrypto\StellarSdk\Horizon\ApiClient;
use ZuluCrypto\StellarSdk\Horizon\Exception\HorizonException;
use ZuluCrypto\StellarSdk\Model\Account;
use ZuluCrypto\StellarSdk\Model\Payment;
use ZuluCrypto\StellarSdk\Signing\SigningInterface;
use ZuluCrypto\StellarSdk\Transaction\TransactionBuilder;

class Server
{
    /**
     * @var ApiClient
     */
    private $apiClient;

    /**
     * @var
     */
    private $isTestnet;

    /**
     * @var SigningInterface
     */
    protected $signingProvider;

    /**
     * @return Server
     */
    public static function testNet()
    {
        $server = new Server(ApiClient::newTestnetClient());
        $server->isTestnet = true;

        return $server;
    }

    /**
     * @return Server
     */
    public static function publicNet()
    {
        $server = new Server(ApiClient::newPublicClient());

        return $server;
    }

    /**
     * Connects to a custom network
     *
     * @param $horizonBaseUrl
     * @param $networkPassphrase
     * @return Server
     */
    public static function customNet($horizonBaseUrl, $networkPassphrase)
    {
        return new Server(ApiClient::newCustomClient($horizonBaseUrl, $networkPassphrase));
    }

    public function __construct(ApiClient $apiClient)
    {
        $this->apiClient = $apiClient;
        $this->isTestnet = false;
    }

    /**
     * Returns the Account that matches $accountId or null if the account does
     * not exist
     *
     * @param $accountId Keypair|string the public account ID
     * @return Account|null
     * @throws Horizon\Exception\HorizonException
     */
    public function getAccount($accountId)
    {
        // Cannot be empty
        if (!$accountId) throw new InvalidArgumentException('Empty accountId');

        if ($accountId instanceof Keypair) {
            $accountId = $accountId->getPublicKey();
        }

        try {
            $response = $this->apiClient->get(sprintf('/accounts/%s', $accountId));
        }
        catch (HorizonException $e) {
            // Account not found, return null
            if ($e->getHttpStatusCode() === 404) {
                return null;
            }

            // A problem we can't handle, rethrow
            throw $e;
        }

        $account = Account::fromHorizonResponse($response);
        $account->setApiClient($this->apiClient);

        return $account;
    }

    /**
     * Returns true if the account exists on this server and has been funded
     *
     * @param $accountId
     * @return bool
     * @throws HorizonException
     * @throws \ErrorException
     */
    public function accountExists($accountId)
    {
        // Handle basic errors such as malformed account IDs
        try {
            $account = $this->getAccount($accountId);
        } catch (\InvalidArgumentException $e) {
            return false;
        }

        // Account ID may be valid but hasn't been funded yet
        if (!$account) return false;

        return $account->getNativeBalanceStroops() != '0';
    }

    /**
     * @param $accountId string|Keypair
     * @return TransactionBuilder
     */
    public function buildTransaction($accountId)
    {
        if ($accountId instanceof Keypair) {
            $accountId = $accountId->getPublicKey();
        }

        return (new TransactionBuilder($accountId))
            ->setApiClient($this->apiClient)
            ->setSigningProvider($this->signingProvider)
        ;
    }

    /**
     * @param $transactionHash
     * @return array|Payment[]
     */
    public function getPaymentsByTransactionHash($transactionHash)
    {
        $url = sprintf('/transactions/%s/payments', $transactionHash);

        $response = $this->apiClient->get($url);

        $payments = [];
        foreach ($response->getRecords() as $rawRecord) {
            $payments[] = Payment::fromRawResponseData($rawRecord);
        }

        return $payments;
    }

    /**
     * @param $accountId
     * @return bool
     * @throws Horizon\Exception\HorizonException
     */
    public function fundAccount($accountId)
    {
        if ($accountId instanceof Keypair) {
            $accountId = $accountId->getPublicKey();
        }

        try {
            $this->apiClient->get(sprintf('/friendbot?addr=%s', $accountId));
            return true;
        }
        catch (HorizonException $e) {
            // Account has already been funded
            if ($e->getHttpStatusCode() == 400) {
                return false;
            }

            // Unexpected exception
            throw $e;
        }

    }

    /**
     * Submits a base64-encoded transaction to the Stellar network.
     *
     * No additional validation is performed on this transaction
     *
     * @param $base64TransactionEnvelope
     * @return Horizon\Api\HorizonResponse
     */
    public function submitB64Transaction($base64TransactionEnvelope)
    {
        return $this->apiClient->submitB64Transaction($base64TransactionEnvelope);
    }

    /**
     * @return string
     */
    public function getHorizonBaseUrl()
    {
        return $this->apiClient->getBaseUrl();
    }

    /**
     * @return SigningInterface
     */
    public function getSigningProvider()
    {
        return $this->signingProvider;
    }

    /**
     * @param SigningInterface $signingProvider
     */
    public function setSigningProvider($signingProvider)
    {
        $this->signingProvider = $signingProvider;
    }

    /**
     * @return ApiClient
     */
    public function getApiClient()
    {
        return $this->apiClient;
    }

    /**
     * @param ApiClient $apiClient
     */
    public function setApiClient($apiClient)
    {
        $this->apiClient = $apiClient;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel\Operation;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;
use ZuluCrypto\StellarSdk\Xdr\XdrEncoder;
use ZuluCrypto\StellarSdk\XdrModel\AccountId;
use ZuluCrypto\StellarSdk\XdrModel\Signer;

class SetOptionsOp extends Operation
{
    const FLAG_AUTH_REQUIRED    = 1;
    const FLAG_AUTH_REVOCABLE   = 2;

    /**
     * @var AccountId
     */
    private $inflationDestinationAccount;

    /**
     * @var integer uint32
     */
    private $clearFlags;

    /**
     * @var integer uint32
     */
    private $setFlags;

    /**
     * @var integer uint32
     */
    private $masterWeight;

    /**
     * @var integer uint32
     */
    private $lowThreshold;

    /**
     * @var integer uint32
     */
    private $mediumThreshold;

    /**
     * @var integer uint32
     */
    private $highThreshold;

    /**
     * Base-32 encoded
     * @var string<32>
     */
    private $homeDomain;

    /**
     * @var Signer
     */
    private $signer;

    /**
     * @return SetOptionsOp
     */
    public function __construct($sourceAccountId = null)
    {
        parent::__construct(Operation::TYPE_SET_OPTIONS, $sourceAccountId);

        $this->setFlags = null;
        $this->clearFlags = null;

        return $this;
    }

    public function toXdr()
    {
        $bytes = parent::toXdr();

        // Treat 0 flags as null
        if ($this->setFlags === 0) $this->setFlags = null;
        if ($this->clearFlags === 0) $this->clearFlags = null;

        // inflation destination
        $bytes .= XdrEncoder::optional($this->inflationDestinationAccount);

        // clear flags
        $bytes .= XdrEncoder::optionalUnsignedInteger($this->clearFlags);

        // set flags
        $bytes .= XdrEncoder::optionalUnsignedInteger($this->setFlags);

        // master weight
        $bytes .= XdrEncoder::optionalUnsignedInteger($this->masterWeight);

        // low threshold
        $bytes .= XdrEncoder::optionalUnsignedInteger($this->lowThreshold);

        // medium threshold
        $bytes .= XdrEncoder::optionalUnsignedInteger($this->mediumThreshold);

        // high threshold
        $bytes .= XdrEncoder::optionalUnsignedInteger($this->highThreshold);

        // home domain
        $bytes .= XdrEncoder::optionalString($this->homeDomain, 32);

        // Signer
        $bytes .= XdrEncoder::optional($this->signer);

        return $bytes;
    }

    /**
     * @deprecated Do not call this directly, instead call Operation::fromXdr()
     * @param XdrBuffer $xdr
     * @return Operation|SetOptionsOp
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new SetOptionsOp();

        // inflation destination
        if ($xdr->readBoolean()) {
            $model->inflationDestinationAccount = AccountId::fromXdr($xdr);
        }
        // clear flags
        if ($xdr->readBoolean()) {
            $model->applyClearFlags($xdr->readUnsignedInteger());
        }
        // set flags
        if ($xdr->readBoolean()) {
            $model->applySetFlags($xdr->readUnsignedInteger());
        }
        // master weight
        if ($xdr->readBoolean()) {
            $model->masterWeight = $xdr->readUnsignedInteger();
        }
        // low threshold
        if ($xdr->readBoolean()) {
            $model->lowThreshold = $xdr->readUnsignedInteger();
        }
        // medium threshold
        if ($xdr->readBoolean()) {
            $model->mediumThreshold = $xdr->readUnsignedInteger();
        }
        // high threshold
        if ($xdr->readBoolean()) {
            $model->highThreshold = $xdr->readUnsignedInteger();
        }
        // home domain
        if ($xdr->readBoolean()) {
            $model->homeDomain = $xdr->readString(32);
        }
        // signer
        if ($xdr->readBoolean()) {
            $model->signer = Signer::fromXdr($xdr);
        }

        return $model;
    }

    /**
     * @param $accountId
     * @return SetOptionsOp
     */
    public function setInflationDestination($accountId)
    {
        $this->inflationDestinationAccount = new AccountId($accountId);

        return $this;
    }

    /**
     * If set, TrustLines are created with authorized set to false requiring the issuer to set it for each TrustLine
     *
     * @param $isRequired
     * @return SetOptionsOp
     */
    public function setAuthRequired($isRequired)
    {
        if ($isRequired) {
            $this->setFlags   = $this->setFlags | static::FLAG_AUTH_REQUIRED;
            $this->clearFlags = $this->clearFlags & ~(static::FLAG_AUTH_REQUIRED);
        }
        else {
            $this->setFlags   = $this->setFlags & ~(static::FLAG_AUTH_REQUIRED);
            $this->clearFlags = $this->clearFlags | static::FLAG_AUTH_REQUIRED;
        }

        return $this;
    }

    /**
     * @return bool
     */
    public function isAuthRequired()
    {
        return boolval($this->setFlags & static::FLAG_AUTH_REQUIRED);
    }

    /**
     * If set, the authorized flag in TrustLines can be cleared. Otherwise, authorization cannot be revoked
     *
     * @param $isRevocable
     * @return SetOptionsOp
     */
    public function setAuthRevocable($isRevocable)
    {
        if ($isRevocable) {
            $this->setFlags   = $this->setFlags | static::FLAG_AUTH_REVOCABLE;
            $this->clearFlags = $this->clearFlags & ~(static::FLAG_AUTH_REVOCABLE);
        }
        else {
            $this->setFlags   = $this->setFlags & ~(static::FLAG_AUTH_REVOCABLE);
            $this->clearFlags = $this->clearFlags | static::FLAG_AUTH_REVOCABLE;
        }

        return $this;
    }

    /**
     * @return bool
     */
    public function isAuthRevocable()
    {
        return boolval($this->setFlags & static::FLAG_AUTH_REVOCABLE);
    }

    /**
     * Set the weight of $signer to 0 to remove it
     *
     * @param Signer $signer
     * @return SetOptionsOp
     */
    public function updateSigner(Signer $signer)
    {
        $this->signer = $signer;

        return $this;
    }

    /**
     * @param $weight
     * @return SetOptionsOp
     */
    public function setMasterWeight($weight)
    {
        if ($weight > 255 || $weight < 0) throw new \InvalidArgumentException('$weight must be between 0 and 255');

        $this->masterWeight = $weight;

        return $this;
    }

    /**
     * @param $threshold
     * @return $this
     */
    public function setLowThreshold($threshold)
    {
        if ($threshold < 0 || $threshold > 255) throw new \InvalidArgumentException('$threshold must be between 0 and ' . (2^32-1));

        $this->lowThreshold = $threshold;

        return $this;
    }

    /**
     * @param $threshold
     * @return $this
     */
    public function setMediumThreshold($threshold)
    {
        if ($threshold < 0 || $threshold > 255) throw new \InvalidArgumentException('$threshold must be between 0 and ' . (2^32-1));

        $this->mediumThreshold = $threshold;

        return $this;
    }

    /**
     * @param $threshold
     * @return $this
     */
    public function setHighThreshold($threshold)
    {
        if ($threshold < 0 || $threshold > 255) throw new \InvalidArgumentException('$threshold must be between 0 and ' . (2^32-1));

        $this->highThreshold = $threshold;

        return $this;
    }

    /**
     * @param $domain string maximum length 32 characters
     * @return $this
     */
    public function setHomeDomain($domain)
    {
        if (strlen($domain) > 32) throw new \InvalidArgumentException('$domain can not be longer than 32 characters');

        $this->homeDomain = $domain;

        return $this;
    }

    /**
     * @return AccountId
     */
    public function getInflationDestinationAccount()
    {
        return $this->inflationDestinationAccount;
    }

    /**
     * @return int
     */
    public function getClearFlags()
    {
        return $this->clearFlags;
    }

    /**
     * @return int
     */
    public function getSetFlags()
    {
        return $this->setFlags;
    }

    /**
     * @return int
     */
    public function getMasterWeight()
    {
        return $this->masterWeight;
    }

    /**
     * @return int
     */
    public function getLowThreshold()
    {
        return $this->lowThreshold;
    }

    /**
     * @return int
     */
    public function getMediumThreshold()
    {
        return $this->mediumThreshold;
    }

    /**
     * @return int
     */
    public function getHighThreshold()
    {
        return $this->highThreshold;
    }

    /**
     * @return string
     */
    public function getHomeDomain()
    {
        return $this->homeDomain;
    }

    /**
     * @return Signer
     */
    public function getSigner()
    {
        return $this->signer;
    }

    /**
     * @param $clearFlags
     */
    protected function applyClearFlags($clearFlags)
    {
        if ($clearFlags & static::FLAG_AUTH_REQUIRED) {
            $this->setAuthRequired(false);
        }
        if ($clearFlags & static::FLAG_AUTH_REVOCABLE) {
            $this->setAuthRevocable(false);
        }
    }

    /**
     * @param $setFlags
     */
    protected function applySetFlags($setFlags)
    {
        if ($setFlags & static::FLAG_AUTH_REQUIRED) {
            $this->setAuthRequired(true);
        }
        if ($setFlags & static::FLAG_AUTH_REVOCABLE) {
            $this->setAuthRevocable(true);
        }
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Keypair;
use ZuluCrypto\StellarSdk\Xdr\Iface\XdrEncodableInterface;
use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;
use ZuluCrypto\StellarSdk\Xdr\XdrEncoder;

/**
 * Used for adding signers to accounts
 */
class SignerKey implements XdrEncodableInterface
{
    const TYPE_ED25519 = 0;
    const TYPE_PRE_AUTH_TX = 1;
    const TYPE_HASH_X = 2;

    /**
     * See the TYPE constants
     *
     * @var int
     */
    private $type;

    /**
     * Binary representation of the key
     *
     * @var string
     */
    private $key;

    /**
     * @param Keypair $keypair
     * @return SignerKey
     */
    public static function fromKeypair(Keypair $keypair)
    {
        $signerKey = new SignerKey(static::TYPE_ED25519);
        $signerKey->key = $keypair->getPublicKeyBytes();

        return $signerKey;
    }

    /**
     * @param $hashBytes
     * @return SignerKey
     */
    public static function fromPreauthorizedHash($hashBytes)
    {
        if (strlen($hashBytes) != 32) {
            throw new \InvalidArgumentException('$hashBytes must be 32 bytes representing the sha256 hash of the transaction');
        }

        $signerKey = new SignerKey(static::TYPE_PRE_AUTH_TX);
        $signerKey->key = $hashBytes;

        return $signerKey;
    }

    /**
     * Adds the value of $x as a signer on the account
     *
     * @param $x
     * @return SignerKey
     */
    public static function fromHashX($x)
    {
        $signerKey = new SignerKey(static::TYPE_HASH_X);
        $signerKey->key = hash('sha256', $x, true);

        return $signerKey;
    }

    public function __construct($type)
    {
        $this->type = $type;
    }

    public function toXdr()
    {
        $bytes = '';

        // Type
        $bytes .= XdrEncoder::unsignedInteger($this->type);

        // Key
        $bytes .= XdrEncoder::unsignedInteger256($this->key);

        return $bytes;
    }

    /**
     * @param XdrBuffer $xdr
     * @return SignerKey
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $type = $xdr->readUnsignedInteger();
        $model = new SignerKey($type);

        $model->key = $xdr->readOpaqueFixed(32);

        return $model;
    }

    /**
     * @return int
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return string
     */
    public function getKey()
    {
        return $this->key;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Horizon\Exception;

use GuzzleHttp\Exception\ClientException;
use Throwable;

class HorizonException extends \ErrorException
{
    /**
     * Usually a URL referencing additional documentation
     *
     * @var string
     */
    protected $type;

    /**
     * @var string
     */
    protected $title;

    /**
     * @var string
     */
    protected $detail;

    /**
     * Horizon instance ID
     *
     * @var string
     */
    protected $instance;

    /**
     * URL that was requested and generated the error
     *
     * @var string
     */
    protected $requestedUrl;

    /**
     * HTTP method used to request $requestedUrl
     *
     * @var string
     */
    protected $httpMethod;

    /**
     * @var int
     */
    protected $httpStatusCode;

    /**
     * Original exception generated by the client
     *
     * @var ClientException
     */
    protected $clientException;

    /**
     * Raw response data, if known
     *
     * @var array
     */
    protected $raw;

    /**
     * Result codes for Horizon operations errors.
     *
     * @var string[]
     */
    protected $operationResultCodes;

    /**
     * @var string
     */
    protected $transactionResultCode;

    /**
     * NOTE: changes here may requires changes to PostTransactionException::fromHorizonException
     *
     * @param                 $requestedUrl
     * @param                 $httpMethod
     * @param                 $raw
     * @param ClientException $clientException
     * @return HorizonException
     */
    public static function fromRawResponse($requestedUrl, $httpMethod, $raw, ClientException $clientException = null)
    {
        $title = isset($raw['title']) ? $raw['title'] : 'Unknown Exception';

        $exception = new HorizonException($title, $clientException);
        $exception->title = $title;
        $exception->requestedUrl = $requestedUrl;
        $exception->httpMethod = $httpMethod;

        if (isset($raw['type'])) $exception->type = $raw['type'];
        if (isset($raw['status'])) $exception->httpStatusCode = $raw['status'];
        if (isset($raw['detail'])) $exception->detail = $raw['detail'];
        if (!empty($raw['extras']['result_codes']['operations'])) {
            $exception->operationResultCodes = $raw['extras']['result_codes']['operations'];
        }
        if (!empty($raw['extras']['result_codes']['transaction'])) {
            $exception->transactionResultCode = $raw['extras']['result_codes']['transaction'];
        }

        // Message can contain better info after we've filled out more fields
        $exception->message = $exception->buildMessage();

        $exception->raw = $raw;
        $exception->clientException = $clientException;

        return $exception;
    }

    /**
     * @param string         $title
     * @param Throwable|null $previous
     */
    public function __construct($title, Throwable $previous = null)
    {
        parent::__construct($title, 0, 1, $previous->getFile(), $previous->getLine(), $previous);
    }

    /**
     * @return string
     */
    protected function buildMessage() {
        // Additional data used to help the user resolve the error
        $hint = '';

        $message = sprintf('[%s] %s: %s (Requested URL: %s %s)',
            $this->httpStatusCode,
            $this->title,
            $this->detail,
            $this->httpMethod,
            $this->requestedUrl
        );

        if ($this->transactionResultCode) {
            $message .= sprintf(" Tx Result: %s", $this->transactionResultCode);
        }
        if (!empty($this->operationResultCodes)) {
            $message .= sprintf(" Op Results: %s", print_r($this->operationResultCodes,true));
        }

        // Rate limit exceeded
        if ('429' == $this->httpStatusCode) {
            // todo: check response headers and provide better error message
        }

        return $message;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @param string $type
     */
    public function setType($type)
    {
        $this->type = $type;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param string $title
     */
    public function setTitle($title)
    {
        $this->title = $title;
    }

    /**
     * @return string
     */
    public function getDetail()
    {
        return $this->detail;
    }

    /**
     * @param string $detail
     */
    public function setDetail($detail)
    {
        $this->detail = $detail;
    }

    /**
     * @return string
     */
    public function getInstance()
    {
        return $this->instance;
    }

    /**
     * @param string $instance
     */
    public function setInstance($instance)
    {
        $this->instance = $instance;
    }

    /**
     * @return string
     */
    public function getRequestedUrl()
    {
        return $this->requestedUrl;
    }

    /**
     * @param string $requestedUrl
     */
    public function setRequestedUrl($requestedUrl)
    {
        $this->requestedUrl = $requestedUrl;
    }

    /**
     * @return int
     */
    public function getHttpStatusCode()
    {
        return $this->httpStatusCode;
    }

    /**
     * @param int $httpStatusCode
     */
    public function setHttpStatusCode($httpStatusCode)
    {
        $this->httpStatusCode = $httpStatusCode;
    }

    /**
     * @return ClientException
     */
    public function getClientException()
    {
        return $this->clientException;
    }

    /**
     * @param ClientException $clientException
     */
    public function setClientException($clientException)
    {
        $this->clientException = $clientException;
    }

     /**
     * Get the result codes from Horizon Response.
     *
     * @return array
     */
    function getOperationResultCodes()
    {
        return $this->operationResultCodes;
    }

    /**
     * Set the result codes from Horizon Response.
     *
     * @param array $operationResultCodes
     */
    function setOperationResultCodes($operationResultCodes)
    {
        $this->operationResultCodes = $operationResultCodes;
    }

    /**
     * @return string
     */
    public function getHttpMethod()
    {
        return $this->httpMethod;
    }

    /**
     * @param string $httpMethod
     */
    public function setHttpMethod($httpMethod)
    {
        $this->httpMethod = $httpMethod;
    }

    /**
     * @return array
     */
    public function getRaw()
    {
        return $this->raw;
    }

    /**
     * @param array $raw
     */
    public function setRaw($raw)
    {
        $this->raw = $raw;
    }

    /**
     * @return string
     */
    public function getTransactionResultCode()
    {
        return $this->transactionResultCode;
    }

    /**
     * @param string $transactionResultCode
     */
    public function setTransactionResultCode($transactionResultCode)
    {
        $this->transactionResultCode = $transactionResultCode;
    }
}


<?php

namespace ZuluCrypto\StellarSdk;

use ParagonIE\Sodium\Core\Ed25519;
use ZuluCrypto\StellarSdk\Derivation\Bip39\Bip39;
use ZuluCrypto\StellarSdk\Derivation\HdNode;
use ZuluCrypto\StellarSdk\XdrModel\DecoratedSignature;

/**
 * A public/private keypair for use with the Stellar network
 */
class Keypair
{

    /**
     * Base-32 encoded seed
     *
     * @var string
     */
    private $seed;

    /**
     * Bytes of the private key
     *
     * @var string
     */
    private $privateKey;

    /**
     * Base-32 public key
     *
     * @var string
     */
    private $publicKeyString;

    /**
     * Bytes of the public key
     *
     * @var string
     */
    private $publicKey;

    /**
     * Creates a new random keypair
     *
     * @return Keypair
     */
    public static function newFromRandom()
    {
        return self::newFromRawSeed(random_bytes(32));
    }

    /**
     * Creates a new keypair from a base-32 encoded string (S...)
     *
     * @param $base32String
     * @return Keypair
     */
    public static function newFromSeed($base32String)
    {
        return new Keypair($base32String);
    }

    /**
     * Creates a new keypair from 32 bytes of entropy
     *
     * @param $rawSeed (32-byte string)
     * @return Keypair
     */
    public static function newFromRawSeed($rawSeed)
    {
        $seedString = AddressableKey::seedFromRawBytes($rawSeed);

        return new Keypair($seedString);
    }

    /**
     * @param $base32String
     * @return Keypair
     */
    public static function newFromPublicKey($base32String)
    {
        $keypair = new Keypair();
        $keypair->setPublicKey($base32String);

        return $keypair;
    }

    /**
     * Creates a new keypair from a mnemonic, passphrase (optional) and index (defaults to 0)
     *
     * For more details, see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md
     *
     * @param        $mnemonic
     * @param string $passphrase
     * @param int    $index
     * @return Keypair
     */
    public static function newFromMnemonic($mnemonic, $passphrase = '', $index = 0)
    {
        $bip39 = new Bip39();
        $seedBytes = $bip39->mnemonicToSeedBytesWithErrorChecking($mnemonic, $passphrase);

        $masterNode = HdNode::newMasterNode($seedBytes);

        $accountNode = $masterNode->derivePath(sprintf("m/44'/148'/%s'", $index));

        return static::newFromRawSeed($accountNode->getPrivateKeyBytes());
    }

    public function __construct($seedString = null)
    {
        if ($seedString) {
            $this->setSeed($seedString);
        }
    }

    /**
     * @param $value
     * @return DecoratedSignature
     */
    public function signDecorated($value)
    {
        $this->requirePrivateKey();

        return new DecoratedSignature(
            $this->getHint(),
            $this->sign($value)
        );
    }

    /**
     * Signs the specified $value with the private key
     *
     * @param $value
     * @return string - raw bytes representing the signature
     */
    public function sign($value)
    {
        $this->requirePrivateKey();

        return Ed25519::sign_detached($value, $this->getEd25519SecretKey());
    }

    /**
     * @param $signature
     * @param $message
     * @return bool
     * @throws \Exception
     */
    public function verifySignature($signature, $message)
    {
        return Ed25519::verify_detached($signature, $message, $this->publicKey);
    }

    /**
     * @param $base32String string GABC...
     */
    public function setPublicKey($base32String)
    {
        // Clear out all private key fields
        $this->privateKey = null;

        $this->publicKey = AddressableKey::getRawBytesFromBase32AccountId($base32String);
        $this->publicKeyString = $base32String;
    }

    /**
     * @param $base32SeedString
     */
    public function setSeed($base32SeedString)
    {
        $this->seed = $base32SeedString;
        $this->privateKey = AddressableKey::getRawBytesFromBase32Seed($base32SeedString);
        $this->publicKeyString = AddressableKey::addressFromRawSeed($this->privateKey);
        $this->publicKey = AddressableKey::getRawBytesFromBase32AccountId($this->publicKeyString);
    }

    /**
     * Returns the last 4 characters of the public key
     *
     * @return string
     */
    public function getHint()
    {
        return substr($this->publicKey, -4);
    }

    /**
     * Returns the raw bytes of the checksum for the public key
     *
     * @return string
     */
    public function getPublicKeyChecksum()
    {
        $checksumBytes = substr($this->getPublicKeyBytes(), -2);

        $unpacked = unpack('v', $checksumBytes);

        return array_shift($unpacked);
    }

    /**
     * Returns the base-32 encoded private key (S...)
     * @return string
     * @throws \ErrorException
     */
    public function getSecret()
    {
        $this->requirePrivateKey();

        return $this->seed;
    }

    /**
     * @return bool|string
     * @throws \ErrorException
     */
    public function getPrivateKeyBytes()
    {
        $this->requirePrivateKey();

        return AddressableKey::getRawBytesFromBase32Seed($this->seed);
    }

    /**
     * Returns the base-32 encoded public key (G...)
     * @return string
     */
    public function getPublicKey()
    {
        return $this->publicKeyString;
    }

    /**
     * @return string
     */
    public function getPublicKeyBytes()
    {
        return $this->publicKey;
    }

    /**
     * Returns the base-32 encoded public key (G...)
     * @return string
     */
    public function getAccountId()
    {
        return $this->publicKeyString;
    }

    /**
     * Used to ensure the private key has been specified for this keypair
     *
     * @throws \ErrorException
     */
    protected function requirePrivateKey()
    {
        if (!$this->privateKey) throw new \ErrorException('Private key is required to perform this operation');
    }

    protected function getEd25519SecretKey()
    {
        $this->requirePrivateKey();

        $pk = '';
        $sk = '';

        Ed25519::seed_keypair($pk, $sk, $this->privateKey);

        return $sk;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Util;

/**
 * Utility methods for calculating checksums
 */
class Checksum
{
    /**
     * @param $binaryString
     * @return string CRC-16 checksum of $binaryString as a 2-byte little-endian
     */
    public static function generate($binaryString)
    {
        return pack('v', self::crc16($binaryString));
    }

    /**
     * Returns true if $expected matches the checksum of $binaryStringToChecksum
     *
     * @param $expected
     * @param $binaryStringToChecksum
     * @return bool
     */
    public static function verify($expected, $binaryStringToChecksum)
    {
        return self::generate($binaryStringToChecksum) === $expected;
    }

    /**
     * Returns the crc16 checksum of $binaryString
     *
     * Ported from Java implementation at: http://introcs.cs.princeton.edu/java/61data/CRC16CCITT.java.html
     *
     * Initial value changed to 0x0000 to match Stellar configuration.
     *
     * @param $binaryString
     * @return int (4-byte checksum)
     */
    protected static function crc16($binaryString)
    {
        $crc = 0x0000;
        $polynomial = 0x1021;

        foreach (str_split($binaryString) as $byte) {
            $byte = ord($byte);

            for ($i = 0; $i < 8; $i++) {
                $bit = (($byte >> (7 - $i) & 1) == 1);
                $c15 = (($crc >> 15 & 1) == 1);
                $crc <<= 1;
                if ($c15 ^ $bit) $crc ^= $polynomial;
            }
        }

        return $crc & 0xffff;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use phpseclib\Math\BigInteger;
use ZuluCrypto\StellarSdk\Model\StellarAmount;
use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class AccountMergeResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L585
    const MALFORMED                 = 'account_merge_malformed';
    const NO_ACCOUNT                = 'account_merge_no_account';       // destination account does not exist
    const IMMUTABLE_SET             = 'account_merge_immutable_set';    // source account has AUTH_IMMUTABLE set
    const HAS_SUB_ENTRIES           = 'account_merge_has_sub_entries';  // account has trust lines, offers, etc.

    /**
     * Amount of XLM transferred from the source account to the destination account
     *
     * @var StellarAmount
     */
    protected $transferredBalance;

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new AccountMergeResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::MALFORMED,
            '-2' => static::NO_ACCOUNT,
            '-3' => static::IMMUTABLE_SET,
            '-4' => static::HAS_SUB_ENTRIES,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
            return $model;
        }

        $model->transferredBalance = new StellarAmount(new BigInteger($xdr->readInteger64()));

        return $model;
    }

    /**
     * @return StellarAmount
     */
    public function getTransferredBalance()
    {
        return $this->transferredBalance;
    }

    /**
     * @param StellarAmount $transferredBalance
     */
    public function setTransferredBalance($transferredBalance)
    {
        $this->transferredBalance = $transferredBalance;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Xdr;

use ZuluCrypto\StellarSdk\Util\MathSafety;

class XdrDecoder
{
    /**
     * NOTE: do not access this directly, it's used for caching whether the current
     * platform is big endian.
     *
     * Instead, call self::nativeIsBigEndian()
     *
     * @deprecated use nativeIsBigEndian() instead
     * @var bool
     */
    private static $nativeIsBigEndian;

    /**
     * @param $xdr
     * @return int
     */
    public static function unsignedInteger($xdr)
    {
        // unsigned 32-bit big-endian
        $unpacked = unpack('N', $xdr);
        return array_pop($unpacked);
    }

    /**
     * @param $xdr
     * @return int
     */
    public static function signedInteger($xdr)
    {
        // pack() does not support a signed 32-byte int, so work around this with
        // custom encoding
        if (!self::nativeIsBigEndian()) {
            $xdr = strrev($xdr);
        }

        $unpacked = unpack('l', $xdr);

        return array_pop($unpacked);
    }

    /**
     * @param $xdr
     * @return integer
     * @throws \ErrorException
     */
    public static function unsignedInteger64($xdr)
    {
        MathSafety::require64Bit();

        // unsigned 64-bit big-endian
        $unpacked = unpack('J', $xdr);
        return array_pop($unpacked);
    }

    /**
     * @param $xdr
     * @return int
     * @throws \ErrorException
     */
    public static function unsignedHyper($xdr)
    {
        return self::unsignedInteger64($xdr);
    }

    /**
     * @param $xdr
     * @return integer
     * @throws \ErrorException
     */
    public static function signedInteger64($xdr)
    {
        MathSafety::require64Bit();

        // pack() does not support a signed 64-byte int, so work around this with
        // custom encoding
        if (!self::nativeIsBigEndian()) {
            $xdr = strrev($xdr);
        }

        $unpacked = unpack('q', $xdr);

        return array_pop($unpacked);
    }

    /**
     * @param $xdr
     * @return int
     * @throws \ErrorException
     */
    public static function signedHyper($xdr)
    {
        return self::signedInteger64($xdr);
    }

    /**
     * @param $xdr
     * @return bool
     */
    public static function boolean($xdr)
    {
        $value = self::unsignedInteger($xdr);
        if ($value !== 1 && $value !== 0) {
            throw new \InvalidArgumentException('Unexpected XDR for a boolean value');
        }

        // Equivalent to 1 or 0 uint32
        return (self::unsignedInteger($xdr)) ? true : false;
    }

    /**
     * @param $xdr
     * @return bool|string
     */
    public static function string($xdr)
    {
        return self::opaqueVariable($xdr);
    }

    /**
     * Reads a fixed opaque value and returns it as a string
     *
     * @param $xdr
     * @param $length
     * @return string
     */
    public static function opaqueFixedString($xdr, $length)
    {
        $bytes = static::opaqueFixed($xdr, $length);

        // remove trailing nulls
        return strval(rtrim($bytes, "\0x00"));
    }

    /**
     * @param $xdr
     * @param $length
     * @return bool|string
     */
    public static function opaqueFixed($xdr, $length)
    {
        return substr($xdr, 0, $length);
    }

    /**
     * @param $xdr
     * @return bool|string
     */
    public static function opaqueVariable($xdr)
    {
        $length = static::unsignedInteger($xdr);

        // first 4 bytes are the length
        return substr($xdr, 4, $length);
    }

    /**
     * @return bool
     */
    private static function nativeIsBigEndian()
    {
        if (null === self::$nativeIsBigEndian) {
            self::$nativeIsBigEndian = pack('L', 1) === pack('N', 1);
        }

        return self::$nativeIsBigEndian;
    }
}


<?php

namespace ZuluCrypto\StellarSdk;

use Base32\Base32;
use ParagonIE\Sodium\Core\Ed25519;
use ZuluCrypto\StellarSdk\Util\Checksum;
use ZuluCrypto\StellarSdk\Util\Debug;

/**
 * Implements a string key that can be used in various locations in the Stellar
 * network.
 *
 * See class constants for items that can be referenced
 */
class AddressableKey
{
    // Encoded stellar addresses (base-32 encodes to 'G...')
    const VERSION_BYTE_ACCOUNT_ID = 6 << 3;

    // Encoded stellar seeds (base-32 encodes to 'S...')
    const VERSION_BYTE_SEED = 18 << 3;

    // Encoded stellar hashTx (base-32 encodes to 'T...')
    const VERSION_BYTE_HASH_TX = 19 << 3;

    // Encoded stellar hashX (base-32 encodes to 'X...')
    const VERSION_BYTE_HASH_X = 23 << 3;

    public static function seedFromRawBytes($rawBytes)
    {
        if (strlen($rawBytes) != 32) throw new \InvalidArgumentException('$rawBytes must be 32 bytes');

        // Must be interpreted as a byte
        $version = pack('C', self::VERSION_BYTE_SEED);

        $payload = $rawBytes;

        $checksum = Checksum::generate($version . $payload);

        $seedString = Base32::encode($version . $payload . $checksum);

        return $seedString;
    }

    public static function addressFromRawSeed($rawBytes)
    {
        $version = pack('C', self::VERSION_BYTE_ACCOUNT_ID);

        $payload = Ed25519::publickey_from_secretkey($rawBytes);

        $checksum = Checksum::generate($version . $payload);

        return Base32::encode($version . $payload . $checksum);
    }

    /**
     * Converts $rawBytes into a public account ID (G...)
     *
     * @param $rawBytes
     * @return string
     */
    public static function addressFromRawBytes($rawBytes)
    {
        $version = pack('C', self::VERSION_BYTE_ACCOUNT_ID);

        $payload = $rawBytes;

        $checksum = Checksum::generate($version . $payload);

        return Base32::encode($version . $payload . $checksum);
    }

    public static function getRawBytesFromBase32Seed($base32Seed)
    {
        $decoded = Base32::decode($base32Seed);

        // Unpack version byte
        $unpacked = unpack('Cversion', substr($decoded, 0, 1));
        $version = $unpacked['version'];

        $payload = substr($decoded, 1, strlen($decoded) - 3);
        $checksum = substr($decoded, -2);

        // Verify version
        if ($version != self::VERSION_BYTE_SEED) {
            throw new \InvalidArgumentException('Invalid version');
        }
        // Verify checksum of version + payload
        if (!Checksum::verify($checksum, substr($decoded, 0, -2))) {
            throw new \InvalidArgumentException('Invalid checksum');
        }

        return $payload;
    }

    public static function getRawBytesFromBase32AccountId($base32AccountId)
    {
        $decoded = Base32::decode($base32AccountId);

        // Unpack version byte
        $unpacked = unpack('Cversion', substr($decoded, 0, 1));
        $version = $unpacked['version'];

        $payload = substr($decoded, 1, strlen($decoded) - 3);
        $checksum = substr($decoded, -2);

        // Verify version
        if ($version != self::VERSION_BYTE_ACCOUNT_ID) {
            $msg  = 'Invalid account ID version.';
            if ($version == self::VERSION_BYTE_SEED) {
                $msg .= ' Got a private key and expected a public account ID';
            }
            throw new \InvalidArgumentException($msg);
        }
        // Verify checksum of version + payload
        if (!Checksum::verify($checksum, substr($decoded, 0, -2))) {
            throw new \InvalidArgumentException('Invalid checksum');
        }

        return $payload;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class BumpSequenceResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-transaction.x#L675
    const BAD_SEQ         = 'bump_sequence_bad_seq';  // "bumpTo" is not within bounds

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new BumpSequenceResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::BAD_SEQ,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        return $model;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class ManageDataResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L630
    const NOT_SUPPORTED_YET         = 'manage_data_not_supported_yet';  // network hasn't upgraded to the right protocol version yet
    const NAME_NOT_FOUND            = 'manage_data_name_not_found';     // trying to remove a data entry that doesn't exist
    const LOW_RESERVE               = 'manage_data_low_reserve';
    const INVALID_NAME              = 'manage_data_invalid_name';

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new ManageDataResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::NOT_SUPPORTED_YET,
            '-2' => static::NAME_NOT_FOUND,
            '-3' => static::LOW_RESERVE,
            '-4' => static::INVALID_NAME,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        return $model;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class PaymentResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-transaction.x#L387
    const MALFORMED             = 'payment_malformed';
    const UNDERFUNDED           = 'payment_underfunded';
    const SRC_NO_TRUST          = 'payment_src_no_trust';
    const SRC_NOT_AUTHORIZED    = 'payment_src_not_authorized';
    const NO_DESTINATION        = 'payment_no_destination';
    const NO_TRUST              = 'payment_no_trust';
    const NOT_AUTHORIZED        = 'payment_not_authorized';
    const LINE_FULL             = 'payment_line_full';
    const NO_ISSUER             = 'payment_no_issuer';

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new PaymentResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::MALFORMED,
            '-2' => static::UNDERFUNDED,
            '-3' => static::SRC_NO_TRUST,
            '-4' => static::SRC_NOT_AUTHORIZED,
            '-5' => static::NO_DESTINATION,
            '-6' => static::NO_TRUST,
            '-7' => static::NOT_AUTHORIZED,
            '-8' => static::LINE_FULL,
            '-9' => static::NO_ISSUER,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        return $model;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\XdrModel;

use phpseclib\Math\BigInteger;
use ZuluCrypto\StellarSdk\Model\StellarAmount;
use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;

class PathPaymentResult extends OperationResult
{
    // https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L414
    const MALFORMED             = 'path_payment_malformed';
    const UNDERFUNDED           = 'path_payment_underfunded';
    const SRC_NO_TRUST          = 'path_payment_src_no_trust';
    const SRC_NOT_AUTHORIZED    = 'path_payment_src_not_authorized';
    const NO_DESTINATION        = 'path_payment_no_destination';
    const NO_TRUST              = 'path_payment_no_trust';
    const NOT_AUTHORIZED        = 'path_payment_not_authorized';
    const LINE_FULL             = 'path_payment_line_full';
    const NO_ISSUER             = 'path_payment_no_issuer';
    const TOO_FEW_OFFERS        = 'path_payment_too_few_offers';
    const OFFER_CROSS_SELF      = 'path_payment_offer_cross_self';
    const OVER_SENDMAX          = 'path_payment_over_sendmax';

    /**
     * @var ClaimOfferAtom[]
     */
    protected $claimedOffers;

    /**
     * @var AccountId
     */
    protected $destination;

    /**
     * @var Asset
     */
    protected $paidAsset;

    /**
     * @var StellarAmount
     */
    protected $paidAmount;

    /**
     * @deprecated Do not call this method directly. Instead, use OperationResult::fromXdr
     *
     * @param XdrBuffer $xdr
     * @return OperationResult|PaymentResult
     * @throws \ErrorException
     */
    public static function fromXdr(XdrBuffer $xdr)
    {
        $model = new PathPaymentResult();

        $rawErrorCode = $xdr->readInteger();
        $errorCodeMap = [
            '0' => 'success',
            '-1' => static::MALFORMED,
            '-2' => static::UNDERFUNDED,
            '-3' => static::SRC_NO_TRUST,
            '-4' => static::SRC_NOT_AUTHORIZED,
            '-5' => static::NO_DESTINATION,
            '-6' => static::NO_TRUST,
            '-7' => static::NOT_AUTHORIZED,
            '-8' => static::LINE_FULL,
            '-9' => static::NO_ISSUER,
            '-10' => static::TOO_FEW_OFFERS,
            '-11' => static::OFFER_CROSS_SELF,
            '-12' => static::OVER_SENDMAX,
        ];
        if (!isset($errorCodeMap[$rawErrorCode])) {
            throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode));
        }

        // Do not store the "success" error code
        if ($errorCodeMap[$rawErrorCode] !== 'success') {
            $model->errorCode = $errorCodeMap[$rawErrorCode];
        }

        // special case: a "no issuer" error code means we need to parse a different
        // value from the XDR
        if ($errorCodeMap[$rawErrorCode] == self::NO_ISSUER) {
            $model->paidAsset = Asset::fromXdr($xdr);
        }
        // Normal XDR parsing https://github.com/stellar/stellar-core/blob/3c4e356803175f6c2645e4437881cf07522df94d/src/xdr/Stellar-transaction.x#L441
        else {
            $numOffersClaimed = $xdr->readUnsignedInteger();
            for ($i=0; $i < $numOffersClaimed; $i++) {
                $model->claimedOffers[] = ClaimOfferAtom::fromXdr($xdr);
            }

            // These fields are combined into a SimplePaymentResult in the XDR spec,
            // but it only appears to be used in one place so I'm leaving them as
            // separate fields for now
            $model->destination = AccountId::fromXdr($xdr);
            $model->paidAsset = Asset::fromXdr($xdr);
            $model->paidAmount = new StellarAmount(new BigInteger($xdr->readInteger64()));
        }

        return $model;
    }

    /**
     * @return ClaimOfferAtom[]
     */
    public function getClaimedOffers()
    {
        return $this->claimedOffers;
    }

    /**
     * @param ClaimOfferAtom[] $claimedOffers
     */
    public function setClaimedOffers($claimedOffers)
    {
        $this->claimedOffers = $claimedOffers;
    }

    /**
     * @return AccountId
     */
    public function getDestination()
    {
        return $this->destination;
    }

    /**
     * @param AccountId $destination
     */
    public function setDestination($destination)
    {
        $this->destination = $destination;
    }

    /**
     * @return Asset
     */
    public function getPaidAsset()
    {
        return $this->paidAsset;
    }

    /**
     * @param Asset $paidAsset
     */
    public function setPaidAsset($paidAsset)
    {
        $this->paidAsset = $paidAsset;
    }

    /**
     * @return StellarAmount
     */
    public function getPaidAmount()
    {
        return $this->paidAmount;
    }

    /**
     * @param StellarAmount $paidAmount
     */
    public function setPaidAmount($paidAmount)
    {
        $this->paidAmount = $paidAmount;
    }
}


<?php

namespace ZuluCrypto\StellarSdk\Horizon\Api;

use ZuluCrypto\StellarSdk\Xdr\XdrBuffer;
use ZuluCrypto\StellarSdk\XdrModel\TransactionResult;

class PostTransactionResponse extends HorizonResponse
{
    /**
     * @var TransactionResult
     */
    protected $result;

    /**
     * @param HorizonResponse $response
     * @return PostTransactionResponse
     */
    public static function fromApiResponse(HorizonResponse $response)
    {
        return new PostTransactionResponse(json_encode($response->rawData));
    }

    public function __construct($jsonEncodedData)
    {
        parent::__construct($jsonEncodedData);

        $this->parseRawData($this->rawData);
    }

    /**
     * Parses the raw response data and populates this object
     *
     * @param $rawData
     * @throws \ErrorException
     */
    protected function parseRawData($rawData)
    {
        if (!$rawData) return;

        if (!empty($rawData['result_xdr'])) {
            $xdr = new XdrBuffer(base64_decode($rawData['result_xdr']));
            $this->result = TransactionResult::fromXdr($xdr);
        }
    }

    /**
     * @return TransactionResult
     */
    public function getResult()
    {
        return $this->result;
    }

    /**
     * @param TransactionResult $result
     */
    public function setResult($result)
    {
        $this->result = $result;
    }
}