Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Maruko70's avatar

How to get correctly sign Solana transaction using Laravel?

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 = {{MyNodeRpcURL}};

        // 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 = '{{MyNodeRpcURL}}';
    $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

0 likes
1 reply
Ali_Najafi's avatar

Hi, sire

Did you completed this code? I want to integrate solana to my project

Thanks to help me

@maruko70

Please or to participate in this conversation.