Skip to content

Commit 1a77e27

Browse files
1.25.0-0.1.2: [feature] KeepKey Wallet (#552)
* Fixes the address derivation * Minor refactors: - add missing dep - adjust pin input for accessibility - use destructuring for readability * Upgrades the pin modal styles * Adds passphrase entry * Update keepkey deps to include fix * Update yarn lock * Fix formatting
1 parent 8c2651c commit 1a77e27

File tree

10 files changed

+1253
-50
lines changed

10 files changed

+1253
-50
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bnc-onboard",
3-
"version": "1.27.0-0.0.2",
3+
"version": "1.27.0-0.1.2",
44
"description": "Onboard users to web3 by allowing them to select a wallet, get that wallet ready to transact and have access to synced wallet state.",
55
"keywords": [
66
"ethereum",
@@ -59,6 +59,9 @@
5959
"@ledgerhq/hw-transport-u2f": "^5.21.0",
6060
"@ledgerhq/hw-transport-webusb": "5.53.0",
6161
"@portis/web3": "^4.0.0",
62+
"@shapeshiftoss/hdwallet-core": "^1.15.2",
63+
"@shapeshiftoss/hdwallet-keepkey": "^1.15.2",
64+
"@shapeshiftoss/hdwallet-keepkey-webusb": "^1.15.2",
6265
"@toruslabs/torus-embed": "^1.10.11",
6366
"@walletconnect/web3-provider": "^1.4.1",
6467
"authereum": "^0.1.12",

rollup.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export default {
8585
'web3-provider-engine/subproviders/subscriptions',
8686
'web3-provider-engine/subproviders/filters',
8787
'eth-provider',
88+
'@shapeshiftoss/hdwallet-keepkey',
89+
'@shapeshiftoss/hdwallet-keepkey-webusb',
90+
'@shapeshiftoss/hdwallet-core',
8891
'@gnosis.pm/safe-apps-sdk',
8992
'@gnosis.pm/safe-apps-provider'
9093
]

src/@types/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ declare module '@ledgerhq/hw-transport-u2f'
1717
declare module '@ledgerhq/hw-transport-webusb'
1818
declare module 'eth-provider'
1919

20+
declare module '@shapeshiftoss/hdwallet-core'
21+
declare module '@shapeshiftoss/hdwallet-keepkey-webusb'
22+
2023
declare module '*.png'
2124
declare module '*.svg'

src/modules/check/connect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function connect(options: WalletCheckCustomOptions = {}): WalletCheckModule {
2222
setTimeout(() => {
2323
if (address === null) {
2424
// if prom isn't resolving after 500ms, then stop waiting
25-
resolve()
25+
resolve(undefined)
2626
}
2727
}, 500)
2828
})

src/modules/check/derivation-path.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const derivationPaths: DerivationPaths = {
1111
{ path: `m/44'/60'`, label: 'Ethereum Ledger Live' }
1212
],
1313
Trezor: [{ path: `m/44'/60'/0'/0`, label: 'Ethereum' }],
14-
Lattice: [{ path: `m/44'/60'/0'/0`, label: 'Ethereum' }]
14+
Lattice: [{ path: `m/44'/60'/0'/0`, label: 'Ethereum' }],
15+
KeepKey: [{ path: `m/44'/60'/0'/0/0`, label: 'Ethereum' }]
1516
}
1617

1718
const styles = `

src/modules/select/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ function getModule(name: string): Promise<{
140140
return import('./wallets/alphawallet')
141141
case 'ownbit':
142142
return import('./wallets/ownbit')
143+
case 'keepkey':
144+
return import('./wallets/keepkey/')
143145
case 'bitpie':
144146
return import('./wallets/bitpie')
145147
case 'gnosis':
16.2 KB
Loading
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import type { KeepKeyHDWallet } from '@shapeshiftoss/hdwallet-keepkey'
2+
import { detach, insert, noop, SvelteComponentDev } from 'svelte/internal'
3+
4+
import Modal from '../../../../components/Modal.svelte'
5+
import Button from '../../../../elements/Button.svelte'
6+
7+
const HANDLE_PIN_PRESS = 'handlePinPress'
8+
const BUTTON_COLOR = `#EBEBED`
9+
const BUTTON_DOT_COLOR = `#33394B`
10+
11+
export enum ModalType {
12+
Pin,
13+
Passphrase
14+
}
15+
16+
interface Slot {
17+
(): {
18+
c: () => void
19+
m: (target: any, anchor: any) => void
20+
d: (detaching: any) => void
21+
l: () => void
22+
}
23+
}
24+
25+
interface Slots {
26+
default: Slot[]
27+
}
28+
29+
const pinButton = (
30+
value: number,
31+
slot?: string,
32+
width = '64px',
33+
height = '64px'
34+
) => `
35+
<button
36+
class="pin-button"
37+
style="width: ${width}; height: ${height};"
38+
type="button"
39+
onclick="window.${HANDLE_PIN_PRESS}(${value})">
40+
${
41+
slot ||
42+
`<svg class="pin-button-dot" viewBox="0 0 18 18" width="18" height="18">
43+
<circle cx="9" cy="9" r="9" ></circle>
44+
</svg>`
45+
}
46+
<div class="pin-button-bg">
47+
</button>
48+
`
49+
50+
const pinButtons = `
51+
<div class="pin-pad-buttons">
52+
${[7, 8, 9, 4, 5, 6, 1, 2, 3].map(val => pinButton(val)).join('')}
53+
</div>
54+
`
55+
56+
const delButtonIcon = `<svg class="del-button-icon" viewBox="0 0 24 24" focusable="false" class="chakra-icon css-onkibi" aria-hidden="true"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"></path></svg>`
57+
58+
const pinPhraseInput = (modalType: ModalType) => `
59+
<form class="pin-phrase-input-container">
60+
<input
61+
id="pin-phrase-input"
62+
placeholder="${modalType === ModalType.Pin ? 'PIN' : ''}"
63+
type="password"
64+
autocomplete="current-password"
65+
/>
66+
${
67+
modalType === ModalType.Pin
68+
? ` <div class="del-button-wrapper">
69+
${pinButton(-1, delButtonIcon, '38px', '38px')}
70+
</div>`
71+
: ''
72+
}
73+
</form>
74+
`
75+
76+
// Contains styles used by both the pin entry modal and the passphrase entry modal
77+
const baseStyles = `
78+
.keepkey-modal {
79+
max-width: 22rem;
80+
padding: 20px 10px;
81+
}
82+
.pin-phrase-input-container {
83+
display: flex;
84+
position: relative;
85+
align-items: center;
86+
margin: 20px 0;
87+
width: 100%;
88+
}
89+
#pin-phrase-input {
90+
background: inherit;
91+
font-size: 0.889em;
92+
font-family: inherit;
93+
border-width: 1px;
94+
border-style: solid;
95+
border-color: #242835;
96+
border-radius: 4px;
97+
padding-left: 0.5rem;
98+
padding-right: 4.1rem;
99+
transition: opacity 150ms ease-in-out;
100+
height: 42px;
101+
width: 100%;
102+
opacity: 0.6;
103+
outline: none;
104+
}
105+
#pin-phrase-input:hover, #pin-phrase-input:focus {
106+
opacity: 1;
107+
}
108+
.unlock-button {
109+
height: 26px;
110+
display: flex;
111+
align-items: center;
112+
width: 100%;
113+
justify-content: center;
114+
}
115+
116+
/* Overrides the branding on the modal*/
117+
.keepkey-modal + .bn-branding { visibility: hidden !important; }
118+
.keepkey-modal .bn-onboard-prepare-button {
119+
width: 100%;
120+
}
121+
`
122+
123+
const pinModalStyles = `
124+
#entry {
125+
align-items: center;
126+
display: flex;
127+
flex-flow: column;
128+
padding: 20px;
129+
}
130+
.pin-pad-buttons {
131+
display: grid;
132+
grid-template-columns: repeat(3, 75px);
133+
grid-template-rows: repeat(3, 75px);
134+
align-items: center;
135+
justify-items: center;
136+
margin-bottom: 15px;
137+
}
138+
.pin-button {
139+
align-items: center;
140+
border-radius: 6px;
141+
border: 1px solid ${BUTTON_COLOR};
142+
cursor: pointer;
143+
display: flex;
144+
justify-content: center;
145+
font-size: 18px;
146+
overflow: hidden;
147+
padding: 0;
148+
background-color: unset;
149+
overflow: hidden;
150+
}
151+
.pin-button-bg {
152+
width: 100%;
153+
height: 100%;
154+
display: flex;
155+
overflow: hidden;
156+
background-color: ${BUTTON_COLOR};
157+
transition: opacity 100ms ease-in;
158+
}
159+
.pin-button-bg:hover {
160+
opacity: .2;
161+
}
162+
.pin-button-dot {
163+
fill: ${BUTTON_DOT_COLOR};
164+
position: absolute;
165+
pointer-events: none;
166+
z-index: 2;
167+
}
168+
.del-button-wrapper {
169+
position: absolute;
170+
height: 42px;
171+
width: 42px;
172+
right: 2px;
173+
display: flex;
174+
align-items: center;
175+
justify-content: center;
176+
}
177+
.del-button-wrapper > .pin-button {
178+
border: none;
179+
}
180+
.del-button-icon {
181+
position: absolute;
182+
width: 20px;
183+
z-index: 2;
184+
pointer-events: none;
185+
}
186+
.del-button-icon + div {
187+
opacity: .5;
188+
}
189+
.del-button-icon + div:hover {
190+
opacity: 1;
191+
}
192+
`
193+
194+
const passphraseModalStyles = `
195+
.keepkey-modal {
196+
padding: 40px 30px;
197+
}
198+
`
199+
200+
const pinHTML = `
201+
<style>${baseStyles}${pinModalStyles}</style>
202+
<h2>Enter Your Pin</h2>
203+
<p>
204+
Use PIN layout shown on your device to find the location to press on this pin pad.
205+
</p>
206+
<div id="entry" class="bn-onboard-custom">
207+
${pinButtons}
208+
${pinPhraseInput(ModalType.Pin)}
209+
</div>
210+
`
211+
212+
const passphraseHTML = `
213+
<style>${baseStyles}${passphraseModalStyles}</style>
214+
<h2 style="margin-bottom: 35px">Enter Your Passphrase</h2>
215+
<div id="entry" class="bn-onboard-custom">
216+
${pinPhraseInput(ModalType.Passphrase)}
217+
</div>
218+
`
219+
220+
export const renderModal = (wallet: KeepKeyHDWallet, modalType: ModalType) => {
221+
const modalHtml = modalType === ModalType.Pin ? pinHTML : passphraseHTML
222+
223+
const getInput = () =>
224+
document.getElementById('pin-phrase-input') as HTMLInputElement
225+
226+
const deleteWindowProperties = () => {
227+
delete (window as any)[HANDLE_PIN_PRESS]
228+
}
229+
230+
if (modalType === ModalType.Pin) {
231+
;(window as any)[HANDLE_PIN_PRESS] = (value: number) => {
232+
const input = getInput()
233+
// A value of -1 signals a backspace e.g. we delete the last char from the input
234+
input.value =
235+
value === -1 ? input.value.slice(0, -1) : input.value + value
236+
}
237+
}
238+
239+
// Creates a modal component which gets mounted to the body and is passed the pin html into it's slot
240+
const div = document.createElement('div')
241+
div.innerHTML = modalHtml
242+
div.className = 'keepkey-modal'
243+
const pinModal = new Modal({
244+
target: document.body,
245+
props: {
246+
closeModal: () => {
247+
// Cancels any action that the keepkey wallet may be doing
248+
wallet.cancel()
249+
deleteWindowProperties()
250+
pinModal.$destroy()
251+
},
252+
$$slots: createSlot(div),
253+
$$scope: {}
254+
}
255+
} as SvelteComponentDev['new'])
256+
257+
// Creates a new Button component used to trigger sending the pin to Keepkey
258+
const entryEl = document.getElementById('entry')
259+
if (entryEl) {
260+
const span = document.createElement('span')
261+
span.innerHTML = `Unlock`
262+
span.className = `unlock-button`
263+
new Button({
264+
target: entryEl,
265+
props: {
266+
onclick: async () => {
267+
const value = getInput().value
268+
269+
modalType === ModalType.Pin
270+
? await wallet.sendPin(value)
271+
: await wallet.sendPassphrase(value)
272+
273+
pinModal.$destroy()
274+
deleteWindowProperties()
275+
},
276+
$$slots: createSlot(span),
277+
$$scope: {}
278+
}
279+
} as SvelteComponentDev['new'])
280+
}
281+
}
282+
283+
/**
284+
* createSlot - creates the necessary object needed to pass
285+
* arbitrary html into a component's default slot
286+
* @param element The html element which is inserted into the components slot
287+
*/
288+
function createSlot(element: HTMLElement): Slots {
289+
return {
290+
default: [
291+
function () {
292+
return {
293+
c: noop,
294+
m: function mount(target: any, anchor: any) {
295+
insert(target, element, anchor)
296+
},
297+
d: function destroy(detaching: any) {
298+
if (detaching) {
299+
detach(element)
300+
}
301+
},
302+
l: noop
303+
}
304+
}
305+
]
306+
}
307+
}

0 commit comments

Comments
 (0)