Skip to content

Commit c70b766

Browse files
Merge branch 'main' into pom-personal-sign
2 parents 71fb820 + f807d27 commit c70b766

File tree

4 files changed

+262
-24
lines changed

4 files changed

+262
-24
lines changed

app/images/genesys.svg

Lines changed: 10 additions & 22 deletions
Loading
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Suite } from 'mocha';
2+
import FixtureBuilder from '../../fixture-builder';
3+
import { withFixtures } from '../../helpers';
4+
import { KNOWN_PUBLIC_KEY_ADDRESSES } from '../../../stub/keyring-bridge';
5+
import ActivityListPage from '../../page-objects/pages/home/activity-list';
6+
import HomePage from '../../page-objects/pages/home/homepage';
7+
import { sendRedesignedTransactionToAddress } from '../../page-objects/flows/send-transaction.flow';
8+
import { loginWithoutBalanceValidation } from '../../page-objects/flows/login.flow';
9+
import HeaderNavbar from '../../page-objects/pages/header-navbar';
10+
import ConnectHardwareWalletPage from '../../page-objects/pages/hardware-wallet/connect-hardware-wallet-page';
11+
import SelectHardwareWalletAccountPage from '../../page-objects/pages/hardware-wallet/select-hardware-wallet-account-page';
12+
import AccountListPage from '../../page-objects/pages/account-list-page';
13+
14+
const RECIPIENT = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3';
15+
16+
describe('Ledger Hardware', function (this: Suite) {
17+
it('send ETH using an EIP1559 transaction', async function () {
18+
await withFixtures(
19+
{
20+
fixtures: new FixtureBuilder().build(),
21+
localNodeOptions: {
22+
hardfork: 'london',
23+
},
24+
title: this.test?.fullTitle(),
25+
},
26+
async ({ driver, localNodes }) => {
27+
// Seed the Ledger account with balance
28+
(await localNodes?.[0]?.setAccountBalance(
29+
KNOWN_PUBLIC_KEY_ADDRESSES[0].address,
30+
'0x100000000000000000000',
31+
)) ?? console.error('localNodes is undefined or empty');
32+
await loginWithoutBalanceValidation(driver);
33+
const homePage = new HomePage(driver);
34+
const headerNavbar = new HeaderNavbar(driver);
35+
await headerNavbar.openAccountMenu();
36+
37+
// Choose connect hardware wallet from the account menu
38+
const accountListPage = new AccountListPage(driver);
39+
await accountListPage.check_pageIsLoaded();
40+
await accountListPage.openConnectHardwareWalletModal();
41+
42+
const connectHardwareWalletPage = new ConnectHardwareWalletPage(driver);
43+
await connectHardwareWalletPage.check_pageIsLoaded();
44+
await connectHardwareWalletPage.openConnectLedgerPage();
45+
46+
const selectLedgerAccountPage = new SelectHardwareWalletAccountPage(
47+
driver,
48+
);
49+
await selectLedgerAccountPage.check_pageIsLoaded();
50+
51+
// Check that the first page of accounts is correct
52+
await selectLedgerAccountPage.check_accountNumber();
53+
for (const { address } of KNOWN_PUBLIC_KEY_ADDRESSES.slice(0, 4)) {
54+
const shortenedAddress = `${address.slice(0, 4)}...${address.slice(
55+
-4,
56+
)}`;
57+
await selectLedgerAccountPage.check_addressIsDisplayed(
58+
shortenedAddress,
59+
);
60+
}
61+
62+
// Unlock first account of first page and check that the correct account has been added
63+
await selectLedgerAccountPage.unlockAccount(1);
64+
await headerNavbar.check_pageIsLoaded();
65+
await homePage.check_expectedBalanceIsDisplayed('1208925.8196');
66+
await sendRedesignedTransactionToAddress({
67+
driver,
68+
recipientAddress: RECIPIENT,
69+
amount: '1',
70+
});
71+
await homePage.check_pageIsLoaded();
72+
const activityList = new ActivityListPage(driver);
73+
await activityList.check_confirmedTxNumberDisplayedInActivity();
74+
await activityList.check_txAmountInActivity();
75+
},
76+
);
77+
});
78+
it('send ETH using a legacy transaction', async function () {
79+
await withFixtures(
80+
{
81+
fixtures: new FixtureBuilder().build(),
82+
localNodeOptions: {
83+
hardfork: 'muirGlacier',
84+
},
85+
title: this.test?.fullTitle(),
86+
},
87+
async ({ driver, localNodes }) => {
88+
// Seed the Ledger account with balance
89+
(await localNodes?.[0]?.setAccountBalance(
90+
KNOWN_PUBLIC_KEY_ADDRESSES[0].address,
91+
'0x100000000000000000000',
92+
)) ?? console.error('localNodes is undefined or empty');
93+
await loginWithoutBalanceValidation(driver);
94+
const homePage = new HomePage(driver);
95+
const headerNavbar = new HeaderNavbar(driver);
96+
await headerNavbar.openAccountMenu();
97+
98+
// Choose connect hardware wallet from the account menu
99+
const accountListPage = new AccountListPage(driver);
100+
await accountListPage.check_pageIsLoaded();
101+
await accountListPage.openConnectHardwareWalletModal();
102+
103+
const connectHardwareWalletPage = new ConnectHardwareWalletPage(driver);
104+
await connectHardwareWalletPage.check_pageIsLoaded();
105+
await connectHardwareWalletPage.openConnectLedgerPage();
106+
107+
const selectLedgerAccountPage = new SelectHardwareWalletAccountPage(
108+
driver,
109+
);
110+
await selectLedgerAccountPage.check_pageIsLoaded();
111+
112+
// Check that the first page of accounts is correct
113+
await selectLedgerAccountPage.check_accountNumber();
114+
for (const { address } of KNOWN_PUBLIC_KEY_ADDRESSES.slice(0, 4)) {
115+
const shortenedAddress = `${address.slice(0, 4)}...${address.slice(
116+
-4,
117+
)}`;
118+
await selectLedgerAccountPage.check_addressIsDisplayed(
119+
shortenedAddress,
120+
);
121+
}
122+
123+
// Unlock first account of first page and check that the correct account has been added
124+
await selectLedgerAccountPage.unlockAccount(1);
125+
await headerNavbar.check_pageIsLoaded();
126+
await homePage.check_expectedBalanceIsDisplayed('1208925.8196');
127+
await sendRedesignedTransactionToAddress({
128+
driver,
129+
recipientAddress: RECIPIENT,
130+
amount: '1',
131+
});
132+
await homePage.check_pageIsLoaded();
133+
const activityList = new ActivityListPage(driver);
134+
await activityList.check_confirmedTxNumberDisplayedInActivity();
135+
await activityList.check_txAmountInActivity();
136+
},
137+
);
138+
});
139+
});

test/stub/keyring-bridge.js

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { TransactionFactory } from '@ethereumjs/tx';
1+
import {
2+
FeeMarketEIP1559Transaction,
3+
LegacyTransaction,
4+
TransactionFactory,
5+
} from '@ethereumjs/tx';
26
import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util';
3-
import { bigIntToHex, bytesToHex } from '@metamask/utils';
7+
import { bigIntToHex, bytesToBigInt, bytesToHex } from '@metamask/utils';
8+
import { rlp } from 'ethereumjs-util';
49
import { Common } from './keyring-utils';
510

611
// BIP32 Public Key: xpub6ELgkkwgfoky9h9fFu4Auvx6oHvJ6XfwiS1NE616fe9Uf4H3JHtLGjCePVkb6RFcyDCqVvjXhNXbDNDqs6Kjoxw7pTAeP1GSEiLHmA5wYa9
@@ -174,6 +179,101 @@ export class FakeLedgerBridge extends FakeKeyringBridge {
174179
return Promise.resolve();
175180
}
176181

182+
/**
183+
* Signs a transaction using a private key.
184+
* This function supports both legacy (type 0) and EIP-1559 (type 2) transactions.
185+
* It decodes the RLP-encoded transaction, signs it, and then returns the
186+
* signature components (v, r, s) as hexadecimal strings.
187+
*
188+
* @param {object} params - The parameters object.
189+
* @param {string} params.tx - The RLP-encoded transaction as a hex string.
190+
* @returns {Promise<object>} A promise that resolves to an object containing the
191+
* signature components:
192+
* - `v`: The recovery id as a hex string.
193+
* - `r`: The R component of the signature as a hex string.
194+
* - `s`: The S component of the signature as a hex string.
195+
* @throws {Error} If the transaction type is unsupported.
196+
*/
197+
async deviceSignTransaction({ tx }) {
198+
const txBuffer = Buffer.from(tx, 'hex');
199+
const firstByte = txBuffer[0];
200+
let txType;
201+
let rlpData;
202+
let parsedChainId;
203+
204+
// Determine the transaction type from the first byte of the buffer
205+
if (firstByte === 1) {
206+
txType = 1; // EIP-2930
207+
// TODO: Add support for type 1 tx if needed, for now, error out
208+
throw new Error(
209+
'Unsupported transaction type: EIP-2930 (type 1) not yet implemented in FakeLedgerBridge.',
210+
);
211+
} else if (firstByte === 2) {
212+
txType = 2; // EIP-1559
213+
rlpData = txBuffer.slice(1);
214+
const decodedRlp = rlp.decode(rlpData);
215+
parsedChainId = bytesToBigInt(decodedRlp[0]); // chainId is the first element
216+
} else {
217+
txType = 0; // Legacy
218+
rlpData = txBuffer;
219+
const decodedRlp = rlp.decode(rlpData);
220+
// For legacy tx, getMessageToSign(false) includes chainId as the 7th element (index 6)
221+
// [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
222+
parsedChainId = bytesToBigInt(decodedRlp[6]);
223+
}
224+
225+
const common = Common.custom({
226+
chain: {
227+
name: 'localhost',
228+
// Use the parsed chainId. networkId can be the same.
229+
chainId: parsedChainId,
230+
networkId: parsedChainId,
231+
},
232+
// Ensure hardfork is appropriate for the transaction type
233+
hardfork: txType === 2 ? 'london' : 'muirGlacier',
234+
});
235+
236+
// removing r, s, v values from the unsigned tx
237+
// Ledger uses v to communicate the chain ID, but we're removing it because these values are not a valid signature at this point.
238+
239+
// Type 1 and type 2 transactions have an explicit type set in the first element of the array
240+
// Type 0 transactions do not have a specific type byte and are identified by their RLP encoding
241+
242+
// TODO: add support to type 1 transactions (already handled by throwing error)
243+
if (txType === 0) {
244+
const rlpTx = rlp.decode(rlpData);
245+
246+
// For legacy tx, fromValuesArray expects [nonce, gasPrice, gasLimit, to, value, data]
247+
// or [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
248+
// Since our rlpTx is [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0],
249+
// we should pass the first 6 elements, and `common` will handle EIP-155.
250+
const signedTx = LegacyTransaction.fromValuesArray(rlpTx.slice(0, 6), {
251+
common,
252+
}).sign(Buffer.from(KNOWN_PRIVATE_KEYS[0], 'hex'));
253+
return {
254+
v: bigIntToHex(signedTx.v),
255+
r: bigIntToHex(signedTx.r),
256+
s: bigIntToHex(signedTx.s),
257+
};
258+
} else if (txType === 2) {
259+
const rlpTx = rlp.decode(rlpData);
260+
261+
// For EIP-1559 tx, fromValuesArray expects:
262+
// [chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]
263+
// or with v, r, s. Our rlpTx matches the unsigned form.
264+
const signedTx = FeeMarketEIP1559Transaction.fromValuesArray(rlpTx, {
265+
common,
266+
}).sign(Buffer.from(KNOWN_PRIVATE_KEYS[0], 'hex'));
267+
return {
268+
v: bigIntToHex(signedTx.v),
269+
r: bigIntToHex(signedTx.r),
270+
s: bigIntToHex(signedTx.s),
271+
};
272+
}
273+
274+
throw new Error('Unsupported transaction type.');
275+
}
276+
177277
updateTransportMethod() {
178278
return true;
179279
}

test/stub/keyring-utils.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ export class Common {
6969
chainId() {
7070
return BigInt(this.chain.chainId);
7171
}
72+
73+
/**
74+
* Checks if a specific EIP (Ethereum Improvement Proposal) is activated.
75+
* In this simplified test version, it always returns true.
76+
* This method is a stub for the actual `isActivatedEIP` method in `@ethereumjs/common`.
77+
*
78+
* @returns {boolean} Always returns true.
79+
*/
80+
isActivatedEIP() {
81+
return true;
82+
}
7283
}
7384

7485
/**

0 commit comments

Comments
 (0)