Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add wallet module with import export #652

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit weird it's named walletService although it's not a service.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we expose initPublicDid and publicDid via the wallet "module"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit weird it's named walletService although it's not a service.

Agreed. Do you have any suggestions of the naming?

What if we expose initPublicDid and publicDid via the wallet "module"?

I would rather not do that at the moment. With the work happening on other did methods and the did module, I'd like to move away from the initPublicDid and publicDid. With supporting multiple types of dids, it'll be more common to have different kind of public dids, so publicDid and initPublicDid won't make a lot of sense anymore. So I'd rather not expose that API right now, but rather keep it as is currently (wallet api has only been exposed after 0.1.0 release, so not a breaking change to not make them public).

We can then start looking at how to handle public dids in general through the dids module. The wallet can then focus more on providing the actual crypto instead of dealing with DIDs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Yes, dids module is a better place for this functionality. That can help with walletService naming issue. We wouldn't need to have a class member referring toWallet instance.


// 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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved

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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })
})

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.create({ ...agentConfig.walletConfig!, keepOpenAfterCreate: true })
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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })
})

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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })

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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })
})

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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })
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: 42 additions & 30 deletions packages/core/src/wallet/IndyWallet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 { Wallet, DidInfo, DidConfig, WalletCreateConfig } from './Wallet'
import type { default as Indy } from 'indy-sdk'

import { Lifecycle, scoped } from 'tsyringe'
Expand Down Expand Up @@ -60,38 +60,16 @@ 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> {
public async create(walletConfig: WalletCreateConfig): Promise<void> {
this.logger.debug(`Creating wallet '${walletConfig.id}' using SQLite storage`)

// Close wallet by default after creating it
const keepOpenAfterCreate = walletConfig.keepOpenAfterCreate ?? false

try {
await this.indy.createWallet(
{ id: walletConfig.id },
Expand All @@ -106,8 +84,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()
// We opened wallet just to create master secret. Close it if desired
if (!keepOpenAfterCreate) {
await this.close()
}
} catch (error) {
if (isIndyError(error, 'WalletAlreadyExistsError')) {
const errorMessage = `Wallet '${walletConfig.id}' already exists`
Expand All @@ -127,6 +107,8 @@ export class IndyWallet implements Wallet {
throw new WalletError(errorMessage, { cause: error })
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

/**
Expand Down Expand Up @@ -172,6 +154,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 +201,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.create({ ...config.walletConfig!, keepOpenAfterCreate: true })

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

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

export interface WalletCreateConfig extends WalletConfig {
keepOpenAfterCreate?: boolean
}

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

initialize(walletConfig: WalletConfig): Promise<void>
create(walletConfig: WalletConfig): Promise<void>
create(walletConfig: WalletCreateConfig): 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
79 changes: 79 additions & 0 deletions packages/core/src/wallet/WalletModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Logger } from '../logger'
import type { WalletConfig, WalletExportImportConfig } from '../types'
import type { WalletCreateConfig } from './Wallet'

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.create({ ...walletConfig, keepOpenAfterCreate: true })
} else {
throw error
}
}
}

public async create(walletConfig: WalletCreateConfig): 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