Skip to content

Commit

Permalink
feat: add wallet module with import export (#652)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Mar 10, 2022
1 parent 8a98e13 commit 6cf5a7b
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 71 deletions.
11 changes: 7 additions & 4 deletions packages/core/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { RecipientModule } from '../modules/routing/RecipientModule'
import { InMemoryMessageRepository } from '../storage/InMemoryMessageRepository'
import { IndyStorageService } from '../storage/IndyStorageService'
import { IndyWallet } from '../wallet/IndyWallet'
import { WalletModule } from '../wallet/WalletModule'
import { WalletError } from '../wallet/error'

import { AgentConfig } from './AgentConfig'
Expand All @@ -45,6 +46,7 @@ export class Agent {
protected messageSender: MessageSender
private _isInitialized = false
public messageSubscription: Subscription
private walletService: Wallet

public readonly connections: ConnectionsModule
public readonly proofs: ProofsModule
Expand All @@ -55,7 +57,7 @@ export class Agent {
public readonly mediator: MediatorModule
public readonly discovery: DiscoverFeaturesModule
public readonly dids: DidsModule
public readonly wallet: Wallet
public readonly wallet: WalletModule

public constructor(initialConfig: InitConfig, dependencies: AgentDependencies) {
// Create child container so we don't interfere with anything outside of this agent
Expand Down Expand Up @@ -93,7 +95,7 @@ export class Agent {
this.messageSender = this.container.resolve(MessageSender)
this.messageReceiver = this.container.resolve(MessageReceiver)
this.transportService = this.container.resolve(TransportService)
this.wallet = this.container.resolve(InjectionSymbols.Wallet)
this.walletService = this.container.resolve(InjectionSymbols.Wallet)

// We set the modules in the constructor because that allows to set them as read-only
this.connections = this.container.resolve(ConnectionsModule)
Expand All @@ -105,6 +107,7 @@ export class Agent {
this.ledger = this.container.resolve(LedgerModule)
this.discovery = this.container.resolve(DiscoverFeaturesModule)
this.dids = this.container.resolve(DidsModule)
this.wallet = this.container.resolve(WalletModule)

// Listen for new messages (either from transports or somewhere else in the framework / extensions)
this.messageSubscription = this.eventEmitter
Expand Down Expand Up @@ -161,7 +164,7 @@ export class Agent {

if (publicDidSeed) {
// If an agent has publicDid it will be used as routing key.
await this.wallet.initPublicDid({ seed: publicDidSeed })
await this.walletService.initPublicDid({ seed: publicDidSeed })
}

// As long as value isn't false we will async connect to all genesis pools on startup
Expand Down Expand Up @@ -211,7 +214,7 @@ export class Agent {
}

public get publicDid() {
return this.wallet.publicDid
return this.walletService.publicDid
}

public async receiveMessage(inboundMessage: unknown, session?: TransportSession) {
Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/agent/__tests__/Agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,20 @@ describe('Agent', () => {
const { walletConfig, ...withoutWalletConfig } = config
agent = new Agent(withoutWalletConfig, dependencies)

const wallet = agent.injectionContainer.resolve<Wallet>(InjectionSymbols.Wallet)

expect(agent.isInitialized).toBe(false)
expect(wallet.isInitialized).toBe(false)
expect(agent.wallet.isInitialized).toBe(false)

expect(agent.initialize()).rejects.toThrowError(WalletError)
expect(agent.isInitialized).toBe(false)
expect(wallet.isInitialized).toBe(false)
expect(agent.wallet.isInitialized).toBe(false)

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(walletConfig!)
await agent.wallet.initialize(walletConfig!)
expect(agent.isInitialized).toBe(false)
expect(wallet.isInitialized).toBe(true)
expect(agent.wallet.isInitialized).toBe(true)

await agent.initialize()
expect(wallet.isInitialized).toBe(true)
expect(agent.wallet.isInitialized).toBe(true)
expect(agent.isInitialized).toBe(true)
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/crypto/__tests__/JwsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('JwsService', () => {
const config = getAgentConfig('JwsService')
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)

jwsService = new JwsService(wallet)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => {
const config = getAgentConfig('SignatureDecoratorUtilsTest')
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)
})

afterAll(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('BasicMessageService', () => {
agentConfig = getAgentConfig('BasicMessageServiceTest')
wallet = new IndyWallet(agentConfig)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(agentConfig.walletConfig!)
await wallet.createAndOpen(agentConfig.walletConfig!)
storageService = new IndyStorageService(wallet, agentConfig)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('ConnectionService', () => {
beforeAll(async () => {
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)
})

afterAll(async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/modules/dids/__tests__/peer-did.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('peer dids', () => {
beforeEach(async () => {
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)

const storageService = new IndyStorageService<DidRecord>(wallet, config)
didRepository = new DidRepository(storageService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('IndyLedgerService', () => {
beforeAll(async () => {
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)
})

afterAll(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('IndyStorageService', () => {
indy = config.agentDependencies.indy
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)
storageService = new IndyStorageService<TestRecord>(wallet, config)
})

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface WalletConfig {
keyDerivationMethod?: KeyDerivationMethod
}

export interface WalletExportImportConfig {
key: string
path: string
}

export type EncryptedMessage = {
protected: unknown
iv: unknown
Expand Down
72 changes: 44 additions & 28 deletions packages/core/src/wallet/IndyWallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Logger } from '../logger'
import type { EncryptedMessage, DecryptedMessageContext, WalletConfig } from '../types'
import type { EncryptedMessage, DecryptedMessageContext, WalletConfig, WalletExportImportConfig } from '../types'
import type { Buffer } from '../utils/buffer'
import type { Wallet, DidInfo, DidConfig } from './Wallet'
import type { default as Indy } from 'indy-sdk'
Expand Down Expand Up @@ -60,36 +60,20 @@ export class IndyWallet implements Wallet {
return this.walletConfig.id
}

public async initialize(walletConfig: WalletConfig) {
this.logger.info(`Initializing wallet '${walletConfig.id}'`, walletConfig)

if (this.isInitialized) {
throw new WalletError(
'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet'
)
}

// Open wallet, creating if it doesn't exist yet
try {
await this.open(walletConfig)
} catch (error) {
// If the wallet does not exist yet, create it and try to open again
if (error instanceof WalletNotFoundError) {
await this.create(walletConfig)
await this.open(walletConfig)
} else {
throw error
}
}

this.logger.debug(`Wallet '${walletConfig.id}' initialized with handle '${this.handle}'`)
/**
* @throws {WalletDuplicateError} if the wallet already exists
* @throws {WalletError} if another error occurs
*/
public async create(walletConfig: WalletConfig): Promise<void> {
await this.createAndOpen(walletConfig)
await this.close()
}

/**
* @throws {WalletDuplicateError} if the wallet already exists
* @throws {WalletError} if another error occurs
*/
public async create(walletConfig: WalletConfig): Promise<void> {
public async createAndOpen(walletConfig: WalletConfig): Promise<void> {
this.logger.debug(`Creating wallet '${walletConfig.id}' using SQLite storage`)

try {
Expand All @@ -105,10 +89,10 @@ export class IndyWallet implements Wallet {

// We need to open wallet before creating master secret because we need wallet handle here.
await this.createMasterSecret(this.handle, walletConfig.id)

// We opened wallet just to create master secret, we can close it now.
await this.close()
} catch (error) {
// If an error ocurred while creating the master secret, we should close the wallet
if (this.isInitialized) await this.close()

if (isIndyError(error, 'WalletAlreadyExistsError')) {
const errorMessage = `Wallet '${walletConfig.id}' already exists`
this.logger.debug(errorMessage)
Expand All @@ -127,6 +111,8 @@ export class IndyWallet implements Wallet {
throw new WalletError(errorMessage, { cause: error })
}
}

this.logger.debug(`Successfully created wallet '${walletConfig.id}'`)
}

/**
Expand Down Expand Up @@ -172,6 +158,8 @@ export class IndyWallet implements Wallet {
throw new WalletError(errorMessage, { cause: error })
}
}

this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this.handle}'`)
}

/**
Expand Down Expand Up @@ -217,6 +205,34 @@ export class IndyWallet implements Wallet {
}
}

public async export(exportConfig: WalletExportImportConfig) {
try {
this.logger.debug(`Exporting wallet ${this.walletConfig?.id} to path ${exportConfig.path}`)
await this.indy.exportWallet(this.handle, exportConfig)
} catch (error) {
const errorMessage = `Error exporting wallet': ${error.message}`
this.logger.error(errorMessage, {
error,
})

throw new WalletError(errorMessage, { cause: error })
}
}

public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) {
try {
this.logger.debug(`Importing wallet ${walletConfig.id} from path ${importConfig.path}`)
await this.indy.importWallet({ id: walletConfig.id }, { key: walletConfig.key }, importConfig)
} catch (error) {
const errorMessage = `Error importing wallet': ${error.message}`
this.logger.error(errorMessage, {
error,
})

throw new WalletError(errorMessage, { cause: error })
}
}

/**
* @throws {WalletError} if the wallet is already closed or another error occurs
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/wallet/Wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Wallet', () => {

test('initialize public did', async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)
await wallet.createAndOpen(config.walletConfig!)

await wallet.initPublicDid({ seed: '00000000000000000000000Forward01' })

Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/wallet/Wallet.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import type { EncryptedMessage, DecryptedMessageContext, WalletConfig } from '../types'
import type { EncryptedMessage, DecryptedMessageContext, WalletConfig, WalletExportImportConfig } from '../types'
import type { Buffer } from '../utils/buffer'

export interface Wallet {
publicDid: DidInfo | undefined
isInitialized: boolean
isProvisioned: boolean

initialize(walletConfig: WalletConfig): Promise<void>
create(walletConfig: WalletConfig): Promise<void>
createAndOpen(walletConfig: WalletConfig): Promise<void>
open(walletConfig: WalletConfig): Promise<void>
close(): Promise<void>
delete(): Promise<void>
export(exportConfig: WalletExportImportConfig): Promise<void>
import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise<void>

initPublicDid(didConfig: DidConfig): Promise<void>
createDid(didConfig?: DidConfig): Promise<DidInfo>
Expand Down
82 changes: 82 additions & 0 deletions packages/core/src/wallet/WalletModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Logger } from '../logger'
import type { WalletConfig, WalletExportImportConfig } from '../types'

import { inject, Lifecycle, scoped } from 'tsyringe'

import { AgentConfig } from '../agent/AgentConfig'
import { InjectionSymbols } from '../constants'

import { Wallet } from './Wallet'
import { WalletError } from './error/WalletError'
import { WalletNotFoundError } from './error/WalletNotFoundError'

@scoped(Lifecycle.ContainerScoped)
export class WalletModule {
private wallet: Wallet
private logger: Logger

public constructor(@inject(InjectionSymbols.Wallet) wallet: Wallet, agentConfig: AgentConfig) {
this.wallet = wallet
this.logger = agentConfig.logger
}

public get isInitialized() {
return this.wallet.isInitialized
}

public get isProvisioned() {
return this.wallet.isProvisioned
}

public async initialize(walletConfig: WalletConfig): Promise<void> {
this.logger.info(`Initializing wallet '${walletConfig.id}'`, walletConfig)

if (this.isInitialized) {
throw new WalletError(
'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet'
)
}

// Open wallet, creating if it doesn't exist yet
try {
await this.open(walletConfig)
} catch (error) {
// If the wallet does not exist yet, create it and try to open again
if (error instanceof WalletNotFoundError) {
// Keep the wallet open after creating it, this saves an extra round trip of closing/opening
// the wallet, which can save quite some time.
await this.createAndOpen(walletConfig)
} else {
throw error
}
}
}

public async createAndOpen(walletConfig: WalletConfig): Promise<void> {
await this.wallet.createAndOpen(walletConfig)
}

public async create(walletConfig: WalletConfig): Promise<void> {
await this.wallet.create(walletConfig)
}

public async open(walletConfig: WalletConfig): Promise<void> {
await this.wallet.open(walletConfig)
}

public async close(): Promise<void> {
await this.wallet.close()
}

public async delete(): Promise<void> {
await this.wallet.delete()
}

public async export(exportConfig: WalletExportImportConfig): Promise<void> {
await this.wallet.export(exportConfig)
}

public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise<void> {
await this.wallet.import(walletConfig, importConfig)
}
}
Loading

0 comments on commit 6cf5a7b

Please sign in to comment.