504 lines
12 KiB
Go
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
|
|
}
|