Skip to content

Commit

Permalink
Move usage of RpcAccountImport to exporter system (#5034)
Browse files Browse the repository at this point in the history
* Move usage of RpcAccountImport to exporter system

This old type was only used by the RPC system, but now is moving into
the JSON encoder to show that it's only used by the JSON encoding system
to export an AccountImport into JSON.

* Fixed tests to encode JSON

* Fixed Base64JSONEncoder throwing Error

This must throw DecodeFailed or else it will crash if decoding using
this decoder failed.

* Remove account after testing its import

* Fix incorrect schemas

* Fix broken bech32 test

The encoder is deprecated because you cannot encode accounts anymore
using it. This test was still trying to encode a new account import
format with an old deprecated encoder.

* Fix JSON test

* Fixed tests
  • Loading branch information
NullSoldier authored Jun 12, 2024
1 parent f32ba97 commit c0f9736
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 224 deletions.
4 changes: 2 additions & 2 deletions ironfish-cli/src/commands/wallet/multisig/dealer/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 { ACCOUNT_SCHEMA_VERSION, RpcClient } from '@ironfish/sdk'
import { ACCOUNT_SCHEMA_VERSION, JsonEncoder, RpcClient } from '@ironfish/sdk'
import { AccountImport } from '@ironfish/sdk/src/wallet/exporter'
import { Flags, ux } from '@oclif/core'
import { IronfishCommand } from '../../../../command'
Expand Down Expand Up @@ -103,7 +103,7 @@ export class MultisigCreateDealer extends IronfishCommand {
}

await client.wallet.importAccount({
account: JSON.stringify(account),
account: new JsonEncoder().encode(account),
})

ux.action.stop()
Expand Down
13 changes: 8 additions & 5 deletions ironfish/src/rpc/routes/wallet/importAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs from 'fs'
import path from 'path'
import { createTrustedDealerKeyPackages } from '../../../testUtilities'
import { createRouteTest } from '../../../testUtilities/routeTest'
import { JsonEncoder } from '../../../wallet'
import { AccountFormat, encodeAccountImport } from '../../../wallet/exporter/account'
import { AccountImport } from '../../../wallet/exporter/accountImport'
import { Bech32Encoder } from '../../../wallet/exporter/encoders/bech32'
Expand Down Expand Up @@ -37,7 +38,7 @@ describe('Route wallet/importAccount', () => {
}

const response = await routeTest.client.wallet.importAccount({
account: JSON.stringify(account),
account: new JsonEncoder().encode(account),
rescan: false,
})

Expand Down Expand Up @@ -67,7 +68,7 @@ describe('Route wallet/importAccount', () => {
}

const response = await routeTest.client.wallet.importAccount({
account: JSON.stringify(account),
account: new JsonEncoder().encode(account),
rescan: false,
})

Expand All @@ -83,7 +84,7 @@ describe('Route wallet/importAccount', () => {

const accountName = 'bar'
const response = await routeTest.client.wallet.importAccount({
account: JSON.stringify({
account: new JsonEncoder().encode({
name: accountName,
viewKey: key.viewKey,
spendingKey: key.spendingKey,
Expand All @@ -110,7 +111,7 @@ describe('Route wallet/importAccount', () => {
const accountName = 'bar'
const overriddenAccountName = 'not-bar'
const response = await routeTest.client.wallet.importAccount({
account: JSON.stringify({
account: new JsonEncoder().encode({
name: accountName,
viewKey: key.viewKey,
spendingKey: key.spendingKey,
Expand Down Expand Up @@ -166,7 +167,7 @@ describe('Route wallet/importAccount', () => {
}

const response = await routeTest.client.wallet.importAccount({
account: JSON.stringify(account),
account: new JsonEncoder().encode(account),
// set rescan to true so that skipRescan should not be called
rescan: true,
})
Expand Down Expand Up @@ -316,6 +317,8 @@ describe('Route wallet/importAccount', () => {

expect(response.status).toBe(200)
expect(response.content.name).not.toBeNull()

await routeTest.client.wallet.removeAccount({ account: testCaseFile })
}
})

Expand Down
57 changes: 1 addition & 56 deletions ironfish/src/rpc/routes/wallet/serializers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 { generateKeyFromPrivateKey } from '@ironfish/rust-nodejs'
import { Assert } from '../../../assert'
import { Config } from '../../../fileStores'
import { BufferUtils, CurrencyUtils } from '../../../utils'
import { Account, ACCOUNT_SCHEMA_VERSION, Wallet } from '../../../wallet'
import { AccountImport } from '../../../wallet/exporter/accountImport'
import { Account, Wallet } from '../../../wallet'
import {
isMultisigSignerImport,
isMultisigSignerTrustedDealerImport,
Expand All @@ -17,7 +14,6 @@ import { DecryptedNoteValue } from '../../../wallet/walletdb/decryptedNoteValue'
import { TransactionValue } from '../../../wallet/walletdb/transactionValue'
import {
RpcAccountAssetBalanceDelta,
RpcAccountImport,
RpcAccountStatus,
RpcMultisigKeys,
RpcWalletNote,
Expand Down Expand Up @@ -100,57 +96,6 @@ export async function serializeRpcWalletTransaction(
}
}

export const serializeRpcImportAccount = (accountImport: AccountImport): RpcAccountImport => {
const createdAt = accountImport.createdAt
? {
hash: accountImport.createdAt.hash.toString('hex'),
sequence: accountImport.createdAt.sequence,
}
: null

return {
version: accountImport.version,
name: accountImport.name,
viewKey: accountImport.viewKey,
incomingViewKey: accountImport.incomingViewKey,
outgoingViewKey: accountImport.outgoingViewKey,
publicAddress: accountImport.publicAddress,
spendingKey: accountImport.spendingKey,
multisigKeys: accountImport.multisigKeys,
proofAuthorizingKey: accountImport.proofAuthorizingKey,
createdAt: createdAt,
}
}

export function deserializeRpcAccountImport(accountImport: RpcAccountImport): AccountImport {
let viewKey: string
if (accountImport.viewKey) {
viewKey = accountImport.viewKey
} else {
Assert.isNotNull(
accountImport.spendingKey,
'Imported account missing both viewKey and spendingKey',
)
viewKey = generateKeyFromPrivateKey(accountImport.spendingKey).viewKey
}

return {
version: ACCOUNT_SCHEMA_VERSION,
...accountImport,
viewKey,
createdAt:
accountImport.createdAt && typeof accountImport.createdAt === 'object'
? {
hash: Buffer.from(accountImport.createdAt.hash, 'hex'),
sequence: accountImport.createdAt.sequence,
}
: null,
multisigKeys: accountImport.multisigKeys
? deserializeRpcAccountMultisigKeys(accountImport.multisigKeys)
: undefined,
}
}

export function deserializeRpcAccountMultisigKeys(
rpcMultisigKeys: RpcMultisigKeys,
): MultisigKeysImport {
Expand Down
41 changes: 0 additions & 41 deletions ironfish/src/rpc/routes/wallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,47 +162,6 @@ export type RpcMultisigKeys = {
publicKeyPackage: string
}

export type RpcAccountImport = {
version?: number
name: string
viewKey?: string
incomingViewKey: string
outgoingViewKey: string
publicAddress: string
spendingKey: string | null
createdAt: { hash: string; sequence: number } | string | null
multisigKeys?: RpcMultisigKeys
proofAuthorizingKey: string | null
}

export const RpcAccountImportSchema: yup.ObjectSchema<RpcAccountImport> = yup
.object({
name: yup.string().defined(),
spendingKey: yup.string().nullable().defined(),
viewKey: yup.string().defined(),
publicAddress: yup.string().defined(),
incomingViewKey: yup.string().defined(),
outgoingViewKey: yup.string().defined(),
version: yup.number().defined(),
createdAt: yup
.object({
hash: yup.string().defined(),
sequence: yup.number().defined(),
})
.nullable()
.defined(),
multisigKeys: yup
.object({
secret: yup.string().optional(),
identity: yup.string().optional(),
keyPackage: yup.string().optional(),
publicKeyPackage: yup.string().defined(),
})
.optional(),
proofAuthorizingKey: yup.string().nullable().defined(),
})
.defined()

export type RpcAccountStatus = {
name: string
id: string
Expand Down
2 changes: 1 addition & 1 deletion ironfish/src/utils/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function parse<T = unknown>(data: string, fileName?: string): T {
function tryParse<T = unknown>(
data: string,
fileName?: string,
): [T, null] | [null, ParseJsonError] {
): [result: T, error: null] | [result: null, error: ParseJsonError] {
try {
const config = parseJson(data, fileName || '') as T
return [config, null]
Expand Down
8 changes: 2 additions & 6 deletions ironfish/src/utils/yup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ export type YupSchemaResult<S extends yup.Schema<unknown, unknown>> = UnwrapProm
ReturnType<S['validate']>
>

export type YupSchemaResultSync<S extends yup.Schema<unknown, unknown>> = ReturnType<
S['validate']
>

export class YupUtils {
static isPositiveInteger = yup.number().integer().min(0)
static isPort = yup.number().integer().min(1).max(65535)
Expand Down Expand Up @@ -83,7 +79,7 @@ export class YupUtils {
value: unknown,
options?: yup.ValidateOptions<unknown>,
):
| { result: YupSchemaResultSync<S>; error: null }
| { result: YupSchemaResult<S>; error: null }
| { result: null; error: yup.ValidationError } {
if (!options) {
options = { stripUnknown: true }
Expand All @@ -94,7 +90,7 @@ export class YupUtils {
}

try {
const result = schema.validateSync(value, options) as YupSchemaResultSync<S>
const result = schema.validateSync(value, options) as YupSchemaResult<S>
return { result: result, error: null }
} catch (e) {
if (e instanceof yup.ValidationError) {
Expand Down
30 changes: 6 additions & 24 deletions ironfish/src/wallet/exporter/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +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 { Assert } from '../../assert'
import { AccountFormat, decodeAccountImport, encodeAccountImport } from './account'
import { Bech32JsonEncoder } from './encoders/bech32json'

describe('decodeAccount/encodeAccount', () => {
describe('when decoding/encoding', () => {
Expand All @@ -13,34 +11,18 @@ describe('decodeAccount/encodeAccount', () => {
'{"version":2,"name":"ffff","spendingKey":"9e02be4c932ebc09c1eba0273a0ea41344615097222a5fb8a8787fba0db1a8fa","viewKey":"8d027bae046d73cf0be07e6024dd5719fb3bbdcac21cbb54b9850f6e4f89cd28fdb49856e5272870e497d65b177682f280938e379696dbdc689868eee5e52c1f","incomingViewKey":"348bd554fa8f1dc9686146ced3d483c48321880fc1a6cf323981bb2a41f99700","outgoingViewKey":"68543a20edaa435fb49155d1defb5141426c84d56728a8c5ae7692bc07875e3b","publicAddress":"471325ab136b883fe3dacff0f288153a9669dd4bae3d73b6578b33722a3bd22c","createdAt":{"hash":"000000000000007e3b8229e5fa28ecf70d7a34c973dd67b87160d4e55275a907","sequence":97654}}'

const decoded = decodeAccountImport(jsonString)
Assert.isNotNull(decoded)
const encoded = encodeAccountImport(decoded, AccountFormat.JSON)
expect(encoded).toEqual(jsonString)
})

it('renames account when option is passed', () => {
const jsonString =
'{"version":2,"name":"ffff","spendingKey":"9e02be4c932ebc09c1eba0273a0ea41344615097222a5fb8a8787fba0db1a8fa","viewKey":"8d027bae046d73cf0be07e6024dd5719fb3bbdcac21cbb54b9850f6e4f89cd28fdb49856e5272870e497d65b177682f280938e379696dbdc689868eee5e52c1f","incomingViewKey":"348bd554fa8f1dc9686146ced3d483c48321880fc1a6cf323981bb2a41f99700","outgoingViewKey":"68543a20edaa435fb49155d1defb5141426c84d56728a8c5ae7692bc07875e3b","publicAddress":"471325ab136b883fe3dacff0f288153a9669dd4bae3d73b6578b33722a3bd22c","createdAt":{"hash":"000000000000007e3b8229e5fa28ecf70d7a34c973dd67b87160d4e55275a907","sequence":97654}}'
const decoded = decodeAccountImport(jsonString)
Assert.isNotNull(decoded)
expect(decoded).toMatchObject({
spendingKey: '9e02be4c932ebc09c1eba0273a0ea41344615097222a5fb8a8787fba0db1a8fa',
})

const encodedJson = encodeAccountImport(decoded, AccountFormat.JSON)
const decodedJson = decodeAccountImport(encodedJson, { name: 'new' })
expect(decodedJson.name).toEqual('new')

const encodedBase64 = encodeAccountImport(decoded, AccountFormat.Base64Json)
const decodedBase64 = decodeAccountImport(encodedBase64, { name: 'new' })
expect(decodedBase64.name).toEqual('new')

const bech32Encoder = new Bech32JsonEncoder()
const encodedBech32Json = bech32Encoder.encode(decoded)
const decodedBech32Json = bech32Encoder.decode(encodedBech32Json, { name: 'new' })
expect(decodedBech32Json.name).toEqual('new')
expect(() => encodeAccountImport(decoded, AccountFormat.JSON)).not.toThrow()
})

it('throws when json is not a valid account', () => {
const invalidJson = '{}'
expect(() => decodeAccountImport(invalidJson)).toThrow()
})

it('throws when name is not passed, but mnemonic is valid', () => {
const mnemonic =
'own bicycle nasty chaos type agent amateur inject cheese spare poverty charge ecology portion frame earn garden shed bulk youth patch sugar physical family'
Expand Down
66 changes: 64 additions & 2 deletions ironfish/src/wallet/exporter/accountImport.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 { generatePublicAddressFromIncomingViewKey } from '@ironfish/rust-nodejs'
import { Account } from '../account/account'
import { HeadValue } from '../walletdb/headValue'
import {
isValidIncomingViewKey,
isValidIVKAndPublicAddressPair,
isValidOutgoingViewKey,
isValidPublicAddress,
isValidSpendingKey,
isValidViewKey,
} from '../validator'
import { MultisigKeysImport } from './multisig'

export type AccountImport = {
Expand All @@ -13,7 +21,10 @@ export type AccountImport = {
incomingViewKey: string
outgoingViewKey: string
publicAddress: string
createdAt: HeadValue | null
createdAt: {
hash: Buffer
sequence: number
} | null
multisigKeys?: MultisigKeysImport
proofAuthorizingKey: string | null
}
Expand Down Expand Up @@ -44,3 +55,54 @@ export function toAccountImport(account: Account, viewOnly: boolean): AccountImp

return value
}

export function validateAccountImport(toImport: AccountImport): void {
if (!toImport.name) {
throw new Error(`Imported account has no name`)
}

if (!toImport.publicAddress) {
throw new Error(`Imported account has no public address`)
}

if (!isValidPublicAddress(toImport.publicAddress)) {
throw new Error(`Provided public address ${toImport.publicAddress} is invalid`)
}

if (!toImport.outgoingViewKey) {
throw new Error(`Imported account has no outgoing view key`)
}

if (!isValidOutgoingViewKey(toImport.outgoingViewKey)) {
throw new Error(`Provided outgoing view key ${toImport.outgoingViewKey} is invalid`)
}

if (!toImport.incomingViewKey) {
throw new Error(`Imported account has no incoming view key`)
}

if (!isValidIncomingViewKey(toImport.incomingViewKey)) {
throw new Error(`Provided incoming view key ${toImport.incomingViewKey} is invalid`)
}

if (!toImport.viewKey) {
throw new Error(`Imported account has no view key`)
}

if (!isValidViewKey(toImport.viewKey)) {
throw new Error(`Provided view key ${toImport.viewKey} is invalid`)
}

if (toImport.spendingKey && !isValidSpendingKey(toImport.spendingKey)) {
throw new Error(`Provided spending key ${toImport.spendingKey} is invalid`)
}

if (!isValidIVKAndPublicAddressPair(toImport.incomingViewKey, toImport.publicAddress)) {
const generatedPublicAddress = generatePublicAddressFromIncomingViewKey(
toImport.incomingViewKey,
)
throw new Error(
`Public address ${toImport.publicAddress} does not match public address generated from incoming view key ${generatedPublicAddress}`,
)
}
}
Loading

0 comments on commit c0f9736

Please sign in to comment.