I’m using getblock.io as a node for Solana network and I’m trying to sign a transaction and send it through that node in Laravel, but I’m facing a problem with the serialization of the transaction every time I’m getting an error that says :
failed to deserialize solana_sdk::transaction::versioned::VersionedTransaction: io error: failed to fill whole buffer
My code for the transaction signing and sending is below:
public function sendTransaction($privateKey, $recipientAddress, $amount)
{
try {
$client = new Client();
$rpcUrl = 'https://go.getblock.io/{{MyNodeRpcKey}}/mainnet';
// Create a transaction
$transaction = $this->createTransaction($privateKey, $recipientAddress, $amount);
// Send the transaction
$response = $client->post($rpcUrl, [
'json' => [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'sendTransaction',
'params' => [$transaction, ['encoding' => 'base58']]
],
'headers' => [
'Content-Type' => 'application/json',
],
]);
$body = json_decode($response->getBody(), true);
dd($body);
if (isset($body['result'])) {
return $body['result']; // Transaction signature
}
return null;
} catch (Exception $e) {
return [$e->getMessage()];
}
}
private function createTransaction($privateKey, $recipientAddress, $amount)
{
$recentBlockhash = $this->getRecentBlockhash();
$publicKey = $this->getPublicKeyFromPrivateKey($privateKey);
$base58 = new Base58();
// Constructing the transaction
$transaction = [
'recentBlockhash' => $recentBlockhash,
'header' => [
'numRequiredSignatures' => 1,
'numReadonlySignedAccounts' => 0,
'numReadonlyUnsignedAccounts' => 1,
],
'accountKeys' => [
$base58->encode($publicKey),
$base58->encode($recipientAddress),
'11111111111111111111111111111111',
],
'instructions' => [
[
'accounts' => [0, 1],
'keys' => [
['pubkey' => $publicKey, 'isSigner' => true, 'isWritable' => true],
['pubkey' => $recipientAddress, 'isSigner' => false, 'isWritable' => true],
],
'programId' => '11111111111111111111111111111111', // System program ID
'programIdIndex' => 2,
"stackHeight" => null,
'data' => $base58->encode(pack('P', $amount * 1000000000)) // Convert amount to lamports
]
],
'signatures' => [] // Add signatures after signing the transaction
];
$signedTransaction = $this->signTransaction($transaction, $privateKey);
// Serialize transaction
return $signedTransaction;
}
private function getRecentBlockhash()
{
$client = new Client();
$rpcUrl = 'https://go.getblock.io/{{MyNodeRpcKey}}/mainnet';
$response = $client->post($rpcUrl, [
'json' => [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'getRecentBlockhash',
],
'headers' => [
'Content-Type' => 'application/json',
],
]);
$body = json_decode($response->getBody(), true);
return $body['result']['value']['blockhash'] ?? null;
}
public function signTransaction(array $transaction, string $privateKeyHex): string
{
try {
// Decode the hex-encoded private key
$fullPrivateKey = hex2bin($privateKeyHex);
if ($fullPrivateKey === false) {
throw new Exception('Invalid private key format');
}
// Extract the private key part
$privateKey = substr($fullPrivateKey, 0, 32);
// Serialize the transaction into a byte array
$transactionBytes = $this->serializeTransaction($transaction);
// Sign the transaction bytes with the private key
$signature = Ed25519::sign_detached($transactionBytes, $privateKey);
// Combine the signature and the transaction
$signedTransaction = $signature . $transactionBytes;
// Base64 encode the signed transaction
return (new Base58())->encode($signedTransaction);
} catch (Throwable $th) {
return $th->getMessage();
}
}
public function serializeTransaction(array $transaction): string
{
$serialized = '';
// Serialize header
$serialized .= chr($transaction['header']['numRequiredSignatures']);
$serialized .= chr($transaction['header']['numReadonlySignedAccounts']);
$serialized .= chr($transaction['header']['numReadonlyUnsignedAccounts']);
// Serialize account keys
foreach ($transaction['accountKeys'] as $accountKey) {
$serialized .= (new Base58())->decode($accountKey); // Decode from Base58
}
// Serialize recent blockhash
$serialized .= (new Base58())->decode($transaction['recentBlockhash']); // Decode from Base58
// Serialize instructions
$serialized .= $this->serializeCompactU16(count($transaction['instructions']));
foreach ($transaction['instructions'] as $instruction) {
$serialized .= $this->serializeInstruction($instruction);
}
return $serialized;
}
private function serializeCompactU16(int $value): string
{
$buf = '';
while ($value > 0) {
$byte = $value & 0x7f;
$value >>= 7;
if ($value > 0) {
$byte |= 0x80;
}
$buf .= chr($byte);
}
return $buf;
}
private function serializeInstruction(array $instruction): string
{
$serialized = '';
$serialized .= chr($instruction['programIdIndex']);
$serialized .= $this->serializeCompactU16(count($instruction['accounts']));
foreach ($instruction['accounts'] as $account) {
$serialized .= chr($account);
}
$dataLength = strlen((new Base58())->decode($instruction['data']));
$serialized .= $this->serializeCompactU16($dataLength);
$serialized .= (new Base58())->decode($instruction['data']); // Decode from Base64
return $serialized;
}
public function getPublicKeyFromPrivateKey(string $privateKeyHex)
{
try {
// Decode the hex-encoded private key
$fullPrivateKey = hex2bin($privateKeyHex);
if ($fullPrivateKey === false) {
throw new Exception('Invalid private key format');
}
// Extract the private key part (first 32 bytes)
$privateKey = substr($fullPrivateKey, 0, 32);
// Generate the public key from the private key
$publicKey = Ed25519::publickey_from_secretkey($privateKey);
return bin2hex($publicKey);
} catch (Throwable $th) {
return $th->getMessage();
}
}
I’m expecting the transaction to go through without a serialization error