From bedb86c7c99ad3f1540f05316e1542e66ddb7597 Mon Sep 17 00:00:00 2001 From: BarryTong65 Date: Wed, 16 Oct 2024 12:05:28 +0800 Subject: [PATCH] fix: address comments --- pkg/paymasterclient/client.go | 75 ++++++++----------- pkg/paymasterclient/types.go | 14 +++- pkg/sponsorclient/client.go | 12 ++- test/paymaster_test.go | 50 +------------ test/sponsor_test.go | 135 +++++++++++++++++++++++++++++++++- 5 files changed, 187 insertions(+), 99 deletions(-) diff --git a/pkg/paymasterclient/client.go b/pkg/paymasterclient/client.go index 7888d53..efb893c 100644 --- a/pkg/paymasterclient/client.go +++ b/pkg/paymasterclient/client.go @@ -31,89 +31,74 @@ type Client interface { } type client struct { - userClient *rpc.Client - sponsorClient *rpc.Client + c *rpc.Client } // New creates a new Client with an optional sponsorURL. // If sponsorURL is provided, it enables the use of private policies. // The sponsorURL is typically in the format: "https://open-platform-ap.nodereal.io/xxxx/megafuel-testnet" // PrivatePolicyUUID can only be used when sponsorURL is provided. -func New(ctx context.Context, userURL string, sponsorURL *string, options ...rpc.ClientOption) (Client, error) { - userClient, err := rpc.DialOptions(ctx, userURL, options...) +func New(ctx context.Context, userURL string, options ...rpc.ClientOption) (Client, error) { + c, err := rpc.DialOptions(ctx, userURL, options...) if err != nil { return nil, err } - var sponsorClient *rpc.Client - if sponsorURL != nil { - sponsorClient, err = rpc.DialOptions(ctx, *sponsorURL, options...) - if err != nil { - userClient.Close() // Close the user client if sponsor client creation fails - return nil, err - } - } - - return &client{ - userClient: userClient, - sponsorClient: sponsorClient, - }, nil + return &client{c}, nil } func (c *client) ChainID(ctx context.Context) (*big.Int, error) { var result hexutil.Big - err := c.userClient.CallContext(ctx, &result, "eth_chainId") + err := c.c.CallContext(ctx, &result, "eth_chainId") if err != nil { return nil, err } return (*big.Int)(&result), err } +// IsSponsorable checks if a transaction is sponsorable. +// If opts.PrivatePolicyUUID is set (for sponsor client use only), it will be included in the request headers. func (c *client) IsSponsorable(ctx context.Context, tx TransactionArgs, opts *IsSponsorableOptions) (*IsSponsorableResponse, error) { var result IsSponsorableResponse - if c.sponsorClient != nil && opts != nil && opts.PrivatePolicyUUID != "" { - c.sponsorClient.SetHeader("X-MegaFuel-Policy-Uuid", opts.PrivatePolicyUUID) - err := c.sponsorClient.CallContext(ctx, &result, "pm_isSponsorable", tx) - if err != nil { - return nil, err - } - return &result, nil + + if opts != nil && opts.PrivatePolicyUUID != "" { + c.c.SetHeader("X-MegaFuel-Policy-Uuid", opts.PrivatePolicyUUID) } - err := c.userClient.CallContext(ctx, &result, "pm_isSponsorable", tx) + + err := c.c.CallContext(ctx, &result, "pm_isSponsorable", tx) if err != nil { return nil, err } + return &result, nil } +// SendRawTransaction sends a raw transaction to the connected domain. +// If opts.PrivatePolicyUUID is set (for sponsor client use only), it will be included in the request headers. +// opts.UserAgent can be set for both sponsor and paymaster client calls. func (c *client) SendRawTransaction(ctx context.Context, input hexutil.Bytes, opts *SendRawTransactionOptions) (common.Hash, error) { var result common.Hash - if c.sponsorClient != nil { - if opts != nil && opts.UserAgent != "" { - c.sponsorClient.SetHeader("User-Agent", opts.UserAgent) + + if opts != nil { + if opts.UserAgent != "" { + c.c.SetHeader("User-Agent", opts.UserAgent) } - if opts != nil && opts.PrivatePolicyUUID != "" { - c.sponsorClient.SetHeader("X-MegaFuel-Policy-Uuid", opts.PrivatePolicyUUID) - err := c.sponsorClient.CallContext(ctx, &result, "eth_sendRawTransaction", input) - if err != nil { - return common.Hash{}, err - } - return result, nil + if opts.PrivatePolicyUUID != "" { + c.c.SetHeader("X-MegaFuel-Policy-Uuid", opts.PrivatePolicyUUID) } } - if opts != nil && opts.UserAgent != "" { - c.userClient.SetHeader("User-Agent", opts.UserAgent) - } - err := c.userClient.CallContext(ctx, &result, "eth_sendRawTransaction", input) + + err := c.c.CallContext(ctx, &result, "eth_sendRawTransaction", input) if err != nil { return common.Hash{}, err } + return result, nil } func (c *client) GetGaslessTransactionByHash(ctx context.Context, txHash common.Hash) (*TransactionResponse, error) { var result TransactionResponse - err := c.userClient.CallContext(ctx, &result, "eth_getGaslessTransactionByHash", txHash) + err := c.c.CallContext(ctx, &result, "eth_getGaslessTransactionByHash", txHash) if err != nil { return nil, err } @@ -122,7 +107,7 @@ func (c *client) GetGaslessTransactionByHash(ctx context.Context, txHash common. func (c *client) GetSponsorTxByTxHash(ctx context.Context, txHash common.Hash) (*SponsorTx, error) { var result SponsorTx - err := c.userClient.CallContext(ctx, &result, "pm_getSponsorTxByTxHash", txHash) + err := c.c.CallContext(ctx, &result, "pm_getSponsorTxByTxHash", txHash) if err != nil { return nil, err } @@ -131,7 +116,7 @@ func (c *client) GetSponsorTxByTxHash(ctx context.Context, txHash common.Hash) ( func (c *client) GetSponsorTxByBundleUUID(ctx context.Context, bundleUUID uuid.UUID) (*SponsorTx, error) { var result SponsorTx - err := c.userClient.CallContext(ctx, &result, "pm_getSponsorTxByBundleUuid", bundleUUID) + err := c.c.CallContext(ctx, &result, "pm_getSponsorTxByBundleUuid", bundleUUID) if err != nil { return nil, err } @@ -140,7 +125,7 @@ func (c *client) GetSponsorTxByBundleUUID(ctx context.Context, bundleUUID uuid.U func (c *client) GetBundleByUUID(ctx context.Context, bundleUUID uuid.UUID) (*Bundle, error) { var result Bundle - err := c.userClient.CallContext(ctx, &result, "pm_getBundleByUuid", bundleUUID) + err := c.c.CallContext(ctx, &result, "pm_getBundleByUuid", bundleUUID) if err != nil { return nil, err } @@ -149,7 +134,7 @@ func (c *client) GetBundleByUUID(ctx context.Context, bundleUUID uuid.UUID) (*Bu func (c *client) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (uint64, error) { var result hexutil.Uint64 - err := c.userClient.CallContext(ctx, &result, "eth_getTransactionCount", address, blockNrOrHash) + err := c.c.CallContext(ctx, &result, "eth_getTransactionCount", address, blockNrOrHash) if err != nil { return 0, err } diff --git a/pkg/paymasterclient/types.go b/pkg/paymasterclient/types.go index e6a2c06..12d5352 100644 --- a/pkg/paymasterclient/types.go +++ b/pkg/paymasterclient/types.go @@ -16,13 +16,25 @@ type TransactionArgs struct { Data *hexutil.Bytes `json:"data"` } +// IsSponsorableOptions defines the options for the IsSponsorable method. type IsSponsorableOptions struct { + // PrivatePolicyUUID is the UUID of a private policy. + // This field should only be set when using a sponsor client. + // When set, it allows the use of a private policy for the sponsorable check. + // For paymaster client calls, this field should be left empty. PrivatePolicyUUID string } +// SendRawTransactionOptions defines the options for the SendRawTransaction method. type SendRawTransactionOptions struct { + // PrivatePolicyUUID is the UUID of a private policy. + // This field should only be set when using a sponsor client. + // When set, it allows the use of a private policy for the transaction. + // For paymaster client calls, this field should be left empty. PrivatePolicyUUID string - UserAgent string + + // UserAgent is an optional field to set a custom User-Agent header for the request. + UserAgent string } type IsSponsorableResponse struct { diff --git a/pkg/sponsorclient/client.go b/pkg/sponsorclient/client.go index 0fcea74..dcf1835 100644 --- a/pkg/sponsorclient/client.go +++ b/pkg/sponsorclient/client.go @@ -6,8 +6,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rpc" "github.com/gofrs/uuid" + + "github.com/node-real/megafuel-go-sdk/pkg/paymasterclient" ) +// Client interface defines the methods available for the sponsor client. +// This client combines sponsor-specific functionality with all paymaster client (USER API) methods. type Client interface { // AddToWhitelist adds a list of values to the whitelist of a policy AddToWhitelist(ctx context.Context, args WhiteListArgs) (bool, error) @@ -22,10 +26,12 @@ type Client interface { GetUserSpendData(ctx context.Context, fromAddress common.Address, policyUUID uuid.UUID) (*UserSpendData, error) // GetPolicySpendData returns the spend data of a policy GetPolicySpendData(ctx context.Context, policyUUID uuid.UUID) (*PolicySpendData, error) + paymasterclient.Client } type client struct { c *rpc.Client + paymasterclient.Client } func New(ctx context.Context, url string, options ...rpc.ClientOption) (Client, error) { @@ -34,7 +40,11 @@ func New(ctx context.Context, url string, options ...rpc.ClientOption) (Client, return nil, err } - return &client{c}, nil + c2, err := paymasterclient.New(ctx, url, options...) + if err != nil { + return nil, err + } + return &client{c, c2}, nil } func (c *client) AddToWhitelist(ctx context.Context, args WhiteListArgs) (bool, error) { diff --git a/test/paymaster_test.go b/test/paymaster_test.go index 801e5b0..8ef6dd7 100644 --- a/test/paymaster_test.go +++ b/test/paymaster_test.go @@ -50,8 +50,7 @@ func paymasterSetup(t *testing.T) (*ethclient.Client, paymasterclient.Client, st } // Create a PaymasterClient (for transaction sending) - sponsorURL := fmt.Sprintf("https://open-platform-ap.nodereal.io/%s/megafuel-testnet/97", key) - paymasterClient, err := paymasterclient.New(context.Background(), PAYMASTER_URL, &sponsorURL) + paymasterClient, err := paymasterclient.New(context.Background(), PAYMASTER_URL) if err != nil { log.Fatalf("Failed to create PaymasterClient: %v", err) } @@ -158,51 +157,4 @@ func TestPaymasterAPI(t *testing.T) { count, err := paymasterClient.GetTransactionCount(context.Background(), common.HexToAddress(RECIPIENT_ADDRESS), rpc.BlockNumberOrHash{BlockNumber: &blockNumber}) require.NoError(t, err, "failed to GetTransactionCount") assert.Greater(t, count, hexutil.Uint64(0)) - - // Fetch the current nonce for the account to ensure the transaction can be processed sequentially. - nonce, err = client.PendingNonceAt(context.Background(), fromAddress) - require.NoError(t, err, "Failed to get nonce") - - // Define the recipient Ethereum address. - toAddress = common.HexToAddress(RECIPIENT_ADDRESS) - - // Construct a new Ethereum transaction. - tx = types.NewTx(&types.LegacyTx{ - Nonce: nonce, - GasPrice: big.NewInt(0), - Gas: 21000, - To: &toAddress, - Value: big.NewInt(0), - }) - - // Prepare a transaction argument for checking if it's sponsorable. - gasLimit = tx.Gas() - - privatePolicySponsorableTx := paymasterclient.TransactionArgs{ - To: &toAddress, - From: fromAddress, - Value: (*hexutil.Big)(big.NewInt(0)), - Gas: (*hexutil.Uint64)(&gasLimit), - Data: &hexutil.Bytes{}, - } - - privatePolicySponsorableInfo, err := paymasterClient.IsSponsorable(context.Background(), privatePolicySponsorableTx, &paymasterclient.IsSponsorableOptions{PrivatePolicyUUID: PRIVATE_POLICY}) - require.NoError(t, err, "Error checking sponsorable private policy status") - require.True(t, privatePolicySponsorableInfo.Sponsorable) - - // Retrieve the blockchain ID to ensure that the transaction is signed correctly. - chainID, err = client.ChainID(context.Background()) - require.NoError(t, err, "Failed to get chain ID") - - // Sign the transaction using the provided private key and the current chain ID. - signedTx, err = types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) - require.NoError(t, err, "Failed to sign transaction") - - // Marshal the signed transaction into a binary format for transmission. - txInput, err = signedTx.MarshalBinary() - require.NoError(t, err, "Failed to marshal transaction") - - _, err = paymasterClient.SendRawTransaction(context.Background(), txInput, &paymasterclient.SendRawTransactionOptions{PrivatePolicyUUID: PRIVATE_POLICY, UserAgent: "Test User Agent"}) - require.NoError(t, err, "Failed to send sponsorable private policy transaction") - log.Infof("Sponsorable private policy transaction sent: %s", signedTx.Hash()) } diff --git a/test/sponsor_test.go b/test/sponsor_test.go index 4d963fc..2e4ae12 100644 --- a/test/sponsor_test.go +++ b/test/sponsor_test.go @@ -2,13 +2,24 @@ package test import ( "context" + "crypto/ecdsa" + "fmt" + "math/big" "os" "testing" + "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/node-real/megafuel-go-sdk/pkg/paymasterclient" "github.com/node-real/megafuel-go-sdk/pkg/sponsorclient" ) @@ -19,18 +30,37 @@ const ( ) // sponsorSetup initializes a sponsor client using the environment variable. -func sponsorSetup(t *testing.T) (sponsorclient.Client, error) { +func sponsorSetup(t *testing.T) (*ethclient.Client, sponsorclient.Client, string, error) { t.Helper() key := os.Getenv("OPEN_PLATFORM_PRIVATE_KEY") if key == "" { log.Fatal("Environment variable OPEN_PLATFORM_PRIVATE_KEY is not set") } - return sponsorclient.New(context.Background(), "https://open-platform-ap.nodereal.io/"+key+"/megafuel-testnet") + + yourPrivateKey := os.Getenv("YOUR_PRIVATE_KEY") + if yourPrivateKey == "" { + log.Fatal("Environment variable YOUR_PRIVATE_KEY is not set") + } + + // Connect to an Ethereum node (for transaction assembly) + client, err := ethclient.Dial(fmt.Sprintf("https://bsc-testnet.nodereal.io/v1/%s", key)) + if err != nil { + log.Fatalf("Failed to connect to the Ethereum network: %v", err) + } + + // Create a client (for transaction sending) + sponsorURL := fmt.Sprintf("https://open-platform-ap.nodereal.io/%s/megafuel-testnet/97", key) + sponsorClient, err := sponsorclient.New(context.Background(), sponsorURL) + if err != nil { + log.Fatalf("Failed to create PaymasterClient: %v", err) + } + + return client, sponsorClient, yourPrivateKey, nil } // TestSponsorAPI conducts several whitelist operations. func TestSponsorAPI(t *testing.T) { - sponsorClient, err := sponsorSetup(t) + _, sponsorClient, _, err := sponsorSetup(t) require.NoError(t, err, "Setup should not fail") policyUUID, err := uuid.FromString(POLICY_UUID) @@ -90,3 +120,102 @@ func testEmptyWhitelist(t *testing.T, client sponsorclient.Client, policyUUID uu assert.True(t, success, "Whitelist emptying should be successful") log.Info("Emptied whitelist successfully") } + +// TestPaymasterAPI tests the critical functionalities related to the Paymaster API. +func TestPrivatePolicyGaslessTransaction(t *testing.T) { + // Setup Ethereum client and Paymaster client. Ensure no errors during the setup. + client, sponsorClient, yourPrivateKey, err := sponsorSetup(t) + require.NoError(t, err, "failed to set up paymaster") + + // Convert the private key from hex string to ECDSA format and check for errors. + privateKey, err := crypto.HexToECDSA(yourPrivateKey) + require.NoError(t, err, "Failed to load private key") + + // Extract the public key from the private key and assert type casting to ECDSA. + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + log.Fatal("Error casting public key to ECDSA") + } + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + // Fetch the current nonce for the account to ensure the transaction can be processed sequentially. + nonce, err := client.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err, "Failed to get nonce") + + // Define the recipient Ethereum address. + toAddress := common.HexToAddress(RECIPIENT_ADDRESS) + + // Construct a new Ethereum transaction. + tx := types.NewTx(&types.LegacyTx{ + Nonce: nonce, + GasPrice: big.NewInt(0), + Gas: 21000, + To: &toAddress, + Value: big.NewInt(0), + }) + + // Prepare a transaction argument for checking if it's sponsorable. + gasLimit := tx.Gas() + + privatePolicySponsorableTx := paymasterclient.TransactionArgs{ + To: &toAddress, + From: fromAddress, + Value: (*hexutil.Big)(big.NewInt(0)), + Gas: (*hexutil.Uint64)(&gasLimit), + Data: &hexutil.Bytes{}, + } + + privatePolicySponsorableInfo, err := sponsorClient.IsSponsorable(context.Background(), privatePolicySponsorableTx, &paymasterclient.IsSponsorableOptions{PrivatePolicyUUID: PRIVATE_POLICY}) + require.NoError(t, err, "Error checking sponsorable private policy status") + require.True(t, privatePolicySponsorableInfo.Sponsorable) + + // Retrieve the blockchain ID to ensure that the transaction is signed correctly. + chainID, err := client.ChainID(context.Background()) + require.NoError(t, err, "Failed to get chain ID") + + // Sign the transaction using the provided private key and the current chain ID. + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) + require.NoError(t, err, "Failed to sign transaction") + + // Marshal the signed transaction into a binary format for transmission. + txInput, err := signedTx.MarshalBinary() + require.NoError(t, err, "Failed to marshal transaction") + + transaction, err := sponsorClient.SendRawTransaction(context.Background(), txInput, &paymasterclient.SendRawTransactionOptions{PrivatePolicyUUID: PRIVATE_POLICY, UserAgent: "Test User Agent"}) + require.NoError(t, err, "Failed to send sponsorable private policy transaction") + log.Infof("Sponsorable private policy transaction sent: %s", signedTx.Hash()) + time.Sleep(10 * time.Second) // Consider replacing with a non-blocking wait or event-driven notification. + + // Check the Paymaster client's chain ID for consistency. + payMasterChainID, err := sponsorClient.ChainID(context.Background()) + require.NoError(t, err, "failed to get paymaster chain id") + assert.Equal(t, payMasterChainID.String(), "97") + + // Retrieve and verify the transaction details by its hash. + txResp, err := sponsorClient.GetGaslessTransactionByHash(context.Background(), transaction) + require.NoError(t, err, "failed to GetGaslessTransactionByHash") + assert.Equal(t, txResp.TxHash.String(), transaction.String()) + + // Check for the related transaction bundle based on the UUID. + bundleUuid := txResp.BundleUUID + sponsorTx, err := sponsorClient.GetSponsorTxByBundleUUID(context.Background(), bundleUuid) + require.NoError(t, err) + + // Retrieve the full bundle using the UUID and verify its existence. + bundle, err := sponsorClient.GetBundleByUUID(context.Background(), bundleUuid) + require.NoError(t, err) + + // Further validate the bundle by fetching the transaction via its hash. + sponsorTx, err = sponsorClient.GetSponsorTxByTxHash(context.Background(), sponsorTx.TxHash) + require.NoError(t, err) + + // Log the UUID of the bundle for reference. + log.Infof("Bundle UUID: %s", bundle.BundleUUID) + + // Obtain and verify the transaction count for the recipient address. + blockNumber := rpc.PendingBlockNumber + count, err := sponsorClient.GetTransactionCount(context.Background(), common.HexToAddress(RECIPIENT_ADDRESS), rpc.BlockNumberOrHash{BlockNumber: &blockNumber}) + require.NoError(t, err, "failed to GetTransactionCount") + assert.Greater(t, count, hexutil.Uint64(0)) +}