从区块链到数据库:使用 PHP 同步 Soroban

发布: (2025年12月7日 GMT+8 17:49)
4 min read
原文: Dev.to

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 实体中。

Back to Blog

相关文章

阅读更多 »