从区块链到数据库:使用 PHP 同步 Soroban
Source: Dev.to
引言
在区块链开发中,最有趣的挑战之一就是保持链上交易与链下系统之间的精确同步。本文将分享我在 Equillar 中如何解决这一挑战:将 Soroban 智能合约调用的结果转换为 PHP Doctrine 实体,实现区块链状态在数据库中的完整复制。
交易的旅程
想象一下用户想要创建一笔投资。从他们点击按钮的那一刻起,到数据完美存入我们的数据库,一连串的事件会发生。让我们一步步跟随这个流程。
1. 入口点:控制器
一切始于 createUserContract 接口:
#[Route('/create-user-investment', name: 'post_create_user_contract_investment', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function createUserContract(
#[MapRequestPayload] CreateUserContractDtoInput $createUserContractDtoInput,
CreateUserContractService $createUserContractService
): JsonResponse
{
$user = $this->getUser();
return $this->json($createUserContractService->createUserContract($createUserContractDtoInput, $user));
}
简单直接:我们接收用户数据,验证其已认证,然后将业务逻辑委托给相应的服务。
2. 准备阶段:CreateUserContractService
CreateUserContractService 在执行区块链交易之前准备所有必要的部件。
public function createUserContract(CreateUserContractDtoInput $createUserContractDtoInput, User $user): UserContractDtoOutput
{
// 1. Get the contract from the blockchain by its address
$contract = $this->contractStorage->getContractByAddress(
StrKey::decodeContractIdHex($createUserContractDtoInput->contractAddress)
);
// 2. Verify or create the user's wallet
$userWallet = $this->userWalletStorage->getWalletByAddress($createUserContractDtoInput->fromAddress);
if (!$userWallet) {
$userWallet = $this->userWalletEntityTransformer->fromUserAndAddressToUserWalletEntity(
$user,
$createUserContractDtoInput->fromAddress
);
$this->persistor->persist($userWallet);
}
// 3. Create the UserContract entity (still without smart contract data)
$userContract = $this->userContractEntityTransformer->fromCreateUserContractInvestmentDtoToEntity(
$createUserContractDtoInput,
$contract,
$userWallet
);
$this->persistor->persist($userContract);
$this->persistor->flush();
// 4. (This is the blockchain part) – Process the blockchain transaction
$this->processUserContractService->processUserContractTransaction($userContract);
return $this->userContractEntityTransformer->fromEntityToOutputDto($userContract);
}
此时我们已经在数据库中创建了一条记录,但仍缺少真实的区块链数据。
3. 智能合约调用:ProcessUserContractService
这里我们真正与 Stellar/Soroban 区块链进行交互。
public function processUserContractTransaction(UserContract $userContract): void
{
$contractTransaction = null;
try {
// 1. Wait for the transaction to be confirmed on the blockchain
$transactionResponse = $this->processTransactionService->waitForTransaction($userContract->getHash());
// 2. Use Soneso / PHP Stellar SDK to transform XDR transaction result to PHP types
$trxResult = $this->scContractResultBuilder->getResultDataFromTransactionResponse($transactionResponse);
// 3. Map the result to our entity
$this->userInvestmentTrxResultMapper->mapToEntity($trxResult, $userContract);
// 4. Save the successful transaction
$contractTransaction = $this->contractTransactionEntityTransformer->fromSuccessfulTransaction(
$userContract->getContract()->getAddress(),
ContractNames::INVESTMENT->value,
ContractFunctions::invest->name,
$trxResult,
$transactionResponse->getTxHash(),
$transactionResponse->getCreatedAt()
);
// 5. Dispatch an event to update the contract balance
$this->bus->dispatch(new CheckContractBalanceMessage(
$userContract->getContract()->getId(),
$transactionResponse->getLedger()
));
} catch (GetTransactionException $ex) {
// If something goes wrong, log the error
$userContract->setStatus($ex->getStatus());
$contractTransaction = $this->contractTransactionEntityTransformer->fromFailedTransaction(
$userContract->getContract()->getAddress(),
ContractNames::INVESTMENT->value,
ContractFunctions::invest->name,
$ex
);
} finally {
// Always persist the final state
$this->persistor->persistAndFlush([$userContract, $contractTransaction]);
}
}
4. 等待确认:waitForTransaction
区块链并非瞬时完成。当我们发送交易时,它必须被写入账本并得到确认。waitForTransaction 方法实现了一个轮询机制。
public function waitForTransaction(string $hash, int $maxIterations = 10, ?int $microseconds = null): GetTransactionResponse
{
$counter = 0;
do {
// Wait a moment between each check
($microseconds > 0) ? usleep($microseconds) : sleep(1);
// Query the transaction status
$transactionResponse = $this->server->getTransaction($hash);
$status = $transactionResponse->status;
++$counter;
} while ($counter getResultValue();
return $this->getValueFromXdrResult($xdrResult, $transactionResponse->getTxHash());
}
private function getValueFromXdrResult(XdrSCVal $xdrResult, string $hash): mixed
{
return match ($xdrResult->type->value) {
XdrSCValType::SCV_VOID => null,
XdrSCValType::SCV_BOOL => $xdrResult->getB(),
XdrSCValType::SCV_ERROR => $this->processFunctionCallError($xdrResult->getError(), $hash),
XdrSCValType::SCV_I128 => $xdrResult->getI128(),
XdrSCValType::SCV_MAP => $this->generateForMap($xdrResult->getMap()), // This is the key!
XdrSCValType::SCV_U32 => $xdrResult->getU32(),
XdrSCValType::SCV_STRING => $xdrResult->getStr(),
default => $xdrResult->encode(),
};
}
SDK 的 XdrSCVal 类表示 Soroban 智能合约的值,使我们能够直接将其映射为原生 PHP 类型并存入 Doctrine 实体中。