diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts index 36e9bc7f7d..ad9eadc6a9 100644 --- a/src/__tests__/helpers.ts +++ b/src/__tests__/helpers.ts @@ -42,7 +42,7 @@ export function getBaseConfig(name: string, extraConfig: Partial = { walletCredentials: { key: `Key: ${name}` }, publicDidSeed, autoAcceptConnections: true, - poolName: `Pool: ${name}`, + poolName: `pool-${name.toLowerCase()}`, logger: testLogger, indy, fileSystem: new NodeFileSystem(), diff --git a/src/__tests__/ledger.test.ts b/src/__tests__/ledger.test.ts index 28d6b7c4e2..15b2b93e94 100644 --- a/src/__tests__/ledger.test.ts +++ b/src/__tests__/ledger.test.ts @@ -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 }) @@ -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) + }) }) diff --git a/src/agent/Agent.ts b/src/agent/Agent.ts index 124371faf1..7cf43c01f3 100644 --- a/src/agent/Agent.ts +++ b/src/agent/Agent.ts @@ -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) } diff --git a/src/agent/AgentConfig.ts b/src/agent/AgentConfig.ts index 054f560df2..376e5a5ed3 100644 --- a/src/agent/AgentConfig.ts +++ b/src/agent/AgentConfig.ts @@ -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 } diff --git a/src/modules/indy/services/IndyIssuerService.ts b/src/modules/indy/services/IndyIssuerService.ts index f394e633c2..901b7b044a 100644 --- a/src/modules/indy/services/IndyIssuerService.ts +++ b/src/modules/indy/services/IndyIssuerService.ts @@ -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) @@ -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}`) diff --git a/src/modules/ledger/LedgerModule.ts b/src/modules/ledger/LedgerModule.ts index fc165c5cce..1804a862ff 100644 --- a/src/modules/ledger/LedgerModule.ts +++ b/src/modules/ledger/LedgerModule.ts @@ -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' @@ -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.') } diff --git a/src/modules/ledger/services/LedgerService.ts b/src/modules/ledger/services/LedgerService.ts index c80c835154..d9e5b6cac1 100644 --- a/src/modules/ledger/services/LedgerService.ts +++ b/src/modules/ledger/services/LedgerService.ts @@ -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 { @@ -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`, { @@ -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) { @@ -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) @@ -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 @@ -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, }) @@ -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 @@ -180,7 +193,7 @@ export class LedgerService { { error, did, - poolHandle: this.poolHandle, + poolHandle: await this.getPoolHandle(), credentialDefinitionTemplate, } ) @@ -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 } @@ -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}`) @@ -231,7 +244,7 @@ export class LedgerService { } private async submitReadRequest(request: LedgerRequest): Promise { - 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}`) @@ -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 { diff --git a/src/storage/fs/NodeFileSystem.ts b/src/storage/fs/NodeFileSystem.ts index 3ea4a6ae25..a5d410676b 100644 --- a/src/storage/fs/NodeFileSystem.ts +++ b/src/storage/fs/NodeFileSystem.ts @@ -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 @@ -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) { @@ -25,6 +27,9 @@ export class NodeFileSystem implements FileSystem { } public async write(path: string, data: string): Promise { + // Make sure parent directories exist + await promises.mkdir(dirname(path), { recursive: true }) + return writeFile(path, data, { encoding: 'utf-8' }) } diff --git a/src/storage/fs/ReactNativeFileSystem.ts b/src/storage/fs/ReactNativeFileSystem.ts index 2f1d73ed77..af584d6f45 100644 --- a/src/storage/fs/ReactNativeFileSystem.ts +++ b/src/storage/fs/ReactNativeFileSystem.ts @@ -1,4 +1,5 @@ import RNFS from 'react-native-fs' +import { getDirFromFilePath } from '../../utils/path' import { FileSystem } from './FileSystem' @@ -8,12 +9,12 @@ 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 { @@ -21,6 +22,9 @@ export class ReactNativeFileSystem implements FileSystem { } public async write(path: string, data: string): Promise { + // Make sure parent directories exist + await RNFS.mkdir(getDirFromFilePath(path)) + return RNFS.writeFile(path, data, 'utf8') } diff --git a/src/types.ts b/src/types.ts index b0bfcbf190..dc9d7a19dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 { diff --git a/src/utils/path.ts b/src/utils/path.ts new file mode 100644 index 0000000000..8b4dc2c26b --- /dev/null +++ b/src/utils/path.ts @@ -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('\\'))) +}