diff --git a/solarkraft/src/fetcher/storage.ts b/solarkraft/src/fetcher/storage.ts index 0cd1fec4..1f108cb6 100644 --- a/solarkraft/src/fetcher/storage.ts +++ b/solarkraft/src/fetcher/storage.ts @@ -12,7 +12,13 @@ import JSONbigint from 'json-bigint' import { OrderedMap } from 'immutable' import { join } from 'node:path/posix' -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs' const JSONbig = JSONbigint({ useNativeBigInt: true }) @@ -68,6 +74,16 @@ export interface ContractCallEntry { oldFields: FieldsMap } +/** + * A listing entry. + */ +export interface ListEntry { + contractId: string + height: number + txHash: string + verification: 'ok' | 'fail' | 'unverified' +} + /** * Serializable fetcher state. */ @@ -128,6 +144,42 @@ export function loadContractCallEntry(filename: string): ContractCallEntry { } } +/** + * Generate storage entries for a given contract id in a path. + * @param contractId contract identifier (address) + * @param path the path to the contract storage + */ +export function* yieldListEntriesForContract( + contractId: string, + path: string +): Generator { + for (const dirent of readdirSync(path, { withFileTypes: true })) { + // match ledger heights, which are positive integers + if (dirent.isDirectory() && /^[0-9]+$/.exec(dirent.name)) { + // This directory may contain several transactions for the same height. + const height = Number.parseInt(dirent.name) + for (const ledgerDirent of readdirSync(join(path, dirent.name), { + withFileTypes: true, + })) { + // match all storage entries, which may be reported in different cases + const matcher = /^entry-([0-9a-fA-F]+)\.json$/.exec( + ledgerDirent.name + ) + if (ledgerDirent.isFile() && matcher) { + const txHash = matcher[1] + // TODO: read the verification result and report it + yield { + contractId, + height, + txHash, + verification: 'unverified', + } + } + } + } + } +} + /** * Load fetcher state from the storage. * @param root the storage root directory diff --git a/solarkraft/src/list.ts b/solarkraft/src/list.ts index 1984e9c8..60cba652 100644 --- a/solarkraft/src/list.ts +++ b/solarkraft/src/list.ts @@ -2,24 +2,49 @@ * @license * [Apache-2.0](https://github.com/freespek/solarkraft/blob/main/LICENSE) */ + /** - * List the transactions fetched from the ledger + * List the transactions in the storage. + * + * Igor Konnov, 2024. */ -export function list() { - console.log( - `*** WARNING: THIS IS A MOCK. NOTHING IS IMPLEMENTED YET. ***\n` - ) - console.log(`Verified:`) - console.log( - `TX d669f322d1011a3726301535f5451ef4398d40ad150b79845fb9a5fc781092cf at 51291024` - ) - console.log( - `TX cf96fc2bb266912f8063c6091a70a680498bbe41e71132814f765186926e4f80 at 51291018` - ) - console.log('') - console.log(`Unverified:`) - console.log( - `TX b6efc58ab03db82b5b2fa44b69ff89b64e54af5e6e5266dbdd70fc57d2dc583e at 51291021` - ) +import { existsSync, readdirSync } from 'fs' +import { storagePath, yieldListEntriesForContract } from './fetcher/storage.js' +import { join } from 'node:path' + +// the length of a contract id in its string representation +const CONTRACT_ID_LENGTH = 56 + +/** + * List the transactions fetched from the ledger. + */ +export function list(args: any) { + // Read from the file system directly. + // In the future versions, we may want to introduce an abstraction in fetcher/storage.ts. + const storageRoot = storagePath(args.home) + if (!existsSync(storageRoot)) { + console.log(`The storage is empty. Run 'solarkraft fetch'`) + return + } + + readdirSync(storageRoot, { withFileTypes: true }).map((dirent) => { + if (dirent.isDirectory() && dirent.name.length === CONTRACT_ID_LENGTH) { + // this is a storage directory for a contract + if (args.id === '' || dirent.name === args.id) { + console.log(`Contract ${dirent.name}:`) + console.log('') + + for (const e of yieldListEntriesForContract( + dirent.name, + join(storageRoot, dirent.name) + )) { + console.log(` [${e.verification}]`) + console.log(` height: ${e.height}`) + console.log(` tx: ${e.txHash}`) + console.log('') + } + } + } + }) } diff --git a/solarkraft/src/main.ts b/solarkraft/src/main.ts index e5179598..9f18b352 100644 --- a/solarkraft/src/main.ts +++ b/solarkraft/src/main.ts @@ -85,7 +85,13 @@ const verifyCmd = { const listCmd = { command: ['list'], desc: 'list the fetched and verified transactions', - builder: (yargs: any) => defaultOpts(yargs), + builder: (yargs: any) => + defaultOpts(yargs).option('id', { + desc: 'Contract id', + type: 'string', + default: '', + require: false, + }), handler: list, } diff --git a/solarkraft/test/e2e/list.test.ts b/solarkraft/test/e2e/list.test.ts new file mode 100644 index 00000000..ff1de6b0 --- /dev/null +++ b/solarkraft/test/e2e/list.test.ts @@ -0,0 +1,40 @@ +// Integration tests for the `verify` command + +import { describe, it } from 'mocha' + +import { spawn } from 'nexpect' +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { saveContractCallEntry } from '../../src/fetcher/storage.js' +import { OrderedMap } from 'immutable' + +const TX_HASH = + '9fb12935fbadcd28aa220d076f11be631590d22c60977a53997a746898322ca3' +const CONTRACT_ID = 'CC22QGTOUMERDNIYN7TPNX3V6EMPHQXVSRR3XY56EADF7YTFISD2ROND' + +describe('list', () => { + it('lists a single entry', function (done) { + // create a single entry in a temporary directory + const root = join(tmpdir(), 'solarkraft-storage-') + mkdtempSync(root) + saveContractCallEntry(root, { + height: 1000, + txHash: TX_HASH, + contractId: CONTRACT_ID, + method: 'set_i32', + methodArgs: [42], + returnValue: 0, + fields: OrderedMap(), + oldFields: OrderedMap(), + }) + + this.timeout(50000) + spawn(`solarkraft list --home ${root}`) + .wait(`Contract ${CONTRACT_ID}`) + .wait(' [unverified]') + .wait(' height: 1000') + .wait(` tx: ${TX_HASH}`) + .run(done) + }) +})