Skip to content

Commit 07036cd

Browse files
authored
Better node demo (#1369)
* better node demo * cursor comment
1 parent 74f90c1 commit 07036cd

File tree

4 files changed

+384
-30
lines changed

4 files changed

+384
-30
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,5 @@ packages/devreact/webpack.config.json
123123

124124
# Turborepo
125125
.turbo
126+
127+
/temp

playground/multichain-node/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
},
1616
"dependencies": {
1717
"@metamask/multichain-sdk": "workspace:*",
18-
"@metamask/sdk": "workspace:^"
18+
"chalk": "^4.1.2",
19+
"inquirer": "^8.2.4",
20+
"ora": "^5.4.1"
1921
},
2022
"devDependencies": {
23+
"@types/inquirer": "^8.2.1",
2124
"@types/node": "^20.4.1",
2225
"ts-node": "^10.9.1",
2326
"tsup": "^8.0.1",
Lines changed: 259 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,263 @@
1-
import { createMetamaskSDK } from "@metamask/multichain-sdk";
1+
import { createMetamaskSDK, type SessionData } from '@metamask/multichain-sdk';
2+
import chalk from 'chalk';
3+
import inquirer from 'inquirer';
4+
import ora, { type Ora } from 'ora';
25

3-
const validCommands = ['connect'];
6+
// Define the states our application can be in
7+
type AppState = 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'SIGNING';
48

5-
(async () => {
9+
// Store our application state in a simple object
10+
const state: {
11+
app: AppState;
12+
sdk: Awaited<ReturnType<typeof createMetamaskSDK>> | null;
13+
accounts: { [chainId: string]: string[] }; // Group accounts by chain
14+
spinner: Ora | null;
15+
} = {
16+
app: 'DISCONNECTED',
17+
sdk: null,
18+
accounts: {}, // Initialize as an empty object
19+
spinner: null,
20+
};
21+
22+
/**
23+
* Renders the main menu and handles user input.
24+
*/
25+
const showMenu = async () => {
26+
// Don't show the menu if we are in a transient state
27+
if (state.app === 'CONNECTING' || state.app === 'SIGNING') {
28+
return;
29+
}
30+
31+
if (state.app === 'DISCONNECTED') {
32+
const { action } = await inquirer.prompt([
33+
{
34+
type: 'list',
35+
name: 'action',
36+
message: 'You are disconnected. What would you like to do?',
37+
choices: ['Connect', 'Exit'],
38+
},
39+
]);
40+
if (action === 'Connect') {
41+
await handleConnect();
42+
} else {
43+
process.exit(0);
44+
}
45+
} else if (state.app === 'CONNECTED') {
46+
console.log(chalk.green('✓ Connected!'));
47+
console.log(chalk.bold('Accounts:'));
48+
for (const chainId in state.accounts) {
49+
console.log(` ${chalk.cyan(chainId)}: ${state.accounts[chainId].join(', ')}`);
50+
}
51+
52+
const { action } = await inquirer.prompt([
53+
{
54+
type: 'list',
55+
name: 'action',
56+
message: 'What would you like to do next?',
57+
choices: [
58+
'Sign Ethereum Message',
59+
'Sign Solana Message',
60+
'Disconnect'
61+
],
62+
},
63+
]);
64+
65+
if (action === 'Sign Ethereum Message') {
66+
await handleEthereumSign();
67+
} else if (action === 'Sign Solana Message') {
68+
await handleSolanaSign();
69+
} else if (action === 'Disconnect') {
70+
await handleDisconnect();
71+
}
72+
}
73+
};
74+
75+
// --- Action Handlers (stubs for now) ---
76+
77+
const handleConnect = async () => {
78+
state.app = 'CONNECTING';
79+
state.spinner = ora('Connecting... Scan the QR code with your MetaMask Mobile app.').start();
80+
81+
try {
82+
// Requesting accounts for Ethereum Mainnet and Solana Mainnet
83+
await state.sdk?.connect(['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], []);
84+
} catch (error: unknown) {
85+
if (state.spinner) {
86+
state.spinner.fail('Connection failed or was cancelled.');
87+
state.spinner = null;
88+
} else {
89+
console.error(chalk.red('Connection failed or was cancelled.'), error instanceof Error ? error.message : String(error));
90+
}
91+
state.app = 'DISCONNECTED'; // Revert state
92+
}
93+
};
94+
95+
const handleDisconnect = async () => {
96+
state.spinner = ora('Disconnecting...').start();
697
try {
7-
const [, , startType] = process.argv;
8-
console.debug(`start NodeJS example`);
9-
const sdk = await createMetamaskSDK({
10-
dapp: {
11-
name: "playground",
12-
url: "https://playground.metamask.io",
98+
await state.sdk?.disconnect();
99+
state.spinner.succeed('Disconnected successfully.');
100+
} catch (error: unknown) {
101+
state.spinner.fail('Failed to disconnect.');
102+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
103+
} finally {
104+
// The 'wallet_sessionChanged' event will handle the state transition
105+
state.spinner = null;
106+
}
107+
};
108+
109+
const handleEthereumSign = async () => {
110+
const chain = 'eip155:1';
111+
if (!state.accounts[chain] || state.accounts[chain].length === 0) {
112+
console.log(chalk.red('No Ethereum account connected.'));
113+
return;
114+
}
115+
const accountAddress = state.accounts[chain][0].split(':')[2];
116+
117+
const { message } = await inquirer.prompt([
118+
{
119+
type: 'input',
120+
name: 'message',
121+
message: 'Enter the message for Ethereum personal_sign:',
122+
default: 'Hello from the Ethereum world!',
123+
},
124+
]);
125+
126+
const messageHex = `0x${Buffer.from(message, 'utf8').toString('hex')}`;
127+
128+
state.app = 'SIGNING';
129+
state.spinner = ora('Waiting for Ethereum signature... Please check your MetaMask Mobile app.').start();
130+
131+
try {
132+
const result = await state.sdk?.invokeMethod({
133+
scope: chain,
134+
request: {
135+
method: 'personal_sign',
136+
params: [messageHex, accountAddress], // CORRECT: Send only the hex address
137+
},
138+
});
139+
state.spinner.succeed('Ethereum message signed successfully!');
140+
console.log(chalk.bold('Signature:'), result);
141+
} catch (error: unknown) {
142+
state.spinner.fail('Failed to sign Ethereum message.');
143+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
144+
} finally {
145+
state.app = 'CONNECTED';
146+
state.spinner = null;
147+
}
148+
};
149+
150+
const handleSolanaSign = async () => {
151+
const chain = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
152+
if (!state.accounts[chain] || state.accounts[chain].length === 0) {
153+
console.log(chalk.red('No Solana account connected.'));
154+
return;
155+
}
156+
const accountAddress = state.accounts[chain][0].split(':')[2]; // Extract address from CAIP format
157+
158+
const { message } = await inquirer.prompt([
159+
{
160+
type: 'input',
161+
name: 'message',
162+
message: 'Enter the message for Solana signMessage:',
163+
default: 'Hello from the Solana world!',
164+
},
165+
]);
166+
167+
const messageBase64 = Buffer.from(message, 'utf8').toString('base64');
168+
169+
state.app = 'SIGNING';
170+
state.spinner = ora('Waiting for Solana signature... Please check your MetaMask Mobile app.').start();
171+
172+
try {
173+
const result = await state.sdk?.invokeMethod({
174+
scope: chain,
175+
request: {
176+
method: 'signMessage',
177+
params: {
178+
account: { address: accountAddress },
179+
message: messageBase64,
180+
},
13181
},
14-
});
15-
if (!validCommands.includes(startType)) {
16-
throw new Error("Invalid command");
17-
}
18-
if (startType === 'connect') {
19-
const connected = await sdk.connect(
20-
['eip155:137'],
21-
['eip155:137:0x1234567890abcdef1234567890abcdef12345678']
22-
);
23-
console.log('connect request accounts', connected);
24-
}
25-
} catch (error) {
26-
console.error(error);
27-
process.exit(1);
28-
}
29-
})();
182+
});
183+
state.spinner.succeed('Solana message signed successfully!');
184+
console.log(chalk.bold('Signature:'), result);
185+
} catch (error: unknown) {
186+
state.spinner.fail('Failed to sign Solana message.');
187+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
188+
} finally {
189+
state.app = 'CONNECTED';
190+
state.spinner = null;
191+
}
192+
};
193+
194+
/**
195+
* Main application function.
196+
*/
197+
const main = async () => {
198+
console.clear();
199+
console.log(chalk.bold.cyan('MetaMask SDK Node.js Playground'));
200+
console.log('------------------------------------');
201+
202+
state.sdk = await createMetamaskSDK({
203+
dapp: {
204+
name: 'Node.js Playground',
205+
url: 'https://playground.metamask.io',
206+
},
207+
});
208+
209+
// --- SDK Event Handler ---
210+
state.sdk.on('wallet_sessionChanged', (session?: SessionData) => {
211+
if (state.app !== 'CONNECTING') {
212+
// Only clear the console if we are not in the middle of a connection flow
213+
console.clear();
214+
console.log(chalk.bold.cyan('MetaMask SDK Node.js Playground'));
215+
console.log('------------------------------------');
216+
}
217+
218+
if (state.spinner && state.app === 'CONNECTING') {
219+
state.spinner.stop();
220+
state.spinner = null;
221+
}
222+
223+
if (session?.sessionScopes) {
224+
const groupedAccounts: { [chainId: string]: string[] } = {};
225+
for (const scope of Object.values(session.sessionScopes)) {
226+
if (scope.accounts) {
227+
for (const acc of scope.accounts) {
228+
const [namespace, reference] = acc.split(':');
229+
const chainId = `${namespace}:${reference}`;
230+
if (!groupedAccounts[chainId]) {
231+
groupedAccounts[chainId] = [];
232+
}
233+
groupedAccounts[chainId].push(acc);
234+
}
235+
}
236+
}
237+
state.accounts = groupedAccounts;
238+
state.app = 'CONNECTED';
239+
} else {
240+
state.accounts = {};
241+
state.app = 'DISCONNECTED';
242+
console.log(chalk.yellow('Session ended. You are now disconnected.'));
243+
}
244+
});
245+
246+
// --- Main application loop ---
247+
// eslint-disable-next-line no-constant-condition
248+
while (true) {
249+
try {
250+
await showMenu();
251+
} catch (error) {
252+
if (state.spinner) state.spinner.stop();
253+
console.error(chalk.red('An error occurred:'), error);
254+
}
255+
await new Promise((resolve) => setTimeout(resolve, 100));
256+
}
257+
};
258+
259+
main().catch((error) => {
260+
if (state.spinner) state.spinner.stop();
261+
console.error(chalk.red('A critical error occurred:'), error);
262+
process.exit(1);
263+
});

0 commit comments

Comments
 (0)