Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
da77415
Harden SEP-10 challenge validation
christian-rogobete Jun 9, 2026
cdf6aaa
Add SEP-10 challenge error-path unit tests
christian-rogobete Jun 9, 2026
b91325b
Fix signed payload signature hint for payloads shorter than 4 bytes
christian-rogobete Jun 9, 2026
5274553
Fix incorrect error messages and private method typos in operation de…
christian-rogobete Jun 9, 2026
a15068d
Reuse shared BigInteger constants in StellarAmount
christian-rogobete Jun 9, 2026
0385c8d
Cache endianness probe in XdrEncoder
christian-rogobete Jun 9, 2026
ce3afb4
Buffer SSE stream reads in RequestBuilder
christian-rogobete Jun 9, 2026
df30d71
Optimize XDR primitive codec hot paths
christian-rogobete Jun 9, 2026
a1f764f
Deduplicate destinations in checkMemoRequired
christian-rogobete Jun 9, 2026
fd7b5c7
Move live SEP-10 web auth tests to the integration suite
christian-rogobete Jun 9, 2026
bc94478
Add PHPStan static analysis to CI
christian-rogobete Jun 9, 2026
9de4e6f
Add composer audit dependency gate to CI
christian-rogobete Jun 10, 2026
8ad5e5d
Harden SEP-7 URI signature verification
christian-rogobete Jun 10, 2026
0ea3417
Validate key lengths in KeyPair construction
christian-rogobete Jun 10, 2026
a6e8f69
Fix pool share asset type constant value
christian-rogobete Jun 10, 2026
9f93fcc
Add SorobanClient unit tests
christian-rogobete Jun 11, 2026
ab33e69
Poll transaction status immediately with exponential backoff
christian-rogobete Jun 11, 2026
c43893d
Mock the sign-and-send test instead of relying on a live RPC failure
christian-rogobete Jun 11, 2026
f0ff53f
Add error-path unit tests for federation, SEP-31 and stellar.toml loa…
christian-rogobete Jun 11, 2026
42bea10
Allow injecting a SorobanServer into the contract client
christian-rogobete Jun 11, 2026
d309f55
Improve HTTP client injection and error handling in SEP services
christian-rogobete Jun 11, 2026
af187ce
Support the full unsigned 64-bit range for id memos
christian-rogobete Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Dependency Audit

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
audit:
name: composer audit (production)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup PHP
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2.37.1
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, json, bcmath, gmp, pcntl, sodium
coverage: none
tools: composer:v2

# composer.lock is intentionally gitignored (library), so dependencies are
# resolved fresh here; do not add --locked to the audit step.
- name: Install production dependencies
run: composer install --no-dev --prefer-dist --no-progress

# Fail only on security advisories in production dependencies; abandoned
# packages are reported but do not fail the build.
- name: Audit production dependencies
run: composer audit --no-dev --abandoned=report
46 changes: 46 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Static Analysis

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
phpstan:
name: PHPStan
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup PHP
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2.37.1
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, json, bcmath, gmp, pcntl, sodium
coverage: none
tools: composer:v2

- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run PHPStan
run: composer analyse
12 changes: 6 additions & 6 deletions Soneso/StellarSDK/AbstractOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public static function fromXdr(XdrOperation $xdrOp) : AbstractOperation {
}
$type = $body->getType()->getValue();
$result = match ($type) {
XdrOperationType::CREATE_ACCOUNT => self::creatAccountOperation($body),
XdrOperationType::CREATE_ACCOUNT => self::createAccountOperation($body),
XdrOperationType::PAYMENT => self::paymentOperation($body),
XdrOperationType::PATH_PAYMENT_STRICT_RECEIVE => self::pathPaymentStrictReceiveOperation($body),
XdrOperationType::MANAGE_SELL_OFFER => self::manageSellOfferOperation($body),
Expand All @@ -117,7 +117,7 @@ public static function fromXdr(XdrOperation $xdrOp) : AbstractOperation {
XdrOperationType::MANAGE_BUY_OFFER => self::manageBuyOfferOperation($body),
XdrOperationType::PATH_PAYMENT_STRICT_SEND => self::pathPaymentStrictSendOperation($body),
XdrOperationType::CREATE_CLAIMABLE_BALANCE => self::createClaimableBalance($body),
XdrOperationType::CLAIM_CLAIMABLE_BALANCE => self::claimClaimableClaimableBalance($body),
XdrOperationType::CLAIM_CLAIMABLE_BALANCE => self::claimClaimableBalance($body),
XdrOperationType::BEGIN_SPONSORING_FUTURE_RESERVES => self::beginSponsoringFutureReserves($body),
XdrOperationType::END_SPONSORING_FUTURE_RESERVES => new EndSponsoringFutureReservesOperation(),
XdrOperationType::REVOKE_SPONSORSHIP => self::revokeSponsorship($body),
Expand All @@ -140,7 +140,7 @@ private static function restoreFootprint(XdrOperationBody $body) : RestoreFootpr
if ($op !== null) {
return RestoreFootprintOperation::fromXdrOperation($op);
} else {
throw new InvalidArgumentException("missing invoke host function operation in xdr operation body");
throw new InvalidArgumentException("missing restore footprint operation in xdr operation body");
}
}

Expand All @@ -149,7 +149,7 @@ private static function extendFootprintTTL(XdrOperationBody $body) : ExtendFootp
if ($op !== null) {
return ExtendFootprintTTLOperation::fromXdrOperation($op);
} else {
throw new InvalidArgumentException("missing invoke host function operation in xdr operation body");
throw new InvalidArgumentException("missing extend footprint TTL operation in xdr operation body");
}
}

Expand Down Expand Up @@ -225,7 +225,7 @@ private static function beginSponsoringFutureReserves(XdrOperationBody $body) :
}
}

private static function claimClaimableClaimableBalance(XdrOperationBody $body) : ClaimClaimableBalanceOperation {
private static function claimClaimableBalance(XdrOperationBody $body) : ClaimClaimableBalanceOperation {
$op = $body->getClaimClaimableBalanceOperation();
if ($op !== null) {
return ClaimClaimableBalanceOperation::fromXdrOperation($op);
Expand Down Expand Up @@ -288,7 +288,7 @@ private static function accountMerge(XdrOperationBody $body) : AccountMergeOpera
}
}

private static function creatAccountOperation(XdrOperationBody $body) : CreateAccountOperation {
private static function createAccountOperation(XdrOperationBody $body) : CreateAccountOperation {
$caOp = $body->getCreateAccountOp();
if ($caOp !== null) {
return CreateAccountOperation::fromXdrOperation($caOp);
Expand Down
12 changes: 10 additions & 2 deletions Soneso/StellarSDK/Asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ abstract class Asset {
public const TYPE_NATIVE = "native";
public const TYPE_CREDIT_ALPHANUM_4 = "credit_alphanum4";
public const TYPE_CREDIT_ALPHANUM_12 = "credit_alphanum12";
public const TYPE_POOL_SHARE = "liquidty_pool_shares";
public const TYPE_POOL_SHARE = "liquidity_pool_shares";

/**
* Returns the type of this asset
Expand All @@ -59,11 +59,15 @@ public abstract function getType(): string;
/**
* Creates an asset from its type, code, and issuer
*
* @param string $type One of the TYPE_* constants
* Pool share assets cannot be created here because they are composed of two
* assets rather than a code and issuer; construct an AssetTypePoolShare directly.
*
* @param string $type TYPE_NATIVE, TYPE_CREDIT_ALPHANUM_4 or TYPE_CREDIT_ALPHANUM_12
* @param string|null $code Asset code (required for non-native assets)
* @param string|null $issuer Issuer account ID (required for non-native assets)
* @return Asset The created asset
* @throws \RuntimeException If parameters are invalid or type is unsupported
* @see AssetTypePoolShare For liquidity pool share assets
*/
public static function create(string $type, ?string $code = null, ?string $issuer = null) : Asset {
if (Asset::TYPE_NATIVE == $type) {
Expand All @@ -80,6 +84,10 @@ public static function create(string $type, ?string $code = null, ?string $issue
return Asset::createNonNativeAsset($code, $issuer);

}
if (Asset::TYPE_POOL_SHARE == $type) {
throw new \RuntimeException(
"pool share assets are composed of two assets and can not be created from a code and issuer; construct AssetTypePoolShare directly");
}
throw new \RuntimeException("unsupported asset type: " . $type);
}

Expand Down
41 changes: 28 additions & 13 deletions Soneso/StellarSDK/Crypto/KeyPair.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
namespace Soneso\StellarSDK\Crypto;

use Exception;
use InvalidArgumentException;
use SodiumException;
use Soneso\StellarSDK\Constants\StellarConstants;
use Soneso\StellarSDK\MuxedAccount;
use Soneso\StellarSDK\SEP\Derivation\HDNode;
use Soneso\StellarSDK\SEP\Derivation\Mnemonic;
Expand Down Expand Up @@ -88,9 +90,18 @@ class KeyPair
*
* @param string $publicKey Raw 32-byte Ed25519 public key
* @param string|null $privateKey Optional raw 32-byte Ed25519 private key (seed)
* @throws InvalidArgumentException If a key does not consist of exactly 32 bytes
*/
public function __construct(string $publicKey, ?string $privateKey = null)
{
if (strlen($publicKey) !== StellarConstants::ED25519_PUBLIC_KEY_LENGTH_BYTES) {
throw new InvalidArgumentException(
'Public key must be 32 bytes, got ' . strlen($publicKey));
}
if ($privateKey && strlen($privateKey) !== StellarConstants::ED25519_PUBLIC_KEY_LENGTH_BYTES) {
throw new InvalidArgumentException(
'Private key must be 32 bytes, got ' . strlen($privateKey));
}
$this->publicKey = $publicKey;
$this->accountId = StrKey::encodeAccountId($publicKey);
if ($privateKey) {
Expand Down Expand Up @@ -153,8 +164,13 @@ public static function fromSeed(string $seed): KeyPair {
*
* @param string $privateKey Raw 32-byte Ed25519 private key seed
* @return KeyPair A complete keypair derived from the private key
* @throws InvalidArgumentException If the private key does not consist of exactly 32 bytes
*/
public static function fromPrivateKey(string $privateKey): KeyPair{
if (strlen($privateKey) !== StellarConstants::ED25519_PUBLIC_KEY_LENGTH_BYTES) {
throw new InvalidArgumentException(
'Private key must be 32 bytes, got ' . strlen($privateKey));
}
return new KeyPair(StrKey::publicKeyFromPrivateKey($privateKey), $privateKey);
}

Expand Down Expand Up @@ -242,20 +258,19 @@ public function signDecorated(string $value): XdrDecoratedSignature
public function signPayloadDecorated(string $signerPayload): XdrDecoratedSignature
{
$payloadSignature = $this->signDecorated($signerPayload);
$payloadSignatureHint = str_split($payloadSignature->getHint());
$hintArr = str_split($signerPayload,1);
$lenBytes = count($hintArr);
if ($lenBytes >= 4) {
$hintArr = array_slice($hintArr, $lenBytes - 4,4);
} else {
while (count($hintArr) < 4) {
$hintArr[] = 0;
}
}
for ($x = 0; $x < count($hintArr); $x++) {
$hintArr[$x] ^= $payloadSignatureHint[$x];
$keyHint = $payloadSignature->getHint();
// Take the last 4 bytes of the payload, right-padding with zeros when the
// payload is shorter than the 4-byte hint, then XOR byte-for-byte with the
// key hint (CAP-40). Padded positions keep the key-hint byte.
$len = strlen($signerPayload);
$payloadHint = $len >= 4
? substr($signerPayload, $len - 4, 4)
: $signerPayload . str_repeat("\x00", 4 - $len);
$hint = '';
for ($x = 0; $x < 4; $x++) {
$hint .= chr(ord($payloadHint[$x]) ^ ord($keyHint[$x]));
}
$payloadSignature->setHint(implode($hintArr));
$payloadSignature->setHint($hint);
return $payloadSignature;
}

Expand Down
Loading