Introduction
Equillar is a fintech platform developed in PHP that enables decentralized investment management using smart contracts on Soroban, Stellar's smart contract platform. One of the most important technical decisions of the project was the integration with PHP-Stellar SDK, which allows the backend to s manage blockchain operations necessary for the platform's functionality, specially the companies' operations.
In this article, we will explore how Equillar uses PHP-Stellar SDK to perform such operations, from initial key-pairs generation to the deployment and execution of smart contracts on Soroban.
System Key Generation
When Equillar initializes for the first time, it needs to create what we call the "system wallet." This custodial wallet is fundamental for the companies platform's operations, as it acts as the digital identity that will deploy contracts, manage trustlines, and execute automatic administrative operations.
So far, all companies uses the same system wallet but it could be change in the future.
The platform operates with an interesting hybrid model: while users maintain full control of their own wallets for making investments, Equillar uses a custodial system wallet for all operations that require automation or are part of the companies operations.
System key generation is performed through the "GenerateSystemWalletCommand" command. The central part that uses PHP-Stellar SDK is the KeyPair generation and automatic testnet funding:
// In GenerateSystemWalletCommand.php
use Soneso\StellarSDK\Crypto\KeyPair;
use Soneso\StellarSDK\Util\FriendBot;
// Generate a new KeyPair if no secret key is provided
$keyPair = $secret ? KeyPair::fromSeed($secret) : KeyPair::random();
// On testnet, automatically fund the account
if ($blockchainNetwork->isTest()) {
FriendBot::fundTestAccount($keyPair->getAccountId());
}
The need for a system wallet arises from several operational requirements. First, Soroban contracts need to be deployed by a specific account that acts as the "owner" of the code. Additionally, to operate with tokens like USDC and EURC, the platform must manage trustlines from its own account to these assets. Finally, many operations such as automated withdrawal processing or benefit distribution require the system to execute transactions autonomously.
Loading the System Wallet Account
To use the system wallet, Equillar implements the "StellarAccountLoader" service. The key elements that use PHP-Stellar SDK are:
// In StellarAccountLoader.php
use Soneso\StellarSDK\Crypto\KeyPair;
use Soneso\StellarSDK\Network;
use Soneso\StellarSDK\StellarSDK;
// Create KeyPair from decrypted private key
$this->keyPair = KeyPair::fromSeed($decryptedPrivateKey);
// Configure the network (testnet or mainnet)
$this->network = $systemWalletData->isTest ? Network::testnet() : Network::public();
// Create SDK instance and load current account data
$this->sdk = StellarSDK::getTestNetInstance();
$this->account = $this->sdk->requestAccount($this->keyPair->getAccountId());
The "requestAccount" method is fundamental because it queries the current state of the account on the Stellar network. It's not enough to have just the local KeyPair; we need to obtain updated information such as the current sequence number, available balances, existing trustlines, and other data that changes with each transaction. This information is essential for building valid transactions, as each transaction must include the correct sequence number of the account sending it.
Establishing Trustlines for Tokens
Once the system wallet is generated and operational, the next critical step is to establish the necessary trustlines. On the Stellar network, before being able to receive or handle any token other than native XLM, an account must explicitly "trust" that token by creating a trustline. This is a security mechanism that prevents unwanted tokens from appearing in an account.
For Equillar, which handles stablecoins like USDC and EURC, it's essential to establish these trustlines from the very beginning. Without them, the platform simply couldn't operate with these assets, which are fundamental for the investments it manages.
The "CreateSystemAddressTokenTrustlineCommand" command uses PHP-Stellar SDK to create and send the trustline:
// In CreateSystemAddressTokenTrustlineCommand.php
use Soneso\StellarSDK\AssetTypeCreditAlphanum4;
use Soneso\StellarSDK\ChangeTrustOperationBuilder;
use Soneso\StellarSDK\TransactionBuilder;
// Create the Stellar asset
$stellarAsset = new AssetTypeCreditAlphanum4($token->getCode(), $token->getIssuerAddress());
// Build the trustline operation
$cto = (new ChangeTrustOperationBuilder($stellarAsset))->build();
// Create, sign and send the transaction
$transaction = (new TransactionBuilder($this->stellarAccountLoader->getAccount()))
->addOperation($cto)->build();
$transaction->sign($this->stellarAccountLoader->getKeyPair(), $this->stellarAccountLoader->getNetwork());
$transactionResponse = $this->stellarAccountLoader->getSdk()->submitTransaction($transaction);
During Equillar's initial setup, this process is completely automated through specific CLI commands that execute the creation of trustlines for the main tokens the platform will handle:
# Create trustlines for main tokens
php bin/console app:system-address:create-token-trustline --token="USDC"
php bin/console app:system-address:create-token-trustline --token="EURC"
Deploying Smart Contract Code
With the system wallet configured and trustlines established, we arrive at one of the most important operations: deploying the contract code. This process is fundamental because it generates the WASM ID (Web Assembly ID), which is essentially the "template" that will be used later to create multiple contract instances.
It's important to understand that in Soroban, code deployment and contract instance creation are two separate operations. First, the compiled code (.wasm file) is uploaded to the network, which generates a unique identifier (WASM ID). Then, each time a new contract is needed, a specific instance can be created using that WASM ID as a base.
The Deployment Process in Detail
The "DeployContractService" service orchestrates the process, but the direct interaction with PHP-Stellar SDK occurs in the deployment operation:
// In DeployContractService.php - relevant part
$wasmContent = (new Filesystem())->readFile($wasmFile);
$wasmId = $this->deployContractOperation->deploy($wasmContent);
Building the Deployment Operation
The "DeployContractOperation" class coordinates the process and extracts the WASM ID from Soroban's response:
// In DeployContractOperation.php
public function deploy(string $wasmCode): string
{
$operation = $this->deployWasmOperationBuilder->build($wasmCode);
$transactionResponse = $this->processTransactionService->sendTransaction($operation);
return $transactionResponse->getWasmId(); // PHP-Stellar SDK method
}
Building the WASM Operation
The "DeployWasmOperationBuilder" builder uses Soroban-specific SDK classes:
// In DeployWasmOperationBuilder.php
use Soneso\StellarSDK\InvokeHostFunctionOperationBuilder;
use Soneso\StellarSDK\UploadContractWasmHostFunction;
$uploadContractHostFunction = new UploadContractWasmHostFunction($wasmCode);
$builder = new InvokeHostFunctionOperationBuilder($uploadContractHostFunction);
return $builder->build();
Automation Through CLI Command
The "GenerateContractCommand" command simplifies the entire process for administrators:
// In GenerateContractCommand.php
$wasmId = $this->deployContractService->deployContract(
$this->wasmFile, // Configured path to .wasm file
$status,
$vers,
$comments
);
$io->writeln("✅ Contract deployed successfully");
$io->writeln("📋 WASM ID: {$wasmId}");
In the platform's initial configuration, this command is executed as part of the automated setup process, generating the WASM ID that will be the base for all investment contracts created subsequently:
# Deploy contract and get WASM ID
php bin/console app:contract:deploy --vers="1.0" --status=STABLE --comments="Main investment contract"
The Transaction Processing Engine
Behind all these operations we've seen - from trustline creation to contract deployment - there exists a central component that orchestrates the entire process: the "ProcessTransactionService". This service is the heart of Soroban integration and handles the complete flow required by smart contract transactions.
Soroban transactions require a multi-step process that "ProcessTransactionService" handles using PHP-Stellar SDK extensively:
// In ProcessTransactionService.php - key SDK elements
use Soneso\StellarSDK\TransactionBuilder;
use Soneso\StellarSDK\Soroban\Requests\SimulateTransactionRequest;
// Build the transaction
$transaction = (new TransactionBuilder($this->stellarAccountLoader->getAccount()))
->addOperation($operation)->build();
// Simulate to get necessary resources
$request = new SimulateTransactionRequest($transaction);
$simulateResponse = $this->server->simulateTransaction($request);
// Apply Soroban data to the transaction
$transaction->setSorobanTransactionData($simulateResponse->transactionData);
$transaction->addResourceFee($simulateResponse->minResourceFee);
// Sign with Stellar KeyPair and Network
$transaction->sign($keyPair, $network);
// Send and get response
$sendTransactionResponse = $this->server->sendTransaction($transaction);
// Wait for transaction confirmation
return $this->waitForTransaction($sendTransactionResponse->hash);
Waiting for Transaction Confirmation
Once the transaction is successfully sent to the Soroban network, it doesn't mean it's immediately confirmed. Soroban transactions are asynchronous and can take a few seconds to be processed and confirmed by the network. This is where the waitForTransaction method comes in, which is crucial for ensuring the operation completes correctly.
// In ProcessTransactionService.php - waitForTransaction method
public function waitForTransaction(string $hash, int $maxIterations = self::MAX_ITERATIONS): GetTransactionResponse
{
$counter = 0;
do {
sleep(self::DEFAULT_WAITING_SLEEP); // Wait 1 second between queries
$transactionResponse = $this->server->getTransaction($hash);
$status = $transactionResponse->status;
++$counter;
} while ($counter < $maxIterations &&
!in_array($status, [GetTransactionResponse::STATUS_SUCCESS, GetTransactionResponse::STATUS_FAILED]));
if (GetTransactionResponse::STATUS_SUCCESS !== $status) {
throw new GetTransactionException($transactionResponse);
}
return $transactionResponse;
}
This method implements a polling pattern that periodically queries the transaction status using the hash returned by Soroban. Equillar configures a maximum of 10 iterations with 1-second intervals, meaning it will wait up to 10 seconds for the transaction to be confirmed. If the transaction fails or doesn't confirm within that time, it throws a specific exception that allows appropriate error handling.
Activating Individual Contracts
Once we have the deployed WASM ID, we can create specific contract instances. In Equillar, each investment project needs its own individual contract with unique parameters such as funding goal, return rate, deadlines, etc.
Individual contract activation is the process where the WASM ID (the template) is taken and a specific instance is created with the project's parameters. This process uses PHP-Stellar SDK to handle the complexity of constructor arguments.
The "ContractActivationService" service coordinates the entire process, but the specific blockchain operation occurs in "ContractActivationOperation":
// In ContractActivationOperation.php
public function activateContract(Contract $contract): GetTransactionResponse
{
$lastDeployedContractCode = $this->contractCodeStorage->getLastdeployedContractCode();
$operation = $this->contractActivationOperationBuilder->build($contract, $lastDeployedContractCode->getWasmId());
return $this->processTransactionService->sendTransaction($operation, true);
}
Building Constructor Arguments
The most complex part of activation is building the constructor arguments. Equillar uses "ContractActivationOperationBuilder" to convert project data into the specific types that Soroban requires:
// In ContractActivationOperationBuilder.php
use Soneso\StellarSDK\CreateContractWithConstructorHostFunction;
use Soneso\StellarSDK\Soroban\Address;
use Soneso\StellarSDK\Xdr\XdrSCVal;
// Convert project data to Soroban types
$goalI128 = $this->tokenNormalizer->normalizeTokenValue($contract->getGoal(), $decimals);
$minPerInvestmentI128 = $this->tokenNormalizer->normalizeTokenValue($contract->getMinPerInvestment(), $decimals);
$constructorArgs = [
Address::fromAccountId($systemAccount)->toXdrSCVal(), // Administrator
Address::fromAccountId($contract->getProjectAddress())->toXdrSCVal(), // Project
Address::fromContractId($contract->getToken()->getAddress())->toXdrSCVal(), // Token
XdrSCVal::forU32($rate), // Return rate
XdrSCVal::forU64($days), // Days to claim
XdrSCVal::forI128Parts($goalI128->getLo(), $goalI128->getHi()), // Funding goal
// ... more arguments
];
// Create the activation function with constructor
$createContractHostFunction = new CreateContractWithConstructorHostFunction(
Address::fromAccountId($this->stellarAccountLoader->getAccount()->getAccountId()),
$wasmId,
$constructorArgs
);
What's fascinating here is how PHP-Stellar SDK handles the conversion from native PHP types to Soroban-specific types (XDR). For example, financial amounts are converted to 128-bit integers, addresses are transformed into specific "Address" objects, and simple numbers are wrapped in appropriate XDR types.
Contract Function Calls
Once contracts are active, Equillar can execute various operations by calling specific functions. Each contract function requires its own builder that handles specific parameters.
Example: Fund Withdrawal
A common operation is withdrawing funds from a contract. Let's see how Equillar uses PHP-Stellar SDK for this operation:
// In ContractWithdrawalOperationBuilder.php
use Soneso\StellarSDK\InvokeContractHostFunction;
use Soneso\StellarSDK\Xdr\XdrSCVal;
public function build(Contract $contract, float $amount): InvokeHostFunctionOperation
{
// Normalize the amount to Soroban I128 format
$amountI128 = $this->tokenNormalizer->normalizeTokenValue($amount, $contract->getToken()->getDecimals());
// Create the contract function call
$invokeContractHostFunction = new InvokeContractHostFunction(
$contract->getAddress(), // Contract address
ContractFunctions::single_withdrawn->name, // Function name
[XdrSCVal::forI128Parts($amountI128->getHi(), $amountI128->getLo())] // Arguments
);
$builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction);
return $builder->build();
}
Example: Balance Query
For read-only operations, such as querying a contract's balance, the pattern is similar but without arguments:
// In GetContractBalanceOperationBuilder.php
$invokeContractHostFunction = new InvokeContractHostFunction(
$contract->getAddress(),
ContractFunctions::get_contract_balance->name // No arguments for this function
);
$builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction);
return $builder->build();
The Invocation Pattern
All contract function calls in Equillar follow a consistent pattern that PHP-Stellar SDK facilitates enormously:
- Prepare arguments: Convert PHP data to XDR types using classes like "XdrSCVal"
- Create InvokeContractHostFunction: Specify contract address, function name and arguments
- Build operation: Use "InvokeHostFunctionOperationBuilder" to create the operation
- Process transaction: Send through "ProcessTransactionService" that handles simulation, authorization and confirmation
This modular approach allows Equillar to add new functionalities simply by creating new builders for different contract operations, keeping the complexity of Soroban integration encapsulated and reusable.
Conclusion
Throughout this complete journey, we have seen how Equillar uses PHP-Stellar SDK to manage the entire lifecycle of Soroban contracts, from initial configuration to daily operations.
We started with the fundamentals: generation of system custodial wallet keys and establishment of trustlines for the stablecoins the platform handles. These components form the foundation upon which all of Equillar's blockchain operations are built.
Then we explored the contract code deployment process, seeing how the WASM ID that acts as a master template is generated. This process, though complex, is greatly simplified thanks to the abstractions provided by PHP-Stellar SDK.
Finally, we delved into the most sophisticated operations: activating individual contracts with their specific parameters and executing function calls for operations like withdrawals and balance queries. Here we saw how PHP-Stellar SDK elegantly handles the conversion between native PHP types and the XDR types that Soroban requires.
Top comments (0)