Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b06b492
feat: init websockets infra code
xavier-brochard Jun 23, 2025
63f6a76
Merge branch 'main' into websockets
xavier-brochard Jun 23, 2025
629c92f
chore: imporve impls
xavier-brochard Jun 25, 2025
b7e3586
chore: clean up
xavier-brochard Jun 25, 2025
b0e2beb
chore: fix name
xavier-brochard Jun 25, 2025
ece665b
refactor: rename things
xavier-brochard Jun 25, 2025
71b957c
chore: reafactor
xavier-brochard Jun 26, 2025
5f9beac
chore: tmp
xavier-brochard Jun 26, 2025
d2baa52
Merge branch 'main' into NWNT-381-solana-snap-initial-websockets-setup
xavier-brochard Jun 26, 2025
db378f5
chore: import paths
xavier-brochard Jun 26, 2025
0a7788c
chore: rename
xavier-brochard Jun 26, 2025
95705db
test: adapt unit tests
xavier-brochard Jun 27, 2025
a96b5e1
feat: handle automatic re-subscription
xavier-brochard Jun 27, 2025
410bacb
chore: fix stuff
xavier-brochard Jun 30, 2025
600ee16
chore: clean up imports
xavier-brochard Jun 30, 2025
e9e9c3f
Merge branch 'main' into NWNT-381-solana-snap-initial-websockets-setup
xavier-brochard Jun 30, 2025
64e3e31
chore: shasum
xavier-brochard Jun 30, 2025
f561338
feat: add new env vars to CI
xavier-brochard Jul 1, 2025
5c46180
chore: lint
xavier-brochard Jul 1, 2025
5351a20
test: disable logging in tests
xavier-brochard Jul 1, 2025
b93ab82
test: fix tests
xavier-brochard Jul 1, 2025
77f5915
chore: clean up
xavier-brochard Jul 1, 2025
d010134
docs: improve docs
xavier-brochard Jul 1, 2025
9c4206e
chore: shasum
xavier-brochard Jul 1, 2025
ec2ad25
test: fix test
xavier-brochard Jul 1, 2025
c25ed65
chore: shasum
xavier-brochard Jul 1, 2025
2f948db
fix: move polyfills out of /infrastructure, it was causing issues w/ …
xavier-brochard Jul 1, 2025
f5b13a9
test: enable manual testing from the test dapp
xavier-brochard Jul 1, 2025
e867583
docs: add jsdoc
xavier-brochard Jul 1, 2025
55a7ae6
chore: improve copy
xavier-brochard Jul 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build-lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ on:
required: true
RPC_URL_LOCALNET_LIST:
required: true
RPC_WEB_SOCKET_URL_MAINNET:
required: true
RPC_WEB_SOCKET_URL_DEVNET:
required: true
RPC_WEB_SOCKET_URL_TESTNET:
required: true
RPC_WEB_SOCKET_URL_LOCALNET:
required: true
PRICE_API_BASE_URL:
required: true
LOCAL_API_BASE_URL:
Expand Down Expand Up @@ -47,6 +55,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down Expand Up @@ -94,6 +106,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/create-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down Expand Up @@ -87,6 +91,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ on:
required: true
RPC_URL_LOCALNET_LIST:
required: true
RPC_WEB_SOCKET_URL_MAINNET:
required: true
RPC_WEB_SOCKET_URL_DEVNET:
required: true
RPC_WEB_SOCKET_URL_TESTNET:
required: true
RPC_WEB_SOCKET_URL_LOCALNET:
required: true
PRICE_API_BASE_URL:
required: true
LOCAL_API_BASE_URL:
Expand Down Expand Up @@ -51,6 +59,10 @@ jobs:
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
Expand Down
2 changes: 2 additions & 0 deletions packages/site/src/components/Handlers/Handlers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Text as ChakraText, Flex, Stack } from '@chakra-ui/react';

import { OnProtocolRequest } from './OnProtocolRequest';
import { WebSockets } from './WebSockets';

export const Handlers = () => (
<Flex direction="column" width="full" marginBottom="5">
Expand All @@ -9,6 +10,7 @@ export const Handlers = () => (
</ChakraText>
<Stack direction="row" gap="5" wrap="wrap">
<OnProtocolRequest />
<WebSockets />
</Stack>
</Flex>
);
68 changes: 68 additions & 0 deletions packages/site/src/components/Handlers/WebSockets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, Card, Flex } from '@chakra-ui/react';

import { TestDappRpcRequestMethod } from '../../../../snap/src/core/handlers/onRpcRequest/types';
import { useInvokeSnap } from '../../hooks';
import { useShowToasterForResponse } from '../../hooks/useToasterForResponse';

export const WebSockets = () => {
const invokeSnap = useInvokeSnap();
const { showToasterForResponse } = useShowToasterForResponse();

const setupAllConnections = async () => {
showToasterForResponse(
{ result: 'ok' },
{
title: 'Initiated WebSocket connections to all active networks',
description: `Inspect the offscreen console`,
},
);

await invokeSnap({
method: TestDappRpcRequestMethod.TestSetupAllConnections,
});
};

const subscribeToAccount = async () => {
await invokeSnap({
method: TestDappRpcRequestMethod.TestSubscribeToAccount,
});

showToasterForResponse(
{ result: 'ok' },
{
title:
'Initiated subscription to account 8A4AptCThfbuknsbteHgGKXczfJpfjuVA9SLTSGaaLGC',
description: `Inspect the offscreen console`,
},
);
};

return (
<Card.Root>
<Card.Header>
<Card.Title>WebSockets</Card.Title>
</Card.Header>
<Card.Body gap="2">
<Flex>
<Button
variant="outline"
onClick={setupAllConnections}
marginRight="1"
>
Setup All Connections
</Button>
</Flex>
<Flex>
<Button
variant="outline"
onClick={subscribeToAccount}
marginRight="1"
>
Subscribe to account
</Button>
</Flex>
</Card.Body>
</Card.Root>
);
};
6 changes: 6 additions & 0 deletions packages/snap/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ RPC_URL_MAINNET_LIST=
RPC_URL_DEVNET_LIST=
RPC_URL_TESTNET_LIST=
RPC_URL_LOCALNET_LIST=

RPC_WEB_SOCKET_URL_MAINNET=
RPC_WEB_SOCKET_URL_DEVNET=
RPC_WEB_SOCKET_URL_TESTNET=
RPC_WEB_SOCKET_URL_LOCALNET=

EXPLORER_BASE_URL=

PRICE_API_BASE_URL=
Expand Down
4 changes: 4 additions & 0 deletions packages/snap/snap.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const environment = {
RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST ?? '',
RPC_URL_TESTNET_LIST: process.env.RPC_URL_TESTNET_LIST ?? '',
RPC_URL_LOCALNET_LIST: process.env.RPC_URL_LOCALNET_LIST ?? '',
RPC_WEB_SOCKET_URL_MAINNET: process.env.RPC_WEB_SOCKET_URL_MAINNET ?? '',
RPC_WEB_SOCKET_URL_DEVNET: process.env.RPC_WEB_SOCKET_URL_DEVNET ?? '',
RPC_WEB_SOCKET_URL_TESTNET: process.env.RPC_WEB_SOCKET_URL_TESTNET ?? '',
RPC_WEB_SOCKET_URL_LOCALNET: process.env.RPC_WEB_SOCKET_URL_LOCALNET ?? '',
EXPLORER_BASE_URL: process.env.EXPLORER_BASE_URL ?? '',
PRICE_API_BASE_URL: process.env.PRICE_API_BASE_URL ?? '',
TOKEN_API_BASE_URL: process.env.TOKEN_API_BASE_URL ?? '',
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snap-solana-wallet.git"
},
"source": {
"shasum": "U6qs3tA3phhscX54m+BAD5Mi1aBgT9p9g3qQ2qHrCsQ=",
"shasum": "XSE/gfpj/gSu5aA9IihnjJ+IczHvXAdJN1Z/EegXk7w=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ describe('PriceApiClient', () => {
mockFetch,
mockLogger,
),
).toThrow('URL must use http or https protocol');
).toThrow(
'URL must use one of the following protocols: http:,https:,wss:',
);
});

it('rejects tokenCaip19Ids that are invalid or that include malicious inputs', async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/snap/src/core/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './onClientRequest';
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { CaipAssetType, OnCronjobHandler } from '@metamask/snaps-sdk';
import { type SpotPrices } from 'src/core/clients/price-api/types';

import { DEFAULT_SEND_CONTEXT } from '../../../../features/send/render';
import { Send } from '../../../../features/send/Send';
import type { SendContext } from '../../../../features/send/types';
import { priceApiClient, state } from '../../../../snapContext';
import type { SpotPrices } from '../../../clients/price-api/types';
import type { UnencryptedStateValue } from '../../../services/state/State';
import {
getInterfaceContextOrThrow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ describe('SolanaKeyring', () => {
metadata: {},
tokenPrices: {},
signatures: {},
subscriptions: {},
});
// Start with no accounts
keyring = new SolanaKeyring({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
asStrictKeyringAccount,
type SolanaKeyringAccount,
} from '../../../entities';
import { type Network } from '../../constants/solana';
import type { Network } from '../../constants/solana';
import type { AssetsService } from '../../services/assets/AssetsService';
import type { ConfirmationHandler } from '../../services/confirmation/ConfirmationHandler';
import type { IStateManager } from '../../services/state/IStateManager';
Expand Down
13 changes: 12 additions & 1 deletion packages/snap/src/core/handlers/onRpcRequest/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { type OnRpcRequestHandler } from '@metamask/snaps-sdk';

import { renderSend } from '../../../features/send/render';
import { eventEmitter } from '../../../snapContext';
import { getFeeForTransaction } from './getFeeForTransaction';
import { RpcRequestMethod } from './types';
import { RpcRequestMethod, TestDappRpcRequestMethod } from './types';

export const handlers: Record<RpcRequestMethod, OnRpcRequestHandler> = {
[RpcRequestMethod.StartSendTransactionFlow]: renderSend,
[RpcRequestMethod.GetFeeForTransaction]: getFeeForTransaction,

// Methods specific to the test dapp
[TestDappRpcRequestMethod.TestSetupAllConnections as any]: async () => {
await eventEmitter.emitSync('onTestSetupAllConnections');
return null;
},
[TestDappRpcRequestMethod.TestSubscribeToAccount as any]: async () => {
await eventEmitter.emitSync('onTestSubscribeToAccount');
return null;
},
};
9 changes: 9 additions & 0 deletions packages/snap/src/core/handlers/onRpcRequest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@ export enum RpcRequestMethod {
StartSendTransactionFlow = 'startSendTransactionFlow',
GetFeeForTransaction = 'getFeeForTransaction',
}

/**
* Methods specific to the test dapp,
* to allow specific flows for manual testing.
*/
export enum TestDappRpcRequestMethod {
TestSetupAllConnections = 'testSetupAllConnections',
TestSubscribeToAccount = 'testSubscribeToAccount',
}
1 change: 1 addition & 0 deletions packages/snap/src/core/ports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './subscriptions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Network } from '../../constants/solana';

/**
* Port interface for managing the lifecycle of JSON-RPC subscription connections.
* It is responsible for opening and closing connections for each network,
* and for re-opening connections when they are dropped.
*/
export type ConnectionManagerPort = {
/**
* Sets up connections for all networks.
* - Opens the connections for all enabled networks that are not already open.
* - Closes the connections for all disabled networks.
* @returns A promise that resolves when the connections are setup.
*/
setupAllConnections(): Promise<void>;

/**
* Registers a callback to be called when connection is recovered.
* @param network - The network to register the callback for.
* @param callback - The callback function to register.
*/
onConnectionRecovery(network: Network, callback: () => Promise<void>): void;

/**
* Gets the connection ID for the specified network.
* @param network - The network to get the connection ID for.
* @returns The connection ID, or null if no connection exists for the network.
*/
getConnectionIdByNetwork(network: Network): Promise<string | null>;
};
Loading
Loading