Files
sui-go-sdk/transaction/transaction.go
2025-04-21 17:13:12 +08:00

504 lines
12 KiB
Go

package transaction
import (
"bytes"
"context"
"math"
"strconv"
"github.com/block-vision/sui-go-sdk/constant"
"github.com/block-vision/sui-go-sdk/models"
"github.com/block-vision/sui-go-sdk/mystenbcs"
"github.com/block-vision/sui-go-sdk/signer"
"github.com/block-vision/sui-go-sdk/sui"
"github.com/block-vision/sui-go-sdk/utils"
"github.com/jinzhu/copier"
"github.com/samber/lo"
)
type Transaction struct {
Data TransactionData
Signer *signer.Signer
SponsoredSigner *signer.Signer
SuiClient *sui.Client
}
func NewTransaction() *Transaction {
data := TransactionData{
V1: &TransactionDataV1{},
}
data.V1.Kind = &TransactionKind{
ProgrammableTransaction: &ProgrammableTransaction{},
}
data.V1.GasData = &GasData{}
return &Transaction{
Data: data,
}
}
func (tx *Transaction) SetSigner(signer *signer.Signer) *Transaction {
tx.Signer = signer
return tx
}
func (tx *Transaction) SetSponsoredSigner(signer *signer.Signer) *Transaction {
tx.SponsoredSigner = signer
return tx
}
func (tx *Transaction) SetSuiClient(client *sui.Client) *Transaction {
tx.SuiClient = client
return tx
}
func (tx *Transaction) SetSender(sender models.SuiAddress) *Transaction {
address := utils.NormalizeSuiAddress(string(sender))
addressBytes, err := ConvertSuiAddressStringToBytes(address)
if err != nil {
panic(err)
}
tx.Data.V1.Sender = addressBytes
return tx
}
func (tx *Transaction) SetSenderIfNotSet(sender models.SuiAddress) *Transaction {
if tx.Data.V1.Sender == nil {
tx.SetSender(sender)
}
return tx
}
func (tx *Transaction) SetExpiration(expiration TransactionExpiration) *Transaction {
tx.Data.V1.Expiration = &expiration
return tx
}
func (tx *Transaction) SetGasPayment(payment []SuiObjectRef) *Transaction {
tx.Data.V1.GasData.Payment = &payment
return tx
}
func (tx *Transaction) SetGasOwner(owner models.SuiAddress) *Transaction {
addressBytes, err := ConvertSuiAddressStringToBytes(owner)
if err != nil {
panic(err)
}
tx.Data.V1.GasData.Owner = addressBytes
return tx
}
func (tx *Transaction) SetGasPrice(price uint64) *Transaction {
tx.Data.V1.GasData.Price = &price
return tx
}
func (tx *Transaction) SetGasBudget(budget uint64) *Transaction {
tx.Data.V1.GasData.Budget = &budget
return tx
}
func (tx *Transaction) SetGasBudgetIfNotSet(budget uint64) *Transaction {
if tx.Data.V1.GasData.Budget == nil {
tx.Data.V1.GasData.Budget = &budget
}
return tx
}
func (tx *Transaction) Gas() Argument {
return Argument{
GasCoin: struct{}{},
}
}
func (tx *Transaction) Add(command Command) Argument {
index := tx.Data.V1.AddCommand(command)
return createTransactionResult(index, nil)
}
func (tx *Transaction) SplitCoins(coin Argument, amount []Argument) Argument {
return tx.Add(splitCoins(SplitCoins{
Coin: &coin,
Amount: convertArgumentsToArgumentPtrs(amount),
}))
}
func (tx *Transaction) MergeCoins(destination Argument, sources []Argument) Argument {
return tx.Add(mergeCoins(MergeCoins{
Destination: &destination,
Sources: convertArgumentsToArgumentPtrs(sources),
}))
}
func (tx *Transaction) Publish(modules []models.SuiAddress, dependencies []models.SuiAddress) Argument {
moduleAddress := make([]models.SuiAddressBytes, len(modules))
for i, module := range modules {
v, err := ConvertSuiAddressStringToBytes(module)
if err != nil {
panic(err)
}
moduleAddress[i] = *v
}
dependenciesAddress := make([]models.SuiAddressBytes, len(dependencies))
for i, dependency := range dependencies {
v, err := ConvertSuiAddressStringToBytes(dependency)
if err != nil {
panic(err)
}
dependenciesAddress[i] = *v
}
return tx.Add(publish(Publish{
Modules: moduleAddress,
Dependencies: dependenciesAddress,
}))
}
func (tx *Transaction) Upgrade(
modules []models.SuiAddress,
dependencies []models.SuiAddress,
packageId models.SuiAddress,
ticket Argument,
) Argument {
moduleAddress := make([]models.SuiAddressBytes, len(modules))
for i, module := range modules {
v, err := ConvertSuiAddressStringToBytes(module)
if err != nil {
panic(err)
}
moduleAddress[i] = *v
}
dependenciesAddress := make([]models.SuiAddressBytes, len(dependencies))
for i, dependency := range dependencies {
v, err := ConvertSuiAddressStringToBytes(dependency)
if err != nil {
panic(err)
}
dependenciesAddress[i] = *v
}
packageIdBytes, err := ConvertSuiAddressStringToBytes(packageId)
if err != nil {
panic(err)
}
return tx.Add(upgrade(Upgrade{
Modules: moduleAddress,
Dependencies: dependenciesAddress,
Package: *packageIdBytes,
Ticket: &ticket,
}))
}
func (tx *Transaction) MoveCall(
packageId models.SuiAddress,
module string,
function string,
typeArguments []TypeTag,
arguments []Argument,
) Argument {
packageIdBytes, err := ConvertSuiAddressStringToBytes(packageId)
if err != nil {
panic(err)
}
return tx.Add(moveCall(ProgrammableMoveCall{
Package: *packageIdBytes,
Module: module,
Function: function,
TypeArguments: convertTypeTagsToTypeTagPtrs(typeArguments),
Arguments: convertArgumentsToArgumentPtrs(arguments),
}))
}
func (tx *Transaction) TransferObjects(objects []Argument, address Argument) Argument {
return tx.Add(transferObjects(TransferObjects{
Objects: convertArgumentsToArgumentPtrs(objects),
Address: &address,
}))
}
func (tx *Transaction) MakeMoveVec(typeValue *string, elements []Argument) Argument {
return tx.Add(makeMoveVec(MakeMoveVec{
Type: typeValue,
Elements: convertArgumentsToArgumentPtrs(elements),
}))
}
// Object
// - input: string | CallArg | Argument
func (tx *Transaction) Object(input any) Argument {
// string
if s, ok := input.(string); ok {
if utils.IsValidSuiAddress(models.SuiAddress(s)) {
address := utils.NormalizeSuiAddress(s)
addressBytes, err := ConvertSuiAddressStringToBytes(address)
if err != nil {
panic(err)
}
// TODO: Support more object types
arg := tx.Data.V1.AddInput(CallArg{
UnresolvedObject: &UnresolvedObject{
ObjectId: *addressBytes,
},
})
return arg
} else {
panic(ErrObjectNotSupportType)
}
}
// Argument
if arg, ok := input.(Argument); ok {
return arg
}
// CallArg
if v, ok := input.(CallArg); ok {
isTypeSupported := false
if v.Object.SharedObject != nil {
// SharedObject
address := ConvertSuiAddressBytesToString(v.Object.SharedObject.ObjectId)
if index := tx.Data.V1.GetInputObjectIndex(address); index != nil {
if v.Object.SharedObject.Mutable {
newExistObject := tx.Data.V1.Kind.ProgrammableTransaction.Inputs[*index]
if newExistObject.Object.SharedObject != nil {
newExistObject.Object.SharedObject.Mutable = true
tx.Data.V1.Kind.ProgrammableTransaction.Inputs[*index] = newExistObject
}
}
return Argument{
Input: index,
}
}
isTypeSupported = true
}
if v.Object.ImmOrOwnedObject != nil {
isTypeSupported = true
}
if v.Object.Receiving != nil {
isTypeSupported = true
}
if isTypeSupported {
arg := tx.Data.V1.AddInput(CallArg{
Object: v.Object,
})
return arg
}
}
panic(ErrObjectNotSupportType)
}
func (tx *Transaction) Pure(input any) Argument {
var val []byte
if s, ok := input.(string); ok && utils.IsValidSuiAddress(models.SuiAddress(s)) {
fixedAddressBytes, err := ConvertSuiAddressStringToBytes(models.SuiAddress(s))
if err != nil {
panic(err)
}
addressBytes := fixedAddressBytes[:]
val = addressBytes
} else {
bcsEncodedMsg := bytes.Buffer{}
bcsEncoder := mystenbcs.NewEncoder(&bcsEncodedMsg)
err := bcsEncoder.Encode(input)
if err != nil {
panic(err)
}
val = bcsEncodedMsg.Bytes()
}
arg := tx.Data.V1.AddInput(CallArg{Pure: &Pure{
Bytes: val,
}})
return arg
}
func (tx *Transaction) Execute(
ctx context.Context,
options models.SuiTransactionBlockOptions,
requestType string,
) (*models.SuiTransactionBlockResponse, error) {
if tx.SuiClient == nil {
return nil, ErrSuiClientNotSet
}
req, err := tx.ToSuiExecuteTransactionBlockRequest(ctx, options, requestType)
if err != nil {
return nil, err
}
rsp, err := tx.SuiClient.SuiExecuteTransactionBlock(ctx, *req)
if err != nil {
return nil, err
}
return &rsp, nil
}
func (tx *Transaction) ToSuiExecuteTransactionBlockRequest(
ctx context.Context,
options models.SuiTransactionBlockOptions,
requestType string,
) (*models.SuiExecuteTransactionBlockRequest, error) {
if tx.Signer == nil {
return nil, ErrSignerNotSet
}
b64TxBytes, err := tx.buildTransaction(ctx)
if err != nil {
return nil, err
}
var signatures []string
if tx.SponsoredSigner != nil {
sponsoredMessage, err := tx.SponsoredSigner.SignMessage(b64TxBytes, constant.TransactionDataIntentScope)
if err != nil {
return nil, err
}
signatures = append(signatures, sponsoredMessage.Signature)
}
message, err := tx.Signer.SignMessage(b64TxBytes, constant.TransactionDataIntentScope)
if err != nil {
return nil, err
}
signatures = append(signatures, message.Signature)
return &models.SuiExecuteTransactionBlockRequest{
TxBytes: b64TxBytes,
Signature: signatures,
Options: options,
RequestType: requestType,
}, nil
}
func (tx *Transaction) buildTransaction(ctx context.Context) (string, error) {
if tx.Signer == nil {
return "", ErrSignerNotSet
}
if tx.Data.V1.GasData.Price == nil {
if tx.SuiClient != nil {
rsp, err := tx.SuiClient.SuiXGetReferenceGasPrice(ctx)
if err != nil {
return "", err
}
tx.SetGasPrice(rsp)
}
}
tx.SetGasBudgetIfNotSet(defaultGasBudget)
tx.SetSenderIfNotSet(models.SuiAddress(tx.Signer.Address))
return tx.build(false)
}
func (tx *Transaction) build(onlyTransactionKind bool) (string, error) {
if onlyTransactionKind {
bcsEncodedMsg, err := tx.Data.V1.Kind.Marshal()
if err != nil {
return "", err
}
bcsBase64 := mystenbcs.ToBase64(bcsEncodedMsg)
return bcsBase64, nil
}
if tx.Data.V1.Sender == nil {
return "", ErrSenderNotSet
}
if tx.Data.V1.GasData.Owner == nil {
tx.SetGasOwner(models.SuiAddress(tx.Signer.Address))
}
if !tx.Data.V1.GasData.IsAllSet() {
return "", ErrGasDataNotAllSet
}
bcsEncodedMsg, err := tx.Data.Marshal()
if err != nil {
return "", err
}
bcsBase64 := mystenbcs.ToBase64(bcsEncodedMsg)
return bcsBase64, nil
}
func (tx *Transaction) NewTransactionFromKind() (newTx *Transaction, err error) {
newTx = NewTransaction()
err = copier.CopyWithOption(&newTx.Data.V1.Kind, &tx.Data.V1.Kind, copier.Option{DeepCopy: true})
if err != nil {
return nil, err
}
return newTx, nil
}
func NewSuiObjectRef(objectId models.SuiAddress, version string, digest models.ObjectDigest) (*SuiObjectRef, error) {
objectIdBytes, err := ConvertSuiAddressStringToBytes(objectId)
if err != nil {
return nil, err
}
digestBytes, err := ConvertObjectDigestStringToBytes(digest)
if err != nil {
return nil, err
}
versionUint64, err := strconv.ParseUint(version, 10, 64)
if err != nil {
return nil, err
}
return &SuiObjectRef{
ObjectId: *objectIdBytes,
Version: versionUint64,
Digest: *digestBytes,
}, nil
}
func createTransactionResult(index uint16, length *uint16) Argument {
if length == nil {
length = lo.ToPtr(uint16(math.MaxUint16))
}
// TODO: Support NestedResult
return Argument{
Result: lo.ToPtr(index),
}
}
func convertArgumentsToArgumentPtrs(args []Argument) []*Argument {
argPtrs := make([]*Argument, len(args))
for i, arg := range args {
v := arg
argPtrs[i] = &v
}
return argPtrs
}
func convertTypeTagsToTypeTagPtrs(tags []TypeTag) []*TypeTag {
tagPtrs := make([]*TypeTag, len(tags))
for i, tag := range tags {
v := tag
tagPtrs[i] = &v
}
return tagPtrs
}