Skip to content

Commit

Permalink
feat: only connect to ledger when needed (#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimoGlastra authored May 17, 2021
1 parent 21b15ff commit a9c261e
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function getBaseConfig(name: string, extraConfig: Partial<InitConfig> = {
walletCredentials: { key: `Key: ${name}` },
publicDidSeed,
autoAcceptConnections: true,
poolName: `Pool: ${name}`,
poolName: `pool-${name.toLowerCase()}`,
logger: testLogger,
indy,
fileSystem: new NodeFileSystem(),
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Agent } from '..'
import { DID_IDENTIFIER_REGEX, VERKEY_REGEX, isFullVerkey, isAbbreviatedVerkey } from '../utils/did'
import { genesisPath, getBaseConfig, sleep } from './helpers'
import testLogger from './logger'
import { promises } from 'fs'

const faberConfig = getBaseConfig('Faber Ledger', { genesisPath })

Expand Down Expand Up @@ -121,4 +122,16 @@ describe('ledger', () => {
})
)
})

it('should correctly store the genesis file if genesis transactions is passed', async () => {
const genesisTransactions = await promises.readFile(genesisPath, { encoding: 'utf-8' })
const agent = new Agent(getBaseConfig('Faber Ledger Genesis Transactions', { genesisTransactions }))

if (!faberAgent.publicDid?.did) {
throw new Error('No public did')
}

const did = await agent.ledger.getPublicDid(faberAgent.publicDid.did)
expect(did.did).toEqual(faberAgent.publicDid.did)
})
})
10 changes: 1 addition & 9 deletions src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,12 @@ export class Agent {
public async init() {
await this.wallet.init()

const { publicDidSeed, genesisPath, poolName } = this.agentConfig
const { publicDidSeed } = this.agentConfig
if (publicDidSeed) {
// If an agent has publicDid it will be used as routing key.
await this.wallet.initPublicDid({ seed: publicDidSeed })
}

// If the genesis is provided in the config, we will automatically handle ledger connection
// otherwise the framework consumer needs to do this manually
if (genesisPath) {
await this.ledger.connect(poolName, {
genesisPath,
})
}

if (this.inboundTransporter) {
await this.inboundTransporter.start(this)
}
Expand Down
4 changes: 4 additions & 0 deletions src/agent/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class AgentConfig {
return this.initConfig.genesisPath
}

public get genesisTransactions() {
return this.initConfig.genesisTransactions
}

public get walletConfig() {
return this.initConfig.walletConfig
}
Expand Down
6 changes: 2 additions & 4 deletions src/modules/indy/services/IndyIssuerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { inject, Lifecycle, scoped } from 'tsyringe'

import { FileSystem } from '../../../storage/fs/FileSystem'
import { Symbols } from '../../../symbols'
import { getDirFromFilePath } from '../../../utils/path'
import { IndyWallet } from '../../../wallet/IndyWallet'

@scoped(Lifecycle.ContainerScoped)
Expand Down Expand Up @@ -120,10 +121,7 @@ export class IndyIssuerService {
const tailsFileExists = await this.fileSystem.exists(tailsFilePath)

// Extract directory from path (should also work with windows paths)
const dirname = tailsFilePath.substring(
0,
Math.max(tailsFilePath.lastIndexOf('/'), tailsFilePath.lastIndexOf('\\'))
)
const dirname = getDirFromFilePath(tailsFilePath)

if (!tailsFileExists) {
throw new Error(`Tails file does not exist at path ${tailsFilePath}`)
Expand Down
6 changes: 1 addition & 5 deletions src/modules/ledger/LedgerModule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CredDefId, Did, SchemaId } from 'indy-sdk'
import { inject, scoped, Lifecycle } from 'tsyringe'

import { LedgerService, SchemaTemplate, CredentialDefinitionTemplate, LedgerConnectOptions } from './services'
import { LedgerService, SchemaTemplate, CredentialDefinitionTemplate } from './services'
import { Wallet } from '../../wallet/Wallet'
import { Symbols } from '../../symbols'
import { AriesFrameworkError } from '../../error'
Expand All @@ -16,10 +16,6 @@ export class LedgerModule {
this.wallet = wallet
}

public async connect(poolName: string, poolConfig: LedgerConnectOptions) {
return this.ledgerService.connect(poolName, poolConfig)
}

public async registerPublicDid() {
throw new AriesFrameworkError('registerPublicDid not implemented.')
}
Expand Down
69 changes: 49 additions & 20 deletions src/modules/ledger/services/LedgerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@ import type {
LedgerWriteReplyResponse,
} from 'indy-sdk'
import { AgentConfig } from '../../../agent/AgentConfig'
import { AriesFrameworkError } from '../../../error'
import { Logger } from '../../../logger'
import { isIndyError } from '../../../utils/indyError'
import { Wallet } from '../../../wallet/Wallet'
import { Symbols } from '../../../symbols'
import { IndyIssuerService } from '../../indy'

export interface LedgerConnectOptions {
genesisPath: string
}
import { FileSystem } from '../../../storage/fs/FileSystem'

@scoped(Lifecycle.ContainerScoped)
export class LedgerService {
Expand All @@ -31,27 +27,43 @@ export class LedgerService {
private _poolHandle?: PoolHandle
private authorAgreement?: AuthorAgreement | null
private indyIssuer: IndyIssuerService

public constructor(@inject(Symbols.Wallet) wallet: Wallet, agentConfig: AgentConfig, indyIssuer: IndyIssuerService) {
private agentConfig: AgentConfig
private fileSystem: FileSystem

public constructor(
@inject(Symbols.Wallet) wallet: Wallet,
agentConfig: AgentConfig,
indyIssuer: IndyIssuerService,
@inject(Symbols.FileSystem) fileSystem: FileSystem
) {
this.wallet = wallet
this.agentConfig = agentConfig
this.indy = agentConfig.indy
this.logger = agentConfig.logger
this.indyIssuer = indyIssuer
this.fileSystem = fileSystem
}

private get poolHandle() {
private async getPoolHandle() {
if (!this._poolHandle) {
throw new AriesFrameworkError('Pool has not been initialized yet.')
return this.connect()
}

return this._poolHandle
}

public async connect(poolName: string, poolConfig: LedgerConnectOptions) {
this.logger.debug(`Connecting to ledger pool '${poolName}'`, poolConfig)
public async connect() {
const poolName = this.agentConfig.poolName
const genesisPath = await this.getGenesisPath()

if (!genesisPath) {
throw new Error('Cannot connect to ledger without genesis file')
}

this.logger.debug(`Connecting to ledger pool '${poolName}'`, { genesisPath })
try {
this.logger.debug(`Creating pool '${poolName}'`)
await this.indy.createPoolLedgerConfig(poolName, { genesis_txn: poolConfig.genesisPath })
await this.indy.createPoolLedgerConfig(poolName, { genesis_txn: genesisPath })
} catch (error) {
if (isIndyError(error, 'PoolLedgerConfigAlreadyExistsError')) {
this.logger.debug(`Pool '${poolName}' already exists`, {
Expand All @@ -67,6 +79,7 @@ export class LedgerService {

this.logger.debug(`Opening pool ${poolName}`)
this._poolHandle = await this.indy.openPoolLedger(poolName)
return this._poolHandle
}

public async getPublicDid(did: Did) {
Expand All @@ -75,7 +88,7 @@ export class LedgerService {
const request = await this.indy.buildGetNymRequest(null, did)

this.logger.debug(`Submitting get did request for did '${did}' to ledger`)
const response = await this.indy.submitRequest(this.poolHandle, request)
const response = await this.indy.submitRequest(await this.getPoolHandle(), request)

const result = await this.indy.parseGetNymResponse(response)
this.logger.debug(`Retrieved did '${did}' from ledger`, result)
Expand All @@ -85,7 +98,7 @@ export class LedgerService {
this.logger.error(`Error retrieving did '${did}' from ledger`, {
error,
did,
poolHandle: this.poolHandle,
poolHandle: await this.getPoolHandle(),
})

throw error
Expand Down Expand Up @@ -113,7 +126,7 @@ export class LedgerService {
this.logger.error(`Error registering schema for did '${did}' on ledger`, {
error,
did,
poolHandle: this.poolHandle,
poolHandle: await this.getPoolHandle(),
schemaTemplate,
})

Expand Down Expand Up @@ -141,7 +154,7 @@ export class LedgerService {
this.logger.error(`Error retrieving schema '${schemaId}' from ledger`, {
error,
schemaId,
poolHandle: this.poolHandle,
poolHandle: await this.getPoolHandle(),
})

throw error
Expand Down Expand Up @@ -180,7 +193,7 @@ export class LedgerService {
{
error,
did,
poolHandle: this.poolHandle,
poolHandle: await this.getPoolHandle(),
credentialDefinitionTemplate,
}
)
Expand Down Expand Up @@ -211,7 +224,7 @@ export class LedgerService {
this.logger.error(`Error retrieving credential definition '${credentialDefinitionId}' from ledger`, {
error,
credentialDefinitionId: credentialDefinitionId,
poolHandle: this.poolHandle,
poolHandle: await this.getPoolHandle(),
})
throw error
}
Expand All @@ -221,7 +234,7 @@ export class LedgerService {
const requestWithTaa = await this.appendTaa(request)
const signedRequestWithTaa = await this.wallet.signRequest(signDid, requestWithTaa)

const response = await this.indy.submitRequest(this.poolHandle, signedRequestWithTaa)
const response = await this.indy.submitRequest(await this.getPoolHandle(), signedRequestWithTaa)

if (response.op === 'REJECT') {
throw Error(`Ledger rejected transaction request: ${response.reason}`)
Expand All @@ -231,7 +244,7 @@ export class LedgerService {
}

private async submitReadRequest(request: LedgerRequest): Promise<LedgerReadReplyResponse> {
const response = await this.indy.submitRequest(this.poolHandle, request)
const response = await this.indy.submitRequest(await this.getPoolHandle(), request)

if (response.op === 'REJECT') {
throw Error(`Ledger rejected transaction request: ${response.reason}`)
Expand Down Expand Up @@ -293,6 +306,22 @@ export class LedgerService {
const [firstMechanism] = Object.keys(authorAgreement.acceptanceMechanisms.aml)
return firstMechanism
}

private async getGenesisPath() {
// If the path is already provided return it
if (this.agentConfig.genesisPath) return this.agentConfig.genesisPath

// Determine the genesisPath
const genesisPath = this.fileSystem.basePath + `/afj/genesis-${this.agentConfig.poolName}.txn`
// Store genesis data if provided
if (this.agentConfig.genesisTransactions) {
await this.fileSystem.write(genesisPath, this.agentConfig.genesisTransactions)
return genesisPath
}

// No genesisPath
return null
}
}

export interface SchemaTemplate {
Expand Down
7 changes: 6 additions & 1 deletion src/storage/fs/NodeFileSystem.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { promises } from 'fs'
import { dirname } from 'path'
import { tmpdir } from 'os'
import { FileSystem } from './FileSystem'

const { access, readFile, writeFile } = promises
Expand All @@ -12,7 +14,7 @@ export class NodeFileSystem implements FileSystem {
* @param basePath The base path to use for reading and writing files. process.cwd() if not specified
*/
public constructor(basePath?: string) {
this.basePath = basePath ?? process.cwd()
this.basePath = basePath ?? tmpdir()
}

public async exists(path: string) {
Expand All @@ -25,6 +27,9 @@ export class NodeFileSystem implements FileSystem {
}

public async write(path: string, data: string): Promise<void> {
// Make sure parent directories exist
await promises.mkdir(dirname(path), { recursive: true })

return writeFile(path, data, { encoding: 'utf-8' })
}

Expand Down
8 changes: 6 additions & 2 deletions src/storage/fs/ReactNativeFileSystem.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import RNFS from 'react-native-fs'
import { getDirFromFilePath } from '../../utils/path'

import { FileSystem } from './FileSystem'

Expand All @@ -8,19 +9,22 @@ export class ReactNativeFileSystem implements FileSystem {
/**
* Create new ReactNativeFileSystem class instance.
*
* @param basePath The base path to use for reading and writing files. RNFS.DocumentDirectoryPath if not specified
* @param basePath The base path to use for reading and writing files. RNFS.TemporaryDirectoryPath if not specified
*
* @see https://github.com/itinance/react-native-fs#constants
*/
public constructor(basePath?: string) {
this.basePath = basePath ?? RNFS.DocumentDirectoryPath
this.basePath = basePath ?? RNFS.TemporaryDirectoryPath
}

public async exists(path: string): Promise<boolean> {
return RNFS.exists(path)
}

public async write(path: string, data: string): Promise<void> {
// Make sure parent directories exist
await RNFS.mkdir(getDirFromFilePath(path))

return RNFS.writeFile(path, data, 'utf8')
}

Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ export interface InitConfig {
walletConfig: WalletConfig
walletCredentials: WalletCredentials
autoAcceptConnections?: boolean
genesisPath?: string
poolName?: string
logger?: Logger
indy: typeof Indy
didCommMimeType?: DidCommMimeType
fileSystem: FileSystem

// Either path or transactions string can be provided
genesisPath?: string
genesisTransactions?: string
}

export interface UnpackedMessage {
Expand Down
9 changes: 9 additions & 0 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Extract directory from path (should also work with windows paths)
*
* @param path the path to extract the directory from
* @returns the directory path
*/
export function getDirFromFilePath(path: string) {
return path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')))
}

0 comments on commit a9c261e

Please sign in to comment.