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/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"