Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,33 @@ pending, _ := cedra.Transaction.SubmitTransaction(ctx, signedBytes)
committed, _ := cedra.WaitForTransaction(ctx, pending.Hash)
```

## Fee-Payer Transactions

```go
feePayer, _ := account.NewEd25519AccountFromHex("0xFEE_PAYER_PRIVATE_KEY")

feePayerTxn, err := cedra.Transaction.BuildFeePayerTransaction(ctx, alice.Address(), transaction.BuildOptions{
Function: "0x1::cedra_account::transfer",
Args: [][]byte{
transaction.SerializeAddressArg(bobAddr),
transaction.SerializeU64Arg(500_000),
},
WithFeePayer: true,
})

senderAuthenticator, _ := transaction.SignFeePayerTransactionSenderAuthenticator(feePayerTxn, alice)
feePayerAuthenticator, _ := transaction.SignFeePayerTransactionFeePayerAuthenticator(feePayerTxn, feePayer)
signedBytes, _ := transaction.AssembleFeePayerSignedTransaction(
feePayerTxn,
senderAuthenticator,
feePayer.Address(),
feePayerAuthenticator,
)

pending, _ := cedra.Transaction.SubmitTransaction(ctx, signedBytes)
committed, _ := cedra.WaitForTransaction(ctx, pending.Hash)
```

## ANS

```go
Expand Down
27 changes: 27 additions & 0 deletions api/cedra.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"fmt"

"github.com/celerfi/cedra-go-kit/account"
"github.com/celerfi/cedra-go-kit/client"
Expand Down Expand Up @@ -43,6 +44,9 @@ func NewCedraWithConfig(cfg client.Config) *Cedra {
}

func (ce *Cedra) SignAndSubmitTransaction(ctx context.Context, signer account.Account, opts transaction.BuildOptions) (*types.CommittedTransaction, error) {
if opts.WithFeePayer || opts.FeePayerAddress != nil {
return nil, fmt.Errorf("fee-payer transactions require SignAndSubmitFeePayerTransaction")
}
rawTxn, err := ce.Transaction.BuildTransaction(ctx, signer.Address(), opts)
if err != nil {
return nil, err
Expand All @@ -58,6 +62,29 @@ func (ce *Cedra) SignAndSubmitTransaction(ctx context.Context, signer account.Ac
return ce.Transaction.WaitForTransaction(ctx, pending.Hash)
}

func (ce *Cedra) SignAndSubmitFeePayerTransaction(ctx context.Context, sender account.Account, feePayer account.Account, opts transaction.BuildOptions) (*types.CommittedTransaction, error) {
if !opts.WithFeePayer {
opts.WithFeePayer = true
}
if opts.FeePayerAddress == nil {
addr := feePayer.Address()
opts.FeePayerAddress = &addr
}
rawTxn, err := ce.Transaction.BuildFeePayerTransaction(ctx, sender.Address(), opts)
if err != nil {
return nil, err
}
signedBytes, err := transaction.SignFeePayerTransaction(rawTxn, sender, feePayer)
if err != nil {
return nil, err
}
pending, err := ce.Transaction.SubmitTransaction(ctx, signedBytes)
if err != nil {
return nil, err
}
return ce.Transaction.WaitForTransaction(ctx, pending.Hash)
}

func (ce *Cedra) WaitForTransaction(ctx context.Context, hash string) (*types.CommittedTransaction, error) {
return ce.Transaction.WaitForTransaction(ctx, hash)
}
16 changes: 16 additions & 0 deletions api/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ func (t *TransactionAPI) BuildTransaction(ctx context.Context, sender account.Ac
return t.builder.Build(ctx, sender, opts)
}

func (t *TransactionAPI) BuildFeePayerTransaction(ctx context.Context, sender account.AccountAddress, opts transaction.BuildOptions) (*transaction.FeePayerRawTransaction, error) {
return t.builder.BuildFeePayer(ctx, sender, opts)
}

func (t *TransactionAPI) SimulateTransaction(ctx context.Context, rawTxn *transaction.RawTransaction, signer account.Account) ([]types.CommittedTransaction, error) {
signedBytes, err := transaction.SimulateTransaction(rawTxn, signer)
if err != nil {
Expand All @@ -78,6 +82,18 @@ func (t *TransactionAPI) SimulateTransaction(ctx context.Context, rawTxn *transa
return results, nil
}

func (t *TransactionAPI) SimulateFeePayerTransaction(ctx context.Context, rawTxn *transaction.FeePayerRawTransaction, signer account.Account, feePayer account.Account) ([]types.CommittedTransaction, error) {
signedBytes, err := transaction.SimulateFeePayerTransaction(rawTxn, signer, feePayer)
if err != nil {
return nil, err
}
var results []types.CommittedTransaction
if err := t.c.PostBCS(ctx, "/transactions/simulate", signedBytes, &results); err != nil {
return nil, err
}
return results, nil
}

func (t *TransactionAPI) IsTransactionPending(ctx context.Context, hash string) (bool, error) {
txn, err := t.GetTransactionByHash(ctx, hash)
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions docs/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,73 @@ 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)
```

```go
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:

```go
// 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
Expand Down
32 changes: 28 additions & 4 deletions transaction/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (
)

type BuildOptions struct {
Function string
TypeArgs []string
Args [][]byte
Options *types.TransactionOptions
Function string
TypeArgs []string
Args [][]byte
WithFeePayer bool
FeePayerAddress *account.AccountAddress
Options *types.TransactionOptions
}

type Builder struct {
Expand All @@ -28,6 +30,28 @@ func NewBuilder(c *client.Client) *Builder {
}

func (b *Builder) Build(ctx context.Context, sender account.AccountAddress, opts BuildOptions) (*RawTransaction, error) {
if opts.WithFeePayer {
return nil, fmt.Errorf("builder: with_fee_payer requested; use BuildFeePayer")
}
if opts.FeePayerAddress != nil {
return nil, fmt.Errorf("builder: fee payer address provided; use BuildFeePayer")
}
return b.buildRawTransaction(ctx, sender, opts)
}

func (b *Builder) BuildFeePayer(ctx context.Context, sender account.AccountAddress, opts BuildOptions) (*FeePayerRawTransaction, error) {
rawTxn, err := b.buildRawTransaction(ctx, sender, opts)
if err != nil {
return nil, err
}
var feePayer account.AccountAddress
if opts.FeePayerAddress != nil {
feePayer = *opts.FeePayerAddress
}
return rawTxn.WithFeePayer(feePayer), nil
}

func (b *Builder) buildRawTransaction(ctx context.Context, sender account.AccountAddress, opts BuildOptions) (*RawTransaction, error) {
var seq uint64
fetchFromNetwork := true

Expand Down
116 changes: 98 additions & 18 deletions transaction/signer.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,84 @@
package transaction

import (
"fmt"

"golang.org/x/crypto/sha3"

"github.com/celerfi/cedra-go-kit/account"
"github.com/celerfi/cedra-go-kit/bcs"
)

const rawTransactionSalt = "CEDRA::RawTransaction"
const rawTransactionWithDataSalt = "CEDRA::RawTransactionWithData"
const feePayerTransactionAuthenticatorVariant = 3

func SignTransaction(rawTxn *RawTransaction, signer account.Account) ([]byte, error) {
txnBytes := serializeRawTxn(rawTxn)
prefix := sha3.Sum256([]byte(rawTransactionSalt))
signingMessage := append(prefix[:], txnBytes...)

authenticator, err := signer.SignTransaction(signingMessage)
authenticator, err := SignTransactionAuthenticator(rawTxn, signer)
if err != nil {
return nil, err
}
return AssembleSignedTransaction(rawTxn, authenticator), nil
}

signed := &bcs.Serializer{}
signed.SerializeFixedBytes(txnBytes)
signed.SerializeFixedBytes(authenticator)
return signed.ToBytes(), nil
func SignFeePayerTransaction(rawTxn *FeePayerRawTransaction, sender account.Account, feePayer account.Account) ([]byte, error) {
senderAuthenticator, err := SignFeePayerTransactionSenderAuthenticator(rawTxn, sender)
if err != nil {
return nil, err
}
feePayerAuthenticator, err := SignFeePayerTransactionFeePayerAuthenticator(rawTxn, feePayer)
if err != nil {
return nil, err
}
return AssembleFeePayerSignedTransaction(rawTxn, senderAuthenticator, feePayer.Address(), feePayerAuthenticator)
}

func SimulateTransaction(rawTxn *RawTransaction, signer account.Account) ([]byte, error) {
txnBytes := serializeRawTxn(rawTxn)
authenticator := zeroedAuthenticator(signer)
return AssembleSignedTransaction(rawTxn, zeroedAuthenticator(signer)), nil
}

signed := &bcs.Serializer{}
signed.SerializeFixedBytes(txnBytes)
signed.SerializeFixedBytes(authenticator)
return signed.ToBytes(), nil
func SimulateFeePayerTransaction(rawTxn *FeePayerRawTransaction, sender account.Account, feePayer account.Account) ([]byte, error) {
return AssembleFeePayerSignedTransaction(rawTxn, zeroedAuthenticator(sender), feePayer.Address(), zeroedAuthenticator(feePayer))
}

func SigningMessage(rawTxn *RawTransaction) []byte {
return signingMessage(rawTransactionSalt, serializeRawTxn(rawTxn))
}

func FeePayerSigningMessage(rawTxn *FeePayerRawTransaction) []byte {
return signingMessage(rawTransactionWithDataSalt, serializeFeePayerRawTxn(rawTxn))
}

func SignTransactionAuthenticator(rawTxn *RawTransaction, signer account.Account) ([]byte, error) {
return signer.SignTransaction(SigningMessage(rawTxn))
}

func SignFeePayerTransactionSenderAuthenticator(rawTxn *FeePayerRawTransaction, signer account.Account) ([]byte, error) {
return signer.SignTransaction(FeePayerSigningMessage(rawTxn))
}

func SignFeePayerTransactionFeePayerAuthenticator(rawTxn *FeePayerRawTransaction, signer account.Account) ([]byte, error) {
return signer.SignTransaction(FeePayerSigningMessage(feePayerSigningTransaction(rawTxn, signer.Address())))
}

func AssembleSignedTransaction(rawTxn *RawTransaction, authenticator []byte) []byte {
return assembleSignedTransaction(serializeRawTxn(rawTxn), authenticator)
}

func AssembleFeePayerSignedTransaction(rawTxn *FeePayerRawTransaction, senderAuthenticator []byte, feePayerAddress account.AccountAddress, feePayerAuthenticator []byte) ([]byte, error) {
if rawTxn == nil || rawTxn.RawTransaction == nil {
return nil, fmt.Errorf("transaction: raw transaction is required")
}

authenticator := &bcs.Serializer{}
authenticator.SerializeULEB128(feePayerTransactionAuthenticatorVariant)
authenticator.SerializeFixedBytes(senderAuthenticator)
authenticator.SerializeULEB128(0)
authenticator.SerializeULEB128(0)
feePayerAddress.Serialize(authenticator)
authenticator.SerializeFixedBytes(feePayerAuthenticator)

return assembleSignedTransaction(serializeRawTxn(rawTxn.RawTransaction), authenticator.ToBytes()), nil
}

func serializeRawTxn(rawTxn *RawTransaction) []byte {
Expand All @@ -41,11 +87,45 @@ func serializeRawTxn(rawTxn *RawTransaction) []byte {
return s.ToBytes()
}

func serializeFeePayerRawTxn(rawTxn *FeePayerRawTransaction) []byte {
s := &bcs.Serializer{}
rawTxn.Serialize(s)
return s.ToBytes()
}

func feePayerSigningTransaction(rawTxn *FeePayerRawTransaction, feePayerAddress account.AccountAddress) *FeePayerRawTransaction {
return &FeePayerRawTransaction{
RawTransaction: rawTxn.RawTransaction,
FeePayerAddress: feePayerAddress,
}
}

func zeroedAuthenticator(signer account.Account) []byte {
pubKey := signer.PublicKeyBytes()
s := &bcs.Serializer{}
s.SerializeULEB128(0)
s.SerializeBytes(pubKey)
s.SerializeBytes(make([]byte, 64))
switch signer.(type) {
case *account.SingleKeyAccount:
s.SerializeULEB128(2)
s.SerializeULEB128(1)
s.SerializeBytes(pubKey)
s.SerializeULEB128(1)
s.SerializeBytes(make([]byte, 64))
default:
s.SerializeULEB128(0)
s.SerializeBytes(pubKey)
s.SerializeBytes(make([]byte, 64))
}
return s.ToBytes()
}

func signingMessage(salt string, txnBytes []byte) []byte {
prefix := sha3.Sum256([]byte(salt))
return append(prefix[:], txnBytes...)
}

func assembleSignedTransaction(txnBytes []byte, authenticator []byte) []byte {
signed := &bcs.Serializer{}
signed.SerializeFixedBytes(txnBytes)
signed.SerializeFixedBytes(authenticator)
return signed.ToBytes()
}
Loading
Loading