From c504342737b573506fb580ee75da3bdec966746e Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Thu, 23 Oct 2025 11:59:15 -0300 Subject: [PATCH 1/3] feat: add GetSupportedTokens and TrackingIDToLatestHash methods --- paymaster/get_tokens.go | 37 +++++++++++++ paymaster/get_tokens_test.go | 75 +++++++++++++++++++++++++++ paymaster/paymaster.go | 4 +- paymaster/tracking_id.go | 97 +++++++++++++++++++++++++++++++++++ paymaster/tracking_id_test.go | 94 +++++++++++++++++++++++++++++++++ 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 paymaster/get_tokens.go create mode 100644 paymaster/get_tokens_test.go create mode 100644 paymaster/tracking_id.go create mode 100644 paymaster/tracking_id_test.go diff --git a/paymaster/get_tokens.go b/paymaster/get_tokens.go new file mode 100644 index 00000000..2444e063 --- /dev/null +++ b/paymaster/get_tokens.go @@ -0,0 +1,37 @@ +package paymaster + +import ( + "context" + + "github.com/NethermindEth/juno/core/felt" +) + +// Get a list of the tokens that the paymaster supports, together with their prices in STRK +// +// Parameters: +// - ctx: The context.Context object for controlling the function call +// +// Returns: +// - []TokenData: An array of token data +// - error: An error if any +func (p *Paymaster) GetSupportedTokens(ctx context.Context) ([]TokenData, error) { + var response []TokenData + if err := p.c.CallContextWithSliceArgs( + ctx, &response, "paymaster_getSupportedTokens", + ); err != nil { + return nil, err + } + + return response, nil +} + +// Object containing data about the token: contract address, number of +// decimals and current price in STRK +type TokenData struct { + // Token contract address + TokenAddress *felt.Felt `json:"token_address"` + // The number of decimals of the token + Decimals uint8 `json:"decimals"` + // Price in STRK (in FRI units) + PriceInStrk string `json:"price_in_strk"` // u256 as a hex string +} diff --git a/paymaster/get_tokens_test.go b/paymaster/get_tokens_test.go new file mode 100644 index 00000000..61f371db --- /dev/null +++ b/paymaster/get_tokens_test.go @@ -0,0 +1,75 @@ +package paymaster + +import ( + "context" + "encoding/json" + "testing" + + "github.com/NethermindEth/starknet.go/internal/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// Test the 'paymaster_getSupportedTokens' method +func TestGetSupportedTokens(t *testing.T) { + t.Parallel() + t.Run("integration", func(t *testing.T) { + tests.RunTestOn(t, tests.IntegrationEnv) + t.Parallel() + + pm, spy := SetupPaymaster(t) + tokens, err := pm.GetSupportedTokens(context.Background()) + require.NoError(t, err) + + rawResult, err := json.Marshal(tokens) + require.NoError(t, err) + assert.EqualValues(t, spy.LastResponse(), rawResult) + }) + + t.Run("mock", func(t *testing.T) { + tests.RunTestOn(t, tests.MockEnv) + t.Parallel() + + pm := SetupMockPaymaster(t) + + expectedRawResult := `[ + { + "token_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "decimals": 18, + "price_in_strk": "0x288aa92ed8c5539ae80" + }, + { + "token_address": "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "decimals": 18, + "price_in_strk": "0xde0b6b3a7640000" + }, + { + "token_address": "0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080", + "decimals": 6, + "price_in_strk": "0x48e1ecdbbe883b08" + }, + { + "token_address": "0x30058f19ed447208015f6430f0102e8ab82d6c291566d7e73fe8e613c3d2ed", + "decimals": 6, + "price_in_strk": "0x2c3460a7992f8a" + } + ]` + + var expectedResult []TokenData + err := json.Unmarshal([]byte(expectedRawResult), &expectedResult) + require.NoError(t, err) + + pm.c.EXPECT(). + CallContextWithSliceArgs(context.Background(), gomock.AssignableToTypeOf(new([]TokenData)), "paymaster_getSupportedTokens"). + SetArg(1, expectedResult). + Return(nil) + result, err := pm.GetSupportedTokens(context.Background()) + assert.NoError(t, err) + assert.Equal(t, expectedResult, result) + + rawResult, err := json.Marshal(result) + require.NoError(t, err) + assert.JSONEq(t, expectedRawResult, string(rawResult)) + }) +} diff --git a/paymaster/paymaster.go b/paymaster/paymaster.go index 99dd616f..a008d41f 100644 --- a/paymaster/paymaster.go +++ b/paymaster/paymaster.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/cookiejar" + "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/starknet.go/client" "golang.org/x/net/publicsuffix" ) @@ -30,8 +31,9 @@ type paymasterInterface interface { ctx context.Context, request *ExecuteTransactionRequest, ) (ExecuteTransactionResponse, error) + GetSupportedTokens(ctx context.Context) ([]TokenData, error) IsAvailable(ctx context.Context) (bool, error) - // More methods coming... + TrackingIDToLatestHash(ctx context.Context, trackingID *felt.Felt) (TrackingIDResponse, error) } var _ paymasterInterface = (*Paymaster)(nil) diff --git a/paymaster/tracking_id.go b/paymaster/tracking_id.go new file mode 100644 index 00000000..b4dd3af7 --- /dev/null +++ b/paymaster/tracking_id.go @@ -0,0 +1,97 @@ +package paymaster + +import ( + "context" + "fmt" + "strconv" + + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/starknet.go/client/rpcerr" +) + +// TrackingIDToLatestHash gets the latest transaction hash and status for a given tracking ID. +// Returns a TrackingIdResponse. +// +// Parameters: +// - ctx: The context.Context object for controlling the function call +// - trackingID: A unique identifier used to track an execution request of a user. +// This identitifier is returned by the paymaster after a successful call to `execute`. +// Its purpose is to track the possibly different transaction hashes in the mempool which +// are associated with a same user request. +// +// Returns: +// - *TrackingIDResponse: The hash of the latest transaction broadcasted by the paymaster +// corresponding to the requested ID and the status of the ID. +// - error: An error if any +func (p *Paymaster) TrackingIDToLatestHash( + ctx context.Context, + trackingID *felt.Felt, +) (TrackingIDResponse, error) { + var response TrackingIDResponse + if err := p.c.CallContextWithSliceArgs( + ctx, + &response, + "paymaster_trackingIdToLatestHash", + trackingID, + ); err != nil { + return TrackingIDResponse{}, rpcerr.UnwrapToRPCErr(err, ErrInvalidID) + } + + return response, nil +} + +// TrackingIDResponse is the response for the `paymaster_trackingIdToLatestHash` method. +type TrackingIDResponse struct { + // The hash of the most recent tx sent by the paymaster and corresponding to the ID + TransactionHash *felt.Felt `json:"transaction_hash"` + // The status of the transaction associated with the ID + Status TxnStatus `json:"status"` +} + +// An enum representing the status of the transaction associated with a tracking ID +type TxnStatus int + +const ( + // Indicates that the latest transaction associated with the ID is not yet + // included in a block but is still being handled and monitored by the paymaster. + // Represents the "active" string value. + TxnStatusActive TxnStatus = iota + 1 + // Indicates that a transaction associated with the ID has been accepted on L2. + // Represents the "accepted" string value. + TxnStatusAccepted + // Indicates that no transaction associated with the ID managed to enter a block + // and the request has been dropped by the paymaster. + // Represents the "dropped" string value. + TxnStatusDropped +) + +// String returns the string representation of the TxnStatus. +func (t TxnStatus) String() string { + return []string{"active", "accepted", "dropped"}[t-1] +} + +// MarshalJSON marshals the TxnStatus to JSON. +func (t TxnStatus) MarshalJSON() ([]byte, error) { + return strconv.AppendQuote(nil, t.String()), nil +} + +// UnmarshalJSON unmarshals the JSON data into a TxnStatus. +func (t *TxnStatus) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + + switch s { + case "active": + *t = TxnStatusActive + case "accepted": + *t = TxnStatusAccepted + case "dropped": + *t = TxnStatusDropped + default: + return fmt.Errorf("invalid transaction status: %s", s) + } + + return nil +} diff --git a/paymaster/tracking_id_test.go b/paymaster/tracking_id_test.go new file mode 100644 index 00000000..78e5156d --- /dev/null +++ b/paymaster/tracking_id_test.go @@ -0,0 +1,94 @@ +package paymaster + +import ( + "context" + "encoding/json" + "testing" + + "github.com/NethermindEth/starknet.go/internal/tests" + internalUtils "github.com/NethermindEth/starknet.go/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// Test the TxnStatus enum type +// +//nolint:dupl // The enum tests are similar, but with different enum values +func TestTxnStatusType(t *testing.T) { + tests.RunTestOn(t, tests.MockEnv) + t.Parallel() + + type testCase struct { + Input string + Expected TxnStatus + ErrorExpected bool + } + + testCases := []testCase{ + { + Input: `"active"`, + Expected: TxnStatusActive, + ErrorExpected: false, + }, + { + Input: `"accepted"`, + Expected: TxnStatusAccepted, + ErrorExpected: false, + }, + { + Input: `"dropped"`, + Expected: TxnStatusDropped, + ErrorExpected: false, + }, + { + Input: `"unknown"`, + ErrorExpected: true, + }, + } + + for _, test := range testCases { + t.Run(test.Input, func(t *testing.T) { + t.Parallel() + CompareEnumsHelper(t, test.Input, test.Expected, test.ErrorExpected) + }) + } +} + +// Test the 'paymaster_trackingIdToLatestHash' method +func TestTrackingIdToLatestHash(t *testing.T) { + // The AVNU paymaster does not support this method yet, so we can't have integration tests + tests.RunTestOn(t, tests.MockEnv) + t.Parallel() + + expectedRawResp := `{ + "transaction_hash": "0xdeadbeef", + "status": "active" + }` + + var expectedResp TrackingIDResponse + err := json.Unmarshal([]byte(expectedRawResp), &expectedResp) + require.NoError(t, err) + + trackingID := internalUtils.DeadBeef + + pm := SetupMockPaymaster(t) + pm.c.EXPECT(). + CallContextWithSliceArgs( + context.Background(), + gomock.AssignableToTypeOf(new(TrackingIDResponse)), + "paymaster_trackingIdToLatestHash", + trackingID, + ). + SetArg(1, expectedResp). + Return(nil) + + response, err := pm.TrackingIDToLatestHash(context.Background(), trackingID) + require.NoError(t, err) + assert.Equal(t, TxnStatusActive, response.Status) + assert.Equal(t, expectedResp.TransactionHash, response.TransactionHash) + + rawResp, err := json.Marshal(response) + require.NoError(t, err) + assert.JSONEq(t, expectedRawResp, string(rawResp)) +} From f7e7e3394c0dead2f8dc71a9f793191f42c3f564 Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Thu, 23 Oct 2025 12:09:01 -0300 Subject: [PATCH 2/3] ci: fix linter --- paymaster/build_txn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paymaster/build_txn_test.go b/paymaster/build_txn_test.go index 588f2c46..6654b906 100644 --- a/paymaster/build_txn_test.go +++ b/paymaster/build_txn_test.go @@ -18,7 +18,7 @@ var STRKContractAddress, _ = internalUtils.HexToFelt( // Test the UserTxnType type // - +//nolint:dupl // The enum tests are similar, but with different enum values func TestUserTxnType(t *testing.T) { tests.RunTestOn(t, tests.MockEnv) t.Parallel() From f5f8ae13883b0cb1b36f270b0dfc31beb56e2d99 Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Thu, 23 Oct 2025 15:55:14 -0300 Subject: [PATCH 3/3] test: replace context.Background() with t.Context() in paymaster tests --- paymaster/get_tokens_test.go | 11 +++++++---- paymaster/is_available_test.go | 11 +++++++---- paymaster/tracking_id_test.go | 5 ++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/paymaster/get_tokens_test.go b/paymaster/get_tokens_test.go index 61f371db..2bf90eef 100644 --- a/paymaster/get_tokens_test.go +++ b/paymaster/get_tokens_test.go @@ -1,7 +1,6 @@ package paymaster import ( - "context" "encoding/json" "testing" @@ -19,7 +18,7 @@ func TestGetSupportedTokens(t *testing.T) { t.Parallel() pm, spy := SetupPaymaster(t) - tokens, err := pm.GetSupportedTokens(context.Background()) + tokens, err := pm.GetSupportedTokens(t.Context()) require.NoError(t, err) rawResult, err := json.Marshal(tokens) @@ -61,10 +60,14 @@ func TestGetSupportedTokens(t *testing.T) { require.NoError(t, err) pm.c.EXPECT(). - CallContextWithSliceArgs(context.Background(), gomock.AssignableToTypeOf(new([]TokenData)), "paymaster_getSupportedTokens"). + CallContextWithSliceArgs( + t.Context(), + gomock.AssignableToTypeOf(new([]TokenData)), + "paymaster_getSupportedTokens", + ). SetArg(1, expectedResult). Return(nil) - result, err := pm.GetSupportedTokens(context.Background()) + result, err := pm.GetSupportedTokens(t.Context()) assert.NoError(t, err) assert.Equal(t, expectedResult, result) diff --git a/paymaster/is_available_test.go b/paymaster/is_available_test.go index 876157f0..aa882787 100644 --- a/paymaster/is_available_test.go +++ b/paymaster/is_available_test.go @@ -1,7 +1,6 @@ package paymaster import ( - "context" "strconv" "testing" @@ -20,7 +19,7 @@ func TestIsAvailable(t *testing.T) { tests.RunTestOn(t, tests.IntegrationEnv) pm, spy := SetupPaymaster(t) - available, err := pm.IsAvailable(context.Background()) + available, err := pm.IsAvailable(t.Context()) require.NoError(t, err) assert.Equal(t, string(spy.LastResponse()), strconv.FormatBool(available)) @@ -32,10 +31,14 @@ func TestIsAvailable(t *testing.T) { pm := SetupMockPaymaster(t) pm.c.EXPECT(). - CallContextWithSliceArgs(context.Background(), gomock.AssignableToTypeOf(new(bool)), "paymaster_isAvailable"). + CallContextWithSliceArgs( + t.Context(), + gomock.AssignableToTypeOf(new(bool)), + "paymaster_isAvailable", + ). SetArg(1, true). Return(nil) - available, err := pm.IsAvailable(context.Background()) + available, err := pm.IsAvailable(t.Context()) assert.NoError(t, err) assert.True(t, available) }) diff --git a/paymaster/tracking_id_test.go b/paymaster/tracking_id_test.go index 78e5156d..14a98d50 100644 --- a/paymaster/tracking_id_test.go +++ b/paymaster/tracking_id_test.go @@ -1,7 +1,6 @@ package paymaster import ( - "context" "encoding/json" "testing" @@ -75,7 +74,7 @@ func TestTrackingIdToLatestHash(t *testing.T) { pm := SetupMockPaymaster(t) pm.c.EXPECT(). CallContextWithSliceArgs( - context.Background(), + t.Context(), gomock.AssignableToTypeOf(new(TrackingIDResponse)), "paymaster_trackingIdToLatestHash", trackingID, @@ -83,7 +82,7 @@ func TestTrackingIdToLatestHash(t *testing.T) { SetArg(1, expectedResp). Return(nil) - response, err := pm.TrackingIDToLatestHash(context.Background(), trackingID) + response, err := pm.TrackingIDToLatestHash(t.Context(), trackingID) require.NoError(t, err) assert.Equal(t, TxnStatusActive, response.Status) assert.Equal(t, expectedResp.TransactionHash, response.TransactionHash)