Skip to content

Commit 9780475

Browse files
authored
feat(ironfish): Add wallet/unlock (#5321)
1 parent 2ab7c46 commit 9780475

File tree

5 files changed

+308
-0
lines changed

5 files changed

+308
-0
lines changed

ironfish/src/rpc/clients/client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ import {
180180
DeleteTransactionResponse,
181181
} from '../routes/wallet/deleteTransaction'
182182
import { EncryptWalletRequest, EncryptWalletResponse } from '../routes/wallet/encrypt'
183+
import { UnlockWalletRequest, UnlockWalletResponse } from '../routes/wallet/unlock'
183184

184185
export abstract class RpcClient {
185186
abstract close(): void
@@ -660,6 +661,13 @@ export abstract class RpcClient {
660661
params,
661662
).waitForEnd()
662663
},
664+
665+
unlock: (params: UnlockWalletRequest): Promise<RpcResponseEnded<UnlockWalletResponse>> => {
666+
return this.request<UnlockWalletResponse>(
667+
`${ApiNamespace.wallet}/unlock`,
668+
params,
669+
).waitForEnd()
670+
},
663671
}
664672

665673
mempool = {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
{
2+
"Route wallet/unlock does nothing if the wallet is decrypted": [
3+
{
4+
"value": {
5+
"encrypted": false,
6+
"version": 4,
7+
"id": "f9a7e6ae-d45b-4015-a42b-f57514584dbf",
8+
"name": "A",
9+
"spendingKey": "68d0226a9b4875fe2a6a67dd62fa9d4001a9e7450679358ca102e19ec0ee9e28",
10+
"viewKey": "1fc4232036da00819a0d596b899c47b210f19b7d1adb580c0d7e7da79695ff64ba010b4b7430b18c3cc6863e1b6f63ee8a4d9a157be39e90cc9c1325f0ec2865",
11+
"incomingViewKey": "f6e111a6d3436b9f62aa233de637cff50a5c1004bb5c185f533c88c671ac7f03",
12+
"outgoingViewKey": "f3bc0a30d0acea319d1ba4287372d8c15395fbdf2bf9637fba6327c4d9b63e5d",
13+
"publicAddress": "c97a6caf06c0a73c7bddc88637acb718912278bd29b6baa444cc99587c2bc172",
14+
"createdAt": {
15+
"hash": {
16+
"type": "Buffer",
17+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
18+
},
19+
"sequence": 1
20+
},
21+
"scanningEnabled": true,
22+
"proofAuthorizingKey": "a193591a4b6f2923670fcf2401a6cd078d5afd6aa1ee08e68b64205e0ed6330a"
23+
},
24+
"head": {
25+
"hash": {
26+
"type": "Buffer",
27+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
28+
},
29+
"sequence": 1
30+
}
31+
},
32+
{
33+
"value": {
34+
"encrypted": false,
35+
"version": 4,
36+
"id": "2da0c285-bd6a-4d67-b2d3-1198e33959c0",
37+
"name": "B",
38+
"spendingKey": "a57081cebe037dbb959681d13043c5c3b5f704bb1dfcaef7ccffc6d518504b41",
39+
"viewKey": "aef088f4000312c703b5d57da6271a848b2610d1124269e142714e957f33b045fab63ae3bf5ee5bef2ad3a0c0aff505d0839909cf5491ad095456a8de012b70e",
40+
"incomingViewKey": "fcd6f4d1d02199744d5041b69848a9c611ea8d3bf28f23d9e741afd058e9fb06",
41+
"outgoingViewKey": "a0303aa1820a398b62781c3fe915414e3b1d2d5248d03342d8e375442a6f6fbb",
42+
"publicAddress": "01d70038bb5cb4dfc6a9ea1355d7d2e0937cadb419590e3f3b8ed9ab044ff29a",
43+
"createdAt": {
44+
"hash": {
45+
"type": "Buffer",
46+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
47+
},
48+
"sequence": 1
49+
},
50+
"scanningEnabled": true,
51+
"proofAuthorizingKey": "1f489a416d31b598f56b357206fceac690d31b8150912ce22bca1bcdc64a270a"
52+
},
53+
"head": {
54+
"hash": {
55+
"type": "Buffer",
56+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
57+
},
58+
"sequence": 1
59+
}
60+
}
61+
],
62+
"Route wallet/unlock throws if wallet decryption fails": [
63+
{
64+
"value": {
65+
"encrypted": false,
66+
"version": 4,
67+
"id": "597c39fc-0ef6-4a68-af95-e6f750ecd55e",
68+
"name": "A",
69+
"spendingKey": "62a2a8a953f0f8a94a137b1a2d78397f45262c929d2e0a2337644ac1bff89e52",
70+
"viewKey": "7005f9c44311d9d9088ada9d5bfedf8c092fcddbb2ce5c2c0cec6d1b1a3a42358c9658d664a2cc4107b3791c440797862c675856c4cf7c92f608e5f043de8f6b",
71+
"incomingViewKey": "7cc00267f1b60ccb7f83c7ab68d1391842a869c3cb247eafbd73d3f92c222801",
72+
"outgoingViewKey": "9ee6d7bba6b3a935e69ff74a4f2d5faf6725ec8580d09312a5f14d1a451b4425",
73+
"publicAddress": "59361460ad98475b51effed3d7b7bdb6fcb9986841ceb3474abeb7b412f8886b",
74+
"createdAt": {
75+
"hash": {
76+
"type": "Buffer",
77+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
78+
},
79+
"sequence": 1
80+
},
81+
"scanningEnabled": true,
82+
"proofAuthorizingKey": "7ba509dc63bc599f778568c99f13a62a0e5a22bab958533db1538aff17daf007"
83+
},
84+
"head": {
85+
"hash": {
86+
"type": "Buffer",
87+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
88+
},
89+
"sequence": 1
90+
}
91+
},
92+
{
93+
"value": {
94+
"encrypted": false,
95+
"version": 4,
96+
"id": "49333fd0-1500-4bfb-ae05-a779ea9c2e55",
97+
"name": "B",
98+
"spendingKey": "7151b4531fe3e196ca8eb4e35fa5c54216eadbb4f0a77979a68d8d9bc6c6384a",
99+
"viewKey": "ad31c32965c9d02a9925473f8b5326747aabe523ea84968878f80bfd67e257ecdff668db58e215cd48fc9a9d57276c3f621e570b7b7c330362b09b138cd2745c",
100+
"incomingViewKey": "aa8e8848fac84f52bccb95889e6e8bda170a9d170c1f80005f60b81802349a00",
101+
"outgoingViewKey": "6d2ab57ca77aa68644a5a9157b1594f9e7a97f0629c11a1fe90d4b2c3f9c874f",
102+
"publicAddress": "7d1534f616082473aee55a872dfc1392ec606c09fad5c8a3065dd44323ee7639",
103+
"createdAt": {
104+
"hash": {
105+
"type": "Buffer",
106+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
107+
},
108+
"sequence": 1
109+
},
110+
"scanningEnabled": true,
111+
"proofAuthorizingKey": "0e21aaef7c8c0ff8ba997e5c6a01368df4769fad8b021863db1689af6af8e502"
112+
},
113+
"head": {
114+
"hash": {
115+
"type": "Buffer",
116+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
117+
},
118+
"sequence": 1
119+
}
120+
}
121+
],
122+
"Route wallet/unlock unlocks the wallet with the correct passphrase": [
123+
{
124+
"value": {
125+
"encrypted": false,
126+
"version": 4,
127+
"id": "050c72be-0d59-4f4c-8400-29363febff73",
128+
"name": "A",
129+
"spendingKey": "8d92b5c7c8479998e070a4d1aad62b328e4ec4e1073bdd240c0f3408e614a088",
130+
"viewKey": "241bbe035cd6ba6cf4bea1a70d9e0c65da5abc192309040691d72b5f3072ebbca650139c67ae85084fbb6587d23b57f30d006e59d656f1e07d88bcd135592d4b",
131+
"incomingViewKey": "ff2946c5714f74658b6fdab386ba606b2f342d431cfb207872060169d2b3f605",
132+
"outgoingViewKey": "65077e7e22c13885ee78f2757cebad8352b6eeec7ef1ba46a778b27b007e6941",
133+
"publicAddress": "56e37f6f80a2d84c575e9507a2a3eb3365a9a85e170ea31ccf6ae00c61aa41c9",
134+
"createdAt": {
135+
"hash": {
136+
"type": "Buffer",
137+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
138+
},
139+
"sequence": 1
140+
},
141+
"scanningEnabled": true,
142+
"proofAuthorizingKey": "c8dcfc8ffaa7f72c406a8ce102922cb5a17c8896391c0c09d03f76f931857a0d"
143+
},
144+
"head": {
145+
"hash": {
146+
"type": "Buffer",
147+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
148+
},
149+
"sequence": 1
150+
}
151+
},
152+
{
153+
"value": {
154+
"encrypted": false,
155+
"version": 4,
156+
"id": "b3d877f2-98c9-48fe-a05d-50ca51b16534",
157+
"name": "B",
158+
"spendingKey": "5a3a3e126577611ab0fbbc421ff428f32fcb672f6541cc18dc0a78a392d6bfbf",
159+
"viewKey": "19304c39803ae2dbd54f521c832620960b82a7cb862281216c768c295b41ad493d340dde3b60904b06afdce789f6f02d7baf2534a69342eb7bf794979513253a",
160+
"incomingViewKey": "b5534c304f50008704173871ef8df9f3cd59a8598fa702371479f2c08fac6e03",
161+
"outgoingViewKey": "a9eb4e2a74ff89abc47479d5dfd2808aeba619548765eed7d1e87c562ecd9420",
162+
"publicAddress": "ba7ccf4ece289fbd56a7aee03790620e003185e7d6164b7bbea33ecb663779ef",
163+
"createdAt": {
164+
"hash": {
165+
"type": "Buffer",
166+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
167+
},
168+
"sequence": 1
169+
},
170+
"scanningEnabled": true,
171+
"proofAuthorizingKey": "1d1519a3b10a0307ff2eb34d6c9d3064cd3c620adf1961ee7998ba9f158d1805"
172+
},
173+
"head": {
174+
"hash": {
175+
"type": "Buffer",
176+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
177+
},
178+
"sequence": 1
179+
}
180+
}
181+
]
182+
}

ironfish/src/rpc/routes/wallet/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ export * from './setAccountHead'
4545
export * from './setScanning'
4646
export * from './signTransaction'
4747
export * from './types'
48+
export * from './unlock'
4849
export * from './use'
4950
export * from './useAccount'
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import { useAccountFixture } from '../../../testUtilities'
5+
import { createRouteTest } from '../../../testUtilities/routeTest'
6+
7+
describe('Route wallet/unlock', () => {
8+
const routeTest = createRouteTest()
9+
10+
it('does nothing if the wallet is decrypted', async () => {
11+
const passphrase = 'foobar'
12+
13+
await useAccountFixture(routeTest.node.wallet, 'A')
14+
await useAccountFixture(routeTest.node.wallet, 'B')
15+
16+
await routeTest.client.wallet.unlock({ passphrase })
17+
18+
const status = await routeTest.client.wallet.getAccountsStatus()
19+
expect(status.content.encrypted).toBe(false)
20+
expect(status.content.locked).toBe(false)
21+
})
22+
23+
it('throws if invalid timeout is provided', async () => {
24+
const timeout = -2
25+
await expect(
26+
routeTest.client.wallet.unlock({ passphrase: 'foobar', timeout }),
27+
).rejects.toThrow(`Request failed (400) validation: Invalid timeout value: ${timeout}`)
28+
})
29+
30+
it('throws if wallet decryption fails', async () => {
31+
const passphrase = 'foobar'
32+
const invalidPassphrase = 'baz'
33+
34+
await useAccountFixture(routeTest.node.wallet, 'A')
35+
await useAccountFixture(routeTest.node.wallet, 'B')
36+
37+
await routeTest.client.wallet.encrypt({ passphrase })
38+
39+
let status = await routeTest.client.wallet.getAccountsStatus()
40+
expect(status.content.encrypted).toBe(true)
41+
expect(status.content.locked).toBe(true)
42+
43+
await expect(
44+
routeTest.client.wallet.unlock({ passphrase: invalidPassphrase }),
45+
).rejects.toThrow('Request failed (400) error: Failed to decrypt account')
46+
47+
status = await routeTest.client.wallet.getAccountsStatus()
48+
expect(status.content.encrypted).toBe(true)
49+
expect(status.content.locked).toBe(true)
50+
})
51+
52+
it('unlocks the wallet with the correct passphrase', async () => {
53+
const passphrase = 'foobar'
54+
55+
const accountA = await useAccountFixture(routeTest.node.wallet, 'A')
56+
const accountB = await useAccountFixture(routeTest.node.wallet, 'B')
57+
58+
await routeTest.client.wallet.encrypt({ passphrase })
59+
60+
let status = await routeTest.client.wallet.getAccountsStatus()
61+
expect(status.content.encrypted).toBe(true)
62+
expect(status.content.locked).toBe(true)
63+
64+
await routeTest.client.wallet.unlock({ passphrase })
65+
66+
status = await routeTest.client.wallet.getAccountsStatus()
67+
expect(status.content.encrypted).toBe(true)
68+
expect(status.content.locked).toBe(false)
69+
70+
const decryptedAccounts = await routeTest.client.wallet.getAccounts()
71+
expect(decryptedAccounts.content.accounts.sort()).toEqual([accountA.name, accountB.name])
72+
73+
// Temporary until the lock RPC is added
74+
await routeTest.node.wallet.lock()
75+
})
76+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
import * as yup from 'yup'
5+
import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors'
6+
import { ApiNamespace } from '../namespaces'
7+
import { routes } from '../router'
8+
import { AssertHasRpcContext } from '../rpcContext'
9+
10+
export type UnlockWalletRequest = { passphrase: string; timeout?: number }
11+
export type UnlockWalletResponse = undefined
12+
13+
export const UnlockWalletRequestSchema: yup.ObjectSchema<UnlockWalletRequest> = yup
14+
.object({
15+
passphrase: yup.string().defined(),
16+
timeout: yup.number().optional(),
17+
})
18+
.defined()
19+
20+
export const UnlockWalletResponseSchema: yup.MixedSchema<UnlockWalletResponse> = yup
21+
.mixed()
22+
.oneOf([undefined] as const)
23+
24+
routes.register<typeof UnlockWalletRequestSchema, UnlockWalletResponse>(
25+
`${ApiNamespace.wallet}/unlock`,
26+
UnlockWalletRequestSchema,
27+
async (request, context): Promise<void> => {
28+
AssertHasRpcContext(request, context, 'wallet')
29+
30+
if (request.data.timeout && request.data.timeout < -1) {
31+
throw new RpcValidationError(
32+
`Invalid timeout value: ${request.data.timeout}`,
33+
400,
34+
RPC_ERROR_CODES.VALIDATION,
35+
)
36+
}
37+
38+
await context.wallet.unlock(request.data.passphrase, request.data.timeout)
39+
request.end()
40+
},
41+
)

0 commit comments

Comments
 (0)