You have a funded devnet keypair from the Quickstart. This guide shows what comes next: your first real transaction. You’ll move SOL between two accounts by building a transfer instruction, compiling it into a transaction, signing, submitting, and monitoring the result — using the System Program and Transaction Service together.
What you’ll build
A transaction that transfers lamports from one account to another using the System Program’s Transfer instruction, compiled and submitted via the Transaction Service.
Prerequisites
Build the transfer instruction
The System Program’s Transfer method returns a SolanaInstruction — it does not execute anything on-chain. The instruction describes the intent (move lamports from A to B); the Transaction Service executes it when you compile, sign, and submit. See Instructions & Transactions for the underlying pattern.
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
system_v1 "github.com/meshtrade/protochain/lib/go/protochain/solana/program/system/v1"
)
conn, err := grpc.NewClient("YOUR_ENDPOINT:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("connect: %v", err)
}
defer conn.Close()
systemClient := system_v1.NewServiceClient(conn)
ctx := context.Background()
transferResp, err := systemClient.Transfer(ctx, &system_v1.TransferRequest{
From: "SenderPubKey111111111111111111111111111111",
To: "RecipientPubKey11111111111111111111111111111",
Lamports: 1_000_000_000, // 1 SOL
})
if err != nil {
log.Fatalf("Transfer: %v", err)
}
instruction := transferResp.Instruction
Compile the transaction
Wrap the instruction in a Transaction object in DRAFT state, then call CompileTransaction. Protochain fetches a recent blockhash automatically when recent_blockhash is left empty — this is the standard pattern. The sender’s public key is the fee payer.
import transaction_v1 "github.com/meshtrade/protochain/lib/go/protochain/solana/transaction/v1"
txnClient := transaction_v1.NewServiceClient(conn)
draftTxn := &transaction_v1.Transaction{
Instructions: []*transaction_v1.SolanaInstruction{instruction},
}
compileResp, err := txnClient.CompileTransaction(ctx, &transaction_v1.CompileTransactionRequest{
Transaction: draftTxn,
FeePayer: "SenderPubKey111111111111111111111111111111",
// recent_blockhash omitted — service fetches latest automatically
})
if err != nil {
log.Fatalf("CompileTransaction: %v", err)
}
compiledTxn := compileResp.Transaction
// compiledTxn.State == TRANSACTION_STATE_COMPILED
Sign the transaction
Call SignTransaction with the compiled transaction and the sender’s private key. The transaction advances from COMPILED to FULLY_SIGNED state.
Private keys are transmitted in plaintext over the gRPC connection. Always use TLS in production. Never call SignTransaction over an unencrypted connection with real keys.
signResp, err := txnClient.SignTransaction(ctx, &transaction_v1.SignTransactionRequest{
Transaction: compiledTxn,
SigningMethod: &transaction_v1.SignTransactionRequest_PrivateKeys{
PrivateKeys: &transaction_v1.SignWithPrivateKeys{
PrivateKeys: []string{"SenderPrivateKey111111111111111111111111111111"},
},
},
})
if err != nil {
log.Fatalf("SignTransaction: %v", err)
}
signedTxn := signResp.Transaction
// signedTxn.State == TRANSACTION_STATE_FULLY_SIGNED
Submit the transaction
Call SubmitTransaction. The call returns immediately with a signature — it does not wait for on-chain confirmation.
SUBMISSION_RESULT_SUBMITTED means the transaction was accepted for broadcast, not confirmed on-chain. Pass the returned signature to MonitorTransaction to track the actual result.
submitResp, err := txnClient.SubmitTransaction(ctx, &transaction_v1.SubmitTransactionRequest{
Transaction: signedTxn,
})
if err != nil {
log.Fatalf("SubmitTransaction: %v", err)
}
fmt.Printf("Signature: %s\n", submitResp.Signature)
fmt.Printf("Result: %v\n", submitResp.SubmissionResult)
if submitResp.SubmissionResult != transaction_v1.SubmissionResult_SUBMISSION_RESULT_SUBMITTED {
log.Fatalf("Submission failed: %s", submitResp.ErrorMessage)
}
signature := submitResp.Signature
Monitor the result
Pass the signature from SubmitTransaction to MonitorTransaction. The stream emits a status update each time the transaction advances, and closes when a terminal state is reached: FINALIZED, FAILED, DROPPED, or TIMEOUT.
import "io"
stream, err := txnClient.MonitorTransaction(ctx, &transaction_v1.MonitorTransactionRequest{
Signature: signature,
})
if err != nil {
log.Fatalf("MonitorTransaction: %v", err)
}
for {
resp, err := stream.Recv()
if err == io.EOF {
break // Stream closed by server
}
if err != nil {
log.Printf("Stream error: %v", err)
break
}
fmt.Printf("Status: %v (slot %d)\n", resp.Status, resp.Slot)
switch resp.Status {
case transaction_v1.TransactionStatus_TRANSACTION_STATUS_FINALIZED:
fmt.Println("Transfer finalized")
return
case transaction_v1.TransactionStatus_TRANSACTION_STATUS_FAILED:
log.Fatalf("Transaction failed: %s", resp.ErrorMessage)
case transaction_v1.TransactionStatus_TRANSACTION_STATUS_DROPPED:
log.Fatal("Transaction dropped — consider resubmitting")
case transaction_v1.TransactionStatus_TRANSACTION_STATUS_TIMEOUT:
log.Fatal("Monitoring timed out — check GetTransaction for current status")
}
}
Next steps