Transactions
Build, sign, and submit
import (
cedra "github.com/celerfi/cedra-go-kit"
"github.com/celerfi/cedra-go-kit/account"
"github.com/celerfi/cedra-go-kit/client"
"github.com/celerfi/cedra-go-kit/transaction"
)
c := cedra.New(client.Testnet)
acct, _ := account.NewEd25519Account()
// 1. Build
rawTxn, err := c.Transaction.BuildTransaction(ctx, acct.Address(), transaction.BuildOptions{
Function: "0x1::aptos_account::transfer",
TypeArguments: []string{},
Arguments: []any{recipientAddr.Hex(), "1000000"},
})
// 2. Sign
signedBytes, err := transaction.SignTransaction(rawTxn, acct)
// 3. Submit
pending, err := c.Transaction.SubmitTransaction(ctx, signedBytes)
// 4. Wait for confirmation
committed, err := c.Transaction.WaitForTransaction(ctx, pending.Hash)
fmt.Println(committed.Success) // true
Simulate before submitting
Simulation lets you check gas cost and output without spending tokens.
results, err := c.Transaction.SimulateTransaction(ctx, rawTxn, acct)
fmt.Println(results[0].GasUsed)
The simulation endpoint requires a zeroed signature (64 zero bytes). The SDK handles this automatically via transaction.SimulateTransaction.
Fee-payer transactions
Use BuildFeePayerTransaction for sponsored transactions where a second account pays gas. The signing message follows the fee-payer domain:
sha3_256("CEDRA::RawTransactionWithData") || bcs(feePayerRawTransaction)
feePayer, _ := account.NewEd25519AccountFromHex("0xFEE_PAYER_PRIVATE_KEY")
feePayerTxn, err := c.Transaction.BuildFeePayerTransaction(ctx, acct.Address(), transaction.BuildOptions{
Function: "0x1::cedra_account::transfer",
Args: [][]byte{
transaction.SerializeAddressArg(recipientAddr),
transaction.SerializeU64Arg(1_000_000),
},
WithFeePayer: true,
})
senderAuthenticator, err := transaction.SignFeePayerTransactionSenderAuthenticator(feePayerTxn, acct)
feePayerAuthenticator, err := transaction.SignFeePayerTransactionFeePayerAuthenticator(feePayerTxn, feePayer)
signedBytes, err := transaction.AssembleFeePayerSignedTransaction(
feePayerTxn,
senderAuthenticator,
feePayer.Address(),
feePayerAuthenticator,
)
pending, err := c.Transaction.SubmitTransaction(ctx, signedBytes)
// Or simulate with zeroed signatures first.
results, err := c.Transaction.SimulateFeePayerTransaction(ctx, feePayerTxn, acct, feePayer)
fmt.Println(results[0].GasUsed)
Browser/backend split:
// Frontend:
feePayerTxn, _ := c.Transaction.BuildFeePayerTransaction(ctx, acct.Address(), transaction.BuildOptions{
Function: "0x1::cedra_account::transfer",
Args: [][]byte{transaction.SerializeAddressArg(recipientAddr), transaction.SerializeU64Arg(1_000_000)},
WithFeePayer: true,
})
senderAuthenticator, _ := transaction.SignFeePayerTransactionSenderAuthenticator(feePayerTxn, acct)
// Backend:
feePayerAuthenticator, _ := transaction.SignFeePayerTransactionFeePayerAuthenticator(feePayerTxn, sponsor)
signedBytes, _ := transaction.AssembleFeePayerSignedTransaction(
feePayerTxn,
senderAuthenticator,
sponsor.Address(),
feePayerAuthenticator,
)
pending, _ := c.Transaction.SubmitTransaction(ctx, signedBytes)
BuildOptions
| Field | Type | Description |
|---|---|---|
Function | string | Module function e.g. 0x1::aptos_account::transfer |
TypeArguments | []string | Generic type params |
Arguments | []any | Function arguments |
WithFeePayer | bool | Build a fee-payer/sponsored transaction wrapper |
FeePayerAddress | *account.AccountAddress | Optional fee payer address included in the signing wrapper |
Options | *TransactionOptions | Optional overrides (gas, expiry, sequence number) |
TransactionOptions
| Field | Type | Description |
|---|---|---|
MaxGasAmount | *uint64 | Override default max gas (200,000) |
GasUnitPrice | *uint64 | Override gas unit price |
ExpirationSecs | *uint64 | TTL from now in seconds (default 20s) |
SequenceNumber | *uint64 | Skip the network fetch and use this value directly. Use this for high-frequency or concurrent submission — see below. |
High-frequency / concurrent transactions
By default BuildTransaction fetches the account’s sequence number from the node on every call. When multiple goroutines call it simultaneously they all receive the same sequence number, causing Transaction already in mempool collisions.
Manage sequence state in memory and pass it in explicitly to avoid this:
var (
mu sync.Mutex
seq uint64
)
// Fetch once at startup
acctData, _ := cedra.Account.GetAccountInfo(ctx, alice.Address().Hex())
seq, _ = strconv.ParseUint(acctData.SequenceNumber, 10, 64)
func submitNext(payload transaction.BuildOptions) {
mu.Lock()
s := seq
seq++
mu.Unlock()
payload.Options = &types.TransactionOptions{SequenceNumber: &s}
rawTxn, _ := cedra.Transaction.BuildTransaction(ctx, alice.Address(), payload)
signed, _ := transaction.SignTransaction(rawTxn, alice)
pending, _ := cedra.Transaction.SubmitTransaction(ctx, signed)
cedra.WaitForTransaction(ctx, pending.Hash)
}
This pattern is the standard approach for keeper bots, activity generators, and any service submitting multiple transactions per second from the same account.
Signing internals
The signing message follows the BCS standard:
sha3_256("CEDRA::RawTransaction") || bcs(rawTransaction)
Public keys and signatures are length-prefixed (ULEB128) in the authenticator, not fixed bytes. The SDK handles this correctly for both Ed25519 and Secp256k1.
Fetch by hash or version
txn, err := c.Transaction.GetTransactionByHash(ctx, "0xabc...")
txn, err := c.Transaction.GetTransactionByVersion(ctx, 12345)
Check pending status
pending, err := c.Transaction.IsTransactionPending(ctx, hash)