Skip to content

Go

HTTP

Code Example

package main

import (
    "bytes"
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"

    "github.com/BlockRazorinc/sui_go_example/blockrzsdk"

    "github.com/block-vision/sui-go-sdk/models"
    "github.com/block-vision/sui-go-sdk/signer"
    "github.com/block-vision/sui-go-sdk/sui"
    "github.com/block-vision/sui-go-sdk/transaction"
)

const (
    // Official Sui fullnode RPC (mainnet)
    OfficialRpcUrl = "https://fullnode.mainnet.sui.io"
    // Gas object id
    GasObjectId = "<YOUR_GAS_OBJECT_ID>"
    // Receiver address
    ReceiverAddr = "<YOUR_RECEIVER_ADDRESS>"
    // Transfer amount in MIST
    SendAmountMist = uint64(100000)
    // Default gas price
    GasPrice = uint64(5000)
    // BlockRz RPC endpoint
    BlockRzRpcUrl = "<YOUR_BLOCKRZ_RPC_URL>"
    // Auth token header for your RPC
    AuthToken = "<YOUR_AUTH_TOKEN>"
    // Timeouts
    BuildTimeout = 5 * time.Second
    HttpTimeout  = 15 * time.Second
)

// fetchLatestObjectRef fetches the latest (version, digest) for an objectId
func fetchLatestObjectRef(ctx context.Context, cli *sui.Client, objectId string) (*transaction.SuiObjectRef, error) {
    obj, err := cli.SuiGetObject(ctx, models.SuiGetObjectRequest{ObjectId: objectId})
    if err != nil {
        return nil, fmt.Errorf("SuiGetObject failed: %w", err)
    }
    if obj.Data == nil {
        return nil, fmt.Errorf("object not found or deleted: %s", objectId)
    }
    if obj.Data.Version == "" || obj.Data.Digest == "" {
        return nil, fmt.Errorf("invalid object data: id=%s version=%s digest=%s", objectId, obj.Data.Version, obj.Data.Digest)
    }

    ref, err := transaction.NewSuiObjectRef(
        models.SuiAddress(objectId),
        obj.Data.Version,
        models.ObjectDigest(obj.Data.Digest),
    )
    if err != nil {
        return nil, fmt.Errorf("NewSuiObjectRef failed: %w", err)
    }
    return ref, nil
}

// makeHttpRequest builds the JSON-RPC request for `sui_executeTransactionBlock`.
func makeHttpRequest(tx *transaction.Transaction) (*http.Request, []byte, error) {
    reqModel, err := tx.ToSuiExecuteTransactionBlockRequest(
        context.Background(),
        models.SuiTransactionBlockOptions{ShowEffects: true},
        "WaitForLocalExecution",
    )
    if err != nil {
        return nil, nil, fmt.Errorf("ToSuiExecuteTransactionBlockRequest failed: %w", err)
    }

    bodyMap := map[string]any{
        "jsonrpc": "2.0",
        "id":      1,
        "method":  "sui_executeTransactionBlock",
        "params": []any{
            reqModel.TxBytes,
            reqModel.Signature,
            reqModel.Options,
            reqModel.RequestType,
        },
    }

    jsonBytes, err := json.Marshal(bodyMap)
    if err != nil {
        return nil, nil, fmt.Errorf("json.Marshal failed: %w", err)
    }
    req, err := http.NewRequest("POST", BlockRzRpcUrl, bytes.NewBuffer(jsonBytes))
    if err != nil {
        return nil, nil, fmt.Errorf("NewRequest failed: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("auth_token", AuthToken)
    rawTxBytes, err := base64.StdEncoding.DecodeString(reqModel.TxBytes)
    if err != nil {
        return nil, nil, fmt.Errorf("base64 decode TxBytes failed: %w", err)
    }
    return req, rawTxBytes, nil
}

// buildBaseTx creates a basic transfer tx: split from gas coin then transfer to receiver.
func buildBaseTx(cli *sui.Client, s *signer.Signer, gasRef *transaction.SuiObjectRef) *transaction.Transaction {
    tx := transaction.NewTransaction()
    tx.SetSuiClient(cli).
        SetSigner(s).
        SetSender(models.SuiAddress(s.Address)).
        SetGasPayment([]transaction.SuiObjectRef{*gasRef}).
        SetGasOwner(models.SuiAddress(s.Address))

    split := tx.SplitCoins(tx.Gas(), []transaction.Argument{tx.Pure(SendAmountMist)})
    tx.TransferObjects([]transaction.Argument{split}, tx.Pure(ReceiverAddr))
    return tx
}

func main() {
    ctx := context.Background()
    // Build signer from secret key
    secretKey := "<YOUR_SECRET_KEY>"
    sender, err := signer.NewSignerWithSecretKey(secretKey)
    if err != nil {
        log.Fatalf("[INIT] NewSignerWithSecretKey failed: %v", err)
    }
    log.Printf("[INIT] sender=%s receiver=%s amountMist=%d", sender.Address, ReceiverAddr, SendAmountMist)

    // Create Sui client (official fullnode)
    cli := sui.NewSuiClient(OfficialRpcUrl).(*sui.Client)

    // Fetch latest gas object ref
    gasRef, err := fetchLatestObjectRef(ctx, cli, GasObjectId)
    if err != nil {
        log.Fatalf("[GAS] fetchLatestObjectRef failed: %v", err)
    }
    log.Printf("[GAS] gasObject=%s version=%d digest=%s", GasObjectId, gasRef.Version, gasRef.Digest)

    // Build base tx
    tx := buildBaseTx(cli, sender, gasRef)

    // ============================================
    //  Estimate fee & tip using dryRun
    // ============================================
    tx.SetGasPrice(GasPrice)
    buildCtx, cancel := context.WithTimeout(context.Background(), BuildTimeout)
    defer cancel()

    // Build TxBytes for estimation
    reqModel, err := tx.ToSuiExecuteTransactionBlockRequest(
        buildCtx,
        models.SuiTransactionBlockOptions{},
        "WaitForLocalExecution",
    )
    if err != nil {
        log.Fatalf("[PREP] build tx bytes failed: %v", err)
    }

    // Caculate Fee
    est, err := blockrzsdk.CalculateFeeFromTxB64(buildCtx, reqModel.TxBytes)
    if err != nil {
        log.Fatalf("[PREP] CalculateFeeFromTxB64 failed: %v", err)
    }
    tx.SetGasBudget(est.GasBudget)

    // Add tip into transaction
    // The required tip must exceed 1,000,000 mist
    // and must exceed 5% of the gas budget
    const minTip uint64 = 1000000
    tipFee := est.TipAmount
    if tipFee < minTip {
        tipFee = minTip
    }
    if err := blockrzsdk.AddTip(tx, tipFee); err != nil {
        log.Fatalf("[PREP] AddTip failed: %v", err)
    }
    log.Printf("[PREP] gasPrice=%d gasBudget=%d tip=%d txBytesLen=%d",
        GasPrice, est.GasBudget, tipFee, len(reqModel.TxBytes))

    // ============================================
    //  Build HTTP request and Send to BlockRz
    // ============================================
    req, _, err := makeHttpRequest(tx)
    if err != nil {
        log.Fatalf("[HTTP] makeHttpRequest failed: %v", err)
    }

    // Send HTTP request and read response body
    client := &http.Client{Timeout: HttpTimeout}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("[HTTP] request failed: %v", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("[HTTP] read response body failed: %v", err)
    }
    log.Printf("[HTTP][RESP] body=%s", body)
}

gRPC

Code Example

package main

import (
    "context"
    "encoding/base64"
    "fmt"
    "log"
    "time"

    "github.com/BlockRazorinc/sui_go_example/blockrzsdk"
    pb "github.com/BlockRazorinc/sui_go_example/sui/rpc/v2"

    "github.com/block-vision/sui-go-sdk/models"
    "github.com/block-vision/sui-go-sdk/signer"
    "github.com/block-vision/sui-go-sdk/sui"
    "github.com/block-vision/sui-go-sdk/transaction"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"
)

const (
    OfficialRpcUrl = "https://fullnode.mainnet.sui.io"
    // Gas object
    GasObjectId = "<YOUR_GAS_OBJECT_ID>"
    // Receiver address
    ReceiverAddr = "<YOUR_RECEIVER_ADDRESS>"
    // Transfer amount in MIST
    SendAmountMist = uint64(100000)
    // Default gas price (can be fetched from RPC if needed)
    GasPrice = uint64(5000)
    // BlockRz GRPC endpoint
    BlockRzGrpcUrl = "<YOUR_BLOCKRZ_GRPC_URL>"
    // Auth token header for your RPC
    AuthToken = "<YOUR_AUTH_TOKEN>"
    // Timeouts
    BuildTimeout = 5 * time.Second
    GrpcTimeout  = 15 * time.Second
)

// fetchLatestObjectRef fetches the latest (version, digest) for an objectId
func fetchLatestObjectRef(ctx context.Context, cli *sui.Client, objectId string) (*transaction.SuiObjectRef, error) {
    obj, err := cli.SuiGetObject(ctx, models.SuiGetObjectRequest{ObjectId: objectId})
    if err != nil {
        return nil, fmt.Errorf("SuiGetObject failed: %w", err)
    }
    if obj.Data == nil {
        return nil, fmt.Errorf("object not found or deleted: %s", objectId)
    }
    if obj.Data.Version == "" || obj.Data.Digest == "" {
        return nil, fmt.Errorf("invalid object data: id=%s version=%s digest=%s", objectId, obj.Data.Version, obj.Data.Digest)
    }

    ref, err := transaction.NewSuiObjectRef(
        models.SuiAddress(objectId),
        obj.Data.Version,
        models.ObjectDigest(obj.Data.Digest),
    )
    if err != nil {
        return nil, fmt.Errorf("NewSuiObjectRef failed: %w", err)
    }
    return ref, nil
}

// buildBaseTx creates a basic transfer tx: split from gas coin then transfer to receiver.
func buildBaseTx(cli *sui.Client, s *signer.Signer, gasRef *transaction.SuiObjectRef) *transaction.Transaction {
    tx := transaction.NewTransaction()
    tx.SetSuiClient(cli).
        SetSigner(s).
        SetSender(models.SuiAddress(s.Address)).
        SetGasPayment([]transaction.SuiObjectRef{*gasRef}).
        SetGasOwner(models.SuiAddress(s.Address))

    split := tx.SplitCoins(tx.Gas(), []transaction.Argument{tx.Pure(SendAmountMist)})
    tx.TransferObjects([]transaction.Argument{split}, tx.Pure(ReceiverAddr))
    return tx
}

// makeGrpcRequest converts transaction into gRPC ExecuteTransactionRequest
func makeGrpcRequest(ctx context.Context, tx *transaction.Transaction) (*pb.ExecuteTransactionRequest, []byte, error) {
    // Align with HTTP demo: use WaitForLocalExecution
    reqModel, err := tx.ToSuiExecuteTransactionBlockRequest(
        ctx,
        models.SuiTransactionBlockOptions{},
        "WaitForLocalExecution",
    )
    if err != nil {
        return nil, nil, fmt.Errorf("ToSuiExecuteTransactionBlockRequest failed: %w", err)
    }

    txBytes, err := base64.StdEncoding.DecodeString(reqModel.TxBytes)
    if err != nil {
        return nil, nil, fmt.Errorf("base64 decode TxBytes failed: %w", err)
    }

    if len(reqModel.Signature) == 0 {
        return nil, nil, fmt.Errorf("empty signature in reqModel")
    }

    sigBytes, err := base64.StdEncoding.DecodeString(reqModel.Signature[0])
    if err != nil {
        return nil, nil, fmt.Errorf("base64 decode signature failed: %w", err)
    }

    return &pb.ExecuteTransactionRequest{
        Transaction: &pb.Transaction{Bcs: &pb.Bcs{Value: txBytes}},
        Signatures:  []*pb.UserSignature{{Bcs: &pb.Bcs{Value: sigBytes}}},
    }, txBytes, nil
}

func main() {
    ctx := context.Background()
    // Build signer from secret key
    secretKey := "<YOUR_SECRET_KEY>"
    sender, err := signer.NewSignerWithSecretKey(secretKey)
    if err != nil {
        log.Fatalf("[INIT] NewSignerWithSecretKey failed: %v", err)
    }
    log.Printf("[INIT] sender=%s receiver=%s amountMist=%d", sender.Address, ReceiverAddr, SendAmountMist)

    // Create Sui client (official fullnode)
    cli := sui.NewSuiClient(OfficialRpcUrl).(*sui.Client)

    // Fetch latest gas object ref
    gasRef, err := fetchLatestObjectRef(ctx, cli, GasObjectId)
    if err != nil {
        log.Fatalf("[GAS] fetchLatestObjectRef failed: %v", err)
    }
    log.Printf("[GAS] gasObject=%s version=%d digest=%s", GasObjectId, gasRef.Version, gasRef.Digest)

    // Build base tx
    tx := buildBaseTx(cli, sender, gasRef)

    // ============================================
    //  Estimate fee & tip using dryRun
    // ============================================
    tx.SetGasPrice(GasPrice)
    buildCtx, cancel := context.WithTimeout(context.Background(), BuildTimeout)
    defer cancel()

    reqModel, err := tx.ToSuiExecuteTransactionBlockRequest(
        buildCtx,
        models.SuiTransactionBlockOptions{}, // dryRun no need showEffects
        "WaitForLocalExecution",
    )
    if err != nil {
        log.Fatalf("[PREP] build tx bytes failed: %v", err)
    }

    // Caculate Fee
    est, err := blockrzsdk.CalculateFeeFromTxB64(buildCtx, reqModel.TxBytes)
    if err != nil {
        log.Fatalf("[PREP] CalculateFeeFromTxB64 failed: %v", err)
    }
    tx.SetGasBudget(est.GasBudget)

    // Add tip into transaction
    // The required tip must exceed 1,000,000 mist
    // and must exceed 5% of the gas budget
    const minTip uint64 = 1000000
    tipFee := est.TipAmount
    if tipFee < minTip {
        tipFee = minTip
    }
    if err := blockrzsdk.AddTip(tx, tipFee); err != nil {
        log.Fatalf("[PREP] AddTip failed: %v", err)
    }
    log.Printf("[PREP] gasPrice=%d gasBudget=%d tip=%d txBytesLen=%d",
        GasPrice, est.GasBudget, tipFee, len(reqModel.TxBytes))

    // ============================================
    //  Build gRPC request and Send to BlockRz
    // ============================================
    conn, err := grpc.Dial(BlockRzGrpcUrl, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("[GRPC] dial failed: %v", err)
    }
    defer conn.Close()

    grpcClient := pb.NewTransactionExecutionServiceClient(conn)

    grpcCtx, cancel := context.WithTimeout(context.Background(), GrpcTimeout)
    defer cancel()

    grpcReq, txBytes, err := makeGrpcRequest(grpcCtx, tx)
    if err != nil {
        log.Fatalf("[GRPC] makeGrpcRequest failed: %v", err)
    }
    log.Printf("[GRPC][REQ] txBytesLen=%d sigCount=%d", len(txBytes), len(grpcReq.Signatures))

    // Attach auth token
    md := metadata.Pairs("auth_token", AuthToken)
    grpcCtx = metadata.NewOutgoingContext(grpcCtx, md)

    start := time.Now()
    resp, err := grpcClient.ExecuteTransaction(grpcCtx, grpcReq)
    if err != nil {
        log.Fatalf("[GRPC] ExecuteTransaction failed: %v", err)
    }

    log.Printf("[GRPC][RESP] cost=%s resp=%+v", time.Since(start), resp)
}