Skip to content

Commit d2baa52

Browse files
Merge branch 'main' into NWNT-381-solana-snap-initial-websockets-setup
2 parents 5f9beac + 5c742cc commit d2baa52

8 files changed

Lines changed: 337 additions & 181 deletions

File tree

.github/workflows/create-release-pr.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ jobs:
2121
contents: write
2222
pull-requests: write
2323
steps:
24-
- name: Checkout and setup environment
25-
uses: MetaMask/action-checkout-and-setup@v1
24+
- uses: actions/checkout@v4
2625
with:
27-
is-high-risk-environment: false
2826
fetch-depth: 0
2927
ref: ${{ github.event.inputs.base-branch }}
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version-file: '.nvmrc'
3032
- uses: MetaMask/action-create-release-pr@v3
3133
env:
3234
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -39,10 +41,11 @@ jobs:
3941
contents: write
4042
needs: create-release-pr
4143
steps:
42-
- name: Checkout and setup environment
43-
uses: MetaMask/action-checkout-and-setup@v1
44+
- uses: actions/checkout@v4
45+
- name: Setup Node.js
46+
uses: actions/setup-node@v4
4447
with:
45-
is-high-risk-environment: false
48+
node-version-file: '.nvmrc'
4649
- name: Update Snap Version
4750
id: update-release-pr
4851
shell: bash

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "solana-wallet-snap-monorepo",
3-
"version": "1.33.2",
3+
"version": "1.33.3",
44
"private": true,
55
"description": "",
66
"homepage": "https://github.com/MetaMask/snap-solana-wallet#readme",

packages/snap/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.33.3]
11+
12+
### Changed
13+
14+
- Use `InMemoryCache` in `PriceApiService` ([#427](https://github.com/MetaMask/snap-solana-wallet/pull/427))
15+
1016
## [1.33.2]
1117

1218
### Added
@@ -675,7 +681,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
675681
- Sonarcloud to github workflow ([#25](https://github.com/MetaMask/snap-solana-wallet/pull/25))
676682
- Snap setup
677683

678-
[Unreleased]: https://github.com/MetaMask/snap-solana-wallet/compare/v1.33.2...HEAD
684+
[Unreleased]: https://github.com/MetaMask/snap-solana-wallet/compare/v1.33.3...HEAD
685+
[1.33.3]: https://github.com/MetaMask/snap-solana-wallet/compare/v1.33.2...v1.33.3
679686
[1.33.2]: https://github.com/MetaMask/snap-solana-wallet/compare/v1.33.1...v1.33.2
680687
[1.33.1]: https://github.com/MetaMask/snap-solana-wallet/compare/v1.33.0...v1.33.1
681688
[1.33.0]: https://github.com/MetaMask/snap-solana-wallet/compare/v1.32.0...v1.33.0

packages/snap/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metamask/solana-wallet-snap",
3-
"version": "1.33.2",
3+
"version": "1.33.3",
44
"description": "A Solana wallet Snap.",
55
"repository": {
66
"type": "git",

packages/snap/snap.manifest.json

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2-
"version": "1.33.2",
2+
"version": "1.33.3",
33
"description": "Manage Solana using MetaMask",
44
"proposedName": "Solana",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/MetaMask/snap-solana-wallet.git"
88
},
99
"source": {
10-
"shasum": "2N205WbziUCKrdOSuHN6JnTtF+hkqXwDtA1p74OBHyo=",
10+
"shasum": "M/TAq4KOxYiej93YfzvH5+bpPF1bJk+sGuy/LbxGV5A=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
@@ -16,32 +16,22 @@
1616
"registry": "https://registry.npmjs.org/"
1717
}
1818
},
19-
"locales": [
20-
"locales/en.json"
21-
]
19+
"locales": ["locales/en.json"]
2220
},
2321
"initialConnections": {
24-
"https://portfolio.metamask.io": {},
25-
"http://localhost:3000": {}
22+
"https://portfolio.metamask.io": {}
2623
},
2724
"initialPermissions": {
2825
"endowment:rpc": {
2926
"dapps": true,
3027
"snaps": false
3128
},
3229
"endowment:keyring": {
33-
"allowedOrigins": [
34-
"https://portfolio.metamask.io",
35-
"http://localhost:3000"
36-
]
30+
"allowedOrigins": ["https://portfolio.metamask.io"]
3731
},
3832
"snap_getBip32Entropy": [
3933
{
40-
"path": [
41-
"m",
42-
"44'",
43-
"501'"
44-
],
34+
"path": ["m", "44'", "501'"],
4535
"curve": "ed25519"
4636
}
4737
],
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { expect } from '@jest/globals';
2+
import { SnapError } from '@metamask/snaps-sdk';
3+
4+
import { withCatchAndThrowSnapError } from './errors';
5+
import logger from './logger';
6+
7+
// Mock the logger to avoid actual console output during tests
8+
jest.mock('./logger', () => ({
9+
error: jest.fn(),
10+
}));
11+
12+
describe('errors', () => {
13+
const mockLogger = logger as jest.Mocked<typeof logger>;
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
describe('handle', () => {
20+
it('returns the result when the function succeeds', async () => {
21+
const mockFn = jest.fn().mockResolvedValue('success');
22+
23+
const result = await withCatchAndThrowSnapError(mockFn);
24+
25+
expect(result).toBe('success');
26+
expect(mockFn).toHaveBeenCalledTimes(1);
27+
expect(mockLogger.error).not.toHaveBeenCalled();
28+
});
29+
30+
it('handles and re-throws errors as SnapError', async () => {
31+
const originalError = new Error('Test error');
32+
const mockFn = jest.fn().mockRejectedValue(originalError);
33+
34+
await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow(
35+
SnapError,
36+
);
37+
38+
expect(mockFn).toHaveBeenCalledTimes(1);
39+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
40+
});
41+
42+
it('logs errors with the correct scope and error details', async () => {
43+
const originalError = new Error('Test error');
44+
const mockFn = jest.fn().mockRejectedValue(originalError);
45+
46+
try {
47+
await withCatchAndThrowSnapError(mockFn);
48+
} catch (error) {
49+
// Expected to throw
50+
}
51+
52+
expect(mockLogger.error).toHaveBeenCalledWith(
53+
{ error: expect.any(SnapError) },
54+
expect.stringContaining(`[SnapError]`),
55+
);
56+
57+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
58+
const logCall = mockLogger.error.mock.calls[0];
59+
const loggedError = logCall?.[0]?.error;
60+
expect(loggedError).toBeInstanceOf(SnapError);
61+
});
62+
63+
it('handles non-Error objects and converts them to SnapError', async () => {
64+
const nonErrorValue = 'string error';
65+
const mockFn = jest.fn().mockRejectedValue(nonErrorValue);
66+
67+
await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow(
68+
SnapError,
69+
);
70+
71+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
72+
const logCall = mockLogger.error.mock.calls[0];
73+
const loggedError = logCall?.[0]?.error;
74+
expect(loggedError).toBeInstanceOf(SnapError);
75+
});
76+
77+
it('handles null and undefined errors', async () => {
78+
const mockFn = jest.fn().mockRejectedValue(null);
79+
80+
await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow(
81+
SnapError,
82+
);
83+
84+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
85+
});
86+
87+
it('preserves the original error message in the SnapError', async () => {
88+
const originalError = new Error('Custom error message');
89+
const mockFn = jest.fn().mockRejectedValue(originalError);
90+
91+
let caughtError: unknown;
92+
try {
93+
await withCatchAndThrowSnapError(mockFn);
94+
} catch (error) {
95+
caughtError = error;
96+
}
97+
98+
expect(caughtError).toBeInstanceOf(SnapError);
99+
const snapError = caughtError as SnapError;
100+
expect(snapError.message).toBe('Custom error message');
101+
});
102+
103+
it('handles async functions that return different types', async () => {
104+
const testCases = [
105+
{ value: 42, type: 'number' },
106+
{ value: { key: 'value' }, type: 'object' },
107+
{ value: [1, 2, 3], type: 'array' },
108+
{ value: true, type: 'boolean' },
109+
{ value: null, type: 'null' },
110+
];
111+
112+
for (const testCase of testCases) {
113+
const mockFn = jest.fn().mockResolvedValue(testCase.value);
114+
115+
const result = await withCatchAndThrowSnapError(mockFn);
116+
117+
expect(result).toBe(testCase.value);
118+
expect(mockLogger.error).not.toHaveBeenCalled();
119+
}
120+
});
121+
122+
it('handles functions that throw different error types', async () => {
123+
const errorTypes = [
124+
new TypeError('Type error'),
125+
new ReferenceError('Reference error'),
126+
new RangeError('Range error'),
127+
new SyntaxError('Syntax error'),
128+
];
129+
130+
for (const errorType of errorTypes) {
131+
const mockFn = jest.fn().mockRejectedValue(errorType);
132+
133+
await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow(
134+
SnapError,
135+
);
136+
}
137+
138+
expect(mockLogger.error).toHaveBeenCalledTimes(errorTypes.length);
139+
const logCalls = mockLogger.error.mock.calls;
140+
expect(logCalls).toHaveLength(errorTypes.length);
141+
142+
for (let i = 0; i < errorTypes.length; i++) {
143+
const logCall = logCalls[i];
144+
const loggedError = logCall?.[0]?.error;
145+
expect(loggedError).toBeInstanceOf(SnapError);
146+
expect(loggedError?.message).toBe(errorTypes[i]?.message);
147+
}
148+
});
149+
150+
it('includes error stack trace in the logged error', async () => {
151+
const originalError = new Error('Test error');
152+
originalError.stack = 'Error: Test error\n at test.js:1:1';
153+
const mockFn = jest.fn().mockRejectedValue(originalError);
154+
155+
try {
156+
await withCatchAndThrowSnapError(mockFn);
157+
} catch (error) {
158+
// Expected to throw
159+
}
160+
161+
expect(mockLogger.error).toHaveBeenCalledWith(
162+
{ error: expect.any(SnapError) },
163+
expect.stringContaining('[SnapError]'),
164+
);
165+
});
166+
167+
it('handles functions that throw promises', async () => {
168+
const rejectedPromise = Promise.reject(new Error('Promise error'));
169+
const mockFn = jest.fn().mockImplementation(async () => rejectedPromise);
170+
171+
await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow(
172+
SnapError,
173+
);
174+
175+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
176+
});
177+
});
178+
});
Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,24 @@
11
import {
22
MethodNotFoundError,
3-
UserRejectedRequestError,
4-
MethodNotSupportedError,
53
ParseError,
64
ResourceNotFoundError,
75
ResourceUnavailableError,
8-
TransactionRejected,
96
ChainDisconnectedError,
7+
TransactionRejected,
108
DisconnectedError,
9+
InternalError,
1110
UnauthorizedError,
1211
UnsupportedMethodError,
13-
InternalError,
1412
InvalidInputError,
1513
InvalidParamsError,
1614
InvalidRequestError,
1715
LimitExceededError,
1816
SnapError,
17+
MethodNotSupportedError,
18+
UserRejectedRequestError,
1919
} from '@metamask/snaps-sdk';
2020

21-
export class CustomError extends Error {
22-
name!: string;
23-
24-
constructor(message?: string) {
25-
super(message);
26-
27-
// set error name as constructor name, make it not enumerable to keep native Error behavior
28-
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors
29-
// see https://github.com/adriengibrat/ts-custom-error/issues/30
30-
Object.defineProperty(this, 'name', {
31-
value: new.target.name,
32-
enumerable: false,
33-
configurable: true,
34-
});
35-
36-
// fix the extended error prototype chain
37-
// because typescript __extends implementation can't
38-
// see https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
39-
Object.setPrototypeOf(this, new.target.prototype);
40-
// remove constructor from stack trace
41-
Error.captureStackTrace(this, this.constructor);
42-
}
43-
}
44-
45-
/**
46-
* Compacts an error to a specific error instance.
47-
*
48-
* @param error - The error instance to be compacted.
49-
* @param ErrCtor - The error constructor for the desired error instance.
50-
* @returns The compacted error instance.
51-
*/
52-
export function compactError<ErrorInstance extends Error>(
53-
error: ErrorInstance,
54-
ErrCtor: new (message?: string) => ErrorInstance,
55-
): ErrorInstance {
56-
if (error instanceof ErrCtor) {
57-
return error;
58-
}
59-
return new ErrCtor(error.message);
60-
}
21+
import logger from './logger';
6122

6223
/**
6324
* Determines if the given error is a Snap RPC error.
@@ -88,3 +49,22 @@ export function isSnapRpcError(error: Error): boolean {
8849
];
8950
return errors.some((errType) => error instanceof errType);
9051
}
52+
53+
export const withCatchAndThrowSnapError = async <ResponseT>(
54+
fn: () => Promise<ResponseT>,
55+
): Promise<ResponseT> => {
56+
try {
57+
return await fn();
58+
} catch (errorInstance: any) {
59+
const error = isSnapRpcError(errorInstance)
60+
? errorInstance
61+
: new SnapError(errorInstance);
62+
63+
logger.error(
64+
{ error },
65+
`[SnapError] ${JSON.stringify(error.toJSON(), null, 2)}`,
66+
);
67+
68+
throw error;
69+
}
70+
};

0 commit comments

Comments
 (0)