Skip to content

Commit f0e51db

Browse files
authored
Paymaster PR 4: add GetSupportedTokens and TrackingIDToLatestHash methods + tests (#808)
* feat: add GetSupportedTokens and TrackingIDToLatestHash methods * ci: fix linter * test: replace context.Background() with t.Context() in paymaster tests
1 parent 7cb4d6c commit f0e51db

File tree

7 files changed

+316
-6
lines changed

7 files changed

+316
-6
lines changed

paymaster/build_txn_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var STRKContractAddress, _ = internalUtils.HexToFelt(
1818

1919
// Test the UserTxnType type
2020
//
21-
21+
//nolint:dupl // The enum tests are similar, but with different enum values
2222
func TestUserTxnType(t *testing.T) {
2323
tests.RunTestOn(t, tests.MockEnv)
2424
t.Parallel()

paymaster/get_tokens.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package paymaster
2+
3+
import (
4+
"context"
5+
6+
"github.com/NethermindEth/juno/core/felt"
7+
)
8+
9+
// Get a list of the tokens that the paymaster supports, together with their prices in STRK
10+
//
11+
// Parameters:
12+
// - ctx: The context.Context object for controlling the function call
13+
//
14+
// Returns:
15+
// - []TokenData: An array of token data
16+
// - error: An error if any
17+
func (p *Paymaster) GetSupportedTokens(ctx context.Context) ([]TokenData, error) {
18+
var response []TokenData
19+
if err := p.c.CallContextWithSliceArgs(
20+
ctx, &response, "paymaster_getSupportedTokens",
21+
); err != nil {
22+
return nil, err
23+
}
24+
25+
return response, nil
26+
}
27+
28+
// Object containing data about the token: contract address, number of
29+
// decimals and current price in STRK
30+
type TokenData struct {
31+
// Token contract address
32+
TokenAddress *felt.Felt `json:"token_address"`
33+
// The number of decimals of the token
34+
Decimals uint8 `json:"decimals"`
35+
// Price in STRK (in FRI units)
36+
PriceInStrk string `json:"price_in_strk"` // u256 as a hex string
37+
}

paymaster/get_tokens_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package paymaster
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/NethermindEth/starknet.go/internal/tests"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"go.uber.org/mock/gomock"
11+
)
12+
13+
// Test the 'paymaster_getSupportedTokens' method
14+
func TestGetSupportedTokens(t *testing.T) {
15+
t.Parallel()
16+
t.Run("integration", func(t *testing.T) {
17+
tests.RunTestOn(t, tests.IntegrationEnv)
18+
t.Parallel()
19+
20+
pm, spy := SetupPaymaster(t)
21+
tokens, err := pm.GetSupportedTokens(t.Context())
22+
require.NoError(t, err)
23+
24+
rawResult, err := json.Marshal(tokens)
25+
require.NoError(t, err)
26+
assert.EqualValues(t, spy.LastResponse(), rawResult)
27+
})
28+
29+
t.Run("mock", func(t *testing.T) {
30+
tests.RunTestOn(t, tests.MockEnv)
31+
t.Parallel()
32+
33+
pm := SetupMockPaymaster(t)
34+
35+
expectedRawResult := `[
36+
{
37+
"token_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
38+
"decimals": 18,
39+
"price_in_strk": "0x288aa92ed8c5539ae80"
40+
},
41+
{
42+
"token_address": "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
43+
"decimals": 18,
44+
"price_in_strk": "0xde0b6b3a7640000"
45+
},
46+
{
47+
"token_address": "0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080",
48+
"decimals": 6,
49+
"price_in_strk": "0x48e1ecdbbe883b08"
50+
},
51+
{
52+
"token_address": "0x30058f19ed447208015f6430f0102e8ab82d6c291566d7e73fe8e613c3d2ed",
53+
"decimals": 6,
54+
"price_in_strk": "0x2c3460a7992f8a"
55+
}
56+
]`
57+
58+
var expectedResult []TokenData
59+
err := json.Unmarshal([]byte(expectedRawResult), &expectedResult)
60+
require.NoError(t, err)
61+
62+
pm.c.EXPECT().
63+
CallContextWithSliceArgs(
64+
t.Context(),
65+
gomock.AssignableToTypeOf(new([]TokenData)),
66+
"paymaster_getSupportedTokens",
67+
).
68+
SetArg(1, expectedResult).
69+
Return(nil)
70+
result, err := pm.GetSupportedTokens(t.Context())
71+
assert.NoError(t, err)
72+
assert.Equal(t, expectedResult, result)
73+
74+
rawResult, err := json.Marshal(result)
75+
require.NoError(t, err)
76+
assert.JSONEq(t, expectedRawResult, string(rawResult))
77+
})
78+
}

paymaster/is_available_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package paymaster
22

33
import (
4-
"context"
54
"strconv"
65
"testing"
76

@@ -20,7 +19,7 @@ func TestIsAvailable(t *testing.T) {
2019
tests.RunTestOn(t, tests.IntegrationEnv)
2120

2221
pm, spy := SetupPaymaster(t)
23-
available, err := pm.IsAvailable(context.Background())
22+
available, err := pm.IsAvailable(t.Context())
2423
require.NoError(t, err)
2524

2625
assert.Equal(t, string(spy.LastResponse()), strconv.FormatBool(available))
@@ -32,10 +31,14 @@ func TestIsAvailable(t *testing.T) {
3231

3332
pm := SetupMockPaymaster(t)
3433
pm.c.EXPECT().
35-
CallContextWithSliceArgs(context.Background(), gomock.AssignableToTypeOf(new(bool)), "paymaster_isAvailable").
34+
CallContextWithSliceArgs(
35+
t.Context(),
36+
gomock.AssignableToTypeOf(new(bool)),
37+
"paymaster_isAvailable",
38+
).
3639
SetArg(1, true).
3740
Return(nil)
38-
available, err := pm.IsAvailable(context.Background())
41+
available, err := pm.IsAvailable(t.Context())
3942
assert.NoError(t, err)
4043
assert.True(t, available)
4144
})

paymaster/paymaster.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66
"net/http/cookiejar"
77

8+
"github.com/NethermindEth/juno/core/felt"
89
"github.com/NethermindEth/starknet.go/client"
910
"golang.org/x/net/publicsuffix"
1011
)
@@ -30,8 +31,9 @@ type paymasterInterface interface {
3031
ctx context.Context,
3132
request *ExecuteTransactionRequest,
3233
) (ExecuteTransactionResponse, error)
34+
GetSupportedTokens(ctx context.Context) ([]TokenData, error)
3335
IsAvailable(ctx context.Context) (bool, error)
34-
// More methods coming...
36+
TrackingIDToLatestHash(ctx context.Context, trackingID *felt.Felt) (TrackingIDResponse, error)
3537
}
3638

3739
var _ paymasterInterface = (*Paymaster)(nil)

paymaster/tracking_id.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package paymaster
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/NethermindEth/juno/core/felt"
9+
"github.com/NethermindEth/starknet.go/client/rpcerr"
10+
)
11+
12+
// TrackingIDToLatestHash gets the latest transaction hash and status for a given tracking ID.
13+
// Returns a TrackingIdResponse.
14+
//
15+
// Parameters:
16+
// - ctx: The context.Context object for controlling the function call
17+
// - trackingID: A unique identifier used to track an execution request of a user.
18+
// This identitifier is returned by the paymaster after a successful call to `execute`.
19+
// Its purpose is to track the possibly different transaction hashes in the mempool which
20+
// are associated with a same user request.
21+
//
22+
// Returns:
23+
// - *TrackingIDResponse: The hash of the latest transaction broadcasted by the paymaster
24+
// corresponding to the requested ID and the status of the ID.
25+
// - error: An error if any
26+
func (p *Paymaster) TrackingIDToLatestHash(
27+
ctx context.Context,
28+
trackingID *felt.Felt,
29+
) (TrackingIDResponse, error) {
30+
var response TrackingIDResponse
31+
if err := p.c.CallContextWithSliceArgs(
32+
ctx,
33+
&response,
34+
"paymaster_trackingIdToLatestHash",
35+
trackingID,
36+
); err != nil {
37+
return TrackingIDResponse{}, rpcerr.UnwrapToRPCErr(err, ErrInvalidID)
38+
}
39+
40+
return response, nil
41+
}
42+
43+
// TrackingIDResponse is the response for the `paymaster_trackingIdToLatestHash` method.
44+
type TrackingIDResponse struct {
45+
// The hash of the most recent tx sent by the paymaster and corresponding to the ID
46+
TransactionHash *felt.Felt `json:"transaction_hash"`
47+
// The status of the transaction associated with the ID
48+
Status TxnStatus `json:"status"`
49+
}
50+
51+
// An enum representing the status of the transaction associated with a tracking ID
52+
type TxnStatus int
53+
54+
const (
55+
// Indicates that the latest transaction associated with the ID is not yet
56+
// included in a block but is still being handled and monitored by the paymaster.
57+
// Represents the "active" string value.
58+
TxnStatusActive TxnStatus = iota + 1
59+
// Indicates that a transaction associated with the ID has been accepted on L2.
60+
// Represents the "accepted" string value.
61+
TxnStatusAccepted
62+
// Indicates that no transaction associated with the ID managed to enter a block
63+
// and the request has been dropped by the paymaster.
64+
// Represents the "dropped" string value.
65+
TxnStatusDropped
66+
)
67+
68+
// String returns the string representation of the TxnStatus.
69+
func (t TxnStatus) String() string {
70+
return []string{"active", "accepted", "dropped"}[t-1]
71+
}
72+
73+
// MarshalJSON marshals the TxnStatus to JSON.
74+
func (t TxnStatus) MarshalJSON() ([]byte, error) {
75+
return strconv.AppendQuote(nil, t.String()), nil
76+
}
77+
78+
// UnmarshalJSON unmarshals the JSON data into a TxnStatus.
79+
func (t *TxnStatus) UnmarshalJSON(b []byte) error {
80+
s, err := strconv.Unquote(string(b))
81+
if err != nil {
82+
return err
83+
}
84+
85+
switch s {
86+
case "active":
87+
*t = TxnStatusActive
88+
case "accepted":
89+
*t = TxnStatusAccepted
90+
case "dropped":
91+
*t = TxnStatusDropped
92+
default:
93+
return fmt.Errorf("invalid transaction status: %s", s)
94+
}
95+
96+
return nil
97+
}

paymaster/tracking_id_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package paymaster
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/NethermindEth/starknet.go/internal/tests"
8+
internalUtils "github.com/NethermindEth/starknet.go/internal/utils"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"go.uber.org/mock/gomock"
12+
)
13+
14+
// Test the TxnStatus enum type
15+
//
16+
//nolint:dupl // The enum tests are similar, but with different enum values
17+
func TestTxnStatusType(t *testing.T) {
18+
tests.RunTestOn(t, tests.MockEnv)
19+
t.Parallel()
20+
21+
type testCase struct {
22+
Input string
23+
Expected TxnStatus
24+
ErrorExpected bool
25+
}
26+
27+
testCases := []testCase{
28+
{
29+
Input: `"active"`,
30+
Expected: TxnStatusActive,
31+
ErrorExpected: false,
32+
},
33+
{
34+
Input: `"accepted"`,
35+
Expected: TxnStatusAccepted,
36+
ErrorExpected: false,
37+
},
38+
{
39+
Input: `"dropped"`,
40+
Expected: TxnStatusDropped,
41+
ErrorExpected: false,
42+
},
43+
{
44+
Input: `"unknown"`,
45+
ErrorExpected: true,
46+
},
47+
}
48+
49+
for _, test := range testCases {
50+
t.Run(test.Input, func(t *testing.T) {
51+
t.Parallel()
52+
CompareEnumsHelper(t, test.Input, test.Expected, test.ErrorExpected)
53+
})
54+
}
55+
}
56+
57+
// Test the 'paymaster_trackingIdToLatestHash' method
58+
func TestTrackingIdToLatestHash(t *testing.T) {
59+
// The AVNU paymaster does not support this method yet, so we can't have integration tests
60+
tests.RunTestOn(t, tests.MockEnv)
61+
t.Parallel()
62+
63+
expectedRawResp := `{
64+
"transaction_hash": "0xdeadbeef",
65+
"status": "active"
66+
}`
67+
68+
var expectedResp TrackingIDResponse
69+
err := json.Unmarshal([]byte(expectedRawResp), &expectedResp)
70+
require.NoError(t, err)
71+
72+
trackingID := internalUtils.DeadBeef
73+
74+
pm := SetupMockPaymaster(t)
75+
pm.c.EXPECT().
76+
CallContextWithSliceArgs(
77+
t.Context(),
78+
gomock.AssignableToTypeOf(new(TrackingIDResponse)),
79+
"paymaster_trackingIdToLatestHash",
80+
trackingID,
81+
).
82+
SetArg(1, expectedResp).
83+
Return(nil)
84+
85+
response, err := pm.TrackingIDToLatestHash(t.Context(), trackingID)
86+
require.NoError(t, err)
87+
assert.Equal(t, TxnStatusActive, response.Status)
88+
assert.Equal(t, expectedResp.TransactionHash, response.TransactionHash)
89+
90+
rawResp, err := json.Marshal(response)
91+
require.NoError(t, err)
92+
assert.JSONEq(t, expectedRawResp, string(rawResp))
93+
}

0 commit comments

Comments
 (0)