├── .gitignore ├── src ├── Xdr │ ├── Iface │ │ └── XdrEncodableInterface.php │ └── Type │ │ └── VariableArray.php ├── XdrModel │ ├── Operation │ │ ├── InflationOp.php │ │ ├── BumpSequenceOp.php │ │ ├── AccountMergeOp.php │ │ ├── ManageDataOp.php │ │ ├── ChangeTrustOp.php │ │ └── CreateAccountOp.php │ ├── BumpSequenceResult.php │ ├── InflationPayout.php │ ├── InflationResult.php │ ├── CreateAccountResult.php │ ├── ChangeTrustResult.php │ ├── ManageDataResult.php │ ├── AllowTrustResult.php │ ├── DecoratedSignature.php │ ├── Signer.php │ ├── PaymentResult.php │ ├── SetOptionsResult.php │ ├── Price.php │ ├── ClaimOfferAtom.php │ ├── AccountMergeResult.php │ ├── TimeBounds.php │ ├── AccountId.php │ ├── SignerKey.php │ └── OperationResult.php ├── Util │ ├── MathSafety.php │ ├── Json.php │ ├── Hash.php │ ├── Debug.php │ └── Checksum.php ├── Signing │ ├── SigningInterface.php │ ├── PrivateKeySigner.php │ └── TrezorSigner.php ├── Model │ ├── CreatePassiveOfferOperation.php │ ├── InflationOperation.php │ ├── AssetTransferInterface.php │ ├── BumpSequenceOperation.php │ ├── ManageDataOperation.php │ ├── ManageOfferOperation.php │ ├── AccountMergeOperation.php │ ├── RestApiModel.php │ ├── ChangeTrustOperation.php │ ├── CreateAccountOperation.php │ ├── AllowTrustOperation.php │ └── Effect.php ├── bin │ └── sync-history.php ├── History │ ├── BucketList.php │ ├── BucketLevel.php │ └── HistoryArchiveState.php ├── Horizon │ ├── Api │ │ ├── PostTransactionResponse.php │ │ └── HorizonResponse.php │ └── Exception │ │ └── PostTransactionException.php └── Transaction │ └── Transaction.php ├── tests ├── run-unit.sh ├── run-integration.sh ├── run-hardware-wallet.sh ├── Unit │ ├── Model │ │ ├── AccountTest.php │ │ └── StellarAmountTest.php │ ├── XdrModel │ │ ├── AccountIdTest.php │ │ ├── TimeBoundsTest.php │ │ ├── Operation │ │ │ ├── InflationOpTest.php │ │ │ ├── BumpSequenceOpTest.php │ │ │ ├── AccountMergeOpTest.php │ │ │ ├── ManageDataOpTest.php │ │ │ ├── ChangeTrustOpTest.php │ │ │ ├── AllowTrustOpTest.php │ │ │ ├── PaymentOpTest.php │ │ │ ├── CreateAccountOpTest.php │ │ │ ├── CreatePassiveOfferOpTest.php │ │ │ ├── ManageOfferOpTest.php │ │ │ ├── PathPaymentOpTest.php │ │ │ └── SetOptionsOpTest.php │ │ ├── TransactionEnvelopeTest.php │ │ ├── TransactionResultTest.php │ │ └── AssetTest.php │ ├── Transaction │ │ └── TransactionTest.php │ ├── Derivation │ │ ├── HdNodeTest.php │ │ └── Bip39 │ │ │ └── Bip39Test.php │ └── Xdr │ │ ├── XdrEncoderTest.php │ │ └── XdrDecoderTest.php ├── integration │ ├── ServerTest.php │ ├── CreateAccountOpTest.php │ ├── ManageOfferOpTest.php │ ├── AccountMergeOpTest.php │ ├── CreatePassiveOfferOpTest.php │ ├── ChangeTrustOpTest.php │ ├── PathPaymentOpTest.php │ ├── AccountTest.php │ └── TransactionBuilderTest.php ├── phpunit.xml.dist ├── HardwareWallet │ ├── AccountMergeOpTest.php │ ├── BumpSequenceOpTest.php │ ├── CreateAccountOpTest.php │ ├── CreatePassiveOfferOpTest.php │ └── ManageDataOpTest.php └── Util │ └── HardwareWalletIntegrationTest.php ├── getting-started ├── 01-create-account.php ├── 02-fund-account.php ├── 05-stream-payments.php ├── 03-account-details.php ├── 06-list-payments.php └── 04-submit-transaction.php ├── examples ├── stream-effects.php ├── stream-operations.php ├── stream-transactions.php ├── stream-ledgers.php ├── stream-payments.php ├── manage-data.php ├── bump-sequence.php ├── merge-account.php ├── account-exists.php ├── mnemonic-address.php ├── transaction-set-sequence-number.php ├── sign-message.php ├── payment-transaction.php ├── add-signer.php ├── manage-offer.php ├── set-options.php ├── transaction-result.php ├── transaction-add-signature.php └── trust-asset.php ├── composer.json ├── LICENSE └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | -------------------------------------------------------------------------------- /src/Xdr/Iface/XdrEncodableInterface.php: -------------------------------------------------------------------------------- 1 | getSecret() . PHP_EOL; 10 | // SAV76USXIJOBMEQXPANUOQM6F5LIOTLPDIDVRJBFFE2MDJXG24TAPUU7 11 | 12 | print $keypair->getPublicKey() . PHP_EOL; 13 | // GCFXHS4GXL6BVUCXBWXGTITROWLVYXQKQLF4YH5O5JT3YZXCYPAFBJZB -------------------------------------------------------------------------------- /src/Util/MathSafety.php: -------------------------------------------------------------------------------- 1 | streamEffects('now', function(Effect $effect) { 11 | printf('[%s] %s' . PHP_EOL, 12 | (new \DateTime())->format('Y-m-d h:i:sa'), 13 | $effect->getType() 14 | ); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /examples/stream-operations.php: -------------------------------------------------------------------------------- 1 | streamOperations('now', function(Operation $operation) { 11 | printf('[%s] %s' . PHP_EOL, 12 | (new \DateTime())->format('Y-m-d h:i:sa'), 13 | $operation->getType() 14 | ); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /src/Signing/SigningInterface.php: -------------------------------------------------------------------------------- 1 | streamTransactions('now', function(Transaction $transaction) { 11 | printf('[%s] Transaction #%s with memo %s' . PHP_EOL, 12 | (new \DateTime())->format('Y-m-d h:i:sa'), 13 | $transaction->getId(), 14 | $transaction->getMemo() 15 | ); 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /src/Model/CreatePassiveOfferOperation.php: -------------------------------------------------------------------------------- 1 | assertTrue(Account::isValidAccount('GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6')); 15 | 16 | $this->assertFalse(Account::isValidAccount(null)); 17 | $this->assertFalse(Account::isValidAccount('G123')); 18 | } 19 | } -------------------------------------------------------------------------------- /examples/stream-ledgers.php: -------------------------------------------------------------------------------- 1 | streamLedgers('now', function(Ledger $ledger) { 11 | printf('[%s] Closed %s at %s with %s operations' . PHP_EOL, 12 | (new \DateTime())->format('Y-m-d h:i:sa'), 13 | $ledger->getId(), 14 | $ledger->getClosedAt()->format('Y-m-d h:i:sa'), 15 | $ledger->getOperationCount() 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /getting-started/02-fund-account.php: -------------------------------------------------------------------------------- 1 | streamPayments('now', function(AssetTransferInterface $assetTransfer) { 11 | printf('[%s] [%s] %s from %s -> %s' . PHP_EOL, 12 | (new \DateTime())->format('Y-m-d h:i:sa'), 13 | $assetTransfer->getAssetTransferType(), 14 | $assetTransfer->getAssetAmount(), 15 | $assetTransfer->getFromAccountId(), 16 | $assetTransfer->getToAccountId() 17 | ); 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /getting-started/05-stream-payments.php: -------------------------------------------------------------------------------- 1 | getAccount('GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5'); 11 | 12 | 13 | print "Waiting for new payments to " . $account->getId() . PHP_EOL; 14 | 15 | $account->streamPayments('now', function(Payment $payment) { 16 | printf('[%s] Amount: %s From %s' . PHP_EOL, 17 | $payment->getType(), 18 | $payment->getAmount(), 19 | $payment->getSourceAccountId() 20 | ); 21 | }); -------------------------------------------------------------------------------- /src/Util/Hash.php: -------------------------------------------------------------------------------- 1 | buildTransaction($keypair) 15 | ->setAccountData('color', 'blue') 16 | ->submit($keypair); 17 | 18 | 19 | printf("Data updated." . PHP_EOL); 20 | printf("View account at: %saccounts/%s" . PHP_EOL, $server->getHorizonBaseUrl(), $keypair->getPublicKey()); -------------------------------------------------------------------------------- /examples/bump-sequence.php: -------------------------------------------------------------------------------- 1 | buildTransaction($currentAccount->getPublicKey()) 23 | ->bumpSequenceTo(new BigInteger('47061756253569030')) 24 | ->submit($currentAccount->getSecret()); -------------------------------------------------------------------------------- /src/bin/sync-history.php: -------------------------------------------------------------------------------- 1 | downloadAll(); 24 | -------------------------------------------------------------------------------- /getting-started/03-account-details.php: -------------------------------------------------------------------------------- 1 | getAccount($publicAccountId); 13 | 14 | print 'Balances for account ' . $publicAccountId . PHP_EOL; 15 | 16 | foreach ($account->getBalances() as $balance) { 17 | printf(' Type: %s, Code: %s, Balance: %s' . PHP_EOL, 18 | $balance->getAssetType(), 19 | $balance->getAssetCode(), 20 | $balance->getBalance() 21 | ); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /tests/Unit/XdrModel/AccountIdTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 19 | 20 | $this->assertEquals(AccountId::KEY_TYPE_ED25519, $decoded->getKeyType()); 21 | $this->assertEquals($accountId, $decoded->getAccountIdString()); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/TimeBoundsTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 18 | 19 | $this->assertEquals($source->getMinTimestamp(), $decoded->getMinTimestamp()); 20 | $this->assertEquals($source->getMaxTimestamp(), $decoded->getMaxTimestamp()); 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/InflationOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 22 | 23 | $this->assertTrue($parsed instanceof InflationOp); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/integration/ServerTest.php: -------------------------------------------------------------------------------- 1 | horizonBaseUrl, $this->networkPassword); 19 | 20 | // Verify one of the fixture accounts can be retrieved 21 | $account = $server->getAccount($this->fixtureAccounts['basic1']->getPublicKey()); 22 | 23 | // Account should have at least one balance 24 | $this->assertNotEmpty($account->getBalances()); 25 | } 26 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zulucrypto/stellar-api", 3 | "description": "API client for the Stellar network", 4 | "autoload": { 5 | "psr-4": { 6 | "ZuluCrypto\\StellarSdk\\": "src/", 7 | "ZuluCrypto\\StellarSdk\\Test\\": "tests/" 8 | } 9 | }, 10 | "require": { 11 | "guzzlehttp/guzzle": "^6.2", 12 | "christian-riesen/base32": "^1.3", 13 | "paragonie/sodium_compat": "^1.0", 14 | "phpseclib/phpseclib": "^2.0.6", 15 | "symfony/filesystem": "^4.1" 16 | }, 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Zulu Crypto", 21 | "email": "zulucrypto@protonmail.com" 22 | } 23 | ], 24 | "require-dev": { 25 | "phpunit/phpunit": "^6.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Model/InflationOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 21 | 22 | return $object; 23 | } 24 | 25 | /** 26 | * @param $id 27 | * @param $type 28 | */ 29 | public function __construct($id, $type) 30 | { 31 | parent::__construct($id, Operation::TYPE_INFLATION); 32 | } 33 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/BumpSequenceOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 21 | 22 | $this->assertTrue($parsed instanceof BumpSequenceOp); 23 | $this->assertEquals($source->getBumpTo()->toString(), $parsed->getBumpTo()->toString()); 24 | } 25 | } -------------------------------------------------------------------------------- /examples/merge-account.php: -------------------------------------------------------------------------------- 1 | fundAccount($fromKeypair); 18 | $server->fundAccount($destinationKeypair); 19 | 20 | // Transfer balance from $fromKeypair to $destinationKeypair 21 | 22 | $server->buildTransaction($fromKeypair) 23 | ->addMergeOperation($destinationKeypair) 24 | ->submit($fromKeypair); -------------------------------------------------------------------------------- /examples/account-exists.php: -------------------------------------------------------------------------------- 1 | accountExists('GCP6IHMHWRCF5TQ4ZP6TVIRNDZD56W42F42VHYWMVDGDAND75YGAHHBQ'); 20 | 21 | if ($exists) { 22 | print "Account exists!" . PHP_EOL; 23 | } 24 | else { 25 | print "Account does not exist." . PHP_EOL; 26 | } 27 | } 28 | // If there's an exception it could be a temporary error, like a connection issue 29 | // to Horizon, so we cannot tell for sure if the account exists or not 30 | catch (\Exception $e) { 31 | print "Error: " . $e->getMessage() . PHP_EOL; 32 | } 33 | -------------------------------------------------------------------------------- /examples/mnemonic-address.php: -------------------------------------------------------------------------------- 1 | getPublicKey() . PHP_EOL; 19 | // SAFWTGXVS7ELMNCXELFWCFZOPMHUZ5LXNBGUVRCY3FHLFPXK4QPXYP2X 20 | print "Seed : " . $primaryAccount->getSecret() . PHP_EOL; -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/AccountMergeOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 21 | 22 | $this->assertTrue($parsed instanceof AccountMergeOp); 23 | $this->assertEquals($source->getDestination()->getAccountIdString(), $parsed->getDestination()->getAccountIdString()); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Model/AssetTransferInterface.php: -------------------------------------------------------------------------------- 1 | buildTransaction($sourceKeypair) 21 | ->addLumenPayment($destinationKeypair, 100); 22 | 23 | $builder->setSequenceNumber(new BigInteger(123)); 24 | 25 | print "Sequence number: " . $builder->getSequenceNumber() . "\n"; -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/ManageDataOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 22 | 23 | $this->assertTrue($parsed instanceof ManageDataOp); 24 | 25 | $this->assertEquals('testkey', $parsed->getKey()); 26 | $this->assertEquals('testvalue', $parsed->getValue()); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Signing/PrivateKeySigner.php: -------------------------------------------------------------------------------- 1 | keypair = $keypair; 24 | } 25 | 26 | /** 27 | * @param TransactionBuilder $builder 28 | * @return DecoratedSignature 29 | */ 30 | public function signTransaction(TransactionBuilder $builder) 31 | { 32 | $hash = $builder 33 | ->getTransactionEnvelope() 34 | ->getHash(); 35 | 36 | return $this->keypair->signDecorated($hash); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/TransactionEnvelopeTest.php: -------------------------------------------------------------------------------- 1 | getDecoratedSignatures(); 21 | 22 | $this->assertCount(1, $signatures); 23 | } 24 | } -------------------------------------------------------------------------------- /examples/sign-message.php: -------------------------------------------------------------------------------- 1 | sign($message); 18 | 19 | printf("Signed (base-64 encoded): " . base64_encode($signatureBytes) . PHP_EOL); 20 | 21 | 22 | // Verify the signature 23 | // Note that only the public key is required 24 | $verifyingKeypair = Keypair::newFromPublicKey('GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6'); 25 | 26 | $isVerified = $verifyingKeypair->verifySignature($signatureBytes, $message); 27 | printf(PHP_EOL); 28 | printf("Verified? %s" . PHP_EOL, ($isVerified) ? 'Yes' : 'No'); -------------------------------------------------------------------------------- /examples/payment-transaction.php: -------------------------------------------------------------------------------- 1 | buildTransaction($sourceKeypair) 21 | ->addLumenPayment($destinationKeypair, 10) 22 | ->getTransactionEnvelope(); 23 | 24 | $txEnvelope->sign($sourceKeypair); 25 | 26 | $b64Tx = base64_encode($txEnvelope->toXdr()); 27 | 28 | print "Submitting transaction: " . PHP_EOL; 29 | print $b64Tx; 30 | print PHP_EOL; 31 | 32 | $server->submitB64Transaction($b64Tx); 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/CreateAccountOpTest.php: -------------------------------------------------------------------------------- 1 | fixtureAccounts['basic1']; 19 | 20 | $newKeypair = Keypair::newFromRandom(); 21 | 22 | $this->horizonServer->buildTransaction($sourceKeypair->getPublicKey()) 23 | ->addCreateAccountOp($newKeypair->getAccountId(), 100.0333) 24 | ->submit($sourceKeypair->getSecret()); 25 | 26 | // Should then be able to retrieve the account and verify the balance 27 | $newAccount = $this->horizonServer->getAccount($newKeypair->getPublicKey()); 28 | 29 | $this->assertEquals("100.0333", $newAccount->getNativeBalance()); 30 | } 31 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/ChangeTrustOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 23 | 24 | $this->assertTrue($parsed instanceof ChangeTrustOp); 25 | 26 | $this->assertEquals('TRUST', $parsed->getAsset()->getAssetCode()); 27 | $this->assertEquals(8888, $parsed->getLimit()->getScaledValue()); 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ZuluCrypto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Model/BumpSequenceOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 26 | 27 | return $object; 28 | } 29 | 30 | /** 31 | * @param $id 32 | * @param $type 33 | */ 34 | public function __construct($id, $type) 35 | { 36 | parent::__construct($id, Operation::TYPE_BUMP_SEQUENCE); 37 | } 38 | 39 | /** 40 | * @param $rawData 41 | */ 42 | public function loadFromRawResponseData($rawData) 43 | { 44 | parent::loadFromRawResponseData($rawData); 45 | 46 | $this->bumpTo = new BigInteger($rawData['bump_to']); 47 | } 48 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/AllowTrustOpTest.php: -------------------------------------------------------------------------------- 1 | setIsAuthorized(true); 21 | 22 | /** @var AllowTrustOp $parsed */ 23 | $parsed = Operation::fromXdr(new XdrBuffer($sourceOp->toXdr())); 24 | 25 | $this->assertTrue($parsed instanceof AllowTrustOp); 26 | 27 | $this->assertEquals('TST', $parsed->getAsset()->getAssetCode()); 28 | $this->assertEquals($sourceOp->getTrustor()->getAccountIdString(), $parsed->getTrustor()->getAccountIdString()); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/TransactionResultTest.php: -------------------------------------------------------------------------------- 1 | getOperationResults(); 22 | 23 | $this->assertEquals(TransactionResult::SUCCESS, $result->getResultCode()); 24 | $this->assertCount(1, $opResults); 25 | $this->assertEquals(100, $result->getFeeCharged()->getUnscaledString()); 26 | $this->assertTrue($result->succeeded()); 27 | 28 | $paymentResult = $opResults[0]; 29 | $this->assertTrue($paymentResult instanceof PaymentResult); 30 | $this->assertTrue($paymentResult->succeeded()); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/integration/ManageOfferOpTest.php: -------------------------------------------------------------------------------- 1 | fixtureAccounts['usdBankKeypair']; 22 | $usdAsset = $this->fixtureAssets['usd']; 23 | 24 | // Sell 100 USDTEST for 0.02 XLM 25 | $xlmPrice = new Price(2, 100); 26 | $offerOp = new ManageOfferOp($usdAsset, Asset::newNativeAsset(), 100, $xlmPrice); 27 | 28 | $response = $this->horizonServer->buildTransaction($usdBankKeypair) 29 | ->addOperation($offerOp) 30 | ->submit($usdBankKeypair); 31 | 32 | // todo: add support for offers and verify here 33 | // todo: verify canceling an offer 34 | } 35 | } -------------------------------------------------------------------------------- /tests/integration/AccountMergeOpTest.php: -------------------------------------------------------------------------------- 1 | getRandomFundedKeypair(); 19 | $mergeTo = $this->getRandomFundedKeypair(); 20 | 21 | 22 | $this->horizonServer->buildTransaction($mergeFrom) 23 | ->addMergeOperation($mergeTo) 24 | ->submit($mergeFrom); 25 | 26 | // Verify mergeFrom account no longer exists 27 | $mergeFromAccount = $this->horizonServer->getAccount($mergeFrom); 28 | $this->assertNull($mergeFromAccount); 29 | 30 | $mergeToAccountBalance = $this->horizonServer 31 | ->getAccount($mergeTo) 32 | ->getNativeBalance(); 33 | 34 | // Verify mergeToAccount has double balance (minus fees) 35 | $this->assertEquals(19999.99999, $mergeToAccountBalance); 36 | } 37 | } -------------------------------------------------------------------------------- /examples/add-signer.php: -------------------------------------------------------------------------------- 1 | setMasterWeight(10); 30 | 31 | $signerKey = SignerKey::fromKeypair($newSigner); 32 | $newAccountSigner = new Signer($signerKey, 1); 33 | $optionsOperation->updateSigner($newAccountSigner); 34 | 35 | // Submit to the network 36 | $server->buildTransaction($currentAccount->getPublicKey()) 37 | ->addOperation($optionsOperation) 38 | ->submit($currentAccount->getSecret()); -------------------------------------------------------------------------------- /getting-started/06-list-payments.php: -------------------------------------------------------------------------------- 1 | getAccount('GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5'); 10 | 11 | $currentCursor = null; 12 | while (true) { 13 | $resultsPerPage = 10; 14 | $payments = $account->getPayments(null, $resultsPerPage); 15 | 16 | $seenResults = 0; 17 | foreach ($payments as $payment) { 18 | /** @var $payment \ZuluCrypto\StellarSdk\Model\Operation|\ZuluCrypto\StellarSdk\Model\AssetTransferInterface */ 19 | // If the same cursor shows up twice, we're repeating results and should exit 20 | if ($payment->getPagingToken() == $currentCursor) break 2; 21 | 22 | printf('[%s] Amount: %s From %s in Tx %s' . PHP_EOL, 23 | $payment->getAssetTransferType(), 24 | $payment->getAssetAmount(), 25 | $payment->getFromAccountId(), 26 | $payment->getTransactionHash() 27 | ); 28 | 29 | $currentCursor = $payment->getPagingToken(); 30 | } 31 | 32 | // Immediate exit if there aren't enough results to fill the page 33 | if ($seenResults < $resultsPerPage) break; 34 | } -------------------------------------------------------------------------------- /tests/integration/CreatePassiveOfferOpTest.php: -------------------------------------------------------------------------------- 1 | fixtureAccounts['usdBankKeypair']; 22 | $usdAsset = $this->fixtureAssets['usd']; 23 | 24 | // Sell 100 USDTEST for 0.0005 XLM 25 | $xlmPrice = new Price(5, 10000); 26 | $offerOp = new CreatePassiveOfferOp($usdAsset, Asset::newNativeAsset(), 100, $xlmPrice); 27 | 28 | $response = $this->horizonServer->buildTransaction($usdBankKeypair) 29 | ->addOperation($offerOp) 30 | ->submit($usdBankKeypair); 31 | 32 | // todo: add support for viewing offers on an account and verify here 33 | // todo: verify canceling an offer works correctly 34 | } 35 | } -------------------------------------------------------------------------------- /src/History/BucketList.php: -------------------------------------------------------------------------------- 1 | bucketLevels = []; 20 | } 21 | 22 | /** 23 | * @param $raw 24 | * @param null $level defaults to the next unoccupied level 25 | * @return null|BucketLevel 26 | */ 27 | public function addLevelFromRaw($raw, $level = null) 28 | { 29 | if ($level === null) { 30 | $level = count($this->bucketLevels); 31 | } 32 | 33 | $bucketLevel = BucketLevel::fromRaw($raw, $level); 34 | 35 | $this->bucketLevels[] = $bucketLevel; 36 | 37 | return $bucketLevel; 38 | } 39 | 40 | /** 41 | * @return array|string[] sha256 bucket hashes 42 | */ 43 | public function getUniqueBucketHashes() 44 | { 45 | $hashes = []; 46 | 47 | foreach ($this->bucketLevels as $bucketLevel) { 48 | $hashes = array_merge($hashes, $bucketLevel->getUniqueBucketHashes()); 49 | } 50 | 51 | return array_values(array_unique($hashes)); 52 | } 53 | } -------------------------------------------------------------------------------- /src/XdrModel/BumpSequenceResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 26 | $errorCodeMap = [ 27 | '0' => 'success', 28 | '-1' => static::BAD_SEQ, 29 | ]; 30 | if (!isset($errorCodeMap[$rawErrorCode])) { 31 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 32 | } 33 | 34 | // Do not store the "success" error code 35 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 36 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 37 | } 38 | 39 | return $model; 40 | } 41 | } -------------------------------------------------------------------------------- /getting-started/04-submit-transaction.php: -------------------------------------------------------------------------------- 1 | getAccount($destinationAccountId); 20 | 21 | // Build the payment transaction 22 | $transaction = \ZuluCrypto\StellarSdk\Server::testNet() 23 | ->buildTransaction($sourceKeypair->getPublicKey()) 24 | ->addOperation( 25 | PaymentOp::newNativePayment($destinationAccountId, 1) 26 | ) 27 | ; 28 | 29 | // Sign and submit the transaction 30 | $response = $transaction->submit($sourceKeypair->getSecret()); 31 | 32 | print "Response:" . PHP_EOL; 33 | print_r($response->getRawData()); 34 | 35 | print PHP_EOL; 36 | print 'Payment succeeded!' . PHP_EOL; -------------------------------------------------------------------------------- /src/Xdr/Type/VariableArray.php: -------------------------------------------------------------------------------- 1 | elements = []; 20 | } 21 | 22 | public function append(XdrEncodableInterface $element) 23 | { 24 | $this->elements[] = $element; 25 | } 26 | 27 | public function toXdr() 28 | { 29 | $bytes = ''; 30 | 31 | if (count($this->elements) > (pow(2, 32) - 1)) { 32 | throw new \ErrorException('Maximum number of elements exceeded'); 33 | } 34 | 35 | $bytes .= XdrEncoder::unsignedInteger(count($this->elements)); 36 | 37 | foreach ($this->elements as $element) { 38 | $bytes .= $element->toXdr(); 39 | } 40 | 41 | return $bytes; 42 | } 43 | 44 | /** 45 | * @return int 46 | */ 47 | public function count() 48 | { 49 | return count($this->elements); 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function toArray() 56 | { 57 | return array_values($this->elements); 58 | } 59 | } -------------------------------------------------------------------------------- /src/XdrModel/InflationPayout.php: -------------------------------------------------------------------------------- 1 | destination = AccountId::fromXdr($xdr); 27 | $model->amount = new StellarAmount($xdr->readInteger64()); 28 | 29 | return $model; 30 | } 31 | 32 | /** 33 | * @return AccountId 34 | */ 35 | public function getDestination() 36 | { 37 | return $this->destination; 38 | } 39 | 40 | /** 41 | * @param AccountId $destination 42 | */ 43 | public function setDestination($destination) 44 | { 45 | $this->destination = $destination; 46 | } 47 | 48 | /** 49 | * @return StellarAmount 50 | */ 51 | public function getAmount() 52 | { 53 | return $this->amount; 54 | } 55 | 56 | /** 57 | * @param StellarAmount $amount 58 | */ 59 | public function setAmount($amount) 60 | { 61 | $this->amount = $amount; 62 | } 63 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/PaymentOpTest.php: -------------------------------------------------------------------------------- 1 | setDestination(new AccountId(Keypair::newFromRandom()->getAccountId())); 22 | $sourceOp->setAmount(100); 23 | $sourceOp->setAsset(Asset::newNativeAsset()); 24 | 25 | 26 | /** @var PaymentOp $parsed */ 27 | $parsed = Operation::fromXdr(new XdrBuffer($sourceOp->toXdr())); 28 | 29 | $this->assertTrue($parsed instanceof PaymentOp); 30 | $this->assertEquals($sourceOp->getDestination()->getAccountIdString(), $parsed->getDestination()->getAccountIdString()); 31 | $this->assertEquals($sourceOp->getAmount()->getScaledValue(), $parsed->getAmount()->getScaledValue()); 32 | $this->assertEquals($sourceOp->getAsset()->getType(), $parsed->getAsset()->getType()); 33 | } 34 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/CreateAccountOpTest.php: -------------------------------------------------------------------------------- 1 | getAccountId()); 22 | $source = new CreateAccountOp($newAccountId, 1000, $sourceAccountKeypair); 23 | 24 | /** @var CreateAccountOp $parsed */ 25 | $parsed = Operation::fromXdr(new XdrBuffer($source->toXdr())); 26 | 27 | $this->assertTrue($parsed instanceof CreateAccountOp); 28 | $this->assertEquals($sourceAccountKeypair->getAccountId(), $parsed->getSourceAccount()->getAccountIdString()); 29 | 30 | $this->assertEquals($source->getNewAccount()->getAccountIdString(), $parsed->getNewAccount()->getAccountIdString()); 31 | $this->assertEquals($source->getStartingBalance()->getScaledValue(), $parsed->getStartingBalance()->getScaledValue()); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Util/Debug.php: -------------------------------------------------------------------------------- 1 | = 0x20 && $i <= 0x7E) ? chr($i) : $pad; 36 | } 37 | } 38 | 39 | $hex = str_split(bin2hex($data), $width*2); 40 | $chars = str_split(strtr($data, $from, $to), $width); 41 | 42 | $offset = 0; 43 | foreach ($hex as $i => $line) 44 | { 45 | $output .= sprintf('%6X',$offset).' : '.implode(' ', str_split($line,2)) . ' [' . $chars[$i] . ']' . $newline; 46 | $offset += $width; 47 | } 48 | 49 | return $output; 50 | } 51 | } -------------------------------------------------------------------------------- /tests/integration/ChangeTrustOpTest.php: -------------------------------------------------------------------------------- 1 | getRandomFundedKeypair(); 19 | $usdAsset = $this->fixtureAssets['usd']; 20 | 21 | $this->horizonServer->buildTransaction($keypair) 22 | ->addChangeTrustOp($usdAsset, 4294967297)// 2^32 + 1 23 | ->submit($keypair); 24 | 25 | // Verify trustline is added 26 | $account = $this->horizonServer->getAccount($keypair); 27 | 28 | $balanceAmount = $account->getCustomAssetBalance($usdAsset); 29 | 30 | $this->assertEquals(4294967297, $balanceAmount->getLimit()->getScaledValue()); 31 | 32 | // Remove trustline by setting to 0 33 | $this->horizonServer->buildTransaction($keypair) 34 | ->addChangeTrustOp($usdAsset, 0) 35 | ->submit($keypair); 36 | 37 | $account = $this->horizonServer->getAccount($keypair); 38 | $balanceAmount = $account->getCustomAssetBalance($usdAsset); 39 | // Should now be null 40 | $this->assertNull($balanceAmount); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/integration/PathPaymentOpTest.php: -------------------------------------------------------------------------------- 1 | JPY trades 16 | * 17 | * todo: integrate this with payment path discovery in Horizon 18 | * 19 | * @group requires-integrationnet 20 | * @throws \ZuluCrypto\StellarSdk\Horizon\Exception\HorizonException 21 | * @throws \ErrorException 22 | */ 23 | public function testSingleStepPathPayment() 24 | { 25 | /** @var Keypair $sourceKeypair */ 26 | $sourceKeypair = $this->fixtureAccounts['basic1']; 27 | $destinationKeypair = $this->fixtureAccounts['jpyMerchantKeypair']; 28 | 29 | $usdAsset = $this->fixtureAssets['usd']; 30 | $jpyAsset = $this->fixtureAssets['jpy']; 31 | 32 | $pathPayment = new PathPaymentOp(Asset::newNativeAsset(), 200, $destinationKeypair, $jpyAsset, 500); 33 | 34 | $pathPayment->addPath($usdAsset); 35 | 36 | $envelope = $this->horizonServer->buildTransaction($sourceKeypair) 37 | ->addOperation($pathPayment) 38 | ->getTransactionEnvelope(); 39 | 40 | // todo: need additional fixtures to verify path payment 41 | } 42 | } -------------------------------------------------------------------------------- /tests/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | requires-integrationnet 29 | 30 | requires-hardwarewallet 31 | 32 | 33 | 34 | 35 | 36 | . 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/HardwareWallet/AccountMergeOpTest.php: -------------------------------------------------------------------------------- 1 | mnemonic); 19 | $destinationKeypair = Keypair::newFromMnemonic($this->mnemonic, 'destination'); 20 | 21 | $transaction = $this->horizonServer 22 | ->buildTransaction($sourceKeypair) 23 | ->setSequenceNumber(new BigInteger(4294967296)) 24 | ->addMergeOperation($destinationKeypair); 25 | $knownSignature = $transaction->signWith($this->privateKeySigner); 26 | 27 | $this->manualVerificationOutput(join(PHP_EOL, [ 28 | 'Merge Account: ', 29 | ' Source: ' . $sourceKeypair->getPublicKey(), 30 | ' Destination: ' . $destinationKeypair->getPublicKey(), 31 | '', 32 | 'B64 Transaction: ' . base64_encode($transaction->toXdr()), 33 | ' Signature: ' . $knownSignature->getWithoutHintBase64(), 34 | ])); 35 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 36 | 37 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 38 | } 39 | } -------------------------------------------------------------------------------- /examples/manage-offer.php: -------------------------------------------------------------------------------- 1 | getPublicKey()); 18 | $nativeAsset = Asset::newNativeAsset(); 19 | 20 | // Offer 5,000 XLM @ 100 XLM per 1 USDTEST 21 | $server->buildTransaction($offeringKeypair->getPublicKey()) 22 | ->addOperation( 23 | new \ZuluCrypto\StellarSdk\XdrModel\Operation\ManageOfferOp( 24 | $usdtestAsset, 25 | $nativeAsset, 26 | 5000, 27 | new \ZuluCrypto\StellarSdk\XdrModel\Price(100, 1) 28 | ) 29 | ) 30 | ->submit($offeringKeypair->getSecret()); 31 | 32 | // Passive offer of 1,000 XLM @ 50 XLM per 1 USDTEST 33 | $server->buildTransaction($offeringKeypair->getPublicKey()) 34 | ->addOperation( 35 | new \ZuluCrypto\StellarSdk\XdrModel\Operation\CreatePassiveOfferOp( 36 | $usdtestAsset, 37 | $nativeAsset, 38 | 1000, 39 | new \ZuluCrypto\StellarSdk\XdrModel\Price(50, 1) 40 | ) 41 | ) 42 | ->submit($offeringKeypair->getSecret()); -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/CreatePassiveOfferOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 33 | 34 | $this->assertTrue($parsed instanceof CreatePassiveOfferOp); 35 | 36 | $this->assertEquals($sellingAsset->getAssetCode(), $parsed->getSellingAsset()->getAssetCode()); 37 | $this->assertEquals($buyingAsset->getAssetCode(), $parsed->getBuyingAsset()->getAssetCode()); 38 | $this->assertEquals($amount, $parsed->getAmount()->getScaledValue()); 39 | $this->assertEquals(.00025, $parsed->getPrice()->toFloat()); 40 | } 41 | } -------------------------------------------------------------------------------- /src/XdrModel/InflationResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 31 | $errorCodeMap = [ 32 | '0' => 'success', 33 | '-1' => static::NOT_TIME, 34 | ]; 35 | if (!isset($errorCodeMap[$rawErrorCode])) { 36 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 37 | } 38 | 39 | // Do not store the "success" error code 40 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 41 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 42 | return $model; 43 | } 44 | 45 | $numPayouts = $xdr->readUnsignedInteger(); 46 | for ($i=0; $i < $numPayouts; $i++) { 47 | $model->payouts[] = InflationPayout::fromXdr($xdr); 48 | } 49 | 50 | return $model; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Horizon/Api/PostTransactionResponse.php: -------------------------------------------------------------------------------- 1 | rawData)); 24 | } 25 | 26 | public function __construct($jsonEncodedData) 27 | { 28 | parent::__construct($jsonEncodedData); 29 | 30 | $this->parseRawData($this->rawData); 31 | } 32 | 33 | /** 34 | * Parses the raw response data and populates this object 35 | * 36 | * @param $rawData 37 | * @throws \ErrorException 38 | */ 39 | protected function parseRawData($rawData) 40 | { 41 | if (!$rawData) return; 42 | 43 | if (!empty($rawData['result_xdr'])) { 44 | $xdr = new XdrBuffer(base64_decode($rawData['result_xdr'])); 45 | $this->result = TransactionResult::fromXdr($xdr); 46 | } 47 | } 48 | 49 | /** 50 | * @return TransactionResult 51 | */ 52 | public function getResult() 53 | { 54 | return $this->result; 55 | } 56 | 57 | /** 58 | * @param TransactionResult $result 59 | */ 60 | public function setResult($result) 61 | { 62 | $this->result = $result; 63 | } 64 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/ManageOfferOpTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 35 | 36 | $this->assertTrue($parsed instanceof ManageOfferOp); 37 | 38 | $this->assertEquals($sellingAsset->getAssetCode(), $parsed->getSellingAsset()->getAssetCode()); 39 | $this->assertEquals($buyingAsset->getAssetCode(), $parsed->getBuyingAsset()->getAssetCode()); 40 | $this->assertEquals($amount, $parsed->getAmount()->getScaledValue()); 41 | $this->assertEquals(.00025, $parsed->getPrice()->toFloat()); 42 | $this->assertEquals($offerId, $parsed->getOfferId()); 43 | } 44 | } -------------------------------------------------------------------------------- /tests/Unit/Transaction/TransactionTest.php: -------------------------------------------------------------------------------- 1 | setSequenceNumber(new BigInteger(123)); 25 | 26 | $createAccountOp = new CreateAccountOp(new AccountId(Keypair::newFromRandom()), 100); 27 | $sourceModel->addOperation($createAccountOp); 28 | 29 | $sourceModel->setLowerTimebound(new \DateTime('2018-01-01 00:00:00')); 30 | $sourceModel->setUpperTimebound(new \DateTime('2018-12-31 00:00:00')); 31 | $sourceModel->setTextMemo('test memo'); 32 | 33 | // Encode and then parse the resulting XDR 34 | $parsed = Transaction::fromXdr(new XdrBuffer($sourceModel->toXdr())); 35 | $parsedOps = $parsed->getOperations(); 36 | 37 | $this->assertCount(1, $parsedOps); 38 | 39 | $this->assertEquals($sourceKeypair->getAccountId(), $parsed->getSourceAccountId()->getAccountIdString()); 40 | $this->assertEquals('test memo', $parsed->getMemo()->getValue()); 41 | } 42 | } -------------------------------------------------------------------------------- /src/XdrModel/Operation/BumpSequenceOp.php: -------------------------------------------------------------------------------- 1 | bumpTo = $bumpTo; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function toXdr() 30 | { 31 | $bytes = parent::toXdr(); 32 | 33 | $bytes .= XdrEncoder::unsignedBigInteger64($this->bumpTo); 34 | 35 | return $bytes; 36 | } 37 | 38 | /** 39 | * NOTE: This only parses the XDR that's specific to this operation and cannot 40 | * load a full Operation 41 | * 42 | * @deprecated Do not call this directly, instead call Operation::fromXdr() 43 | * @param XdrBuffer $xdr 44 | * @return BumpSequenceOp 45 | * @throws \ErrorException 46 | */ 47 | public static function fromXdr(XdrBuffer $xdr) 48 | { 49 | return new BumpSequenceOp($xdr->readBigInteger()); 50 | } 51 | 52 | /** 53 | * @return BigInteger 54 | */ 55 | public function getBumpTo() 56 | { 57 | return $this->bumpTo; 58 | } 59 | 60 | /** 61 | * @param BigInteger $bumpTo 62 | */ 63 | public function setBumpTo($bumpTo) 64 | { 65 | $this->bumpTo = $bumpTo; 66 | } 67 | } -------------------------------------------------------------------------------- /src/Util/Checksum.php: -------------------------------------------------------------------------------- 1 | > (7 - $i) & 1) == 1); 53 | $c15 = (($crc >> 15 & 1) == 1); 54 | $crc <<= 1; 55 | if ($c15 ^ $bit) $crc ^= $polynomial; 56 | } 57 | } 58 | 59 | return $crc & 0xffff; 60 | } 61 | } -------------------------------------------------------------------------------- /src/XdrModel/CreateAccountResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 29 | $errorCodeMap = [ 30 | '0' => 'success', 31 | '-1' => static::MALFORMED, 32 | '-2' => static::UNDERFUNDED, 33 | '-3' => static::LOW_RESERVE, 34 | '-4' => static::ALREADY_EXIST, 35 | ]; 36 | if (!isset($errorCodeMap[$rawErrorCode])) { 37 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 38 | } 39 | 40 | // Do not store the "success" error code 41 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 42 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 43 | } 44 | 45 | return $model; 46 | } 47 | } -------------------------------------------------------------------------------- /examples/set-options.php: -------------------------------------------------------------------------------- 1 | setInflationDestination('GBLGN5K633LO5BMWEKSVHKXTPZHBVVBNSXRGXRDGZUANXWQ4LBWES3BK'); 21 | 22 | // Set auth required 23 | $optionsOperation->setAuthRequired(true); 24 | 25 | // Set auth revokable 26 | $optionsOperation->setAuthRevocable(true); 27 | 28 | // Add a new account as a signer (GAJCCCRIRXAYEU2ATNQAFYH4E2HKLN2LCKM2VPXCTJKIBVTRSOLEGCJZ) 29 | $newSignerKeypair = Keypair::newFromSeed('SDJCZISO5M5XAUV6Y7MZJNN3JZ5BWPXDHV4GXP3MYNACVDNQRQSERXBC'); 30 | $signerKey = SignerKey::fromKeypair($newSignerKeypair); 31 | $newAccountSigner = new Signer($signerKey, 5); 32 | 33 | $optionsOperation->updateSigner($newAccountSigner); 34 | 35 | // Set weight of the master key 36 | $optionsOperation->setMasterWeight(255); 37 | 38 | // Set threshold values 39 | $optionsOperation->setLowThreshold(1); 40 | $optionsOperation->setMediumThreshold(2); 41 | $optionsOperation->setHighThreshold(3); 42 | 43 | // Set home domain 44 | $optionsOperation->setHomeDomain('example.com'); 45 | 46 | 47 | // Submit options to the network 48 | $server->buildTransaction($keypair->getPublicKey()) 49 | ->addOperation($optionsOperation) 50 | ->submit($keypair->getSecret()); -------------------------------------------------------------------------------- /src/XdrModel/ChangeTrustResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 30 | $errorCodeMap = [ 31 | '0' => 'success', 32 | '-1' => static::MALFORMED, 33 | '-2' => static::NO_ISSUER, 34 | '-3' => static::INVALID_LIMIT, 35 | '-4' => static::LOW_RESERVE, 36 | '-5' => static::SELF_NOT_ALLOWED, 37 | ]; 38 | if (!isset($errorCodeMap[$rawErrorCode])) { 39 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 40 | } 41 | 42 | // Do not store the "success" error code 43 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 44 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 45 | } 46 | 47 | return $model; 48 | } 49 | } -------------------------------------------------------------------------------- /src/XdrModel/ManageDataResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 29 | $errorCodeMap = [ 30 | '0' => 'success', 31 | '-1' => static::NOT_SUPPORTED_YET, 32 | '-2' => static::NAME_NOT_FOUND, 33 | '-3' => static::LOW_RESERVE, 34 | '-4' => static::INVALID_NAME, 35 | ]; 36 | if (!isset($errorCodeMap[$rawErrorCode])) { 37 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 38 | } 39 | 40 | // Do not store the "success" error code 41 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 42 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 43 | } 44 | 45 | return $model; 46 | } 47 | } -------------------------------------------------------------------------------- /src/XdrModel/AllowTrustResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 30 | $errorCodeMap = [ 31 | '0' => 'success', 32 | '-1' => static::MALFORMED, 33 | '-2' => static::NO_TRUST_LINE, 34 | '-3' => static::TRUST_NOT_REQUIRED, 35 | '-4' => static::CANT_REVOKE, 36 | '-5' => static::SELF_NOT_ALLOWED, 37 | ]; 38 | if (!isset($errorCodeMap[$rawErrorCode])) { 39 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 40 | } 41 | 42 | // Do not store the "success" error code 43 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 44 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 45 | } 46 | 47 | return $model; 48 | } 49 | } -------------------------------------------------------------------------------- /src/XdrModel/DecoratedSignature.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private $hint; 17 | 18 | /** 19 | * @var string opaque<64> 20 | */ 21 | private $signature; 22 | 23 | public function __construct($hint, $signature) 24 | { 25 | $this->hint = $hint; 26 | $this->signature = $signature; 27 | } 28 | 29 | public function toXdr() 30 | { 31 | $bytes = ''; 32 | 33 | $bytes .= XdrEncoder::opaqueFixed($this->hint, 4); 34 | $bytes .= XdrEncoder::opaqueVariable($this->signature); 35 | 36 | return $bytes; 37 | } 38 | 39 | /** 40 | * @param XdrBuffer $xdr 41 | * @return DecoratedSignature 42 | * @throws \ErrorException 43 | */ 44 | public static function fromXdr(XdrBuffer $xdr) 45 | { 46 | $hint = $xdr->readOpaqueFixed(4); 47 | $signature = $xdr->readOpaqueVariable(); 48 | 49 | return new DecoratedSignature($hint, $signature); 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function toBase64() 56 | { 57 | return base64_encode($this->toXdr()); 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getWithoutHintBase64() 64 | { 65 | return base64_encode($this->signature); 66 | } 67 | 68 | /** 69 | * Returns the raw 64 bytes representing the signature 70 | * 71 | * This does not include the hint 72 | * 73 | * @return string 74 | */ 75 | public function getRawSignature() 76 | { 77 | return $this->signature; 78 | } 79 | } -------------------------------------------------------------------------------- /src/XdrModel/Operation/AccountMergeOp.php: -------------------------------------------------------------------------------- 1 | getPublicKey(); 24 | } 25 | 26 | $this->destination = new AccountId($destinationAccountId); 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function toXdr() 33 | { 34 | $bytes = parent::toXdr(); 35 | 36 | // Destination account 37 | $bytes .= $this->destination->toXdr(); 38 | 39 | return $bytes; 40 | } 41 | 42 | /** 43 | * NOTE: This only parses the XDR that's specific to this operation and cannot 44 | * load a full Operation 45 | * 46 | * @deprecated Do not call this directly, instead call Operation::fromXdr() 47 | * @param XdrBuffer $xdr 48 | * @return AccountMergeOp 49 | * @throws \ErrorException 50 | */ 51 | public static function fromXdr(XdrBuffer $xdr) 52 | { 53 | $destination = AccountId::fromXdr($xdr); 54 | 55 | return new AccountMergeOp($destination->getAccountIdString()); 56 | } 57 | 58 | /** 59 | * @return AccountId 60 | */ 61 | public function getDestination() 62 | { 63 | return $this->destination; 64 | } 65 | 66 | /** 67 | * @param AccountId $destination 68 | */ 69 | public function setDestination($destination) 70 | { 71 | $this->destination = $destination; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Horizon/Api/HorizonResponse.php: -------------------------------------------------------------------------------- 1 | rawData = @json_decode($jsonEncodedData, true); 19 | 20 | if (null === $this->rawData && json_last_error() != JSON_ERROR_NONE) { 21 | throw new \InvalidArgumentException(sprintf("Error in json_decode: %s", json_last_error_msg())); 22 | } 23 | } 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function getRawData() 29 | { 30 | return $this->rawData; 31 | } 32 | 33 | /** 34 | * Returns the value of $fieldName or null if $fieldName is not in the response 35 | * 36 | * @param $fieldName 37 | * @return mixed|null 38 | */ 39 | public function getField($fieldName) 40 | { 41 | if (!isset($this->rawData[$fieldName])) return null; 42 | 43 | return $this->rawData[$fieldName]; 44 | } 45 | 46 | /** 47 | * Throws an exception if $fieldName is not present in the response 48 | * 49 | * @param $fieldName 50 | * @throws \InvalidArgumentException 51 | * @return mixed|null 52 | */ 53 | public function mustGetField($fieldName) 54 | { 55 | if (!isset($this->rawData[$fieldName])) throw new \InvalidArgumentException(sprintf("Field '%s' not present in response", $fieldName)); 56 | 57 | return $this->rawData[$fieldName]; 58 | } 59 | 60 | public function getRecords($limit = 100) 61 | { 62 | // todo: support paging 63 | 64 | $records = []; 65 | foreach ($this->rawData['_embedded']['records'] as $rawRecord) { 66 | $record = $rawRecord; 67 | unset($record['_links']); 68 | 69 | $records[] = $record; 70 | } 71 | 72 | return $records; 73 | } 74 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/PathPaymentOpTest.php: -------------------------------------------------------------------------------- 1 | addPath($pathA); 30 | $sourceOp->addPath($pathB); 31 | 32 | /** @var PathPaymentOp $parsed */ 33 | $parsed = Operation::fromXdr(new XdrBuffer($sourceOp->toXdr())); 34 | 35 | $this->assertTrue($parsed instanceof PathPaymentOp); 36 | 37 | $this->assertEquals($sourceOp->getDestinationAccount()->getAccountIdString(), $parsed->getDestinationAccount()->getAccountIdString()); 38 | $this->assertEquals($sendMax, $parsed->getSendMax()->getScaledValue()); 39 | $this->assertEquals($destinationKeypair->getAccountId(), $parsed->getDestinationAccount()->getAccountIdString()); 40 | $this->assertEquals($destinationAsset->getAssetCode(), $parsed->getDestinationAsset()->getAssetCode()); 41 | $this->assertEquals($desinationAmount, $parsed->getDestinationAmount()->getScaledValue()); 42 | } 43 | } -------------------------------------------------------------------------------- /src/XdrModel/Signer.php: -------------------------------------------------------------------------------- 1 | key = $key; 31 | $this->weight = $weight; 32 | } 33 | 34 | public function toXdr() 35 | { 36 | $bytes = ''; 37 | 38 | // key 39 | $bytes .= $this->key->toXdr(); 40 | 41 | // weight 42 | $bytes .= XdrEncoder::unsignedInteger($this->weight); 43 | 44 | return $bytes; 45 | } 46 | 47 | /** 48 | * @param XdrBuffer $xdr 49 | * @return Signer 50 | * @throws \ErrorException 51 | */ 52 | public static function fromXdr(XdrBuffer $xdr) 53 | { 54 | $signerKey = SignerKey::fromXdr($xdr); 55 | $weight = $xdr->readUnsignedInteger(); 56 | 57 | return new Signer($signerKey, $weight); 58 | } 59 | 60 | /** 61 | * @return SignerKey 62 | */ 63 | public function getKey() 64 | { 65 | return $this->key; 66 | } 67 | 68 | /** 69 | * @param SignerKey $key 70 | */ 71 | public function setKey($key) 72 | { 73 | $this->key = $key; 74 | } 75 | 76 | /** 77 | * @return int 78 | */ 79 | public function getWeight() 80 | { 81 | return $this->weight; 82 | } 83 | 84 | /** 85 | * @param int $weight 86 | */ 87 | public function setWeight($weight) 88 | { 89 | if ($weight > 255 || $weight < 0) throw new \InvalidArgumentException('weight must be between 0 and 255'); 90 | 91 | $this->weight = $weight; 92 | } 93 | } -------------------------------------------------------------------------------- /src/XdrModel/PaymentResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 34 | $errorCodeMap = [ 35 | '0' => 'success', 36 | '-1' => static::MALFORMED, 37 | '-2' => static::UNDERFUNDED, 38 | '-3' => static::SRC_NO_TRUST, 39 | '-4' => static::SRC_NOT_AUTHORIZED, 40 | '-5' => static::NO_DESTINATION, 41 | '-6' => static::NO_TRUST, 42 | '-7' => static::NOT_AUTHORIZED, 43 | '-8' => static::LINE_FULL, 44 | '-9' => static::NO_ISSUER, 45 | ]; 46 | if (!isset($errorCodeMap[$rawErrorCode])) { 47 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 48 | } 49 | 50 | // Do not store the "success" error code 51 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 52 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 53 | } 54 | 55 | return $model; 56 | } 57 | } -------------------------------------------------------------------------------- /src/History/BucketLevel.php: -------------------------------------------------------------------------------- 1 | curr = $raw['curr']; 61 | $object->snap = $raw['snap']; 62 | $object->next = $raw['next']; 63 | 64 | return $object; 65 | } 66 | 67 | public function __construct($level) 68 | { 69 | $this->level = $level; 70 | } 71 | 72 | /** 73 | * Returns an array of unique sha256 hashes in this bucket level 74 | * 75 | * @return array 76 | */ 77 | public function getUniqueBucketHashes() 78 | { 79 | $hashes = []; 80 | 81 | if ($this->curr != self::HASH_EMPTY) $hashes[] = $this->curr; 82 | if ($this->snap != self::HASH_EMPTY) $hashes[] = $this->snap; 83 | 84 | return array_values(array_unique($hashes)); 85 | } 86 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/AssetTest.php: -------------------------------------------------------------------------------- 1 | toXdr())); 19 | 20 | $this->assertEquals($asset->getType(), $assetFromXdr->getType()); 21 | } 22 | 23 | public function testCustomAsset4FromXdr() 24 | { 25 | $issuerKeypair = Keypair::newFromRandom(); 26 | $asset = Asset::newCustomAsset('TEST', $issuerKeypair); 27 | 28 | $assetFromXdr = Asset::fromXdr(new XdrBuffer($asset->toXdr())); 29 | 30 | $this->assertEquals($asset->getType(), $assetFromXdr->getType()); 31 | $this->assertEquals('TEST', $assetFromXdr->getAssetCode()); 32 | $this->assertEquals($issuerKeypair->getPublicKey(), $assetFromXdr->getIssuer()->getAccountIdString()); 33 | } 34 | 35 | public function testCustomAsset7FromXdr() 36 | { 37 | $issuerKeypair = Keypair::newFromRandom(); 38 | $asset = Asset::newCustomAsset('TESTABC', $issuerKeypair); 39 | 40 | $assetFromXdr = Asset::fromXdr(new XdrBuffer($asset->toXdr())); 41 | 42 | $this->assertEquals($asset->getType(), $assetFromXdr->getType()); 43 | $this->assertEquals('TESTABC', $assetFromXdr->getAssetCode()); 44 | $this->assertEquals($issuerKeypair->getPublicKey(), $assetFromXdr->getIssuer()->getAccountIdString()); 45 | } 46 | 47 | public function testCustomAsset12FromXdr() 48 | { 49 | $issuerKeypair = Keypair::newFromRandom(); 50 | $asset = Asset::newCustomAsset('ABCDEFGHIJKL', $issuerKeypair); 51 | 52 | $assetFromXdr = Asset::fromXdr(new XdrBuffer($asset->toXdr())); 53 | 54 | $this->assertEquals($asset->getType(), $assetFromXdr->getType()); 55 | $this->assertEquals('ABCDEFGHIJKL', $assetFromXdr->getAssetCode()); 56 | $this->assertEquals($issuerKeypair->getPublicKey(), $assetFromXdr->getIssuer()->getAccountIdString()); 57 | } 58 | } -------------------------------------------------------------------------------- /examples/transaction-result.php: -------------------------------------------------------------------------------- 1 | buildTransaction($sourceKeypair) 20 | ->addCreateAccountOp($newAccount1, 5) 21 | ->addCreateAccountOp($newAccount2, 5) 22 | ->submit($sourceKeypair); 23 | 24 | /* 25 | * Get information on the overall result of the transaction 26 | */ 27 | /** @var \ZuluCrypto\StellarSdk\XdrModel\TransactionResult $result */ 28 | $result = $response->getResult(); 29 | 30 | print "Fee charged: " . $result->getFeeCharged()->getScaledValue() . " XLM" . PHP_EOL; 31 | 32 | 33 | /* 34 | * Each operation within the transaction has its own result 35 | */ 36 | $operationResults = $result->getOperationResults(); 37 | 38 | /* 39 | * Each result will be a child class of OperationResult depending on what the 40 | * original operation was. 41 | * 42 | * See these classes for additional details that can be retrieved for each type 43 | * of result 44 | */ 45 | foreach ($operationResults as $operationResult) { 46 | print "Operation result is a: " . get_class($operationResult) . PHP_EOL; 47 | } 48 | 49 | /* 50 | * Exception handling 51 | */ 52 | 53 | // This transaction will fail because there aren't enough lumens in the source 54 | // account 55 | try { 56 | $response = $server->buildTransaction($sourceKeypair) 57 | ->addLumenPayment($newAccount1, 9999999) 58 | ->submit($sourceKeypair); 59 | } catch (PostTransactionException $e) { 60 | // Details operation information can be retrieved from this exception 61 | $operationResults = $e->getResult()->getOperationResults(); 62 | 63 | foreach ($operationResults as $result) { 64 | // Skip through the ones that worked 65 | if ($result->succeeded()) continue; 66 | 67 | // Print out the first failed one 68 | print "Operation failed with code: " . $result->getErrorCode() . PHP_EOL; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/History/HistoryArchiveState.php: -------------------------------------------------------------------------------- 1 | version = $raw['version']; 52 | $object->server = $raw['server']; 53 | $object->currentLedger = $raw['currentLedger']; 54 | 55 | foreach ($raw['currentBuckets'] as $rawBucketLevel) { 56 | $object->addBucketFromRaw($rawBucketLevel); 57 | } 58 | 59 | return $object; 60 | } 61 | 62 | /** 63 | * @param $path 64 | * @return HistoryArchiveState 65 | */ 66 | public static function fromFile($path) 67 | { 68 | try { 69 | return static::fromRaw(Json::mustDecode(file_get_contents($path))); 70 | } catch (\InvalidArgumentException $e) { 71 | throw new \ErrorException(sprintf('Error decoding json in %s: %s', $path, $e->getMessage())); 72 | } 73 | } 74 | 75 | public function __construct() 76 | { 77 | $this->currentBuckets = new BucketList(); 78 | } 79 | 80 | /** 81 | * @param $raw 82 | * @param null $level 83 | * @return null|BucketLevel 84 | */ 85 | public function addBucketFromRaw($raw, $level = null) 86 | { 87 | return $this->currentBuckets->addLevelFromRaw($raw, $level); 88 | } 89 | 90 | /** 91 | * @return array|string[] sha256 hashes 92 | */ 93 | public function getUniqueBucketHashes() 94 | { 95 | return $this->currentBuckets->getUniqueBucketHashes(); 96 | } 97 | } -------------------------------------------------------------------------------- /src/XdrModel/SetOptionsResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 34 | $errorCodeMap = [ 35 | '0' => 'success', 36 | '-1' => static::LOW_RESERVE, 37 | '-2' => static::TOO_MANY_SIGNERS, 38 | '-3' => static::BAD_FLAGS, 39 | '-4' => static::INVALID_INFLATION, 40 | '-5' => static::CANT_CHANGE, 41 | '-6' => static::UNKNOWN_FLAG, 42 | '-7' => static::THRESHOLD_OUT_OF_RANGE, 43 | '-8' => static::BAD_SIGNER, 44 | '-9' => static::INVALID_HOME_DOMAIN, 45 | ]; 46 | if (!isset($errorCodeMap[$rawErrorCode])) { 47 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 48 | } 49 | 50 | // Do not store the "success" error code 51 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 52 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 53 | } 54 | 55 | return $model; 56 | } 57 | } -------------------------------------------------------------------------------- /src/Horizon/Exception/PostTransactionException.php: -------------------------------------------------------------------------------- 1 | result = TransactionResult::fromXdr($xdr); 28 | } 29 | 30 | return $postTransactionEx; 31 | } 32 | 33 | /** 34 | * @param HorizonException $horizonException 35 | * @return PostTransactionException 36 | */ 37 | public static function fromHorizonException(HorizonException $horizonException) 38 | { 39 | $ex = new PostTransactionException($horizonException->getTitle(), $horizonException->getPrevious()); 40 | 41 | $ex->requestedUrl = $horizonException->getRequestedUrl(); 42 | $ex->httpMethod = $horizonException->getHttpMethod(); 43 | $ex->type = $horizonException->getType(); 44 | $ex->httpStatusCode = $horizonException->getHttpStatusCode(); 45 | $ex->detail = $horizonException->getDetail(); 46 | $ex->operationResultCodes = $horizonException->getOperationResultCodes(); 47 | $ex->transactionResultCode = $horizonException->getTransactionResultCode(); 48 | $ex->message = $horizonException->getMessage(); 49 | $ex->raw = $horizonException->getRaw(); 50 | $ex->clientException = $horizonException->getClientException(); 51 | 52 | return $ex; 53 | } 54 | 55 | /** 56 | * @return TransactionResult 57 | */ 58 | public function getResult() 59 | { 60 | return $this->result; 61 | } 62 | 63 | /** 64 | * @param TransactionResult $result 65 | */ 66 | public function setResult($result) 67 | { 68 | $this->result = $result; 69 | } 70 | } -------------------------------------------------------------------------------- /tests/HardwareWallet/BumpSequenceOpTest.php: -------------------------------------------------------------------------------- 1 | mnemonic); 19 | 20 | $bumpTo = new BigInteger(1234567890); 21 | 22 | $transaction = $this->horizonServer 23 | ->buildTransaction($sourceKeypair) 24 | ->setSequenceNumber(new BigInteger(4294967296)) 25 | ->bumpSequenceTo($bumpTo); 26 | 27 | $knownSignature = $transaction->signWith($this->privateKeySigner); 28 | 29 | $this->manualVerificationOutput(join(PHP_EOL, [ 30 | ' Bump Sequence: basic', 31 | ' Source: ' . $sourceKeypair->getPublicKey(), 32 | ' Bump To: ' . $bumpTo->toString(), 33 | '', 34 | 'B64 Transaction: ' . base64_encode($transaction->toXdr()), 35 | ' Signature: ' . $knownSignature->getWithoutHintBase64(), 36 | ])); 37 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 38 | 39 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 40 | } 41 | 42 | /** 43 | * @group requires-hardwarewallet 44 | */ 45 | public function testMaxBumpSequence() 46 | { 47 | $sourceKeypair = Keypair::newFromMnemonic($this->mnemonic); 48 | 49 | $bumpTo = new BigInteger('9223372036854775807'); 50 | 51 | $transaction = $this->horizonServer 52 | ->buildTransaction($sourceKeypair) 53 | ->setSequenceNumber(new BigInteger(4294967296)) 54 | ->bumpSequenceTo($bumpTo); 55 | 56 | $knownSignature = $transaction->signWith($this->privateKeySigner); 57 | 58 | $this->manualVerificationOutput(join(PHP_EOL, [ 59 | ' Bump Sequence: max', 60 | ' Source: ' . $sourceKeypair->getPublicKey(), 61 | ' Bump To: ' . $bumpTo->toString(), 62 | ])); 63 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 64 | 65 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 66 | } 67 | } -------------------------------------------------------------------------------- /src/XdrModel/Price.php: -------------------------------------------------------------------------------- 1 | numerator = $numerator; 31 | $this->denominator = $denominator; 32 | } 33 | 34 | public function __toString() 35 | { 36 | return strval($this->numerator / $this->denominator); 37 | } 38 | 39 | public function toXdr() 40 | { 41 | $bytes = ''; 42 | 43 | $bytes .= XdrEncoder::unsignedInteger($this->numerator); 44 | $bytes .= XdrEncoder::unsignedInteger($this->denominator); 45 | 46 | return $bytes; 47 | } 48 | 49 | /** 50 | * @param XdrBuffer $xdr 51 | * @return Price 52 | * @throws \ErrorException 53 | */ 54 | public static function fromXdr(XdrBuffer $xdr) 55 | { 56 | $numerator = $xdr->readUnsignedInteger(); 57 | $denominator = $xdr->readUnsignedInteger(); 58 | 59 | return new Price($numerator, $denominator); 60 | } 61 | 62 | /** 63 | * @return float 64 | */ 65 | public function toFloat() 66 | { 67 | return floatval($this->numerator / $this->denominator); 68 | } 69 | 70 | /** 71 | * @return int 72 | */ 73 | public function getNumerator() 74 | { 75 | return $this->numerator; 76 | } 77 | 78 | /** 79 | * @param int $numerator 80 | */ 81 | public function setNumerator($numerator) 82 | { 83 | $this->numerator = $numerator; 84 | } 85 | 86 | /** 87 | * @return int 88 | */ 89 | public function getDenominator() 90 | { 91 | return $this->denominator; 92 | } 93 | 94 | /** 95 | * @param int $denominator 96 | */ 97 | public function setDenominator($denominator) 98 | { 99 | $this->denominator = $denominator; 100 | } 101 | } -------------------------------------------------------------------------------- /examples/transaction-add-signature.php: -------------------------------------------------------------------------------- 1 | sign($payingKeypair, $server); 50 | 51 | 52 | // TransactionEnvelope now has two signatures and can be submitted 53 | $server->submitB64Transaction($transactionEnvelope->toBase64()); 54 | 55 | print "Done!" . PHP_EOL; -------------------------------------------------------------------------------- /examples/trust-asset.php: -------------------------------------------------------------------------------- 1 | getPublicKey()); 19 | 20 | 21 | // First, the receiving account must add a trustline for the issuer 22 | $server->buildTransaction($receivingKeypair) 23 | ->addChangeTrustOp($asset) // this will default to the maximum value 24 | ->submit($receivingKeypair); 25 | 26 | // Then, the issuing account can transfer assets 27 | $server->buildTransaction($issuingKeypair) 28 | ->addCustomAssetPaymentOp($asset, 50, $receivingKeypair->getPublicKey()) 29 | ->submit($issuingKeypair->getSecret()); 30 | 31 | 32 | // ------------------------------------ 33 | // An asset that requires authorization 34 | 35 | // GDYW5Y5PCHC3RGUPME4MIFBQCDLFMCSFEB6EAA7P2PJRAKGPGUDZX64Q 36 | $issuingKeypair = Keypair::newFromSeed('SC5AQ5K332ZIZB5MWG7FA64JURJXOR4B7VAIDIV7ORIEV7LSGNYFNPL3'); 37 | 38 | $asset = Asset::newCustomAsset('AUTHTEST', $issuingKeypair->getPublicKey()); 39 | 40 | // Issuing keypair indicates that authorization is required and revocable 41 | $accountOptions = new SetOptionsOp(); 42 | $accountOptions->setAuthRequired(true); 43 | $accountOptions->setAuthRevocable(true); // This is optional 44 | 45 | $server->buildTransaction($issuingKeypair) 46 | ->addOperation($accountOptions) 47 | ->submit($issuingKeypair); 48 | 49 | // Receiving account adds a trustline 50 | $server->buildTransaction($receivingKeypair->getPublicKey()) 51 | ->addChangeTrustOp($asset) 52 | ->submit($receivingKeypair->getSecret()); 53 | 54 | // Issuing account indicates the receiving account is authorized 55 | $server->buildTransaction($issuingKeypair->getPublicKey()) 56 | ->authorizeTrustline($asset, $receivingKeypair) 57 | ->submit($issuingKeypair); 58 | 59 | // Issuing account can now transfer to receiving account 60 | $server->buildTransaction($issuingKeypair) 61 | ->addCustomAssetPaymentOp($asset, 75, $receivingKeypair->getPublicKey()) 62 | ->submit($issuingKeypair->getSecret()); -------------------------------------------------------------------------------- /src/Model/ManageDataOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 38 | 39 | return $object; 40 | } 41 | 42 | /** 43 | * @param $id 44 | * @param $type 45 | */ 46 | public function __construct($id, $type) 47 | { 48 | parent::__construct($id, Operation::TYPE_MANAGE_DATA); 49 | } 50 | 51 | /** 52 | * @param $rawData 53 | */ 54 | public function loadFromRawResponseData($rawData) 55 | { 56 | parent::loadFromRawResponseData($rawData); 57 | 58 | $this->sourceAccountId = $rawData['source_account']; 59 | $this->name = $rawData['name']; 60 | $this->value = $rawData['value']; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getSourceAccountId() 67 | { 68 | return $this->sourceAccountId; 69 | } 70 | 71 | /** 72 | * @param string $sourceAccountId 73 | */ 74 | public function setSourceAccountId($sourceAccountId) 75 | { 76 | $this->sourceAccountId = $sourceAccountId; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getName() 83 | { 84 | return $this->name; 85 | } 86 | 87 | /** 88 | * @param string $name 89 | */ 90 | public function setName($name) 91 | { 92 | $this->name = $name; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getValue() 99 | { 100 | return $this->value; 101 | } 102 | 103 | /** 104 | * @param string $value 105 | */ 106 | public function setValue($value) 107 | { 108 | $this->value = $value; 109 | } 110 | } -------------------------------------------------------------------------------- /src/Model/ManageOfferOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 53 | 54 | return $object; 55 | } 56 | 57 | /** 58 | * @param $id 59 | * @param $type 60 | */ 61 | public function __construct($id, $type = Operation::TYPE_MANAGE_OFFER) 62 | { 63 | parent::__construct($id, $type); 64 | } 65 | 66 | /** 67 | * @param $rawData 68 | */ 69 | public function loadFromRawResponseData($rawData) 70 | { 71 | parent::loadFromRawResponseData($rawData); 72 | 73 | $this->offerId = $rawData['offer_id']; 74 | $this->price = $rawData['price']; 75 | $this->priceR = $rawData['price_r']; 76 | 77 | // todo: verify that amount is shared between buying and selling? 78 | if (isset($rawData['buying_asset_code'])) { 79 | $this->buyingAsset = new AssetAmount($rawData['amount'], $rawData['buying_asset_code']); 80 | $this->buyingAsset->setAssetIssuerAccountId($rawData['buying_asset_issuer']); 81 | $this->buyingAsset->setAssetType($rawData['buying_asset_type']); 82 | } 83 | else if (isset($rawData['selling_asset_code'])) { 84 | $this->sellingAsset = new AssetAmount($rawData['amount'], $rawData['selling_asset_code']); 85 | $this->sellingAsset->setAssetIssuerAccountId($rawData['selling_asset_issuer']); 86 | $this->sellingAsset->setAssetType($rawData['selling_asset_type']); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/XdrModel/ClaimOfferAtom.php: -------------------------------------------------------------------------------- 1 | seller = AccountId::fromXdr($xdr); 56 | $model->offerId = $xdr->readUnsignedInteger64(); 57 | 58 | $model->assetSold = Asset::fromXdr($xdr); 59 | $model->amountSold = new StellarAmount(new BigInteger($xdr->readInteger64())); 60 | 61 | $model->assetBought = Asset::fromXdr($xdr); 62 | $model->amountBought = new StellarAmount(new BigInteger($xdr->readInteger64())); 63 | 64 | return $model; 65 | } 66 | 67 | /** 68 | * @return AccountId 69 | */ 70 | public function getSeller() 71 | { 72 | return $this->seller; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getOfferId() 79 | { 80 | return $this->offerId; 81 | } 82 | 83 | /** 84 | * @return Asset 85 | */ 86 | public function getAssetSold() 87 | { 88 | return $this->assetSold; 89 | } 90 | 91 | /** 92 | * @return StellarAmount 93 | */ 94 | public function getAmountSold() 95 | { 96 | return $this->amountSold; 97 | } 98 | 99 | /** 100 | * @return Asset 101 | */ 102 | public function getAssetBought() 103 | { 104 | return $this->assetBought; 105 | } 106 | 107 | /** 108 | * @return StellarAmount 109 | */ 110 | public function getAmountBought() 111 | { 112 | return $this->amountBought; 113 | } 114 | } -------------------------------------------------------------------------------- /src/XdrModel/AccountMergeResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 38 | $errorCodeMap = [ 39 | '0' => 'success', 40 | '-1' => static::MALFORMED, 41 | '-2' => static::NO_ACCOUNT, 42 | '-3' => static::IMMUTABLE_SET, 43 | '-4' => static::HAS_SUB_ENTRIES, 44 | ]; 45 | if (!isset($errorCodeMap[$rawErrorCode])) { 46 | throw new \ErrorException(sprintf('Unknown error code %s', $rawErrorCode)); 47 | } 48 | 49 | // Do not store the "success" error code 50 | if ($errorCodeMap[$rawErrorCode] !== 'success') { 51 | $model->errorCode = $errorCodeMap[$rawErrorCode]; 52 | return $model; 53 | } 54 | 55 | $model->transferredBalance = new StellarAmount(new BigInteger($xdr->readInteger64())); 56 | 57 | return $model; 58 | } 59 | 60 | /** 61 | * @return StellarAmount 62 | */ 63 | public function getTransferredBalance() 64 | { 65 | return $this->transferredBalance; 66 | } 67 | 68 | /** 69 | * @param StellarAmount $transferredBalance 70 | */ 71 | public function setTransferredBalance($transferredBalance) 72 | { 73 | $this->transferredBalance = $transferredBalance; 74 | } 75 | } -------------------------------------------------------------------------------- /tests/Unit/XdrModel/Operation/SetOptionsOpTest.php: -------------------------------------------------------------------------------- 1 | setAuthRequired(true); 23 | $op->setAuthRevocable(false); 24 | $this->assertTrue($op->isAuthRequired()); 25 | $this->assertFalse($op->isAuthRevocable()); 26 | } 27 | 28 | 29 | public function testFromXdr() 30 | { 31 | $inflationDestinationKeypair = Keypair::newFromRandom(); 32 | $masterWeight = 10; 33 | $highThreshold = 9; 34 | $mediumThreshold = 8; 35 | $lowThreshold = 7; 36 | $homeDomain = 'example.com'; 37 | 38 | $signer = new Signer(SignerKey::fromHashX('hashx'), 6); 39 | 40 | $sourceOp = new SetOptionsOp(); 41 | $sourceOp->setInflationDestination($inflationDestinationKeypair->getPublicKey()); 42 | $sourceOp->setAuthRequired(true); 43 | $sourceOp->setAuthRevocable(false); 44 | $sourceOp->setMasterWeight($masterWeight); 45 | $sourceOp->setHighThreshold($highThreshold); 46 | $sourceOp->setMediumThreshold($mediumThreshold); 47 | $sourceOp->setLowThreshold($lowThreshold); 48 | $sourceOp->setHomeDomain($homeDomain); 49 | $sourceOp->updateSigner($signer); 50 | 51 | 52 | /** @var SetOptionsOp $parsed */ 53 | $parsed = Operation::fromXdr(new XdrBuffer($sourceOp->toXdr())); 54 | 55 | $this->assertTrue($parsed instanceof SetOptionsOp); 56 | 57 | $this->assertEquals($inflationDestinationKeypair->getAccountId(), $parsed->getInflationDestinationAccount()->getAccountIdString()); 58 | $this->assertEquals(true, $parsed->isAuthRequired()); 59 | $this->assertEquals(false, $parsed->isAuthRevocable()); 60 | $this->assertEquals($masterWeight, $parsed->getMasterWeight()); 61 | $this->assertEquals($highThreshold, $parsed->getHighThreshold()); 62 | $this->assertEquals($mediumThreshold, $parsed->getMediumThreshold()); 63 | $this->assertEquals($lowThreshold, $parsed->getLowThreshold()); 64 | $this->assertEquals($homeDomain, $parsed->getHomeDomain()); 65 | } 66 | } -------------------------------------------------------------------------------- /tests/integration/AccountTest.php: -------------------------------------------------------------------------------- 1 | horizonServer->getAccount($this->fixtureAccounts['basic1']->getPublicKey()); 23 | 24 | $destinationAccountBefore = $this->horizonServer->getAccount($this->fixtureAccounts['basic2']->getPublicKey()); 25 | 26 | // Send a payment to basic2 27 | $response = $sourceAccount->sendNativeAsset( 28 | // Send to basic2 account 29 | $this->fixtureAccounts['basic2']->getPublicKey(), 30 | $paymentAmount, 31 | // Sign with basic1 seed 32 | $this->fixtureAccounts['basic1']->getSecret() 33 | ); 34 | 35 | $destinationAccountAfter = $this->horizonServer->getAccount($this->fixtureAccounts['basic2']->getPublicKey()); 36 | 37 | // Must be a valid hash 38 | $this->assertNotEmpty($response->mustGetField('hash')); 39 | 40 | // Balance should have gone up by the paymentAmount 41 | $this->assertEquals($destinationAccountBefore->getNativeBalance() + $paymentAmount, $destinationAccountAfter->getNativeBalance()); 42 | } 43 | 44 | /** 45 | * @group requires-integrationnet 46 | */ 47 | public function testGetPayments() 48 | { 49 | // Create a new account to receive the payments and fund via friendbot 50 | $paymentDestKeypair = $this->getRandomFundedKeypair(); 51 | 52 | // Create a payment from a regular account 53 | $payingKeypair = $this->fixtureAccounts['basic1']; 54 | $payingAccount = $this->horizonServer->getAccount($payingKeypair->getPublicKey()); 55 | $payingAccount->sendNativeAsset($paymentDestKeypair, 100, $payingKeypair); 56 | 57 | // Merge an account into the destination account 58 | $mergingKeypair = $this->getRandomFundedKeypair(); 59 | $this->horizonServer->buildTransaction($mergingKeypair) 60 | ->addMergeOperation($paymentDestKeypair) 61 | ->submit($mergingKeypair); 62 | 63 | // loading this too fast will miss the last payment 64 | sleep(1); 65 | $account = $this->horizonServer->getAccount($paymentDestKeypair->getPublicKey()); 66 | 67 | $this->assertCount(2, $account->getPayments()); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Signing/TrezorSigner.php: -------------------------------------------------------------------------------- 1 | accountIndex = $accountIndex; 43 | 44 | $this->trezorBinPath = 'trezorctl'; 45 | } 46 | 47 | public function signTransaction(TransactionBuilder $builder) 48 | { 49 | $xdr = $builder 50 | ->getTransactionEnvelope() 51 | ->toXdr(); 52 | 53 | $networkPassphrase = $builder->getApiClient()->getNetworkPassphrase(); 54 | 55 | $bip32Path = sprintf("m/44'/148'/%s'", $this->accountIndex); 56 | 57 | $cmd = sprintf('%s stellar_sign_transaction --address %s -n %s %s', 58 | $this->trezorBinPath, 59 | escapeshellarg($bip32Path), 60 | escapeshellarg($networkPassphrase), 61 | escapeshellarg(base64_encode($xdr)) 62 | ); 63 | 64 | $output = []; 65 | $retval = null; 66 | $signatureb64 = exec($cmd, $output, $retval); 67 | 68 | // convert signature to raw bytes 69 | $signatureBytes = base64_decode($signatureb64); 70 | 71 | // Convert to DecoratedSignature 72 | $hint = substr($this->publickKeyBytes, -4); 73 | return new DecoratedSignature($hint, $signatureBytes); 74 | } 75 | 76 | public function setPublicKey(Keypair $keypair) 77 | { 78 | $this->publickKeyBytes = $keypair->getPublicKeyBytes(); 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getTrezorBinPath() 85 | { 86 | return $this->trezorBinPath; 87 | } 88 | 89 | /** 90 | * @param string $trezorBinPath 91 | */ 92 | public function setTrezorBinPath($trezorBinPath) 93 | { 94 | $this->trezorBinPath = $trezorBinPath; 95 | } 96 | } -------------------------------------------------------------------------------- /tests/Unit/Derivation/HdNodeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 18 | '2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7', 19 | bin2hex($node->getPrivateKeyBytes())); 20 | 21 | $this->assertEquals( 22 | '90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb', 23 | bin2hex($node->getChainCodeBytes())); 24 | } 25 | 26 | public function testMasterNodeDerives() 27 | { 28 | $entropy = hex2bin('000102030405060708090a0b0c0d0e0f'); 29 | $node = HdNode::newMasterNode($entropy); 30 | 31 | // m/0' 32 | $derived = $node->derive(0); 33 | $this->assertEquals( 34 | '68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3', 35 | bin2hex($derived->getPrivateKeyBytes())); 36 | $this->assertEquals( 37 | '8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69', 38 | bin2hex($derived->getChainCodeBytes())); 39 | 40 | // m/0'/1' 41 | $derived = $derived->derive(1); 42 | $this->assertEquals( 43 | 'b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2', 44 | bin2hex($derived->getPrivateKeyBytes())); 45 | $this->assertEquals( 46 | 'a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14', 47 | bin2hex($derived->getChainCodeBytes()));; 48 | } 49 | 50 | public function testMasterNodeDerivesPath() 51 | { 52 | $entropy = hex2bin('000102030405060708090a0b0c0d0e0f'); 53 | $node = HdNode::newMasterNode($entropy); 54 | 55 | $derived = $node->derivePath("m/0'/1'/2'"); 56 | $this->assertEquals( 57 | '92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9', 58 | bin2hex($derived->getPrivateKeyBytes())); 59 | $this->assertEquals( 60 | '2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c', 61 | bin2hex($derived->getChainCodeBytes())); 62 | } 63 | 64 | private function debugNode(HdNode $node) 65 | { 66 | print "\n"; 67 | print "private : " . bin2hex($node->getPrivateKeyBytes()) . "\n"; 68 | print "chaincode: " . bin2hex($node->getChainCodeBytes()) . "\n"; 69 | } 70 | } -------------------------------------------------------------------------------- /tests/HardwareWallet/CreateAccountOpTest.php: -------------------------------------------------------------------------------- 1 | mnemonic); 19 | $newKeypair = Keypair::newFromMnemonic($this->mnemonic, 'destination'); 20 | 21 | $transaction = $this->horizonServer 22 | ->buildTransaction($sourceKeypair) 23 | ->setSequenceNumber(new BigInteger(4294967296)) 24 | ->addCreateAccountOp($newKeypair, 100.0333); 25 | $knownSignature = $transaction->signWith($this->privateKeySigner); 26 | 27 | $this->manualVerificationOutput(join(PHP_EOL, [ 28 | 'Create Account operation', 29 | ' Source: ' . $sourceKeypair->getPublicKey(), 30 | 'Creating account: ' . $newKeypair->getPublicKey(), 31 | ' Initial balance: ' . 100.0333, 32 | '', 33 | ' B64 Transaction: ' . base64_encode($transaction->toXdr()), 34 | ' Signature: ' . $knownSignature->getWithoutHintBase64(), 35 | ])); 36 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 37 | 38 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 39 | } 40 | 41 | /** 42 | * @group requires-hardwarewallet 43 | */ 44 | public function testCreateAccountWithMaximumValue() 45 | { 46 | $sourceKeypair = Keypair::newFromMnemonic($this->mnemonic); 47 | $newKeypair = Keypair::newFromMnemonic($this->mnemonic, 'destination'); 48 | 49 | $transaction = $this->horizonServer 50 | ->buildTransaction($sourceKeypair) 51 | ->setSequenceNumber(new BigInteger(4294967296)) 52 | ->addCreateAccountOp($newKeypair, new BigInteger('9223372036854775807')); 53 | $knownSignature = $transaction->signWith($this->privateKeySigner); 54 | 55 | $this->manualVerificationOutput(join(PHP_EOL, [ 56 | 'Create Account operation', 57 | ' Source: ' . $sourceKeypair->getPublicKey(), 58 | 'Creating account: ' . $newKeypair->getPublicKey(), 59 | ' Initial balance: ' . '922337203685.4775807', 60 | ])); 61 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 62 | 63 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Model/AccountMergeOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 35 | 36 | return $object; 37 | } 38 | 39 | /** 40 | * @param $id 41 | * @param $type 42 | */ 43 | public function __construct($id, $type) 44 | { 45 | parent::__construct($id, Operation::TYPE_ACCOUNT_MERGE); 46 | } 47 | 48 | /** 49 | * @param $rawData 50 | */ 51 | public function loadFromRawResponseData($rawData) 52 | { 53 | parent::loadFromRawResponseData($rawData); 54 | 55 | $this->sourceAccountId = $rawData['account']; 56 | $this->mergeIntoAccountId = $rawData['into']; 57 | } 58 | 59 | public function getAssetTransferType() 60 | { 61 | return $this->type; 62 | } 63 | 64 | public function getFromAccountId() 65 | { 66 | return $this->sourceAccountId; 67 | } 68 | 69 | public function getToAccountId() 70 | { 71 | return $this->mergeIntoAccountId; 72 | } 73 | 74 | public function getAssetAmount() 75 | { 76 | return null; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getSourceAccountId() 83 | { 84 | return $this->sourceAccountId; 85 | } 86 | 87 | /** 88 | * @param string $sourceAccountId 89 | */ 90 | public function setSourceAccountId($sourceAccountId) 91 | { 92 | $this->sourceAccountId = $sourceAccountId; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getMergeIntoAccountId() 99 | { 100 | return $this->mergeIntoAccountId; 101 | } 102 | 103 | /** 104 | * @param string $mergeIntoAccountId 105 | */ 106 | public function setMergeIntoAccountId($mergeIntoAccountId) 107 | { 108 | $this->mergeIntoAccountId = $mergeIntoAccountId; 109 | } 110 | } -------------------------------------------------------------------------------- /src/Model/RestApiModel.php: -------------------------------------------------------------------------------- 1 | rawData = $rawData; 45 | 46 | if (isset($rawData['_links'])) $this->links = $rawData['_links']; 47 | if (isset($rawData['id'])) $this->id = $rawData['id']; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getId() 54 | { 55 | return $this->id; 56 | } 57 | 58 | /** 59 | * @param string $id 60 | */ 61 | public function setId($id) 62 | { 63 | $this->id = $id; 64 | } 65 | 66 | /** 67 | * @return ApiClient 68 | */ 69 | public function getApiClient() 70 | { 71 | return $this->apiClient; 72 | } 73 | 74 | /** 75 | * @param ApiClient $apiClient 76 | */ 77 | public function setApiClient($apiClient) 78 | { 79 | $this->apiClient = $apiClient; 80 | } 81 | 82 | /** 83 | * @return array 84 | */ 85 | public function getLinks() 86 | { 87 | return $this->links; 88 | } 89 | 90 | /** 91 | * @param array $links 92 | */ 93 | public function setLinks($links) 94 | { 95 | $this->links = $links; 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getPagingToken() 102 | { 103 | return $this->pagingToken; 104 | } 105 | 106 | /** 107 | * @param string $pagingToken 108 | */ 109 | public function setPagingToken($pagingToken) 110 | { 111 | $this->pagingToken = $pagingToken; 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | public function getRawData() 118 | { 119 | return $this->rawData; 120 | } 121 | 122 | /** 123 | * @param array $rawData 124 | */ 125 | public function setRawData($rawData) 126 | { 127 | $this->rawData = $rawData; 128 | } 129 | } -------------------------------------------------------------------------------- /tests/Unit/Model/StellarAmountTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, (new StellarAmount('1'))->getScaledValue()); 19 | $this->assertEquals(20.1, (new StellarAmount(20.1))->getScaledValue()); 20 | $this->assertEquals(0.005, (new StellarAmount(0.005))->getScaledValue()); 21 | $this->assertEquals(922337203685.1, (new StellarAmount(922337203685.1))->getScaledValue()); 22 | $this->assertEquals(0, (new StellarAmount(0))->getScaledValue()); 23 | $this->assertEquals(10103.0150003, (new StellarAmount(10103.0150003))->getScaledValue()); 24 | $this->assertEquals(10103.0150003, (new StellarAmount('10103.0150003'))->getScaledValue()); 25 | } 26 | 27 | public function testBigIntegers() 28 | { 29 | // Create with the maximum amount 30 | new StellarAmount(new BigInteger('9223372036854775807')); 31 | // Create with the minimum amount 32 | new StellarAmount(new BigInteger('0')); 33 | 34 | $this->assertEquals(0, (new StellarAmount(new BigInteger('0')))->getScaledValue()); 35 | $this->assertEquals(1, (new StellarAmount(new BigInteger('10000000')))->getScaledValue()); 36 | $this->assertEquals(1.1, (new StellarAmount(new BigInteger('11000000')))->getScaledValue()); 37 | $this->assertEquals(1.0000001, (new StellarAmount(new BigInteger('10000001')))->getScaledValue()); 38 | $this->assertEquals(922337203685.4775807, (new StellarAmount(new BigInteger('9223372036854775807')))->getScaledValue()); 39 | $this->assertEquals(922337203685.4775807, (StellarAmount::newMaximum())->getScaledValue()); 40 | } 41 | 42 | public function testSmallValues() 43 | { 44 | $this->assertEquals(0.00001, (new StellarAmount(0.00001))->getScaledValue()); 45 | // 1 stroop 46 | $this->assertEquals(0.0000001, (new StellarAmount(0.0000001))->getScaledValue()); 47 | } 48 | 49 | /** 50 | * @expectedException \InvalidArgumentException 51 | */ 52 | public function testGetScaledValueTooBig() 53 | { 54 | // Maximum number of XLM that can be held is 922337203685.4775807 55 | $amount = new StellarAmount(922337203686); 56 | 57 | // With throw an exception 58 | $amount->getScaledValue(); 59 | } 60 | 61 | /** 62 | * @expectedException \InvalidArgumentException 63 | */ 64 | public function testExceptionWhenNegative() 65 | { 66 | $amount = new StellarAmount(-1); 67 | } 68 | } -------------------------------------------------------------------------------- /tests/Unit/Xdr/XdrEncoderTest.php: -------------------------------------------------------------------------------- 1 | assertBytesEqual('00 00 00 00 00 00 00 64', XdrEncoder::signedBigInteger64(new BigInteger("100"))); 18 | 19 | // -1 20 | $this->assertBytesEqual('ff ff ff ff ff ff ff ff', XdrEncoder::signedBigInteger64(new BigInteger("-1"))); 21 | 22 | // MAX_INT 23 | $this->assertBytesEqual('00 00 00 00 ff ff ff ff', XdrEncoder::signedBigInteger64(new BigInteger("4294967295"))); 24 | 25 | // MAX_INT + 1 26 | $this->assertBytesEqual('00 00 00 01 00 00 00 00', XdrEncoder::signedBigInteger64(new BigInteger("4294967296"))); 27 | 28 | // max positive signed int64 29 | $this->assertBytesEqual('7f ff ff ff ff ff ff ff', XdrEncoder::signedBigInteger64(new BigInteger("9223372036854775807"))); 30 | 31 | // max negative signed int64 32 | $this->assertBytesEqual('80 00 00 00 00 00 00 00', XdrEncoder::signedBigInteger64(new BigInteger("-9223372036854775808"))); 33 | } 34 | 35 | public function testUnsignedBigInteger64() 36 | { 37 | // 100 38 | $this->assertBytesEqual('00 00 00 00 00 00 00 64', XdrEncoder::unsignedBigInteger64(new BigInteger("100"))); 39 | 40 | // 18446744073709551615 41 | $this->assertBytesEqual('ff ff ff ff ff ff ff ff', XdrEncoder::unsignedBigInteger64(new BigInteger("18446744073709551615"))); 42 | 43 | // MAX_INT 44 | $this->assertBytesEqual('00 00 00 00 ff ff ff ff', XdrEncoder::unsignedBigInteger64(new BigInteger("4294967295"))); 45 | 46 | // MAX_INT + 1 47 | $this->assertBytesEqual('00 00 00 01 00 00 00 00', XdrEncoder::unsignedBigInteger64(new BigInteger("4294967296"))); 48 | 49 | // max positive signed int64 50 | $this->assertBytesEqual('7f ff ff ff ff ff ff ff', XdrEncoder::unsignedBigInteger64(new BigInteger("9223372036854775807"))); 51 | } 52 | 53 | public function testOpaqueVariable() 54 | { 55 | // Test padding is applied when characters are not a multiple of 4 56 | $this->assertBytesEqual('00 00 00 05 31 32 33 34 35 00 00 00', XdrEncoder::opaqueVariable('12345')); 57 | } 58 | 59 | 60 | protected function assertBytesEqual($expectedString, $actualBytes) 61 | { 62 | $expectedBytes = ''; 63 | $rawExpected = explode(' ', $expectedString); 64 | foreach ($rawExpected as $raw) { 65 | $expectedBytes .= hex2bin($raw); 66 | } 67 | 68 | $this->assertEquals(hash('sha256', $expectedBytes), hash('sha256', $actualBytes)); 69 | } 70 | } -------------------------------------------------------------------------------- /src/XdrModel/Operation/ManageDataOp.php: -------------------------------------------------------------------------------- 1 | setKey($key); 33 | $this->setValue($value); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function toXdr() 40 | { 41 | $bytes = parent::toXdr(); 42 | 43 | // Key 44 | $bytes .= XdrEncoder::string($this->key, 64); 45 | 46 | // Value (an optional field) 47 | if ($this->value) { 48 | $bytes .= XdrEncoder::boolean(true); 49 | $bytes .= XdrEncoder::opaqueVariable($this->value); 50 | } 51 | else { 52 | $bytes .= XdrEncoder::boolean(false); 53 | } 54 | 55 | return $bytes; 56 | } 57 | 58 | /** 59 | * @deprecated Do not call this directly, instead call Operation::fromXdr() 60 | * @param XdrBuffer $xdr 61 | * @return ManageDataOp|Operation 62 | * @throws \ErrorException 63 | */ 64 | public static function fromXdr(XdrBuffer $xdr) 65 | { 66 | $key = $xdr->readString(64); 67 | 68 | $value = null; 69 | if ($xdr->readBoolean()) { 70 | $value = $xdr->readOpaqueVariable(64); 71 | } 72 | 73 | return new ManageDataOp($key, $value); 74 | } 75 | 76 | /** 77 | * @return string 78 | */ 79 | public function getKey() 80 | { 81 | return $this->key; 82 | } 83 | 84 | /** 85 | * @param string $key 86 | */ 87 | public function setKey($key) 88 | { 89 | if (strlen($key) > 64) throw new \InvalidArgumentException('key cannot be longer than 64 characters'); 90 | 91 | $this->key = $key; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getValue() 98 | { 99 | return $this->value; 100 | } 101 | 102 | /** 103 | * @param string $value 104 | */ 105 | public function setValue($value = null) 106 | { 107 | if (strlen($value) > 64) throw new \InvalidArgumentException('value cannot be longer than 64 characters'); 108 | 109 | $this->value = $value; 110 | } 111 | } -------------------------------------------------------------------------------- /src/XdrModel/TimeBounds.php: -------------------------------------------------------------------------------- 1 | minTime = $minTime; 41 | $this->maxTime = $maxTime; 42 | } 43 | 44 | /** 45 | * @param \DateTime $min 46 | */ 47 | public function setMinTime(\DateTime $min) 48 | { 49 | $this->minTime = clone $min; 50 | } 51 | 52 | /** 53 | * @param \DateTime $max 54 | */ 55 | public function setMaxTime(\DateTime $max) 56 | { 57 | $this->maxTime = clone $max; 58 | } 59 | 60 | public function toXdr() 61 | { 62 | $bytes = ''; 63 | 64 | $bytes .= XdrEncoder::unsignedInteger64($this->getMinTimestamp()); 65 | $bytes .= XdrEncoder::unsignedInteger64($this->getMaxTimestamp()); 66 | 67 | return $bytes; 68 | } 69 | 70 | /** 71 | * @param XdrBuffer $xdr 72 | * @return null|TimeBounds 73 | * @throws \ErrorException 74 | */ 75 | public static function fromXdr(XdrBuffer $xdr) 76 | { 77 | $model = new TimeBounds(); 78 | $model->minTime = \DateTime::createFromFormat('U', $xdr->readUnsignedInteger64()); 79 | $model->maxTime = \DateTime::createFromFormat('U', $xdr->readUnsignedInteger64()); 80 | 81 | return $model; 82 | } 83 | 84 | /** 85 | * @return bool 86 | */ 87 | public function isEmpty() 88 | { 89 | return $this->minTime === null && $this->maxTime === null; 90 | } 91 | 92 | /** 93 | * @return \DateTime 94 | */ 95 | public function getMinTime() 96 | { 97 | return $this->minTime; 98 | } 99 | 100 | /** 101 | * @return \DateTime 102 | */ 103 | public function getMaxTime() 104 | { 105 | return $this->maxTime; 106 | } 107 | 108 | /** 109 | * @return int 110 | */ 111 | public function getMinTimestamp() 112 | { 113 | return $this->minTime->format('U'); 114 | } 115 | 116 | /** 117 | * @return int 118 | */ 119 | public function getMaxTimestamp() 120 | { 121 | return $this->maxTime->format('U'); 122 | } 123 | } -------------------------------------------------------------------------------- /src/XdrModel/AccountId.php: -------------------------------------------------------------------------------- 1 | getPublicKey(); 59 | } 60 | 61 | $this->accountIdString = $accountIdString; 62 | $this->accountIdBytes = AddressableKey::getRawBytesFromBase32AccountId($accountIdString); 63 | 64 | $this->keyType = 0; 65 | } 66 | 67 | /** 68 | * @return string|Keypair 69 | */ 70 | public function getAccountIdString() 71 | { 72 | return $this->accountIdString; 73 | } 74 | 75 | public function toXdr() 76 | { 77 | $bytes = ""; 78 | 79 | $bytes .= XdrEncoder::unsignedInteger(self::KEY_TYPE_ED25519); 80 | $bytes .= XdrEncoder::opaqueFixed($this->accountIdBytes); 81 | 82 | return $bytes; 83 | } 84 | 85 | /** 86 | * @param XdrBuffer $xdr 87 | * @return AccountId 88 | * @throws \ErrorException 89 | */ 90 | public static function fromXdr(XdrBuffer $xdr) 91 | { 92 | $keyType = $xdr->readUnsignedInteger(); 93 | $accountIdBytes = $xdr->readOpaqueFixed(32); 94 | $accountIdString = AddressableKey::addressFromRawBytes($accountIdBytes); 95 | 96 | $model = new AccountId($accountIdString); 97 | 98 | $model->keyType = $keyType; 99 | 100 | return $model; 101 | } 102 | 103 | /** 104 | * @return int 105 | */ 106 | public function getKeyType() 107 | { 108 | return $this->keyType; 109 | } 110 | } -------------------------------------------------------------------------------- /src/Model/ChangeTrustOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 43 | 44 | return $object; 45 | } 46 | 47 | /** 48 | * @param $id 49 | * @param $type 50 | */ 51 | public function __construct($id, $type) 52 | { 53 | parent::__construct($id, Operation::TYPE_CHANGE_TRUST); 54 | } 55 | 56 | /** 57 | * @param $rawData 58 | */ 59 | public function loadFromRawResponseData($rawData) 60 | { 61 | parent::loadFromRawResponseData($rawData); 62 | 63 | $this->trusteeAccountId = $rawData['trustee']; 64 | $this->trustorAccountId = $rawData['trustor']; 65 | 66 | $this->asset = new AssetAmount($rawData['limit'], $rawData['asset_type']); 67 | $this->asset->setAssetIssuerAccountId($rawData['asset_issuer']); 68 | $this->asset->setAssetCode($rawData['asset_code']); 69 | } 70 | 71 | /** 72 | * @return AssetAmount 73 | */ 74 | public function getAsset() 75 | { 76 | return $this->asset; 77 | } 78 | 79 | /** 80 | * @param AssetAmount $asset 81 | */ 82 | public function setAsset($asset) 83 | { 84 | $this->asset = $asset; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | public function getTrusteeAccountId() 91 | { 92 | return $this->trusteeAccountId; 93 | } 94 | 95 | /** 96 | * @param string $trusteeAccountId 97 | */ 98 | public function setTrusteeAccountId($trusteeAccountId) 99 | { 100 | $this->trusteeAccountId = $trusteeAccountId; 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | public function getTrustorAccountId() 107 | { 108 | return $this->trustorAccountId; 109 | } 110 | 111 | /** 112 | * @param string $trustorAccountId 113 | */ 114 | public function setTrustorAccountId($trustorAccountId) 115 | { 116 | $this->trustorAccountId = $trustorAccountId; 117 | } 118 | } -------------------------------------------------------------------------------- /src/XdrModel/Operation/ChangeTrustOp.php: -------------------------------------------------------------------------------- 1 | asset = $asset; 44 | $this->setLimit($limit); 45 | } 46 | 47 | public function toXdr() 48 | { 49 | $bytes = parent::toXdr(); 50 | 51 | $bytes .= $this->asset->toXdr(); 52 | $bytes .= XdrEncoder::signedBigInteger64($this->limit->getUnscaledBigInteger()); 53 | 54 | return $bytes; 55 | } 56 | 57 | /** 58 | * @deprecated Do not call this directly, instead call Operation::fromXdr() 59 | * @param XdrBuffer $xdr 60 | * @return ChangeTrustOp|Operation 61 | * @throws \ErrorException 62 | */ 63 | public static function fromXdr(XdrBuffer $xdr) 64 | { 65 | $asset = Asset::fromXdr($xdr); 66 | $limit = StellarAmount::fromXdr($xdr); 67 | 68 | return new ChangeTrustOp($asset, $limit->getUnscaledBigInteger()); 69 | } 70 | 71 | /** 72 | * Sets the limit of the trust line to the maximum amount 73 | */ 74 | public function setMaxLimit() 75 | { 76 | $this->limit = StellarAmount::newMaximum(); 77 | } 78 | 79 | /** 80 | * @return StellarAmount 81 | */ 82 | public function getLimit() 83 | { 84 | return $this->limit; 85 | } 86 | 87 | /** 88 | * @param int|BigInteger $limit int representing lumens or BigInteger representing stroops 89 | */ 90 | public function setLimit($limit) 91 | { 92 | $this->limit = new StellarAmount($limit); 93 | } 94 | 95 | /** 96 | * @return Asset 97 | */ 98 | public function getAsset() 99 | { 100 | return $this->asset; 101 | } 102 | 103 | /** 104 | * @param Asset $asset 105 | */ 106 | public function setAsset($asset) 107 | { 108 | $this->asset = $asset; 109 | } 110 | } -------------------------------------------------------------------------------- /tests/Unit/Xdr/XdrDecoderTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(0, XdrDecoder::unsignedInteger("\x00\x00\x00\x00")); 14 | $this->assertEquals(4294967295, XdrDecoder::unsignedInteger("\xFF\xFF\xFF\xFF")); 15 | 16 | $this->assertEquals(268435456, XdrDecoder::unsignedInteger(XdrEncoder::unsignedInteger(268435456))); 17 | } 18 | 19 | public function testSignedInteger() 20 | { 21 | $this->assertEquals(0, XdrDecoder::signedInteger("\x00\x00\x00\x00")); 22 | $this->assertEquals(1, XdrDecoder::signedInteger("\x00\x00\x00\x01")); 23 | $this->assertEquals(-1, XdrDecoder::signedInteger("\xFF\xFF\xFF\xFF")); 24 | 25 | $this->assertEquals(-1024, XdrDecoder::signedInteger(XdrEncoder::unsignedInteger(-1024))); 26 | } 27 | 28 | public function testUnsignedInteger64() 29 | { 30 | $this->assertEquals(0, XdrDecoder::unsignedInteger64("\x00\x00\x00\x00\x00\x00\x00\x00")); 31 | 32 | $this->assertEquals(4294967295, XdrDecoder::unsignedInteger64("\x00\x00\x00\x00\xFF\xFF\xFF\xFF")); 33 | 34 | $this->assertEquals(72057594000000000, XdrDecoder::unsignedInteger64(XdrEncoder::unsignedInteger64(72057594000000000))); 35 | $this->assertEquals(9223372036854775807, XdrDecoder::unsignedInteger64(XdrEncoder::unsignedInteger64(9223372036854775807))); 36 | } 37 | 38 | public function testSignedInteger64() 39 | { 40 | $this->assertEquals(0, XdrDecoder::signedInteger64("\x00\x00\x00\x00\x00\x00\x00\x00")); 41 | 42 | $this->assertEquals(4294967295, XdrDecoder::signedInteger64("\x00\x00\x00\x00\xFF\xFF\xFF\xFF")); 43 | 44 | $this->assertEquals(-1, XdrDecoder::signedInteger64(XdrEncoder::signedInteger64(-1))); 45 | $this->assertEquals(9223372036854775807, XdrDecoder::signedInteger64(XdrEncoder::signedInteger64(9223372036854775807))); 46 | } 47 | 48 | public function testOpaqueFixed() 49 | { 50 | $this->assertBytesEqual('00 11 22', XdrDecoder::opaqueFixed("\x00\x11\x22\x33\x44\x55", 3)); 51 | } 52 | 53 | public function testOpaqueVariable() 54 | { 55 | $this->assertBytesEqual('00 11 22', XdrDecoder::opaqueVariable("\x00\x00\x00\x03\x00\x11\x22\x00")); 56 | } 57 | 58 | public function testBoolean() 59 | { 60 | $this->assertEquals(true, XdrDecoder::boolean("\x00\x00\x00\x01")); 61 | $this->assertEquals(false, XdrDecoder::boolean("\x00\x00\x00\x00")); 62 | } 63 | 64 | protected function assertBytesEqual($expectedString, $actualBytes) 65 | { 66 | $expectedBytes = ''; 67 | $rawExpected = explode(' ', $expectedString); 68 | foreach ($rawExpected as $raw) { 69 | $expectedBytes .= hex2bin($raw); 70 | } 71 | 72 | $this->assertEquals(hash('sha256', $expectedBytes), hash('sha256', $actualBytes)); 73 | } 74 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | PHP Library for interacting with the Stellar network. 4 | 5 | * Communicate with Horizon server 6 | * Build and sign transactions 7 | 8 | ## :warning: Danger Zone :warning: 9 | 10 | **Development Status** 11 | 12 | This library is under active development and should be considered beta quality. 13 | Please ensure that you've tested extensively on a test network and have added 14 | sanity checks in other places in your code. 15 | 16 | :warning: [See the release notes for breaking changes](CHANGELOG.md) 17 | 18 | **Large Integer Support** 19 | 20 | The largest PHP integer is 64-bits when on a 64-bit platform. This is especially 21 | important to pay attention to when working with large balance transfers. The native 22 | representation of a single XLM (1 XLM) is 10000000 stroops. 23 | 24 | Therefore, if you try to use a `MAX_INT` number of XLM (or a custom asset) it is 25 | possible to overflow PHP's integer type when the value is converted to stroops and 26 | sent to the network. 27 | 28 | This library attempts to add checks for this scenario and also uses a `BigInteger` 29 | class to work around this problem. 30 | 31 | If your application uses large amounts of XLM or a custom asset please do extensive 32 | testing with large values and use the `StellarAmount` helper class or the `BigInteger` 33 | class if possible. 34 | 35 | **Floating point issues** 36 | 37 | Although not specific to Stellar or PHP, it's important to be aware of problems 38 | when doing comparisons between floating point numbers. 39 | 40 | For example: 41 | 42 | ```php 43 | $oldBalance = 1.605; 44 | $newBalance = 1.61; 45 | 46 | var_dump($oldBalance + 0.005); 47 | var_dump($newBalance); 48 | if ($oldBalance + 0.005 === $newBalance) { 49 | print "Equal\n"; 50 | } 51 | else { 52 | print "Not Equal\n"; 53 | } 54 | ``` 55 | 56 | The above code considers the two values not to be equal even though the same value 57 | is printed out: 58 | 59 | Output: 60 | ``` 61 | float(1.61) 62 | float(1.61) 63 | Not Equal 64 | ``` 65 | 66 | To work around this issue, always work with and store amounts as an integer representing stroops. Only convert 67 | back to a decimal number when you need to display a balance to the user. 68 | 69 | The static `StellarAmount::STROOP_SCALE` property can be used to help with this conversion. 70 | 71 | ## Installation 72 | 73 | To install the latest release for usage in your project: 74 | 75 | cd your_project/ 76 | composer require zulucrypto/stellar-api 77 | 78 | If you want to work with the most recent development version you can use this repository: 79 | 80 | git clone https://github.com/zulucrypto/stellar-api.git 81 | cd stellar-api/ 82 | composer install 83 | 84 | ## Getting Started 85 | 86 | See the [getting-started](getting-started/) directory for examples of how to use this library. 87 | 88 | These examples are modeled after the ones in Stellar's getting started guide: 89 | 90 | https://www.stellar.org/developers/guides/get-started/create-account.html 91 | 92 | Additional examples are available in the [examples](examples/) directory 93 | 94 | ## Donations 95 | 96 | Stellar: GCUVDZRQ6CX347AMUUWZDYSNDFAWDN6FUYM5DVYYVO574NHTAUCQAK53 97 | -------------------------------------------------------------------------------- /src/XdrModel/SignerKey.php: -------------------------------------------------------------------------------- 1 | key = $keypair->getPublicKeyBytes(); 42 | 43 | return $signerKey; 44 | } 45 | 46 | /** 47 | * @param $hashBytes 48 | * @return SignerKey 49 | */ 50 | public static function fromPreauthorizedHash($hashBytes) 51 | { 52 | if (strlen($hashBytes) != 32) { 53 | throw new \InvalidArgumentException('$hashBytes must be 32 bytes representing the sha256 hash of the transaction'); 54 | } 55 | 56 | $signerKey = new SignerKey(static::TYPE_PRE_AUTH_TX); 57 | $signerKey->key = $hashBytes; 58 | 59 | return $signerKey; 60 | } 61 | 62 | /** 63 | * Adds the value of $x as a signer on the account 64 | * 65 | * @param $x 66 | * @return SignerKey 67 | */ 68 | public static function fromHashX($x) 69 | { 70 | $signerKey = new SignerKey(static::TYPE_HASH_X); 71 | $signerKey->key = hash('sha256', $x, true); 72 | 73 | return $signerKey; 74 | } 75 | 76 | public function __construct($type) 77 | { 78 | $this->type = $type; 79 | } 80 | 81 | public function toXdr() 82 | { 83 | $bytes = ''; 84 | 85 | // Type 86 | $bytes .= XdrEncoder::unsignedInteger($this->type); 87 | 88 | // Key 89 | $bytes .= XdrEncoder::unsignedInteger256($this->key); 90 | 91 | return $bytes; 92 | } 93 | 94 | /** 95 | * @param XdrBuffer $xdr 96 | * @return SignerKey 97 | * @throws \ErrorException 98 | */ 99 | public static function fromXdr(XdrBuffer $xdr) 100 | { 101 | $type = $xdr->readUnsignedInteger(); 102 | $model = new SignerKey($type); 103 | 104 | $model->key = $xdr->readOpaqueFixed(32); 105 | 106 | return $model; 107 | } 108 | 109 | /** 110 | * @return int 111 | */ 112 | public function getType() 113 | { 114 | return $this->type; 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getKey() 121 | { 122 | return $this->key; 123 | } 124 | } -------------------------------------------------------------------------------- /src/Model/CreateAccountOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 37 | 38 | return $object; 39 | } 40 | 41 | public function __construct($id, $type) 42 | { 43 | parent::__construct($id, Operation::TYPE_CREATE_ACCOUNT); 44 | } 45 | 46 | /** 47 | * @param $rawData 48 | */ 49 | public function loadFromRawResponseData($rawData) 50 | { 51 | parent::loadFromRawResponseData($rawData); 52 | 53 | $this->newAccountId = $rawData['account']; 54 | $this->fundingAccountId = $rawData['funder']; 55 | 56 | $this->startingBalance = new AssetAmount($rawData['starting_balance']); 57 | } 58 | 59 | public function getAssetTransferType() 60 | { 61 | return $this->type; 62 | } 63 | 64 | public function getFromAccountId() 65 | { 66 | return $this->fundingAccountId; 67 | } 68 | 69 | public function getToAccountId() 70 | { 71 | return $this->newAccountId; 72 | } 73 | 74 | public function getAssetAmount() 75 | { 76 | return $this->startingBalance; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getNewAccountId() 83 | { 84 | return $this->newAccountId; 85 | } 86 | 87 | /** 88 | * @param string $newAccountId 89 | */ 90 | public function setNewAccountId($newAccountId) 91 | { 92 | $this->newAccountId = $newAccountId; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getFundingAccountId() 99 | { 100 | return $this->fundingAccountId; 101 | } 102 | 103 | /** 104 | * @param string $fundingAccountId 105 | */ 106 | public function setFundingAccountId($fundingAccountId) 107 | { 108 | $this->fundingAccountId = $fundingAccountId; 109 | } 110 | 111 | /** 112 | * @return AssetAmount 113 | */ 114 | public function getStartingBalance() 115 | { 116 | return $this->startingBalance; 117 | } 118 | 119 | /** 120 | * @param AssetAmount $startingBalance 121 | */ 122 | public function setStartingBalance($startingBalance) 123 | { 124 | $this->startingBalance = $startingBalance; 125 | } 126 | } -------------------------------------------------------------------------------- /tests/Unit/Derivation/Bip39/Bip39Test.php: -------------------------------------------------------------------------------- 1 | assertEquals( 32 | bin2hex($bip39->mnemonicToEntropy($mnemonic)), 33 | $entropyHex 34 | ); 35 | 36 | $this->assertEquals( 37 | bin2hex($bip39->mnemonicToSeedBytesWithErrorChecking($mnemonic, 'TREZOR')), 38 | $seedHex 39 | ); 40 | } 41 | public function mnemonicToEntropyProvider() 42 | { 43 | return [ 44 | [ 45 | 'mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', 46 | 'entropyHex' => '00000000000000000000000000000000', 47 | 'seedHex' => 'c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04', 48 | ], 49 | [ 50 | 'mnemonic' => 'legal winner thank year wave sausage worth useful legal winner thank yellow', 51 | 'entropyHex' => '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f', 52 | 'seedHex' => '2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607', 53 | ], 54 | [ 55 | 'mnemonic' => 'legal winner thank year wave sausage worth useful legal winner thank yellow', 56 | 'entropyHex' => '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f', 57 | 'seedHex' => '2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607', 58 | ], 59 | [ 60 | 'mnemonic' => 'letter advice cage absurd amount doctor acoustic avoid letter advice cage above', 61 | 'entropyHex' => '80808080808080808080808080808080', 62 | 'seedHex' => 'd71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8', 63 | ], 64 | 65 | [ 66 | 'mnemonic' => 'all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform', 67 | 'entropyHex' => '066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad', 68 | 'seedHex' => '26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d', 69 | ], 70 | ]; 71 | } 72 | } -------------------------------------------------------------------------------- /src/XdrModel/Operation/CreateAccountOp.php: -------------------------------------------------------------------------------- 1 | getPublicKey()); 40 | } 41 | if (is_string($sourceAccount)) { 42 | $sourceAccount = new AccountId($sourceAccount); 43 | } 44 | 45 | parent::__construct( Operation::TYPE_CREATE_ACCOUNT, $sourceAccount); 46 | 47 | $this->newAccount = $newAccount; 48 | $this->startingBalance = new StellarAmount($startingBalance); 49 | } 50 | 51 | /** 52 | * @return string XDR bytes 53 | */ 54 | public function toXdr() 55 | { 56 | $bytes = parent::toXdr(); 57 | 58 | $bytes .= $this->newAccount->toXdr(); 59 | $bytes .= XdrEncoder::signedBigInteger64($this->startingBalance->getUnscaledBigInteger()); 60 | 61 | return $bytes; 62 | } 63 | 64 | /** 65 | * @param XdrBuffer $xdr 66 | * @return CreateAccountOp|Operation 67 | * @throws \ErrorException 68 | */ 69 | public static function fromXdr(XdrBuffer $xdr) 70 | { 71 | $newAccountId = AccountId::fromXdr($xdr); 72 | $startingBalance = new BigInteger($xdr->readInteger64()); 73 | 74 | return new CreateAccountOp($newAccountId, $startingBalance); 75 | } 76 | 77 | /** 78 | * @return AccountId 79 | */ 80 | public function getNewAccount() 81 | { 82 | return $this->newAccount; 83 | } 84 | 85 | /** 86 | * @param AccountId $newAccount 87 | */ 88 | public function setNewAccount($newAccount) 89 | { 90 | $this->newAccount = $newAccount; 91 | } 92 | 93 | /** 94 | * @return StellarAmount 95 | */ 96 | public function getStartingBalance() 97 | { 98 | return $this->startingBalance; 99 | } 100 | 101 | /** 102 | * @param StellarAmount $startingBalance 103 | */ 104 | public function setStartingBalance($startingBalance) 105 | { 106 | $this->startingBalance = $startingBalance; 107 | } 108 | } -------------------------------------------------------------------------------- /src/Model/AllowTrustOperation.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 50 | 51 | return $object; 52 | } 53 | 54 | /** 55 | * @param $id 56 | * @param $type 57 | */ 58 | public function __construct($id, $type) 59 | { 60 | parent::__construct($id, Operation::TYPE_ALLOW_TRUST); 61 | } 62 | 63 | /** 64 | * @param $rawData 65 | */ 66 | public function loadFromRawResponseData($rawData) 67 | { 68 | parent::loadFromRawResponseData($rawData); 69 | 70 | $this->trusteeAccountId = $rawData['trustee']; 71 | $this->trustorAccountId = $rawData['trustor']; 72 | $this->isAuthorized = $rawData['authorize']; 73 | 74 | $this->asset = new AssetAmount($rawData['limit'], $rawData['asset_type']); 75 | $this->asset->setAssetIssuerAccountId($rawData['asset_issuer']); 76 | $this->asset->setAssetCode($rawData['asset_code']); 77 | } 78 | 79 | /** 80 | * @return AssetAmount 81 | */ 82 | public function getAsset() 83 | { 84 | return $this->asset; 85 | } 86 | 87 | /** 88 | * @param AssetAmount $asset 89 | */ 90 | public function setAsset($asset) 91 | { 92 | $this->asset = $asset; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getTrusteeAccountId() 99 | { 100 | return $this->trusteeAccountId; 101 | } 102 | 103 | /** 104 | * @param string $trusteeAccountId 105 | */ 106 | public function setTrusteeAccountId($trusteeAccountId) 107 | { 108 | $this->trusteeAccountId = $trusteeAccountId; 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | public function getTrustorAccountId() 115 | { 116 | return $this->trustorAccountId; 117 | } 118 | 119 | /** 120 | * @param string $trustorAccountId 121 | */ 122 | public function setTrustorAccountId($trustorAccountId) 123 | { 124 | $this->trustorAccountId = $trustorAccountId; 125 | } 126 | 127 | /** 128 | * @return bool 129 | */ 130 | public function isAuthorized() 131 | { 132 | return $this->isAuthorized; 133 | } 134 | 135 | /** 136 | * @param bool $isAuthorized 137 | */ 138 | public function setIsAuthorized($isAuthorized) 139 | { 140 | $this->isAuthorized = $isAuthorized; 141 | } 142 | } -------------------------------------------------------------------------------- /tests/HardwareWallet/CreatePassiveOfferOpTest.php: -------------------------------------------------------------------------------- 1 | mnemonic); 21 | $sellingAsset = Asset::newNativeAsset(); 22 | $buyingAsset = Asset::newCustomAsset('USD', Keypair::newFromMnemonic($this->mnemonic, 'usd issuer')); 23 | 24 | $amount = 200; 25 | $price = new Price(674614, 1000000); 26 | 27 | $operation = new CreatePassiveOfferOp($sellingAsset, $buyingAsset, $amount, $price); 28 | 29 | $transaction = $this->horizonServer 30 | ->buildTransaction($sourceKeypair) 31 | ->setSequenceNumber(new BigInteger(4294967296)) 32 | ->addOperation($operation); 33 | $knownSignature = $transaction->signWith($this->privateKeySigner); 34 | 35 | $this->manualVerificationOutput(join(PHP_EOL, [ 36 | base64_encode($transaction->toXdr()), 37 | 'Passive Offer: new offer', 38 | ' Source: ' . $sourceKeypair->getPublicKey(), 39 | ' Selling: ' . $amount . 'XLM', 40 | ' Buying: ' . 'For ' . $price . ' per ' . $buyingAsset->getAssetCode() . ' (' . $buyingAsset->getIssuer()->getAccountIdString() . ')', 41 | '', 42 | 'B64 Transaction: ' . base64_encode($transaction->toXdr()), 43 | ' Signature: ' . $knownSignature->getWithoutHintBase64(), 44 | ])); 45 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 46 | 47 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 48 | } 49 | 50 | /** 51 | * @group requires-hardwarewallet 52 | */ 53 | public function testDeletePassiveOffer() 54 | { 55 | $sourceKeypair = Keypair::newFromMnemonic($this->mnemonic); 56 | $sellingAsset = Asset::newNativeAsset(); 57 | $buyingAsset = Asset::newCustomAsset('USD', Keypair::newFromMnemonic($this->mnemonic, 'usd issuer')); 58 | 59 | $amount = 0; 60 | $price = new Price(674614, 1000000); 61 | 62 | $operation = new CreatePassiveOfferOp($sellingAsset, $buyingAsset, $amount, $price); 63 | 64 | $transaction = $this->horizonServer 65 | ->buildTransaction($sourceKeypair) 66 | ->setSequenceNumber(new BigInteger(4294967296)) 67 | ->addOperation($operation); 68 | $knownSignature = $transaction->signWith($this->privateKeySigner); 69 | 70 | $this->manualVerificationOutput(join(PHP_EOL, [ 71 | base64_encode($transaction->toXdr()), 72 | 'Passive Offer: delete offer', 73 | ' Source: ' . $sourceKeypair->getPublicKey(), 74 | ' Selling: ' . $amount . 'XLM', 75 | ' Buying: ' . 'For ' . $price . ' per ' . $buyingAsset->getAssetCode() . ' (' . $buyingAsset->getIssuer()->getAccountIdString() . ')', 76 | ])); 77 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 78 | 79 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 80 | } 81 | } -------------------------------------------------------------------------------- /tests/Util/HardwareWalletIntegrationTest.php: -------------------------------------------------------------------------------- 1 | mnemonic = 'illness spike retreat truth genius clock brain pass fit cave bargain toe'; 49 | 50 | // Mnemonic to match trezor-python test suite 51 | // GAK5MSF74TJW6GLM7NLTL76YZJKM2S4CGP3UH4REJHPHZ4YBZW2GSBPW 52 | // SDE2YU4V2IYSJIUH7MONDYZTSSLDXV5QDEGUUOLCU4TK7CZWTAXZ5CEG 53 | $this->mnemonic = 'alcohol woman abuse must during monitor noble actual mixed trade anger aisle'; 54 | 55 | 56 | $this->privateKeySigner = new PrivateKeySigner(Keypair::newFromMnemonic($this->mnemonic)); 57 | } 58 | 59 | public function setUp() 60 | { 61 | parent::setUp(); 62 | 63 | $signingProvider = getenv('STELLAR_SIGNING_PROVIDER'); 64 | if (!$signingProvider) { 65 | printf('STELLAR_SIGNING_PROVIDER must be defined' . PHP_EOL); 66 | printf('For example: ' . PHP_EOL); 67 | printf('export STELLAR_SIGNING_PROVIDER=trezor' . PHP_EOL); 68 | die(); 69 | } 70 | 71 | if ($signingProvider == 'trezor') { 72 | $signer = new TrezorSigner(); 73 | $trezorBinPath = getenv('TREZOR_BIN_PATH'); 74 | if ($trezorBinPath) { 75 | $signer->setTrezorBinPath($trezorBinPath); 76 | } 77 | 78 | // Set the public key of the signer to the default to prevent 79 | // unnecessary calls to the hardware wallet to retrieve the public key 80 | $signer->setPublicKey(Keypair::newFromMnemonic($this->mnemonic)); 81 | 82 | $this->horizonServer->setSigningProvider($signer); 83 | } 84 | else { 85 | die('Unsupported STELLAR_SIGNING_PROVIDER'); 86 | } 87 | } 88 | 89 | /** 90 | * Immediately writes output so the tester can verify the correct information 91 | * is displayed on the hardware wallet 92 | * 93 | * @param $message 94 | */ 95 | public function manualVerificationOutput($message) 96 | { 97 | print PHP_EOL . $message . PHP_EOL; 98 | 99 | ob_flush(); 100 | } 101 | } -------------------------------------------------------------------------------- /src/Model/Effect.php: -------------------------------------------------------------------------------- 1 | loadFromRawResponseData($rawData); 48 | 49 | return $object; 50 | } 51 | 52 | /** 53 | * Operation type 54 | * 55 | * @param $type 56 | */ 57 | public function __construct($type) 58 | { 59 | $this->type = $type; 60 | 61 | $this->extraData = []; 62 | } 63 | 64 | public function __toString() 65 | { 66 | return sprintf('%s:%s', $this->type, $this->id); 67 | } 68 | 69 | /** 70 | * @param $rawData 71 | */ 72 | public function loadFromRawResponseData($rawData) 73 | { 74 | parent::loadFromRawResponseData($rawData); 75 | 76 | // The fields in the Effect response differ slightly from those available 77 | // in the Operation, so known fields are stored in this class and everything 78 | // else goes in extraData 79 | $knownFields = ['_links', 'id', 'type', 'type_i']; 80 | 81 | if (isset($rawData['type'])) $this->type = $rawData['type']; 82 | if (isset($rawData['type_i'])) $this->typeI = $rawData['type_i']; 83 | 84 | foreach ($rawData as $key => $value) { 85 | if (in_array($key, $knownFields)) continue; 86 | 87 | $this->extraData[$key] = $value; 88 | } 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | public function getType() 95 | { 96 | return $this->type; 97 | } 98 | 99 | /** 100 | * @param string $type 101 | */ 102 | public function setType($type) 103 | { 104 | $this->type = $type; 105 | } 106 | 107 | /** 108 | * @return int 109 | */ 110 | public function getTypeI() 111 | { 112 | return $this->typeI; 113 | } 114 | 115 | /** 116 | * @param int $typeI 117 | */ 118 | public function setTypeI($typeI) 119 | { 120 | $this->typeI = $typeI; 121 | } 122 | 123 | /** 124 | * @return Operation 125 | */ 126 | public function getOperation() 127 | { 128 | return $this->operation; 129 | } 130 | 131 | /** 132 | * @param Operation $operation 133 | */ 134 | public function setOperation(Operation $operation) 135 | { 136 | $this->operation = $operation; 137 | } 138 | 139 | /** 140 | * @return array 141 | */ 142 | public function getExtraData() 143 | { 144 | return $this->extraData; 145 | } 146 | 147 | /** 148 | * @param array $extraData 149 | */ 150 | public function setExtraData($extraData) 151 | { 152 | $this->extraData = $extraData; 153 | } 154 | } -------------------------------------------------------------------------------- /src/XdrModel/OperationResult.php: -------------------------------------------------------------------------------- 1 | readInteger(); 30 | 31 | // Early return for operations that failed 32 | if ($opResultCode === -1) { 33 | $opResult = new OperationResult(); 34 | $opResult->errorCode = static::BAD_AUTH; 35 | 36 | return $opResult; 37 | } 38 | if ($opResultCode === -2) { 39 | $opResult = new OperationResult(); 40 | $opResult->errorCode = static::NO_ACCOUNT; 41 | 42 | return $opResult; 43 | } 44 | 45 | // Past this point there can be a variety of operation results 46 | $opType = $xdr->readInteger(); 47 | switch ($opType) { 48 | case Operation::TYPE_CREATE_ACCOUNT: 49 | return CreateAccountResult::fromXdr($xdr); 50 | break; 51 | case Operation::TYPE_PAYMENT: 52 | return PaymentResult::fromXdr($xdr); 53 | break; 54 | case Operation::TYPE_PATH_PAYMENT; 55 | return PathPaymentResult::fromXdr($xdr); 56 | break; 57 | case Operation::TYPE_MANAGE_OFFER: 58 | return ManageOfferResult::fromXdr($xdr); 59 | break; 60 | case Operation::TYPE_SET_OPTIONS: 61 | return SetOptionsResult::fromXdr($xdr); 62 | break; 63 | case Operation::TYPE_CHANGE_TRUST: 64 | return ChangeTrustResult::fromXdr($xdr); 65 | break; 66 | case Operation::TYPE_ALLOW_TRUST: 67 | return AllowTrustResult::fromXdr($xdr); 68 | break; 69 | case Operation::TYPE_ACCOUNT_MERGE: 70 | return AccountMergeResult::fromXdr($xdr); 71 | break; 72 | case Operation::TYPE_INFLATION: 73 | return InflationResult::fromXdr($xdr); 74 | break; 75 | case Operation::TYPE_MANAGE_DATA: 76 | return ManageDataResult::fromXdr($xdr); 77 | break; 78 | case Operation::TYPE_BUMP_SEQUENCE: 79 | return BumpSequenceResult::fromXdr($xdr); 80 | break; 81 | default: 82 | throw new \ErrorException(sprintf('Unknown operation type: %s', $opType)); 83 | } 84 | } 85 | 86 | public function __construct() 87 | { 88 | $this->errorCode = null; 89 | } 90 | 91 | /** 92 | * Returns true if this operation succeeded 93 | * 94 | * @return bool 95 | */ 96 | public function succeeded() 97 | { 98 | return $this->errorCode === null; 99 | } 100 | 101 | /** 102 | * Returns true if this operation failed 103 | * 104 | * @return bool 105 | */ 106 | public function failed() 107 | { 108 | return !$this->succeeded(); 109 | } 110 | 111 | /** 112 | * @return null|string 113 | */ 114 | public function getErrorCode() 115 | { 116 | return $this->errorCode; 117 | } 118 | } -------------------------------------------------------------------------------- /tests/integration/TransactionBuilderTest.php: -------------------------------------------------------------------------------- 1 | fixtureAccounts['basic1']; 26 | $destinationKeypair = $this->fixtureAccounts['basic2']; 27 | 28 | $response = $this->horizonServer->buildTransaction($sourceKeypair) 29 | ->addLumenPayment($destinationKeypair, 3) 30 | ->submit($sourceKeypair); 31 | 32 | // All operations should have succeeded 33 | $result = $response->getResult(); 34 | 35 | $this->assertTrue($result->succeeded()); 36 | $this->assertCount(1, $result->getOperationResults()); 37 | } 38 | 39 | /** 40 | * Helper method to extract OperationResult XDR for writing other tests / debugging 41 | * 42 | * @group requires-integrationnet 43 | */ 44 | public function testGetXdr() 45 | { 46 | $this->markTestSkipped('For debugging'); 47 | 48 | $sourceKeypair = $this->fixtureAccounts['basic1']; 49 | $destinationKeypair = $this->fixtureAccounts['basic2']; 50 | $usdIssuingKeypair = Keypair::newFromSeed('SBJXZEVYRX244HKDY6L5JZYPWDQW6D3WLEE3PTMQM4CSUKGE37J4AC3W'); 51 | $usdBankKeypair = Keypair::newFromSeed('SDJOXTS4TE3Q3HUIFQK5AQCTRML6HIOUQIXDLCEQHICOFHU5CQN6DBLS'); 52 | $authRequiredIssuingKeypair = Keypair::newFromSeed('SABFYGWPSP3EEJ2EURHQYAIRTNK3SVQPED5PWOHGCWKPZBSCWBV4QGKE'); 53 | 54 | $usdAsset = Asset::newCustomAsset('USDTEST', $usdIssuingKeypair->getPublicKey()); 55 | $authRequiredAsset = Asset::newCustomAsset('AUTHREQ', $authRequiredIssuingKeypair->getPublicKey()); 56 | 57 | //$op = new ManageOfferOp($usdAsset, Asset::newNativeAsset(), 0, new Price(3, 1), 8); 58 | 59 | $op = new ManageDataOp('asdf', 'jkl'); 60 | 61 | $response = $this->horizonServer->buildTransaction($sourceKeypair) 62 | ->addOperation($op) 63 | ->submit($sourceKeypair); 64 | 65 | $rawData = $response->getRawData(); 66 | $xdrB64 = $rawData['result_xdr']; 67 | $xdr = base64_decode($xdrB64); 68 | 69 | $xdr = substr($xdr,8 + 4 + 4); 70 | print "XDR: \n"; 71 | print base64_encode($xdr); 72 | } 73 | 74 | /** 75 | * @group requires-integrationnet 76 | */ 77 | public function testFailedTransactionResultSingleOp() 78 | { 79 | $sourceKeypair = $this->fixtureAccounts['basic1']; 80 | $destinationKeypair = $this->fixtureAccounts['basic2']; 81 | 82 | // This should fail since the source account doesn't have enough funds 83 | try { 84 | $response = $this->horizonServer->buildTransaction($sourceKeypair) 85 | ->addLumenPayment($destinationKeypair, 99999) 86 | ->submit($sourceKeypair); 87 | 88 | $this->fail('Exception was expected'); 89 | } 90 | catch (PostTransactionException $ex) { 91 | $result = $ex->getResult(); 92 | $opResults = $result->getOperationResults(); 93 | $this->assertCount(1, $opResults); 94 | $this->assertEquals('payment_underfunded', $opResults[0]->getErrorCode()); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /tests/HardwareWallet/ManageDataOpTest.php: -------------------------------------------------------------------------------- 1 | mnemonic); 20 | 21 | $dataKey = 'test data'; 22 | $dataValue = 'asdf'; 23 | 24 | $transaction = $this->horizonServer 25 | ->buildTransaction($sourceKeypair) 26 | ->setSequenceNumber(new BigInteger(4294967296)) 27 | ->setAccountData($dataKey, $dataValue); 28 | $knownSignature = $transaction->signWith($this->privateKeySigner); 29 | 30 | $this->manualVerificationOutput(join(PHP_EOL, [ 31 | 'Manage Data: set data', 32 | ' Source: ' . $sourceKeypair->getPublicKey(), 33 | ' Key: ' . $dataKey, 34 | ' Value Hash: ' . hash('sha256', $dataValue), 35 | '', 36 | 'B64 Transaction: ' . base64_encode($transaction->toXdr()), 37 | ' Signature: ' . $knownSignature->getWithoutHintBase64(), 38 | ])); 39 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 40 | 41 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 42 | } 43 | 44 | /** 45 | * @group requires-hardwarewallet 46 | */ 47 | public function testSetMaximumLengthDataValue() 48 | { 49 | $sourceKeypair = Keypair::newFromMnemonic($this->mnemonic); 50 | 51 | $dataKey = join('', [ 52 | 'abcdefghijklmnop', 53 | 'abcdefghijklmnop', 54 | 'abcdefghijklmnop', 55 | 'abcdefghijklmnop', 56 | ]); 57 | 58 | $dataValue = random_bytes(64); 59 | 60 | $transaction = $this->horizonServer 61 | ->buildTransaction($sourceKeypair) 62 | ->setSequenceNumber(new BigInteger(4294967296)) 63 | ->setAccountData($dataKey, $dataValue); 64 | $knownSignature = $transaction->signWith($this->privateKeySigner); 65 | 66 | $this->manualVerificationOutput(join(PHP_EOL, [ 67 | 'Manage Data: set data (maximum length key and value)', 68 | ' Source: ' . $sourceKeypair->getPublicKey(), 69 | ' Key: ' . $dataKey, 70 | ' Value Hash: ' . hash('sha256', $dataValue), 71 | ])); 72 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 73 | 74 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 75 | } 76 | 77 | /** 78 | * @group requires-hardwarewallet 79 | */ 80 | public function testClearDataValue() 81 | { 82 | $sourceKeypair = Keypair::newFromMnemonic($this->mnemonic); 83 | 84 | $dataKey = 'test data'; 85 | $dataValue = null; 86 | 87 | $transaction = $this->horizonServer 88 | ->buildTransaction($sourceKeypair) 89 | ->setSequenceNumber(new BigInteger(4294967296)) 90 | ->setAccountData($dataKey, $dataValue); 91 | $knownSignature = $transaction->signWith($this->privateKeySigner); 92 | 93 | $this->manualVerificationOutput(join(PHP_EOL, [ 94 | 'Manage Data: clear data', 95 | ' Source: ' . $sourceKeypair->getPublicKey(), 96 | ' Key: ' . $dataKey, 97 | ' Value Hash: ' . hash('sha256', $dataValue), 98 | ])); 99 | $hardwareSignature = $transaction->signWith($this->horizonServer->getSigningProvider()); 100 | 101 | $this->assertEquals($knownSignature->toBase64(), $hardwareSignature->toBase64()); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Transaction/Transaction.php: -------------------------------------------------------------------------------- 1 | sourceAccountId = AccountId::fromXdr($xdr); 62 | $tx->feePaid = new StellarAmount($xdr->readUnsignedInteger()); 63 | $tx->sequenceNumber = $xdr->readBigInteger(); 64 | 65 | // time bounds are optional 66 | if ($xdr->readBoolean()) { 67 | $tx->timeBounds = TimeBounds::fromXdr($xdr); 68 | } 69 | 70 | $tx->memo = Memo::fromXdr($xdr); 71 | 72 | $numOperations = $xdr->readUnsignedInteger(); 73 | for ($i=0; $i < $numOperations; $i++) { 74 | $tx->operations[] = Operation::fromXdr($xdr); 75 | } 76 | 77 | // 4 bytes at the end are reserved for future use 78 | $xdr->readOpaqueFixed(4); 79 | 80 | return $tx; 81 | } 82 | 83 | public function __construct() 84 | { 85 | $this->timeBounds = new TimeBounds(); 86 | } 87 | 88 | /** 89 | * @param Server $server 90 | * @return TransactionBuilder 91 | */ 92 | public function toTransactionBuilder(Server $server = null) 93 | { 94 | $builder = new TransactionBuilder($this->sourceAccountId->getAccountIdString()); 95 | 96 | if ($server) { 97 | $builder->setApiClient($server->getApiClient()); 98 | } 99 | 100 | 101 | $builder->setSequenceNumber($this->sequenceNumber); 102 | 103 | if (!$this->timeBounds->isEmpty()) { 104 | $builder->setLowerTimebound($this->timeBounds->getMinTime()); 105 | $builder->setUpperTimebound($this->timeBounds->getMaxTime()); 106 | } 107 | 108 | $builder->setMemo($this->memo); 109 | 110 | foreach ($this->operations as $operation) { 111 | $builder->addOperation($operation); 112 | } 113 | 114 | return $builder; 115 | } 116 | 117 | /** 118 | * @return AccountId 119 | */ 120 | public function getSourceAccountId() 121 | { 122 | return $this->sourceAccountId; 123 | } 124 | 125 | /** 126 | * @return TimeBounds 127 | */ 128 | public function getTimeBounds() 129 | { 130 | return $this->timeBounds; 131 | } 132 | 133 | /** 134 | * @return Memo 135 | */ 136 | public function getMemo() 137 | { 138 | return $this->memo; 139 | } 140 | 141 | /** 142 | * @return Operation[] 143 | */ 144 | public function getOperations() 145 | { 146 | return $this->operations; 147 | } 148 | 149 | /** 150 | * @return BigInteger 151 | */ 152 | public function getSequenceNumber() 153 | { 154 | return $this->sequenceNumber; 155 | } 156 | 157 | /** 158 | * @return StellarAmount 159 | */ 160 | public function getFeePaid() 161 | { 162 | return $this->feePaid; 163 | } 164 | } --------------------------------------------------------------------------------