diff --git a/bcs/serializer.go b/bcs/serializer.go index 56d49f9..8de6329 100644 --- a/bcs/serializer.go +++ b/bcs/serializer.go @@ -303,6 +303,12 @@ func SerializeBytes(input []byte) ([]byte, error) { }) } +func SerializeUleb128(input uint32) ([]byte, error) { + return SerializeSingle(func(ser *Serializer) { + ser.Uleb128(input) + }) +} + // SerializeSingle is a convenience function, to not have to create a serializer to serialize one value // // Here's an example for handling a nested byte array diff --git a/client.go b/client.go index 8aa35b0..3258789 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "time" "github.com/aptos-labs/aptos-go-sdk/api" + "github.com/aptos-labs/aptos-go-sdk/crypto" "github.com/hasura/go-graphql-client" ) @@ -749,6 +750,50 @@ func (client *Client) SimulateTransaction(rawTxn *RawTransaction, sender Transac return client.nodeClient.SimulateTransaction(rawTxn, sender, options...) } +// SimulateMultiTransaction simulates a multi-transaction on the Aptos blockchain. +// It takes a raw transaction with data, a transaction signer, and optional parameters. +// It returns a slice of UserTransaction and an error if the simulation fails. +// +// Parameters: +// - rawTxnWithData: A pointer to RawTransactionWithData containing the transaction details. +// - sender: A TransactionSigner that signs the transaction. +// - options: Optional parameters for the simulation. +// +// Returns: +// - data: A slice of UserTransaction containing the simulated transaction results. +// - err: An error if the simulation fails. +// SimulateMultiTransaction simulates a multi-signature transaction without broadcasting it to the network. +// This function takes a raw transaction with data, a sender transaction signer, and additional signers. +// It returns the simulated user transactions or an error if the simulation fails. +// +// Parameters: +// - rawTxnWithData: A pointer to the raw transaction data to be simulated. +// - sender: The primary transaction signer. +// - additionalSigners: A slice of additional signers' account authenticators. +// - options: Additional options for the simulation. +// +// Returns: +// - data: A slice of pointers to the simulated user transactions. +// - err: An error if the simulation fails. +func (client *Client) SimulateMultiTransaction(rawTxnWithData *RawTransactionWithData, sender TransactionSigner, additionalSigners []crypto.AccountAuthenticator, options ...any) (data []*api.UserTransaction, err error) { + return client.nodeClient.SimulateMultiTransaction(rawTxnWithData, sender, additionalSigners, options...) +} + +// SimulateTransactionWithSignedTxn simulates a transaction using a signed transaction. +// This function sends the signed transaction to the node client for simulation and returns +// the resulting user transactions and any error encountered during the simulation. +// +// Parameters: +// - signedTxn: A pointer to the SignedTransaction struct representing the signed transaction to be simulated. +// - options: Additional optional parameters that can be passed to the simulation. +// +// Returns: +// - data: A slice of pointers to api.UserTransaction structs representing the simulated user transactions. +// - err: An error object if an error occurred during the simulation, otherwise nil. +func (client *Client) SimulateTransactionWithSignedTxn(signedTxn *SignedTransaction, options ...any) (data []*api.UserTransaction, err error) { + return client.nodeClient.SimulateTransactionWithSignedTxn(signedTxn, options...) +} + // GetChainId Retrieves the ChainId of the network // Note this will be cached forever, or taken directly from the config func (client *Client) GetChainId() (chainId uint8, err error) { diff --git a/client_test.go b/client_test.go index 89e10e9..ea18e71 100644 --- a/client_test.go +++ b/client_test.go @@ -7,11 +7,14 @@ import ( "time" "github.com/aptos-labs/aptos-go-sdk/api" + "github.com/aptos-labs/aptos-go-sdk/bcs" + "github.com/aptos-labs/aptos-go-sdk/crypto" "github.com/stretchr/testify/assert" ) const ( singleSignerScript = "a11ceb0b060000000701000402040a030e0c041a04051e20073e30086e2000000001010204010001000308000104030401000105050601000002010203060c0305010b0001080101080102060c03010b0001090002050b00010900000a6170746f735f636f696e04636f696e04436f696e094170746f73436f696e087769746864726177076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001080b000b0138000c030b020b03380102" + multiSignerScript = "a11ceb0b0700000a0601000203020605080d071525083a40107a1f010200030201000104060c060c03050003060c0503083c53454c463e5f30046d61696e0d6170746f735f6163636f756e74087472616e73666572ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000114636f6d70696c6174696f6e5f6d65746164617461090003322e3003322e31000001090b000a030a0211000b010b030b02110002" fundAmount = 100_000_000 vmStatusSuccess = "Executed successfully" ) @@ -22,11 +25,19 @@ var TestSigners map[string]CreateSigner type CreateSingleSignerPayload func(client *Client, sender TransactionSigner, options ...any) (*RawTransaction, error) +type CreateMultiSignerPayload func(client *Client, sender TransactionSigner, options ...any) (*RawTransactionWithData, error) + var TestSingleSignerPayloads map[string]CreateSingleSignerPayload +var TestFeePayerSignerPayloads map[string]CreateMultiSignerPayload + +var TestMultiSignerPayloads map[string]CreateMultiSignerPayload + func init() { initSigners() initSingleSignerPayloads() + initMultiSignerPayloads() + initFeePayerSignerPayloads() } func initSigners() { @@ -65,6 +76,17 @@ func initSingleSignerPayloads() { TestSingleSignerPayloads["Script"] = buildSingleSignerScript } +func initFeePayerSignerPayloads() { + TestFeePayerSignerPayloads = make(map[string]CreateMultiSignerPayload) + TestFeePayerSignerPayloads["Entry Function"] = buildFeePayerSignerEntryFunction + TestFeePayerSignerPayloads["Script"] = buildFeePayerSignerScript +} + +func initMultiSignerPayloads() { + TestMultiSignerPayloads = make(map[string]CreateMultiSignerPayload) + TestMultiSignerPayloads["Script"] = buildMultiSignerScript +} + func TestNamedConfig(t *testing.T) { names := []string{"mainnet", "devnet", "testnet", "localnet"} for _, name := range names { @@ -90,6 +112,32 @@ func Test_SingleSignerFlows(t *testing.T) { } } +func Test_MultiSignerFlows(t *testing.T) { + for name, signer := range TestSigners { + for payloadName, buildMultiSignerPayload := range TestFeePayerSignerPayloads { + t.Run(name+" "+payloadName+" Feepayer", func(t *testing.T) { + testMultiTransaction(t, signer, &signer, &[]CreateSigner{}, buildMultiSignerPayload) + }) + t.Run(name+" "+payloadName+" Multi Signer", func(t *testing.T) { + testMultiTransaction(t, signer, nil, &[]CreateSigner{ + func() (TransactionSigner, error) { + signer, err := NewEd25519Account() + return any(signer).(TransactionSigner), err + }}, buildMultiSignerPayload) + }) + t.Run(name+" "+payloadName+" simulation", func(t *testing.T) { + additionalSigners := []CreateSigner{ + func() (TransactionSigner, error) { + signer, err := NewEd25519Account() + return any(signer).(TransactionSigner), err + }, + } + testMultiTransactionSimulation(t, signer, nil, &additionalSigners, buildMultiSignerPayload) + }) + } + } +} + func setupIntegrationTest(t *testing.T, createAccount CreateSigner) (*Client, TransactionSigner) { // All of these run against localnet if testing.Short() { @@ -215,6 +263,165 @@ func testTransactionSimulation(t *testing.T, createAccount CreateSigner, buildTr assert.Greater(t, simulatedTxn[0].MaxGasAmount, uint64(0)) } +func testMultiTransaction(t *testing.T, createAccount CreateSigner, feePayer *CreateSigner, additional *[]CreateSigner, buildTransaction CreateMultiSignerPayload) { + + client, account := setupIntegrationTest(t, createAccount) + + // Build transaction + var buildTransactionOptions []any + + var feePayerAddress AccountAddress + var feePayerSigner TransactionSigner + + // Add feePayer to options if provided + if feePayer != nil { + _, feePayerSigner = setupIntegrationTest(t, *feePayer) + feePayerAddress = feePayerSigner.AccountAddress() + buildTransactionOptions = append(buildTransactionOptions, FeePayer(&feePayerAddress)) + } + + var additionalAddress []AccountAddress + var additionalSigners []TransactionSigner + // Add additionalSigners to options if provided + if additional != nil && len(*additional) > 0 { + // Use the spread operator to properly expand the slice of signers + for _, signerCreator := range *additional { + _, signerAccount := setupIntegrationTest(t, signerCreator) + additionalAddress = append(additionalAddress, signerAccount.AccountAddress()) + additionalSigners = append(additionalSigners, signerAccount) + } + buildTransactionOptions = append(buildTransactionOptions, AdditionalSigners(additionalAddress)) + } + + // Build transaction with dynamically constructed options + rawTxn, err := buildTransaction(client, account, buildTransactionOptions...) + assert.NoError(t, err) + + senderAuth, err := rawTxn.Sign(account) + assert.NoError(t, err) + var FeePayerAuthenticator *crypto.AccountAuthenticator + if feePayer != nil { + feePayerAuth, err := rawTxn.Sign(feePayerSigner) + assert.NoError(t, err) + FeePayerAuthenticator = feePayerAuth + } + + // Sign with additional signers and collect authenticators + var additionalAuths []crypto.AccountAuthenticator + for _, additionalSigner := range additionalSigners { + additionalAuth, err := rawTxn.Sign(additionalSigner) + assert.NoError(t, err) + additionalAuths = append(additionalAuths, *additionalAuth) + } + var signedTxn *SignedTransaction + + if feePayer != nil { + signed_txn, ok := rawTxn.ToFeePayerSignedTransaction(senderAuth, FeePayerAuthenticator, additionalAuths) + if !ok { + t.Fatal("Failed to build a signed multiagent transaction") + } + signedTxn = signed_txn + } else { + signed_txn, ok := rawTxn.ToMultiAgentSignedTransaction(senderAuth, additionalAuths) + if !ok { + t.Fatal("Failed to build a signed multiagent transaction") + } + signedTxn = signed_txn + } + + // Send transaction + result, err := client.SubmitTransaction(signedTxn) + assert.NoError(t, err) + + hash := result.Hash + + // Wait for the transaction + _, err = client.WaitForTransaction(hash) + assert.NoError(t, err) + + // Read transaction by hash + txn, err := client.TransactionByHash(hash) + assert.NoError(t, err) + + // Read transaction by version + userTxn, _ := txn.Inner.(*api.UserTransaction) + version := userTxn.Version + + // Load the transaction again + txnByVersion, err := client.TransactionByVersion(version) + assert.NoError(t, err) + + // Assert that both are the same + expectedTxn, err := txn.UserTransaction() + assert.NoError(t, err) + actualTxn, err := txnByVersion.UserTransaction() + assert.NoError(t, err) + assert.Equal(t, expectedTxn, actualTxn) +} + +func testMultiTransactionSimulation(t *testing.T, createAccount CreateSigner, feePayer *CreateSigner, additional *[]CreateSigner, buildTransaction CreateMultiSignerPayload) { + client, account := setupIntegrationTest(t, createAccount) + + // Build transaction + var buildTransactionOptions []any + + var feePayerAddress AccountAddress + var feePayerSigner TransactionSigner + + // Add feePayer to options if provided + if feePayer != nil { + _, feePayerSigner = setupIntegrationTest(t, *feePayer) + feePayerAddress = feePayerSigner.AccountAddress() + buildTransactionOptions = append(buildTransactionOptions, FeePayer(&feePayerAddress)) + } + + // Build transaction with dynamically constructed options + rawTxn, err := buildTransaction(client, account, buildTransactionOptions...) + assert.NoError(t, err) + + simulatedTxn, err := client.SimulateMultiTransaction(rawTxn, account, []crypto.AccountAuthenticator{}) + switch account.(type) { + case *MultiKeyTestSigner: + return + default: + assert.NoError(t, err) + assert.Equal(t, true, simulatedTxn[0].Success) + assert.Equal(t, vmStatusSuccess, simulatedTxn[0].VmStatus) + assert.Greater(t, simulatedTxn[0].GasUsed, uint64(0)) + } + + // simulate transaction (estimate gas unit price) + rawTxnZeroGasUnitPrice, err := buildTransaction(client, account, GasUnitPrice(0)) + assert.NoError(t, err) + simulatedTxn, err = client.SimulateMultiTransaction(rawTxnZeroGasUnitPrice, account, []crypto.AccountAuthenticator{}, EstimateGasUnitPrice(true)) + assert.NoError(t, err) + assert.Equal(t, true, simulatedTxn[0].Success) + assert.Equal(t, vmStatusSuccess, simulatedTxn[0].VmStatus) + estimatedGasUnitPrice := simulatedTxn[0].GasUnitPrice + assert.Greater(t, estimatedGasUnitPrice, uint64(0)) + + // simulate transaction (estimate max gas amount) + rawTxnZeroMaxGasAmount, err := buildTransaction(client, account, MaxGasAmount(0)) + assert.NoError(t, err) + simulatedTxn, err = client.SimulateMultiTransaction(rawTxnZeroMaxGasAmount, account, []crypto.AccountAuthenticator{}, EstimateMaxGasAmount(true)) + assert.NoError(t, err) + assert.Equal(t, true, simulatedTxn[0].Success) + assert.Equal(t, vmStatusSuccess, simulatedTxn[0].VmStatus) + assert.Greater(t, simulatedTxn[0].MaxGasAmount, uint64(0)) + + // simulate transaction (estimate prioritized gas unit price and max gas amount) + rawTxnZeroGasConfig, err := buildTransaction(client, account, GasUnitPrice(0), MaxGasAmount(0)) + assert.NoError(t, err) + simulatedTxn, err = client.SimulateMultiTransaction(rawTxnZeroGasConfig, account, []crypto.AccountAuthenticator{}, EstimatePrioritizedGasUnitPrice(true), EstimateMaxGasAmount(true)) + assert.NoError(t, err) + assert.Equal(t, true, simulatedTxn[0].Success) + assert.Equal(t, vmStatusSuccess, simulatedTxn[0].VmStatus) + estimatedGasUnitPrice = simulatedTxn[0].GasUnitPrice + assert.Greater(t, estimatedGasUnitPrice, uint64(0)) + assert.Greater(t, simulatedTxn[0].MaxGasAmount, uint64(0)) + +} + func TestAPTTransferTransaction(t *testing.T) { sender, err := NewEd25519Account() assert.NoError(t, err) @@ -685,6 +892,38 @@ func buildSingleSignerEntryFunction(client *Client, sender TransactionSigner, op return APTTransferTransaction(client, sender, AccountOne, 100, options...) } +func buildFeePayerSignerEntryFunction(client *Client, sender TransactionSigner, options ...any) (*RawTransactionWithData, error) { + amount := uint64(100) + amountBytes, err := bcs.SerializeU64(amount) + if err != nil { + return nil, err + } + dest := AccountOne + + rawTxn, err := client.BuildTransactionMultiAgent( + sender.AccountAddress(), + TransactionPayload{ + Payload: &EntryFunction{ + Module: ModuleId{ + Address: AccountOne, + Name: "aptos_account", + }, + Function: "transfer", + ArgTypes: []TypeTag{}, + Args: [][]byte{ + dest[:], + amountBytes, + }, + }}, + options..., + ) + if err != nil { + return nil, err + } + + return rawTxn, nil +} + func buildSingleSignerScript(client *Client, sender TransactionSigner, options ...any) (*RawTransaction, error) { scriptBytes, err := ParseHex(singleSignerScript) if err != nil { @@ -714,3 +953,79 @@ func buildSingleSignerScript(client *Client, sender TransactionSigner, options . return rawTxn, nil } + +func buildFeePayerSignerScript(client *Client, sender TransactionSigner, options ...any) (*RawTransactionWithData, error) { + scriptBytes, err := ParseHex(singleSignerScript) + if err != nil { + return nil, err + } + + amount := uint64(1) + dest := AccountOne + + rawTxn, err := client.BuildTransactionMultiAgent( + sender.AccountAddress(), + TransactionPayload{ + Payload: &Script{ + Code: scriptBytes, + ArgTypes: []TypeTag{}, + Args: []ScriptArgument{{ + Variant: ScriptArgumentU64, + Value: amount, + }, { + Variant: ScriptArgumentAddress, + Value: dest, + }}, + }, + }, + options..., + ) + if err != nil { + panic("Failed to build multiagent raw transaction:" + err.Error()) + } + + if err != nil { + return nil, err + } + + return rawTxn, nil +} + +func buildMultiSignerScript(client *Client, sender TransactionSigner, options ...any) (*RawTransactionWithData, error) { + scriptBytes, err := ParseHex(multiSignerScript) + if err != nil { + return nil, err + } + + amount := uint64(1) + + rawTxn, err := client.BuildTransactionMultiAgent( + sender.AccountAddress(), + TransactionPayload{ + Payload: &Script{ + Code: scriptBytes, + ArgTypes: []TypeTag{}, + Args: []ScriptArgument{ + { + Variant: ScriptArgumentU64, + Value: uint64(amount), + }, + { + Variant: ScriptArgumentAddress, + Value: AccountOne, + }, + }, + }, + }, + options..., + ) + if err != nil { + panic("Failed to build multiagent raw transaction:" + err.Error()) + } + + if err != nil { + return nil, err + } + + return rawTxn, nil +} diff --git a/crypto/AccountAuthenticatorNoAccountAuthenticator.go b/crypto/AccountAuthenticatorNoAccountAuthenticator.go new file mode 100644 index 0000000..26a24e2 --- /dev/null +++ b/crypto/AccountAuthenticatorNoAccountAuthenticator.go @@ -0,0 +1,45 @@ +package crypto + +import "github.com/aptos-labs/aptos-go-sdk/bcs" + +type AccountAuthenticatorNoAccountAuthenticator struct { +} + +func (aa *AccountAuthenticatorNoAccountAuthenticator) UnmarshalBCS(des *bcs.Deserializer) { + +} + +func (aa *AccountAuthenticatorNoAccountAuthenticator) MarshalBCS(ser *bcs.Serializer) { +} + +func (aa *AccountAuthenticatorNoAccountAuthenticator) PublicKey() PublicKey { + var publicKey PublicKey + err := (publicKey).FromHex("0x0000000000000000000000000000000000000000000000000000000000000000") + println(publicKey.ToHex()) + if err != nil { + + // Handle error or log it + // For this case, it should never fail since we're using a valid zero key + } + + return publicKey +} + +// Signature returns the signature of the authenticator +// +// Implements: +// - [AccountAuthenticatorImpl] +func (ea *AccountAuthenticatorNoAccountAuthenticator) Signature() Signature { + var signature Signature + (signature).FromHex("0x0000000000000000000000000000000000000000000000000000000000000000") + println(signature.ToHex()) + return signature +} + +// Verify verifies the signature against the message +// +// Implements: +// - [AccountAuthenticatorImpl] +func (aa *AccountAuthenticatorNoAccountAuthenticator) Verify(msg []byte) bool { + return false +} diff --git a/crypto/authenticator.go b/crypto/authenticator.go index a03bb8a..d862fc1 100644 --- a/crypto/authenticator.go +++ b/crypto/authenticator.go @@ -3,6 +3,7 @@ package crypto import ( "errors" "fmt" + "github.com/aptos-labs/aptos-go-sdk/bcs" ) @@ -38,6 +39,7 @@ const ( AccountAuthenticatorMultiEd25519 AccountAuthenticatorType = 1 // AccountAuthenticatorMultiEd25519 is the authenticator type for multi-ed25519 accounts AccountAuthenticatorSingleSender AccountAuthenticatorType = 2 // AccountAuthenticatorSingleSender is the authenticator type for single-key accounts AccountAuthenticatorMultiKey AccountAuthenticatorType = 3 // AccountAuthenticatorMultiKey is the authenticator type for multi-key accounts + AccountAuthenticatorNoAccount AccountAuthenticatorType = 4 ) // AccountAuthenticator a generic authenticator type for a transaction diff --git a/examples/script_args_transaction/main.go b/examples/script_args_transaction/main.go new file mode 100644 index 0000000..9c9dda0 --- /dev/null +++ b/examples/script_args_transaction/main.go @@ -0,0 +1,317 @@ +package main + +import ( + "fmt" + "math/big" + + "github.com/aptos-labs/aptos-go-sdk" + "github.com/aptos-labs/aptos-go-sdk/bcs" + "github.com/aptos-labs/aptos-go-sdk/internal/util" +) + +/* +script { + use std::string::String; + + fun main( + bool: bool, + u8: u8, + u16: u16, + u32: u32, + u64: u64, + u128: u128, + u256: u256, + address: address, + string: String, + vec_u8: vector, + vec_u16: vector, + vec_u32: vector, + vec_u64: vector, + vec_u128: vector, + vec_u256: vector, + vec_address: vector
, + vec_string: vector, + ){ + + } +} +*/ + +const scriptBytes = "a11ceb0b0700000a0601000202020405061d07231c083f40107f1f0103000207001101020d0e03040f0508000a020a0d0a0e0a030a040a0f0a050a080000083c53454c463e5f30046d61696e06537472696e6706737472696e67ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000114636f6d70696c6174696f6e5f6d65746164617461090003322e3003322e310000010102" +const FundAmount = uint64(100_000_000) + +func example(networkConfig aptos.NetworkConfig) { + // Create a client for Aptos + client, err := aptos.NewClient(networkConfig) + if err != nil { + panic("Failed to create client:" + err.Error()) + } + + // Create a sender + sender, err := aptos.NewEd25519Account() + if err != nil { + panic("Failed to create sender:" + err.Error()) + } + + // Fund the sender with the faucet to create it on-chain + println("SENDER: ", sender.Address.String()) + err = client.Fund(sender.Address, FundAmount) + if err != nil { + panic("Failed to fund sender:" + err.Error()) + } + + // Now run a script version + fmt.Printf("\n== Now running script version ==\n") + runScript(client, sender) + + if err != nil { + panic("Failed to get store balance:" + err.Error()) + } + // fmt.Printf("After Script: Receiver Before transfer: %d, after transfer: %d\n", receiverAfterBalance, receiverAfterAfterBalance) +} + +func runScript(client *aptos.Client, alice *aptos.Account) { + scriptBytes, err := aptos.ParseHex(scriptBytes) + if err != nil { + panic("Failed to parse script:" + err.Error()) + } + + u128_arg ,err:= util.StrToBigInt("128") + if err != nil { + panic("Failed to convert u128:" + err.Error()) + } + + u256_arg ,err:= util.StrToBigInt("256") + if err != nil { + panic("Failed to convert u256:" + err.Error()) + } + + vec_u16 := []uint16{1, 2, 3, 4, 5} + + vec_u16_len, err := bcs.SerializeUleb128(uint32(len(vec_u16))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + + var vec_u16_arg []byte + vec_u16_arg = append(vec_u16_arg, vec_u16_len...) + for _, v := range vec_u16 { + bytes, err := bcs.SerializeU16(v) + if err != nil { + panic("Failed to serialize u16:" + err.Error()) + } + vec_u16_arg = append(vec_u16_arg, bytes...) + } + + vec_u32 := []uint32{1, 2, 3, 4, 5} + vec_u32_len, err := bcs.SerializeUleb128(uint32(len(vec_u32))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + var vec_u32_arg []byte + vec_u32_arg = append(vec_u32_arg, vec_u32_len...) + for _, v := range vec_u32 { + bytes, err := bcs.SerializeU32(v) + if err != nil { + panic("Failed to serialize u32:" + err.Error()) + } + vec_u32_arg = append(vec_u32_arg, bytes...) + } + + vec_u64 := []uint64{1, 2, 3, 4, 5} + vec_u64_len, err := bcs.SerializeUleb128(uint32(len(vec_u64))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + var vec_u64_arg []byte + vec_u64_arg = append(vec_u64_arg, vec_u64_len...) + for _, v := range vec_u64 { + bytes, err := bcs.SerializeU64(v) + if err != nil { + panic("Failed to serialize u64:" + err.Error()) + } + vec_u64_arg = append(vec_u64_arg, bytes...) + } + + vec_u128 := []big.Int{*big.NewInt(1), *big.NewInt(1),*big.NewInt(2), *big.NewInt(3), *big.NewInt(4)} + vec_u128_len, err := bcs.SerializeUleb128(uint32(len(vec_u128))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + var vec_u128_arg []byte + vec_u128_arg = append(vec_u128_arg, vec_u128_len...) + for _, v := range vec_u128 { + bytes, err := bcs.SerializeU128(v) + if err != nil { + panic("Failed to serialize u128:" + err.Error()) + } + vec_u128_arg = append(vec_u128_arg, bytes...) + } + + vec_u256 := []big.Int{*big.NewInt(1), *big.NewInt(1),*big.NewInt(2), *big.NewInt(3), *big.NewInt(4)} + vec_u256_len, err := bcs.SerializeUleb128(uint32(len(vec_u256))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + var vec_u256_arg []byte + vec_u256_arg = append(vec_u256_arg, vec_u256_len...) + for _, v := range vec_u256 { + bytes, err := bcs.SerializeU256(v) + if err != nil { + panic("Failed to serialize u256:" + err.Error()) + } + vec_u256_arg = append(vec_u256_arg, bytes...) + } + + vec_address := []aptos.AccountAddress{alice.AccountAddress(), alice.AccountAddress(), alice.AccountAddress(), alice.AccountAddress(), alice.AccountAddress()} + vec_address_len, err := bcs.SerializeUleb128(uint32(len(vec_address))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + var vec_address_arg []byte + vec_address_arg = append(vec_address_arg, vec_address_len...) + for _, v := range vec_address { + ser := bcs.Serializer{}; + v.MarshalBCS(&ser) + bytes := ser.ToBytes() + vec_address_arg = append(vec_address_arg, bytes...) + } + + vec_string := []string{"string", "string", "string", "string", "string"} + vec_string_len, err := bcs.SerializeUleb128(uint32(len(vec_string))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + var vec_string_arg []byte + vec_string_arg = append(vec_string_arg, vec_string_len...) + + for _, v := range vec_string { + string_len, err := bcs.SerializeUleb128(uint32(len([]byte(v)))); + if err != nil { + panic("Failed to serialize uleb128:" + err.Error()) + } + vec_string_arg = append(vec_string_arg, string_len...) + vec_string_arg = append(vec_string_arg, []byte(v)...) + } + + // 1. Build transaction + rawTxn, err := client.BuildTransaction(alice.AccountAddress(), aptos.TransactionPayload{ + Payload: &aptos.Script{ + Code: scriptBytes, + ArgTypes: []aptos.TypeTag{ }, + Args: []aptos.ScriptArgument{ + { + Variant: aptos.ScriptArgumentBool, + Value: bool(true), + }, + { + Variant: aptos.ScriptArgumentU8, + Value: uint8(8), + }, + { + Variant: aptos.ScriptArgumentU16, + Value: uint16(16), + }, + { + Variant: aptos.ScriptArgumentU32, + Value: uint32(32), + }, + { + Variant: aptos.ScriptArgumentU64, + Value: uint64(64), + }, + { + Variant: aptos.ScriptArgumentU128, + Value: *u128_arg, + }, + { + Variant: aptos.ScriptArgumentU256, + Value: *u256_arg, + }, + { + Variant: aptos.ScriptArgumentAddress, + Value: alice.Address, + }, + { + Variant: aptos.ScriptArgumentU8Vector, + Value: []byte("string"), + }, + { + Variant: aptos.ScriptArgumentU8Vector, + Value: []byte{1, 2, 3, 4, 5}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_u16_arg}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_u32_arg}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_u64_arg}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_u128_arg}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_u256_arg}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_address_arg}, + }, + { + Variant: aptos.ScriptArgumentSerialized, + Value: &bcs.Serialized{Value: vec_string_arg}, + }, + }, + }}) + if err != nil { + panic("Failed to build multiagent raw transaction:" + err.Error()) + } + + + if err != nil { + panic("Failed to build transaction:" + err.Error()) + } + + // 2. Simulate transaction (optional) + // This is useful for understanding how much the transaction will cost + // and to ensure that the transaction is valid before sending it to the network + // This is optional, but recommended + simulationResult, err := client.SimulateTransaction(rawTxn, alice) + if err != nil { + panic("Failed to simulate transaction:" + err.Error()) + } + fmt.Printf("\n=== Simulation ===\n") + fmt.Printf("Gas unit price: %d\n", simulationResult[0].GasUnitPrice) + fmt.Printf("Gas used: %d\n", simulationResult[0].GasUsed) + fmt.Printf("Total gas fee: %d\n", simulationResult[0].GasUsed*simulationResult[0].GasUnitPrice) + fmt.Printf("Status: %s\n", simulationResult[0].VmStatus) + + // 3. Sign transaction + signedTxn, err := rawTxn.SignedTransaction(alice) + if err != nil { + panic("Failed to sign transaction:" + err.Error()) + } + + // 4. Submit transaction + submitResult, err := client.SubmitTransaction(signedTxn) + if err != nil { + panic("Failed to submit transaction:" + err.Error()) + } + txnHash := submitResult.Hash + + // 5. Wait for the transaction to complete + _, err = client.WaitForTransaction(txnHash) + if err != nil { + panic("Failed to wait for transaction:" + err.Error()) + } +} +func main() { + example(aptos.DevnetConfig) +} \ No newline at end of file diff --git a/examples/simulate_multi_transaction/.gitignore b/examples/simulate_multi_transaction/.gitignore new file mode 100644 index 0000000..1dda0aa --- /dev/null +++ b/examples/simulate_multi_transaction/.gitignore @@ -0,0 +1 @@ +fungible_assets diff --git a/examples/simulate_multi_transaction/main.go b/examples/simulate_multi_transaction/main.go new file mode 100644 index 0000000..1fff462 --- /dev/null +++ b/examples/simulate_multi_transaction/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "fmt" + + "github.com/aptos-labs/aptos-go-sdk" + "github.com/aptos-labs/aptos-go-sdk/bcs" + "github.com/aptos-labs/aptos-go-sdk/crypto" +) + +const FundAmount = 100_000_000 +const TransferAmount uint64 = 1 + +// example This example shows you how to make an APT transfer transaction in the simplest possible way +func example(networkConfig aptos.NetworkConfig) { + // Create a client for Aptos + client, err := aptos.NewClient(networkConfig) + if err != nil { + panic("Failed to create client:" + err.Error()) + } + + // Create accounts locally for alice and bob + + alice, err := aptos.NewEd25519Account() + if err != nil { + panic("Failed to create sender:" + err.Error()) + } + + bob, err := aptos.NewEd25519Account() + if err != nil { + panic("Failed to create sender:" + err.Error()) + } + + fmt.Printf("\n=== Addresses ===\n") + fmt.Printf("Alice: %s\n", alice.Address.String()) + fmt.Printf("Bob:%s\n", bob.Address.String()) + + // Fund the sender with the faucet to create it on-chain + err = client.Fund(alice.Address, FundAmount) + if err != nil { + panic("Failed to fund alice:" + err.Error()) + } + err = client.Fund(bob.Address, FundAmount) + if err != nil { + panic("Failed to fund bob:" + err.Error()) + } + aliceBalance, err := client.AccountAPTBalance(alice.Address) + if err != nil { + panic("Failed to retrieve alice balance:" + err.Error()) + } + bobBalance, err := client.AccountAPTBalance(bob.Address) + if err != nil { + panic("Failed to retrieve bob balance:" + err.Error()) + } + fmt.Printf("\n=== Initial Balances ===\n") + fmt.Printf("Alice: %d\n", aliceBalance) + fmt.Printf("Bob:%d\n", bobBalance) + + serializer := &bcs.Serializer{} + + bob.Address.MarshalBCS(serializer) + accountBytes := serializer.ToBytes() + + serializer = &bcs.Serializer{} + + serializer.U64(TransferAmount) + amountBytes := serializer.ToBytes() + + // 1. Build transaction + + rawTxn, err := client.BuildTransactionMultiAgent(alice.AccountAddress(), aptos.TransactionPayload{ + Payload: &aptos.EntryFunction{ + Module: aptos.ModuleId{ + Address: aptos.AccountOne, + Name: "aptos_account", + }, + Function: "transfer", + ArgTypes: []aptos.TypeTag{}, + Args: [][]byte{ + accountBytes, + amountBytes, + }, + }}, aptos.FeePayer(&bob.Address)) + if err != nil { + panic("Failed to build multiagent raw transaction:" + err.Error()) + } + + // 2. Simulate transaction (optional) + // This is useful for understanding how much the transaction will cost + // and to ensure that the transaction is valid before sending it to the network + simulationResult, err := client.SimulateMultiTransaction(rawTxn, alice, []crypto.AccountAuthenticator{}) + if err != nil { + panic("Failed to simulate transaction:" + err.Error()) + } + fmt.Printf("\n=== Simulation ===\n") + fmt.Printf("Gas unit price: %d\n", simulationResult[0].GasUnitPrice) + fmt.Printf("Gas used: %d\n", simulationResult[0].GasUsed) + fmt.Printf("Total gas fee: %d\n", simulationResult[0].GasUsed*simulationResult[0].GasUnitPrice) + fmt.Printf("Status: %s\n", simulationResult[0].VmStatus) + + // 3. Sign transaction with both parties separately, this would be on different machines or places + aliceAuth, err := rawTxn.Sign(alice) + if err != nil { + panic("Failed to sign multiagent transaction with alice:" + err.Error()) + } + bobAuth, err := rawTxn.Sign(bob) + if err != nil { + panic("Failed to sign multiagent transaction with bob:" + err.Error()) + } + + // 4.a. merge the signatures together into a single transaction + signedTxn, ok := rawTxn.ToFeePayerSignedTransaction(aliceAuth, bobAuth, []crypto.AccountAuthenticator{}) + if !ok { + panic("Failed to build a signed multiagent transaction") + } + + // 5. Submit transaction + submitResult, err := client.SubmitTransaction(signedTxn) + if err != nil { + panic("Failed to submit transaction:" + err.Error()) + } + txnHash := submitResult.Hash + + // 6. Wait for the transaction to complete + _, err = client.WaitForTransaction(txnHash) + if err != nil { + panic("Failed to wait for transaction:" + err.Error()) + } + + // Check balances + aliceBalance, err = client.AccountAPTBalance(alice.Address) + if err != nil { + panic("Failed to retrieve alice balance:" + err.Error()) + } + bobBalance, err = client.AccountAPTBalance(bob.Address) + if err != nil { + panic("Failed to retrieve bob balance:" + err.Error()) + } + fmt.Printf("\n=== Intermediate Balances ===\n") + fmt.Printf("Alice: %d\n", aliceBalance) + fmt.Printf("Bob:%d\n", bobBalance) +} + +func main() { + example(aptos.DevnetConfig) +} diff --git a/examples/simulate_multi_transaction/main_test.go b/examples/simulate_multi_transaction/main_test.go new file mode 100644 index 0000000..d03ec01 --- /dev/null +++ b/examples/simulate_multi_transaction/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/aptos-labs/aptos-go-sdk" + "testing" +) + +func Test_Main(t *testing.T) { + t.Parallel() + example(aptos.LocalnetConfig) +} diff --git a/nodeClient.go b/nodeClient.go index 7f67563..2fab8b1 100644 --- a/nodeClient.go +++ b/nodeClient.go @@ -5,6 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/aptos-labs/aptos-go-sdk/api" + "github.com/aptos-labs/aptos-go-sdk/bcs" + "github.com/aptos-labs/aptos-go-sdk/crypto" "io" "log/slog" "net/http" @@ -13,10 +16,6 @@ import ( "sort" "strconv" "time" - - "github.com/aptos-labs/aptos-go-sdk/api" - "github.com/aptos-labs/aptos-go-sdk/bcs" - "github.com/aptos-labs/aptos-go-sdk/crypto" ) const ( @@ -730,6 +729,76 @@ func (rc *NodeClient) SimulateTransaction(rawTxn *RawTransaction, sender Transac return data, nil } +// SimulateMultiTransaction simulates a transaction with optional fee payer and secondary signers +// If feePayerAddress is nil, no fee payer will be used +// If secondarySignerAddresses is nil or empty, no secondary signers will be used +func (rc *NodeClient) SimulateMultiTransaction(rawTxnWithData *RawTransactionWithData, sender TransactionSigner, additionalSigners []crypto.AccountAuthenticator, options ...any) (data []*api.UserTransaction, err error) { + if rawTxnWithData == nil { + return nil, fmt.Errorf("rawTxnWithData is nil") + } + switch rawTxnWithData.Variant { + case MultiAgentWithFeePayerRawTransactionWithDataVariant: + signedFeePayerTxn, ok := rawTxnWithData.ToFeePayerSignedTransaction( + sender.SimulationAuthenticator(), + &crypto.AccountAuthenticator{ + Variant: crypto.AccountAuthenticatorNoAccount, + Auth: &crypto.AccountAuthenticatorNoAccountAuthenticator{}, + }, + additionalSigners, + ) + if !ok { + return nil, fmt.Errorf("failed to sign fee payer transaction") + } + return rc.SimulateTransactionWithSignedTxn(signedFeePayerTxn, options...) + case MultiAgentRawTransactionWithDataVariant: + signedAgentTxn, ok := rawTxnWithData.ToMultiAgentSignedTransaction( + sender.SimulationAuthenticator(), + additionalSigners, + ) + if !ok { + return nil, fmt.Errorf("failed to sign multi agent transaction") + } + return rc.SimulateTransactionWithSignedTxn(signedAgentTxn, options...) + default: + return nil, fmt.Errorf("unsupported raw transaction with data variant %v", rawTxnWithData.Variant) + } +} + +// Helper method to avoid code duplication +func (rc *NodeClient) SimulateTransactionWithSignedTxn(signedTxn *SignedTransaction, options ...any) ([]*api.UserTransaction, error) { + sblob, err := bcs.Serialize(signedTxn) + if err != nil { + return nil, err + } + bodyReader := bytes.NewReader(sblob) + au := rc.baseUrl.JoinPath("transactions/simulate") + + // parse simulate tx options + params := url.Values{} + for i, arg := range options { + switch value := arg.(type) { + case EstimateGasUnitPrice: + params.Set("estimate_gas_unit_price", strconv.FormatBool(bool(value))) + case EstimateMaxGasAmount: + params.Set("estimate_max_gas_amount", strconv.FormatBool(bool(value))) + case EstimatePrioritizedGasUnitPrice: + params.Set("estimate_prioritized_gas_unit_price", strconv.FormatBool(bool(value))) + default: + return nil, fmt.Errorf("SimulateTransaction arg %d bad type %T", i+1, arg) + } + } + if len(params) != 0 { + au.RawQuery = params.Encode() + } + + data, err := Post[[]*api.UserTransaction](rc, au.String(), ContentTypeAptosSignedTxnBcs, bodyReader) + if err != nil { + return nil, fmt.Errorf("simulate transaction api err: %w", err) + } + + return data, nil +} + // GetChainId gets the chain ID of the network func (rc *NodeClient) GetChainId() (chainId uint8, err error) { if rc.chainId == 0 { diff --git a/rawTransaction.go b/rawTransaction.go index 0fd7a20..6d03313 100644 --- a/rawTransaction.go +++ b/rawTransaction.go @@ -2,6 +2,7 @@ package aptos import ( "fmt" + "github.com/aptos-labs/aptos-go-sdk/bcs" "github.com/aptos-labs/aptos-go-sdk/crypto" "golang.org/x/crypto/sha3"