Skip to content

Commit 5dae8eb

Browse files
committed
feat: new paymaster pkg. 2 out of 5 method implemented
1 parent cea2adc commit 5dae8eb

File tree

8 files changed

+378
-1
lines changed

8 files changed

+378
-1
lines changed

internal/tests/tests.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ var TEST_ENV TestEnv
1818
type TestEnv string
1919

2020
const (
21-
MockEnv TestEnv = "mock"
21+
MockEnv TestEnv = "mock"
22+
// Used to run account and rpc tests on the Integration network.
23+
// Also, used to run paymaster tests with the Avnu Sepolia paymaster.
2224
IntegrationEnv TestEnv = "integration"
2325
TestnetEnv TestEnv = "testnet"
2426
MainnetEnv TestEnv = "mainnet"

paymaster/errors.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package paymaster
2+
3+
import (
4+
"github.com/NethermindEth/starknet.go/client/rpcerr"
5+
)
6+
7+
// aliases to facilitate usage
8+
9+
type (
10+
RPCError = rpcerr.RPCError
11+
StringErrData = rpcerr.StringErrData
12+
)
13+
14+
// Paymaster-specific errors based on SNIP-29 specification
15+
//
16+
//nolint:exhaustruct
17+
var (
18+
ErrInvalidAddress = &RPCError{
19+
Code: 150,
20+
Message: "An error occurred (INVALID_ADDRESS)",
21+
}
22+
23+
ErrTokenNotSupported = &RPCError{
24+
Code: 151,
25+
Message: "An error occurred (TOKEN_NOT_SUPPORTED)",
26+
}
27+
28+
ErrInvalidSignature = &RPCError{
29+
Code: 153,
30+
Message: "An error occurred (INVALID_SIGNATURE)",
31+
}
32+
33+
ErrMaxAmountTooLow = &RPCError{
34+
Code: 154,
35+
Message: "An error occurred (MAX_AMOUNT_TOO_LOW)",
36+
}
37+
38+
ErrClassHashNotSupported = &RPCError{
39+
Code: 155,
40+
Message: "An error occurred (CLASS_HASH_NOT_SUPPORTED)",
41+
}
42+
43+
ErrTransactionExecutionError = &RPCError{
44+
Code: 156,
45+
Message: "An error occurred (TRANSACTION_EXECUTION_ERROR)",
46+
Data: &TxnExecutionErrData{},
47+
}
48+
49+
ErrInvalidTimeBounds = &RPCError{
50+
Code: 157,
51+
Message: "An error occurred (INVALID_TIME_BOUNDS)",
52+
}
53+
54+
ErrInvalidDeploymentData = &RPCError{
55+
Code: 158,
56+
Message: "An error occurred (INVALID_DEPLOYMENT_DATA)",
57+
}
58+
59+
ErrInvalidClassHash = &RPCError{
60+
Code: 159,
61+
Message: "An error occurred (INVALID_ADDRESS)",
62+
}
63+
64+
ErrInvalidID = &RPCError{
65+
Code: 160,
66+
Message: "An error occurred (INVALID_ID)",
67+
}
68+
69+
ErrUnknownError = &RPCError{
70+
Code: 163,
71+
Message: "An error occurred (UNKNOWN_ERROR)",
72+
Data: StringErrData(""),
73+
}
74+
)
75+
76+
// TxnExecutionErrData represents the structured data for TRANSACTION_EXECUTION_ERROR
77+
type TxnExecutionErrData struct {
78+
ExecutionError string `json:"execution_error"`
79+
}
80+
81+
// ErrorMessage implements the RPCData interface
82+
func (t TxnExecutionErrData) ErrorMessage() string {
83+
return t.ExecutionError
84+
}

paymaster/get_tokens.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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(ctx, &response, "paymaster_getSupportedTokens"); err != nil {
20+
return nil, err
21+
}
22+
23+
return response, nil
24+
}
25+
26+
// Object containing data about the token: contract address, number of decimals and current price in STRK
27+
type TokenData struct {
28+
// Token contract address
29+
TokenAddress *felt.Felt `json:"token_address"`
30+
// The number of decimals of the token
31+
Decimals uint8 `json:"decimals"`
32+
// Price in STRK (in FRI units)
33+
PriceInStrk string `json:"price_in_strk"` // u256 as a hex string
34+
}

paymaster/get_tokens_test.go

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

paymaster/is_available.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package paymaster
2+
3+
import "context"
4+
5+
// IsAvailable returns the status of the paymaster service.
6+
// If the paymaster service is correctly functioning, return true. Else, return false
7+
//
8+
// Parameters:
9+
// - ctx: The context.Context object for controlling the function call
10+
//
11+
// Returns:
12+
// - bool: True if the paymaster service is correctly functioning, false otherwise
13+
// - error: An error if any
14+
func (p *Paymaster) IsAvailable(ctx context.Context) (bool, error) {
15+
var response bool
16+
if err := p.c.CallContextWithSliceArgs(ctx, &response, "paymaster_isAvailable"); err != nil {
17+
return false, err
18+
}
19+
20+
return response, nil
21+
}

paymaster/is_available_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package paymaster
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"testing"
7+
8+
"github.com/NethermindEth/starknet.go/internal/tests"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"go.uber.org/mock/gomock"
12+
)
13+
14+
// Test the 'paymaster_isAvailable' method
15+
func TestIsAvailable(t *testing.T) {
16+
t.Parallel()
17+
t.Run("integration", func(t *testing.T) {
18+
tests.RunTestOn(t, tests.IntegrationEnv)
19+
t.Parallel()
20+
21+
pm, spy := SetupPaymaster(t)
22+
available, err := pm.IsAvailable(context.Background())
23+
require.NoError(t, err)
24+
25+
assert.Equal(t, string(spy.LastResponse()), strconv.FormatBool(available))
26+
assert.True(t, available)
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+
pm.c.EXPECT().
35+
CallContextWithSliceArgs(context.Background(), gomock.AssignableToTypeOf(new(bool)), "paymaster_isAvailable").
36+
SetArg(1, true).
37+
Return(nil)
38+
available, err := pm.IsAvailable(context.Background())
39+
assert.NoError(t, err)
40+
assert.True(t, available)
41+
})
42+
}

paymaster/main_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package paymaster
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/NethermindEth/starknet.go/client"
8+
"github.com/NethermindEth/starknet.go/internal/tests"
9+
"github.com/NethermindEth/starknet.go/mocks"
10+
"github.com/stretchr/testify/require"
11+
"go.uber.org/mock/gomock"
12+
)
13+
14+
const avnuPaymasterURL = "https://sepolia.paymaster.avnu.fi"
15+
16+
func TestMain(m *testing.M) {
17+
tests.LoadEnv()
18+
os.Exit(m.Run())
19+
}
20+
21+
type MockPaymaster struct {
22+
*Paymaster
23+
// this should be a pointer to the mock client used in the Paymaster struct.
24+
// This is intended to have an easy access to the mock client, without having to
25+
// type cast it from the `callCloser` interface every time.
26+
c *mocks.MockClient
27+
}
28+
29+
// Creates a real Sepolia paymaster client and a spy for integration tests.
30+
func SetupPaymaster(t *testing.T, debug ...bool) (*Paymaster, tests.Spyer) {
31+
t.Helper()
32+
33+
apiKey := os.Getenv("AVNU_API_KEY")
34+
require.NotEmpty(t, apiKey, "AVNU_API_KEY is not set")
35+
apiHeader := client.WithHeader("x-paymaster-api-key", apiKey)
36+
37+
pm, err := New(avnuPaymasterURL, apiHeader)
38+
require.NoError(t, err, "failed to create paymaster client")
39+
40+
spy := tests.NewJSONRPCSpy(pm.c, debug...)
41+
pm.c = spy
42+
43+
return pm, spy
44+
}
45+
46+
// Creates a mock paymaster client.
47+
func SetupMockPaymaster(t *testing.T) *MockPaymaster {
48+
t.Helper()
49+
50+
pmClient := mocks.NewMockClient(gomock.NewController(t))
51+
mpm := &MockPaymaster{
52+
Paymaster: &Paymaster{c: pmClient},
53+
c: pmClient,
54+
}
55+
56+
return mpm
57+
}

paymaster/paymaster.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package paymaster
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/cookiejar"
7+
8+
"github.com/NethermindEth/starknet.go/client"
9+
"golang.org/x/net/publicsuffix"
10+
)
11+
12+
// Paymaster is a client for interacting with a paymaster service via the SNIP-29 API.
13+
// It provides methods to build and execute transactions, check service status, and track transaction status.
14+
type Paymaster struct {
15+
// c is the underlying client for the paymaster service.
16+
c callCloser
17+
}
18+
19+
// Used to assert that the Paymaster struct implements all the paymaster methods.
20+
// Ref: https://github.com/starknet-io/SNIPs/blob/ea46a8777d8c8d53a43f45b7beb1abcc301a1a69/assets/snip-29/paymaster_api.json
21+
type paymasterInterface interface {
22+
IsAvailable(ctx context.Context) (bool, error)
23+
GetSupportedTokens(ctx context.Context) ([]TokenData, error)
24+
}
25+
26+
var _ paymasterInterface = &Paymaster{} //nolint:exhaustruct
27+
28+
// callCloser is an interface that defines the methods for calling a remote procedure.
29+
// It was created to match the Client struct from the 'client' package.
30+
type callCloser interface {
31+
CallContext(ctx context.Context, result interface{}, method string, args interface{}) error
32+
CallContextWithSliceArgs(ctx context.Context, result interface{}, method string, args ...interface{}) error
33+
Close()
34+
}
35+
36+
// Creates a new paymaster client for the given service URL.
37+
// Additional options can be passed to the client to configure the connection.
38+
//
39+
// Parameters:
40+
// - url: The URL of the paymaster service
41+
// - options: Additional options to configure the client
42+
//
43+
// Returns:
44+
// - *Paymaster: A new paymaster client instance
45+
// - error: An error if the client creation fails
46+
func New(url string, options ...client.ClientOption) (*Paymaster, error) {
47+
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
48+
if err != nil {
49+
return nil, err
50+
}
51+
httpClient := &http.Client{Jar: jar} //nolint:exhaustruct
52+
// prepend the custom client to allow users to override
53+
options = append([]client.ClientOption{client.WithHTTPClient(httpClient)}, options...)
54+
c, err := client.DialOptions(context.Background(), url, options...)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
paymaster := &Paymaster{c: c}
60+
61+
return paymaster, nil
62+
}

0 commit comments

Comments
 (0)