Skip to content

Commit

Permalink
adds 'format' option to account import (#5653)
Browse files Browse the repository at this point in the history
* adds 'format' option to account import

adds a field to the wallet/importAccount RPC that allows clients to specify the
format of the account (Base64Json, JSON, Mnemonic, or SpendingKey)

if format is specified in the request then the decoding is only attempted for
the specified format

results in more specific error messages instead of an error message containing
the errors for each encoder

* updates test not to expect within except

test would still pass if an error were not thrown
  • Loading branch information
hughy authored Nov 14, 2024
1 parent e942146 commit c18f4d4
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 14 deletions.
38 changes: 38 additions & 0 deletions ironfish/src/rpc/routes/wallet/importAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,4 +642,42 @@ describe('Route wallet/importAccount', () => {
Assert.isNotUndefined(existingIdentity)
expect(existingIdentity.name).toEqual('existingIdentity')
})

describe('account format', () => {
it('should decode an account import with the requested format', async () => {
const name = 'mnemonic-format'
const mnemonic = spendingKeyToWords(generateKey().spendingKey, LanguageCode.English)

const response = await routeTest.client.wallet.importAccount({
account: mnemonic,
name: name,
rescan: false,
format: AccountFormat.Mnemonic,
})

expect(response.status).toBe(200)
expect(response.content).toMatchObject({
name: name,
})
})

it('should fail to decode an account import with the incorrect format', async () => {
const name = 'mnemonic-format'
const mnemonic = spendingKeyToWords(generateKey().spendingKey, LanguageCode.English)

await expect(
routeTest.client.wallet.importAccount({
account: mnemonic,
name,
rescan: false,
format: AccountFormat.JSON,
}),
).rejects.toMatchObject({
status: 400,
message:
expect.not.stringContaining('decoder errors:') &&
expect.stringContaining('Invalid JSON'),
})
})
})
})
9 changes: 7 additions & 2 deletions ironfish/src/rpc/routes/wallet/importAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as yup from 'yup'
import { DecodeInvalidName } from '../../../wallet'
import { DuplicateAccountNameError, DuplicateIdentityNameError } from '../../../wallet/errors'
import { decodeAccountImport } from '../../../wallet/exporter/account'
import { AccountFormat, decodeAccountImport } from '../../../wallet/exporter/account'
import { decryptEncodedAccount } from '../../../wallet/exporter/encryption'
import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters'
import { ApiNamespace } from '../namespaces'
Expand All @@ -18,6 +18,7 @@ export type ImportAccountRequest = {
name?: string
rescan?: boolean
createdAt?: number
format?: AccountFormat
}

export type ImportResponse = {
Expand All @@ -31,6 +32,7 @@ export const ImportAccountRequestSchema: yup.ObjectSchema<ImportAccountRequest>
name: yup.string().optional(),
account: yup.string().defined(),
createdAt: yup.number().optional(),
format: yup.mixed<AccountFormat>().oneOf(Object.values(AccountFormat)).optional(),
})
.defined()

Expand All @@ -50,7 +52,10 @@ routes.register<typeof ImportAccountRequestSchema, ImportResponse>(

request.data.account = await decryptEncodedAccount(request.data.account, context.wallet)

const decoded = decodeAccountImport(request.data.account, { name: request.data.name })
const decoded = decodeAccountImport(request.data.account, {
name: request.data.name,
format: request.data.format,
})
const account = await context.wallet.importAccount(decoded, {
createdAt: request.data.createdAt,
})
Expand Down
30 changes: 18 additions & 12 deletions ironfish/src/wallet/exporter/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,22 @@ export enum AccountFormat {
SpendingKey = 'SpendingKey',
}

const accountFormatToEncoder = new Map([
[AccountFormat.Base64Json, Base64JsonEncoder],
[AccountFormat.JSON, JsonEncoder],
[AccountFormat.Mnemonic, MnemonicEncoder],
[AccountFormat.SpendingKey, SpendingKeyEncoder],
])

export function encodeAccountImport(
value: AccountImport,
format: AccountFormat,
options: AccountEncodingOptions = {},
): string {
switch (format) {
case AccountFormat.JSON:
return new JsonEncoder().encode(value)
case AccountFormat.Base64Json:
return new Base64JsonEncoder().encode(value)
case AccountFormat.SpendingKey:
return new SpendingKeyEncoder().encode(value)
case AccountFormat.Mnemonic:
return new MnemonicEncoder().encode(value, options)
default:
return Assert.isUnreachable(format)
}
const encoder = accountFormatToEncoder.get(format)
Assert.isNotUndefined(encoder, `Invalid account encoding format: ${format}`)

return new encoder().encode(value, options)
}

export function decodeAccountImport(
Expand All @@ -52,6 +51,13 @@ export function decodeAccountImport(
): AccountImport {
const errors: DecodeFailed[] = []

if (options.format) {
const encoder = accountFormatToEncoder.get(options.format)
Assert.isNotUndefined(encoder, `Invalid account decoding format: ${options.format}`)

return new encoder().decode(value, options)
}

for (const encoder of ENCODER_VERSIONS) {
try {
const decoded = new encoder().decode(value, options)
Expand Down
2 changes: 2 additions & 0 deletions ironfish/src/wallet/exporter/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { LanguageKey } from '../../utils'
import { AccountFormat } from './account'
import { AccountImport } from './accountImport'

export class DecodeInvalid extends Error {}
Expand All @@ -25,6 +26,7 @@ export type AccountEncodingOptions = {

export type AccountDecodingOptions = {
name?: string
format?: AccountFormat
}

export type AccountEncoder = {
Expand Down

0 comments on commit c18f4d4

Please sign in to comment.