Skip to content

Commit c303529

Browse files
PierreJeanjacquotCopilotLe-Caignec
authored
feat: multichain support with wallet management (#212)
feat: derive address from private key feat: add arbitrum-sepolia-testnet under EXPERIMENTAL_NETWORKS flag feat: ensure wallet balance before deploy and run feat: add iapp wallet import feat: save wallet in ethereum keystore feat: add iapp wallet select refactor: abort handling * fix: improve balance hint feat: check required RLC balance for iapp run fix: clear spinner after async operation completion feat: warn before sending transactions with fees * fix: typo * Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: typo * Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> refactor: add spinner.reset() helper * fix: typo * Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: return checksummed address for keystore wallets * Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: reword transaction fees warning * Co-authored-by: Robin Le Caignec <72495599+Le-Caignec@users.noreply.github.com> * fix: reword wallet command action description * refactor: split askForWallet and askForImportWallet Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Robin Le Caignec <72495599+Le-Caignec@users.noreply.github.com>
1 parent 058adac commit c303529

27 files changed

+685
-167
lines changed

cli/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"dockerode": "^4.0.5",
4141
"ethers": "^6.13.5",
4242
"figlet": "^1.8.1",
43-
"iexec": "^8.15.0",
43+
"iexec": "^8.16.1",
4444
"jszip": "^3.10.1",
4545
"magic-bytes.js": "^1.10.0",
4646
"msgpackr": "^1.11.2",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AbortError } from '../utils/errors.js';
2+
import { Spinner } from './spinner.js';
3+
4+
export async function askForAcknowledgment({
5+
spinner,
6+
message = 'Would you like to continue?',
7+
}: {
8+
spinner: Spinner;
9+
message?: string;
10+
}) {
11+
const { ack } = await spinner.prompt({
12+
type: 'confirm',
13+
name: 'ack',
14+
message,
15+
initial: true,
16+
});
17+
18+
if (!ack) {
19+
throw new AbortError('Operation cancelled');
20+
}
21+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { AbstractSigner, Wallet } from 'ethers';
2+
import { readIAppConfig, writeIAppConfig } from '../utils/iAppConfigFile.js';
3+
import { CONFIG_FILE } from '../config/config.js';
4+
import * as color from './color.js';
5+
import type { Spinner } from './spinner.js';
6+
import {
7+
KEYSTORE_PATH,
8+
loadWalletFromKeystore,
9+
loadWalletInfoFromKeystore,
10+
saveWalletToKeystore,
11+
walletFileExistsInKeystore,
12+
} from '../utils/keystore.js';
13+
import { AbortError } from '../utils/errors.js';
14+
15+
async function createWalletFromPrivateKey(spinner: Spinner) {
16+
const { answer } = await spinner.prompt({
17+
type: 'password',
18+
name: 'answer',
19+
message: 'What is your wallet private key?',
20+
mask: '*',
21+
});
22+
let wallet;
23+
try {
24+
wallet = new Wallet(answer);
25+
} catch {
26+
spinner.log(color.error('Invalid wallet private key'));
27+
return createWalletFromPrivateKey(spinner);
28+
}
29+
return wallet;
30+
}
31+
32+
const MIN_PASSWORD_LENGTH = 8;
33+
34+
async function walletToKeyStore({
35+
spinner,
36+
wallet,
37+
warnReplace = true,
38+
}: {
39+
spinner: Spinner;
40+
wallet: Wallet;
41+
warnReplace?: boolean;
42+
}) {
43+
if (warnReplace) {
44+
const exists = await walletFileExistsInKeystore({ wallet });
45+
if (exists) {
46+
// TODO ask confirmation?
47+
spinner.warn(
48+
'A wallet file with this address already exists in the keystore, it will be replaced'
49+
);
50+
}
51+
}
52+
const { password } = await spinner.prompt({
53+
type: 'password',
54+
name: 'password',
55+
message: `Choose a password to encrypt your wallet`,
56+
mask: '*',
57+
});
58+
if (password?.length < 8) {
59+
spinner.log(
60+
color.error(
61+
`Password must contain at least ${MIN_PASSWORD_LENGTH} characters`
62+
)
63+
);
64+
return walletToKeyStore({ spinner, wallet, warnReplace: false });
65+
}
66+
67+
const walletFileName = await saveWalletToKeystore({ wallet, password });
68+
const config = await readIAppConfig();
69+
config.walletPrivateKey = undefined;
70+
config.walletFileName = walletFileName;
71+
await writeIAppConfig(config);
72+
spinner.log(
73+
`walletFileName ${color.file(walletFileName)} saved to ${color.file(CONFIG_FILE)}`
74+
);
75+
}
76+
77+
async function walletFromKeystore({
78+
spinner,
79+
walletFileName,
80+
}: {
81+
spinner: Spinner;
82+
walletFileName: string;
83+
}) {
84+
const { password } = await spinner.prompt({
85+
type: 'password',
86+
name: 'password',
87+
message: `Enter the password to unlock your wallet ${color.file(walletFileName)}`,
88+
mask: '*',
89+
});
90+
try {
91+
const wallet = await loadWalletFromKeystore({ walletFileName, password });
92+
return wallet;
93+
} catch {
94+
spinner.log(color.error(`Failed to unlock wallet`));
95+
return walletFromKeystore({
96+
spinner,
97+
walletFileName,
98+
});
99+
}
100+
}
101+
102+
async function walletToPrivateKeyInConfig({
103+
spinner,
104+
wallet,
105+
}: {
106+
spinner: Spinner;
107+
wallet: Wallet;
108+
}) {
109+
const config = await readIAppConfig();
110+
config.walletFileName = undefined;
111+
config.walletPrivateKey = wallet.privateKey;
112+
await writeIAppConfig(config);
113+
spinner.log(`walletPrivateKey saved to ${color.file(CONFIG_FILE)}`);
114+
}
115+
116+
async function askSaveWallet({
117+
spinner,
118+
wallet,
119+
forceSave = false,
120+
}: {
121+
spinner: Spinner;
122+
wallet: Wallet;
123+
forceSave?: boolean;
124+
}) {
125+
const { saveWalletAnswer } = await spinner.prompt([
126+
{
127+
type: 'select',
128+
name: 'saveWalletAnswer',
129+
message: 'Where do you want to save your wallet?',
130+
choices: [
131+
{
132+
title: 'in the encrypted keystore',
133+
value: 'keystore',
134+
description: `encrypted file in ${KEYSTORE_PATH}`,
135+
selected: true,
136+
},
137+
{
138+
title: 'in iapp config file',
139+
value: 'config',
140+
description: `plain text private key in ${CONFIG_FILE}`,
141+
},
142+
...(!forceSave
143+
? [
144+
{
145+
title: 'do not save',
146+
value: 'none',
147+
},
148+
]
149+
: []),
150+
],
151+
},
152+
]);
153+
if (saveWalletAnswer === 'config') {
154+
await walletToPrivateKeyInConfig({ spinner, wallet });
155+
} else if (saveWalletAnswer === 'keystore') {
156+
await walletToKeyStore({ spinner, wallet });
157+
}
158+
}
159+
160+
export async function askForWallet({
161+
spinner,
162+
}: {
163+
spinner: Spinner;
164+
}): Promise<AbstractSigner> {
165+
const config = await readIAppConfig();
166+
167+
// try loading wallet from config
168+
const { walletPrivateKey, walletFileName } = config;
169+
if (walletPrivateKey) {
170+
try {
171+
const wallet = new Wallet(walletPrivateKey);
172+
spinner.log(
173+
`Using saved walletPrivateKey ${color.comment(`(from ${color.file(CONFIG_FILE)})`)}`
174+
);
175+
return wallet;
176+
} catch {
177+
spinner.warn(
178+
`Invalid walletPrivateKey ${color.comment(`(in ${color.file(CONFIG_FILE)})`)}`
179+
);
180+
}
181+
} else if (walletFileName) {
182+
try {
183+
spinner.log(
184+
`Using wallet ${color.file(walletFileName)} ${color.comment(`(from ${color.file(KEYSTORE_PATH)})`)}`
185+
);
186+
const { address, isEncrypted } = await loadWalletInfoFromKeystore({
187+
walletFileName,
188+
});
189+
if (address && isEncrypted) {
190+
const wallet = await walletFromKeystore({ spinner, walletFileName });
191+
return wallet;
192+
}
193+
} catch (e) {
194+
if (e instanceof AbortError) throw e;
195+
spinner.warn(
196+
`Invalid walletFileName ${color.comment(`(in ${color.file(CONFIG_FILE)})`)}`
197+
);
198+
}
199+
}
200+
201+
// if no wallet is found in config, ask for a new one
202+
const wallet = await createWalletFromPrivateKey(spinner);
203+
await askSaveWallet({
204+
spinner,
205+
wallet,
206+
});
207+
return wallet;
208+
}
209+
210+
export async function askForImportWallet({
211+
spinner,
212+
}: {
213+
spinner: Spinner;
214+
}): Promise<AbstractSigner> {
215+
const wallet = await createWalletFromPrivateKey(spinner);
216+
await askSaveWallet({ spinner, wallet, forceSave: true }); // always save when importing
217+
return wallet;
218+
}

cli/src/cli-helpers/askForWalletAddress.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

cli/src/cli-helpers/askForWalletPrivateKey.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

cli/src/cli-helpers/box.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ export function hintBox(message: string) {
99
});
1010
}
1111

12+
export function warnBox(message: string) {
13+
return boxen(message, {
14+
padding: 1,
15+
margin: 1,
16+
borderStyle: 'round',
17+
borderColor: 'yellow',
18+
});
19+
}
20+
1221
export function objectBox(message: string) {
1322
return boxen(message, { margin: 1 });
1423
}

0 commit comments

Comments
 (0)