Skip to content

Commit 98e6015

Browse files
1.20.0-0.1.0: [feature]: Add Cobo Vault hardware wallet support (#508)
* add cobovault support * Update src/modules/check/derivation-path.ts Co-authored-by: Aaron Barnard <abarnard@protonmail.com> * Update src/modules/check/derivation-path.ts Co-authored-by: Aaron Barnard <abarnard@protonmail.com> * fix code issue * resolve code issue * update dependency * bump version code Co-authored-by: Aaron Barnard <abarnard@protonmail.com>
1 parent 8e4964f commit 98e6015

File tree

8 files changed

+535
-48
lines changed

8 files changed

+535
-48
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bnc-onboard",
3-
"version": "1.20.0-0.0.1",
3+
"version": "1.20.0-0.1.0",
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",
@@ -45,6 +45,7 @@
4545
"typescript": "^3.6.4"
4646
},
4747
"dependencies": {
48+
"@cvbb/eth-keyring": "^1.1.0",
4849
"@ledgerhq/hw-app-eth": "^5.21.0",
4950
"@ledgerhq/hw-transport-u2f": "^5.21.0",
5051
"@portis/web3": "^2.0.0-beta.57",

rollup.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default {
6969
'ethereumjs-tx',
7070
'ethereumjs-util',
7171
'eth-lattice-keyring',
72+
'@cvbb/eth-keyring',
7273
'hdkey',
7374
'@ledgerhq/hw-transport-u2f',
7475
'@ledgerhq/hw-app-eth',

src/modules/check/derivation-path.ts

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -182,57 +182,60 @@ function derivationPath(
182182
}
183183
;(window as any).handleCustomInput = handleCustomInput
184184
;(window as any).handleDerivationClick = handleDerivationClick
185-
return {
186-
heading: heading || 'Hardware Wallet Connect',
187-
description:
188-
description ||
189-
`Make sure your ${wallet.name} is plugged in, ${
190-
wallet.name === 'Ledger' ? 'and the Ethereum app is open, ' : ''
191-
}then select a derivation path to connect your accounts:`,
192-
eventCode: 'derivationPath',
193-
html: derivationSelectHtmlString(wallet.name as string),
194-
button: {
195-
text: 'Connect',
196-
onclick: async () => {
197-
state.loading = true
198-
const path =
199-
state.dPath || derivationPaths[wallet.name as string][0].path
200-
try {
201-
const validPath = await wallet.provider.setPath(
202-
path,
203-
state.showCustomInput
204-
)
205-
206-
if (!validPath) {
207-
state.error = `${path} is not a valid derivation path`
185+
186+
return (
187+
derivationPaths[wallet.name as string] && {
188+
heading: heading || 'Hardware Wallet Connect',
189+
description:
190+
description ||
191+
`Make sure your ${wallet.name} is plugged in, ${
192+
wallet.name === 'Ledger' ? 'and the Ethereum app is open, ' : ''
193+
}then select a derivation path to connect your accounts:`,
194+
eventCode: 'derivationPath',
195+
html: derivationSelectHtmlString(wallet.name as string),
196+
button: {
197+
text: 'Connect',
198+
onclick: async () => {
199+
state.loading = true
200+
const path =
201+
state.dPath || derivationPaths[wallet.name as string][0].path
202+
try {
203+
const validPath = await wallet.provider.setPath(
204+
path,
205+
state.showCustomInput
206+
)
207+
208+
if (!validPath) {
209+
state.error = `${path} is not a valid derivation path`
210+
state.loading = false
211+
return
212+
}
213+
} catch (error) {
214+
state.error = error
208215
state.loading = false
209216
return
210217
}
211-
} catch (error) {
212-
state.error = error
213-
state.loading = false
214-
return
215-
}
216218

217-
state.error = ''
218-
219-
if (wallet.connect) {
220-
;(wallet.connect as Connect)()
221-
.then(() => {
222-
deleteWindowProperties()
223-
state.loading = false
224-
state.completed = true
225-
})
226-
.catch(error => {
227-
state.error = error.message
228-
state.loading = false
229-
})
219+
state.error = ''
220+
221+
if (wallet.connect) {
222+
;(wallet.connect as Connect)()
223+
.then(() => {
224+
deleteWindowProperties()
225+
state.loading = false
226+
state.completed = true
227+
})
228+
.catch(error => {
229+
state.error = error.message
230+
state.loading = false
231+
})
232+
}
230233
}
231-
}
232-
},
234+
},
233235

234-
icon: icon || usbIcon
235-
}
236+
icon: icon || usbIcon
237+
}
238+
)
236239
}
237240
}
238241

src/modules/select/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ function getModule(
103103
return import('./wallets/trezor')
104104
case 'lattice':
105105
return import('./wallets/lattice')
106+
case 'cobovault':
107+
return import('./wallets/cobovault')
106108
case 'ledger':
107109
return import('./wallets/ledger')
108110
case 'walletLink':
Loading
Loading
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import AirGapedKeyring from '@cvbb/eth-keyring'
2+
import { Helpers, LatticeOptions, WalletModule } from '../../../interfaces'
3+
import cobovaultIcon from '../wallet-icons/icon-cobovault.png'
4+
import cobovaultIcon2x from '../wallet-icons/icon-cobovault@2x.png'
5+
6+
function cobovault(
7+
options: LatticeOptions & { networkId: number }
8+
): WalletModule {
9+
const { appName, rpcUrl, networkId, preferred, label, iconSrc, svg } = options
10+
11+
return {
12+
name: label || 'CoboVault',
13+
svg: svg,
14+
iconSrc: cobovaultIcon,
15+
iconSrcSet: iconSrc || cobovaultIcon2x,
16+
wallet: async (helpers: Helpers) => {
17+
const { BigNumber, networkName, resetWalletState } = helpers
18+
19+
const provider = await cobovaultProvider({
20+
appName,
21+
rpcUrl,
22+
networkId,
23+
BigNumber,
24+
networkName,
25+
resetWalletState
26+
})
27+
28+
return {
29+
provider,
30+
interface: {
31+
name: 'CoboVault',
32+
connect: provider.enable,
33+
disconnect: provider.disconnect,
34+
address: {
35+
get: async () => provider.getPrimaryAddress()
36+
},
37+
network: {
38+
get: async () => networkId
39+
},
40+
balance: {
41+
get: async () => {
42+
const address = provider.getPrimaryAddress()
43+
return address && provider.getBalance(address)
44+
}
45+
}
46+
}
47+
}
48+
},
49+
type: 'hardware',
50+
desktop: true,
51+
mobile: true,
52+
osExclusions: [],
53+
preferred
54+
}
55+
}
56+
57+
async function cobovaultProvider(options: {
58+
appName: string
59+
networkId: number
60+
rpcUrl: string
61+
BigNumber: any
62+
networkName: (id: number) => string
63+
resetWalletState: (options?: {
64+
disconnected: boolean
65+
walletName: string
66+
}) => void
67+
}) {
68+
const EthereumTx = await import('ethereumjs-tx')
69+
const { default: createProvider } = await import('./providerEngine')
70+
71+
const BASE_PATH = "m/44'/60'/0'/0"
72+
73+
const { networkId, appName, rpcUrl, BigNumber, networkName } = options
74+
75+
const keyring = AirGapedKeyring.getEmptyKeyring()
76+
77+
let dPath = ''
78+
79+
let addressList = Array.from<string>([])
80+
let enabled = false
81+
let customPath = false
82+
83+
const provider = createProvider({
84+
getAccounts: (callback: any) => {
85+
getAccounts()
86+
.then((res: Array<string | undefined>) => callback(null, res))
87+
.catch((err: any) => callback(err, null))
88+
},
89+
signTransaction: (transactionData: any, callback: any) => {
90+
signTransaction(transactionData)
91+
.then((res: string) => callback(null, res))
92+
.catch(err => callback(err, null))
93+
},
94+
processMessage: (messageData: any, callback: any) => {
95+
signMessage(messageData)
96+
.then((res: string) => callback(null, res))
97+
.catch(err => callback(err, null))
98+
},
99+
processPersonalMessage: (messageData: any, callback: any) => {
100+
signMessage(messageData)
101+
.then((res: string) => callback(null, res))
102+
.catch(err => callback(err, null))
103+
},
104+
signMessage: (messageData: any, callback: any) => {
105+
signMessage(messageData)
106+
.then((res: string) => callback(null, res))
107+
.catch(err => callback(err, null))
108+
},
109+
signPersonalMessage: (messageData: any, callback: any) => {
110+
signMessage(messageData)
111+
.then((res: string) => callback(null, res))
112+
.catch(err => callback(err, null))
113+
},
114+
rpcUrl
115+
})
116+
117+
provider.setPath = setPath
118+
provider.dPath = dPath
119+
provider.enable = enable
120+
provider.setPrimaryAccount = setPrimaryAccount
121+
provider.getPrimaryAddress = getPrimaryAddress
122+
provider.getAccounts = getAccounts
123+
provider.getMoreAccounts = getMoreAccounts
124+
provider.getBalance = getBalance
125+
provider.getBalances = getBalances
126+
provider.send = provider.sendAsync
127+
provider.disconnect = disconnect
128+
provider.isCustomPath = isCustomPath
129+
130+
function disconnect() {
131+
dPath = ''
132+
enabled = false
133+
provider.stop()
134+
}
135+
136+
async function setPath(path: string) {
137+
if (path !== BASE_PATH)
138+
throw new Error("CoboVault only supports standard path: m/44'/0'/0'/0/x")
139+
customPath = false
140+
dPath = path
141+
return true
142+
}
143+
144+
function isCustomPath() {
145+
return customPath
146+
}
147+
148+
function enable() {
149+
if (enabled) {
150+
return getAccounts()
151+
}
152+
return keyring.readKeyring().then(() => {
153+
enabled = true
154+
return getAccounts()
155+
})
156+
}
157+
158+
function setPrimaryAccount(address: string) {
159+
return keyring.setCurrentAccount(
160+
addressList.findIndex(addr => addr === address) || 0
161+
)
162+
}
163+
164+
function getPrimaryAddress() {
165+
return keyring.getCurrentAddress()
166+
}
167+
168+
async function getMoreAccounts() {
169+
const accounts = await getAccounts(true)
170+
return accounts && getBalances(accounts)
171+
}
172+
173+
async function getAccounts(getMore?: boolean) {
174+
if (!enabled) {
175+
return []
176+
}
177+
178+
if (keyring.getAccounts().length > 0 && !getMore) {
179+
return keyring.getAccounts()
180+
}
181+
182+
try {
183+
addressList = await keyring.addAccounts(keyring.getAccounts().length + 5)
184+
} catch (error) {
185+
throw error
186+
}
187+
return addressList
188+
}
189+
190+
function getBalances(addresses: Array<string>) {
191+
return Promise.all(
192+
addresses.map(
193+
address =>
194+
new Promise(async resolve => {
195+
const balance = await getBalance(address)
196+
resolve({ address, balance })
197+
})
198+
)
199+
)
200+
}
201+
202+
function getBalance(address: string) {
203+
return new Promise((resolve, reject) => {
204+
provider.sendAsync(
205+
{
206+
jsonrpc: '2.0',
207+
method: 'eth_getBalance',
208+
params: [address, 'latest'],
209+
id: 42
210+
},
211+
(e: any, res: any) => {
212+
e && reject(e)
213+
const result = res && res.result
214+
215+
if (result != null) {
216+
resolve(new BigNumber(result).toString(10))
217+
} else {
218+
resolve(null)
219+
}
220+
}
221+
)
222+
})
223+
}
224+
225+
async function signTransaction(transactionData: any) {
226+
if (addressList.length === 0) {
227+
await enable()
228+
}
229+
230+
const transaction = new EthereumTx.Transaction(transactionData, {
231+
chain: networkName(networkId)
232+
})
233+
234+
try {
235+
const signedTx = await keyring.signTransaction(
236+
getPrimaryAddress(),
237+
transaction
238+
)
239+
return `0x${signedTx.serialize().toString('hex')}`
240+
} catch (err) {
241+
throw err
242+
}
243+
}
244+
245+
async function signMessage(message: { data: string }): Promise<string> {
246+
if (addressList.length === 0) {
247+
await enable()
248+
}
249+
250+
try {
251+
return keyring.signPersonalMessage(getPrimaryAddress(), message.data)
252+
} catch (err) {
253+
throw err
254+
}
255+
}
256+
257+
return provider
258+
}
259+
260+
export default cobovault

0 commit comments

Comments
 (0)