블록체인에서 데이터베이스까지: Soroban을 PHP와 동기화하기
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 엔티티에 저장할 수 있게 해줍니다.