Skip to main content
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