Skip to content

Commit dd8f936

Browse files
authored
feat(ironfish): Add wallet/decrypt (#5319)
1 parent be4afa5 commit dd8f936

File tree

7 files changed

+302
-0
lines changed

7 files changed

+302
-0
lines changed

ironfish/src/rpc/adapters/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum RPC_ERROR_CODES {
1414
DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name',
1515
IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required',
1616
MULTISIG_SECRET_NOT_FOUND = 'multisig-secret-not-found',
17+
WALLET_ALREADY_DECRYPTED = 'wallet-already-decrypted',
1718
WALLET_ALREADY_ENCRYPTED = 'wallet-already-encrypted',
1819
}
1920

ironfish/src/rpc/clients/client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ import type {
174174
UseAccountResponse,
175175
} from '../routes'
176176
import { ApiNamespace } from '../routes/namespaces'
177+
import { DecryptWalletRequest, DecryptWalletResponse } from '../routes/wallet/decrypt'
177178
import {
178179
DeleteTransactionRequest,
179180
DeleteTransactionResponse,
@@ -650,6 +651,15 @@ export abstract class RpcClient {
650651
params,
651652
).waitForEnd()
652653
},
654+
655+
decrypt: (
656+
params: DecryptWalletRequest,
657+
): Promise<RpcResponseEnded<DecryptWalletResponse>> => {
658+
return this.request<DecryptWalletResponse>(
659+
`${ApiNamespace.wallet}/decrypt`,
660+
params,
661+
).waitForEnd()
662+
},
653663
}
654664

655665
mempool = {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
{
2+
"Route wallet/encrypt decrypts accounts": [
3+
{
4+
"value": {
5+
"encrypted": false,
6+
"version": 4,
7+
"id": "bd48f7e3-1fdc-444c-8da2-a69b5929c266",
8+
"name": "A",
9+
"spendingKey": "b650292782c859e02bb02ecc72b5a652c8d4e06b212fc48daa9826a84ec29eed",
10+
"viewKey": "c58ef0ecc3ddb83e70c16595ff4507ebed969bd2d4717b0223cfda5fb3e3c12eeaaa2bee08f33a8f71e79a7546a9201e7c2803136d4d1559ee0c7d061edd0c18",
11+
"incomingViewKey": "ed46f4be99df94d4ff73fdff53ce14f723c8875873271ba7677f0e0bfcff8d01",
12+
"outgoingViewKey": "e9a4c430af2b19a1f459e8f5bbc236b1f50c88eae932e7f24c81b750bb6ccbb3",
13+
"publicAddress": "17002598ba11259a7f825aa9097aad74e21020c30ac005ed5a9b452eccacbee5",
14+
"createdAt": {
15+
"hash": {
16+
"type": "Buffer",
17+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
18+
},
19+
"sequence": 1
20+
},
21+
"scanningEnabled": true,
22+
"proofAuthorizingKey": "7147af8c39390e803e4b83618878a369070910086119c011f4485d46932ef308"
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": "b8aaf95a-4fc8-4779-9651-c4a66958d68b",
37+
"name": "B",
38+
"spendingKey": "ed3aa48b6fa3dba1eb9023ce609a724629dcd9a6cd1f6749b9c7052a9616f8a5",
39+
"viewKey": "d498fa26c41423f21042a65035c891652407e9b72ec5815bd55462ac7546969715468bd5b13b8a062cbb5ff83317f9327b30e62c2da1cb87b7c3d57a00ae529a",
40+
"incomingViewKey": "9f1d2adc095b549d2e0939064c2d3c16b09a207c6778c68b8aa84a1a90c7ee04",
41+
"outgoingViewKey": "075d6b5eddd608da7adf58d0d20e291fbcf33b06e13e381c56072627e6bb91a3",
42+
"publicAddress": "8288102c344d529a9ad80062bc4e9fc12e96c0837f8502764b22abe4275e91dd",
43+
"createdAt": {
44+
"hash": {
45+
"type": "Buffer",
46+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
47+
},
48+
"sequence": 1
49+
},
50+
"scanningEnabled": true,
51+
"proofAuthorizingKey": "10917f752c51a7ac8aec4a43aa1dd531b78ffb17709a54646f026243f7810d08"
52+
},
53+
"head": {
54+
"hash": {
55+
"type": "Buffer",
56+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
57+
},
58+
"sequence": 1
59+
}
60+
}
61+
],
62+
"Route wallet/encrypt throws if wallet is already decrypted": [
63+
{
64+
"value": {
65+
"encrypted": false,
66+
"version": 4,
67+
"id": "5e904ab8-0299-478a-b8ac-3140bd22e49b",
68+
"name": "A",
69+
"spendingKey": "23016779802774aafd71b9a65a692784a72a3519956e42096c91d7f07c9feb2e",
70+
"viewKey": "5b1e183effe90ba7046f3a5016fb4caada071566a92494e96558c8b644e5758111bc2bd5e471322192b12525c40f71cc60ed1c0e98c40059f24962920a33982b",
71+
"incomingViewKey": "1dd148bb07ab6628456d97c72d4c498b2261de06315fc7276cc49d5d90222303",
72+
"outgoingViewKey": "8c38ad5428fb3af935c47055a31a57b5d494710a4fe952b47b0d9c9b3f31e207",
73+
"publicAddress": "32a97426553a305863356aa0415b17850c2fc1a378cff4489a4977a526724cd8",
74+
"createdAt": {
75+
"hash": {
76+
"type": "Buffer",
77+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
78+
},
79+
"sequence": 1
80+
},
81+
"scanningEnabled": true,
82+
"proofAuthorizingKey": "33a2cfdd1531e2384981e98f8044b579b1578981d87a47f82376ce19a378720e"
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": "a3e01e93-e5b1-4a24-9581-03038220a909",
97+
"name": "B",
98+
"spendingKey": "dde3954bd4d8a97017bd99be3ba40774949bff30814090e20deda0858b51fe37",
99+
"viewKey": "ef2530818f5fb4b2a21c25788193e927ad24cb8ba988edb31b18603d4c29433a9b8e0c57ef33073f6822efa6fc828631fc69d26c437897035ac59ca7b8c89bed",
100+
"incomingViewKey": "3d9782327454977b5671a144eb4662aadbc0348010d1cd5153d4dd75bdffba03",
101+
"outgoingViewKey": "c13d7a59bbf9aa6ad5b21e10f20fe95f46beae789f966fcab12c1d69c61d7e24",
102+
"publicAddress": "d90f0656660eb92683818c18ff66f7bcf445a3c9fff0a7b703e5dc74e349a630",
103+
"createdAt": {
104+
"hash": {
105+
"type": "Buffer",
106+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
107+
},
108+
"sequence": 1
109+
},
110+
"scanningEnabled": true,
111+
"proofAuthorizingKey": "0c22a2440735d1754388208f74d20ef3ddb5e9b2393ffc2e33b4cd4f50cb6f02"
112+
},
113+
"head": {
114+
"hash": {
115+
"type": "Buffer",
116+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
117+
},
118+
"sequence": 1
119+
}
120+
}
121+
],
122+
"Route wallet/encrypt throws if wallet decryption fails": [
123+
{
124+
"value": {
125+
"encrypted": false,
126+
"version": 4,
127+
"id": "8dbed752-3b7c-45b4-b967-afebffecd45b",
128+
"name": "A",
129+
"spendingKey": "20a319f5c4cc53cb09ba3a9c75d5563f1223b7f05b0dcae7cdd839f7c6e271b5",
130+
"viewKey": "3418a81c59d6c253d0e0d04f13fee14c7742430f6776856c1ef355b29ecc111d2ab615f0024143ecfe0cb031cbb7728f54c9b78a799d9fa59457f9e68c6c5298",
131+
"incomingViewKey": "48f265e4afe6074fc6691f07f1da7071b61307a6ea559b5b95a4ccd6d55d4903",
132+
"outgoingViewKey": "f77e9066200ec9990edd3b79478cdadf576879049f00b284d827abe03e08ed30",
133+
"publicAddress": "4a2ebb0b985d39f97f3eab809688e317b615070c36e6e3cf0fa95409caa0f1af",
134+
"createdAt": {
135+
"hash": {
136+
"type": "Buffer",
137+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
138+
},
139+
"sequence": 1
140+
},
141+
"scanningEnabled": true,
142+
"proofAuthorizingKey": "80e1ba98c7409e9c63bba9ae390eef3a2960cf07727774d0a30b548448df0201"
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": "890f2d68-48da-4bdd-ab5c-084a8e9fd307",
157+
"name": "B",
158+
"spendingKey": "297dc206316d47b1e26ad8b6b773dcbb4ade162ccf78ee4c91cf35203c3518b9",
159+
"viewKey": "5f7734257759f5af6117c2bd2afe2ee5cd6a210fab0ef6dde4d87a0017b1e971f7704f47c44084051a56ec9a68cfdff31911776beb103fe3508b4e73d8a2dd9f",
160+
"incomingViewKey": "09c08e34a5d9b758d1ef255fddd61a6f97ad5e43c300f1191c1d4d15f6b5cb04",
161+
"outgoingViewKey": "b85737c7ef8337d4fb0d02398e6dc4cc0723f43ae1746b1cf6cc869c03f67d01",
162+
"publicAddress": "5b261e39488361a0420eaffc9c96917c38b1d80e04352a66dd85a4f6948f4590",
163+
"createdAt": {
164+
"hash": {
165+
"type": "Buffer",
166+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
167+
},
168+
"sequence": 1
169+
},
170+
"scanningEnabled": true,
171+
"proofAuthorizingKey": "5d5110516992247f029e1f861e9e956bda6cf550aabbab7afc002b3a12263709"
172+
},
173+
"head": {
174+
"hash": {
175+
"type": "Buffer",
176+
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
177+
},
178+
"sequence": 1
179+
}
180+
}
181+
]
182+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
import { RPC_ERROR_CODES } from '../../adapters/errors'
7+
8+
describe('Route wallet/encrypt', () => {
9+
const routeTest = createRouteTest()
10+
11+
it('decrypts accounts', async () => {
12+
const passphrase = 'foobar'
13+
14+
await useAccountFixture(routeTest.node.wallet, 'A')
15+
await useAccountFixture(routeTest.node.wallet, 'B')
16+
17+
await routeTest.client.wallet.encrypt({ passphrase })
18+
19+
let status = await routeTest.client.wallet.getAccountsStatus()
20+
expect(status.content.encrypted).toBe(true)
21+
expect(status.content.locked).toBe(true)
22+
23+
await routeTest.client.wallet.decrypt({ passphrase })
24+
25+
status = await routeTest.client.wallet.getAccountsStatus()
26+
expect(status.content.encrypted).toBe(false)
27+
expect(status.content.locked).toBe(false)
28+
})
29+
30+
it('throws if wallet is already decrypted', async () => {
31+
await useAccountFixture(routeTest.node.wallet, 'A')
32+
await useAccountFixture(routeTest.node.wallet, 'B')
33+
34+
await expect(routeTest.client.wallet.decrypt({ passphrase: 'foobar' })).rejects.toThrow(
35+
expect.objectContaining({
36+
message: expect.any(String),
37+
status: 400,
38+
code: RPC_ERROR_CODES.WALLET_ALREADY_DECRYPTED,
39+
}),
40+
)
41+
})
42+
43+
it('throws if wallet decryption fails', async () => {
44+
const passphrase = 'foobar'
45+
const invalidPassphrase = 'baz'
46+
47+
await useAccountFixture(routeTest.node.wallet, 'A')
48+
await useAccountFixture(routeTest.node.wallet, 'B')
49+
50+
await routeTest.client.wallet.encrypt({ passphrase })
51+
52+
let status = await routeTest.client.wallet.getAccountsStatus()
53+
expect(status.content.encrypted).toBe(true)
54+
expect(status.content.locked).toBe(true)
55+
56+
await expect(
57+
routeTest.client.wallet.decrypt({ passphrase: invalidPassphrase }),
58+
).rejects.toThrow('Request failed (400) error: Failed to decrypt account')
59+
60+
status = await routeTest.client.wallet.getAccountsStatus()
61+
expect(status.content.encrypted).toBe(true)
62+
expect(status.content.locked).toBe(true)
63+
})
64+
})
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 DecryptWalletRequest = { passphrase: string }
11+
export type DecryptWalletResponse = undefined
12+
13+
export const DecryptWalletRequestSchema: yup.ObjectSchema<DecryptWalletRequest> = yup
14+
.object({
15+
passphrase: yup.string().defined(),
16+
})
17+
.defined()
18+
19+
export const DecryptWalletResponseSchema: yup.MixedSchema<DecryptWalletResponse> = yup
20+
.mixed()
21+
.oneOf([undefined] as const)
22+
23+
routes.register<typeof DecryptWalletRequestSchema, DecryptWalletResponse>(
24+
`${ApiNamespace.wallet}/decrypt`,
25+
DecryptWalletRequestSchema,
26+
async (request, context): Promise<void> => {
27+
AssertHasRpcContext(request, context, 'wallet')
28+
29+
const encrypted = await context.wallet.accountsEncrypted()
30+
if (!encrypted) {
31+
throw new RpcValidationError(
32+
'Wallet is already decrypted',
33+
400,
34+
RPC_ERROR_CODES.WALLET_ALREADY_DECRYPTED,
35+
)
36+
}
37+
38+
await context.wallet.decrypt(request.data.passphrase)
39+
request.end()
40+
},
41+
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './burnAsset'
99
export * from './create'
1010
export * from './createAccount'
1111
export * from './createTransaction'
12+
export * from './decrypt'
1213
export * from './deleteTransaction'
1314
export * from './estimateFeeRates'
1415
export * from './encrypt'

ironfish/src/wallet/wallet.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,9 @@ export class Wallet {
18551855
try {
18561856
await this.walletDb.decryptAccounts(passphrase, tx)
18571857
await this.load()
1858+
} catch (e) {
1859+
this.logger.error(ErrorUtils.renderError(e, true))
1860+
throw e
18581861
} finally {
18591862
unlock()
18601863
}

0 commit comments

Comments
 (0)