diff --git a/servers/cu/src/domain/client/ao-su.js b/servers/cu/src/domain/client/ao-su.js index 4b452e3e2..9bff71ce8 100644 --- a/servers/cu/src/domain/client/ao-su.js +++ b/servers/cu/src/domain/client/ao-su.js @@ -3,7 +3,7 @@ import { Transform, pipeline } from 'node:stream' import { of } from 'hyper-async' import { always, applySpec, evolve, filter, isNotNil, last, path, pathOr, pipe, prop } from 'ramda' -import { findRawTag, padBlockHeight } from '../lib/utils.js' +import { findRawTag, padBlockHeight } from '../utils.js' export const loadMessagesWith = ({ fetch, SU_URL, logger: _logger, pageSize }) => { const logger = _logger.child('ao-su:loadMessages') diff --git a/servers/cu/src/domain/lib/evaluate.js b/servers/cu/src/domain/lib/evaluate.js index 240328100..d6ba3f404 100644 --- a/servers/cu/src/domain/lib/evaluate.js +++ b/servers/cu/src/domain/lib/evaluate.js @@ -26,7 +26,7 @@ const ctxSchema = z.object({ */ function addHandler (ctx) { - return of(ctx.src) + return of(ctx.module) .chain(fromPromise(AoLoader)) .map((handle) => ({ handle, ...ctx })) } @@ -64,7 +64,7 @@ function doesMessageIdExistWith ({ findMessageId }) { * @property {string} id - the contract id * @property {Record} state - the initial state * @property {string} from - the initial state sortKey - * @property {ArrayBuffer} src - the contract wasm as an array buffer + * @property {ArrayBuffer} module - the contract wasm as an array buffer * @property {Record[]} action - an array of interactions to apply * * @callback Evaluate diff --git a/servers/cu/src/domain/lib/evaluate.test.js b/servers/cu/src/domain/lib/evaluate.test.js index 183f372b2..ec5aa9148 100644 --- a/servers/cu/src/domain/lib/evaluate.test.js +++ b/servers/cu/src/domain/lib/evaluate.test.js @@ -25,7 +25,7 @@ describe('evaluate', () => { ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/happy/process.wasm'), + module: readFileSync('./test/processes/happy/process.wasm'), buffer: null, messages: toAsyncIterable([ { @@ -142,7 +142,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/happy/process.wasm'), + module: readFileSync('./test/processes/happy/process.wasm'), buffer: null, messages: toAsyncIterable([ { @@ -197,7 +197,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/happy/process.wasm'), + module: readFileSync('./test/processes/happy/process.wasm'), buffer: null, messages: toAsyncIterable([ { @@ -255,7 +255,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/happy/process.wasm'), + module: readFileSync('./test/processes/happy/process.wasm'), /** * In reality this would be an illegible byte array, since it's format * will be determined by whatever the underlying runtime is, in this case, @@ -291,7 +291,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/sad/process.wasm'), + module: readFileSync('./test/processes/sad/process.wasm'), buffer: Buffer.from('Hello', 'utf-8'), messages: toAsyncIterable([ { @@ -338,7 +338,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/sad/process.wasm'), + module: readFileSync('./test/processes/sad/process.wasm'), buffer: Buffer.from('Hello', 'utf-8'), messages: toAsyncIterable([ { @@ -378,7 +378,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/sad/process.wasm'), + module: readFileSync('./test/processes/sad/process.wasm'), buffer: Buffer.from('Hello', 'utf-8'), messages: toAsyncIterable([ { @@ -417,7 +417,7 @@ describe('evaluate', () => { const ctx = { id: 'ctr-1234', from: 'sort-key-start', - src: readFileSync('./test/processes/sad/process.wasm'), + module: readFileSync('./test/processes/sad/process.wasm'), buffer: null, messages: toAsyncIterable([ { diff --git a/servers/cu/src/domain/lib/hydrateMessages.js b/servers/cu/src/domain/lib/hydrateMessages.js index 5cbc221e4..350b56d4a 100644 --- a/servers/cu/src/domain/lib/hydrateMessages.js +++ b/servers/cu/src/domain/lib/hydrateMessages.js @@ -7,7 +7,7 @@ import WarpArBundles from 'warp-arbundles' import { loadTransactionDataSchema, loadTransactionMetaSchema } from '../dal.js' import { streamSchema } from '../model.js' -import { findRawTag } from './utils.js' +import { findRawTag } from '../utils.js' const { createData } = WarpArBundles diff --git a/servers/cu/src/domain/lib/loadMessages.js b/servers/cu/src/domain/lib/loadMessages.js index f85c810b2..5004c8f2f 100644 --- a/servers/cu/src/domain/lib/loadMessages.js +++ b/servers/cu/src/domain/lib/loadMessages.js @@ -7,7 +7,7 @@ import ms from 'ms' import { messageSchema, streamSchema } from '../model.js' import { loadBlocksMetaSchema, loadMessagesSchema, loadTimestampSchema } from '../dal.js' -import { padBlockHeight } from './utils.js' +import { padBlockHeight } from '../utils.js' /** * - { name: 'Cron-Interval', value: 'interval' } diff --git a/servers/cu/src/domain/lib/loadMessages.test.js b/servers/cu/src/domain/lib/loadMessages.test.js index ce851c070..ef0ffe511 100644 --- a/servers/cu/src/domain/lib/loadMessages.test.js +++ b/servers/cu/src/domain/lib/loadMessages.test.js @@ -5,8 +5,9 @@ import * as assert from 'node:assert' import ms from 'ms' import { countBy, prop, uniqBy } from 'ramda' +import { padBlockHeight } from '../utils.js' + import { CRON_INTERVAL, parseCrons, isBlockOnSchedule, isTimestampOnSchedule, scheduleMessagesBetweenWith } from './loadMessages.js' -import { padBlockHeight } from './utils.js' describe('loadMessages', () => { describe('parseCrons', () => { diff --git a/servers/cu/src/domain/lib/loadSource.js b/servers/cu/src/domain/lib/loadModule.js similarity index 57% rename from servers/cu/src/domain/lib/loadSource.js rename to servers/cu/src/domain/lib/loadModule.js index b0e4193a6..ca6c3e671 100644 --- a/servers/cu/src/domain/lib/loadSource.js +++ b/servers/cu/src/domain/lib/loadModule.js @@ -3,7 +3,7 @@ import { mergeRight, prop } from 'ramda' import { z } from 'zod' import { loadTransactionDataSchema } from '../dal.js' -import { parseTags } from './utils.js' +import { parseTags } from '../utils.js' /** * The result that is produced from this step @@ -13,26 +13,26 @@ import { parseTags } from './utils.js' * is always added to context */ const ctxSchema = z.object({ - src: z.any().refine((val) => !!val, { - message: 'process src must be attached to context' + module: z.any().refine((val) => !!val, { + message: 'process module must be attached to context' }), - srcId: z.string().refine((val) => !!val, { - message: 'process srcId must be attached to context' + moduleId: z.string().refine((val) => !!val, { + message: 'process moduleId must be attached to context' }) }).passthrough() -function getSourceBufferWith ({ loadTransactionData }) { +function getModuleBufferWith ({ loadTransactionData }) { loadTransactionData = fromPromise(loadTransactionDataSchema.implement(loadTransactionData)) return (tags) => { return of(tags) .map(parseTags) - .map(prop('Contract-Src')) - .chain(srcId => - of(srcId) + .map(prop('Module')) + .chain(moduleId => + of(moduleId) .chain(loadTransactionData) .chain(fromPromise((res) => res.arrayBuffer())) - .map(src => ({ src, srcId })) + .map(module => ({ module, moduleId })) ) } } @@ -42,25 +42,25 @@ function getSourceBufferWith ({ loadTransactionData }) { * @property {string} id - the id of the process * * @typedef Result - * @property {string} srcId - the id of the process source - * @property {ArrayBuffer} src - an array buffer that contains the Contract Wasm Src + * @property {string} moduleId - the id of the process source + * @property {ArrayBuffer} module - an array buffer that contains the Contract Wasm Src * - * @callback LoadSource + * @callback LoadModule * @param {Args} args * @returns {Async} * * @param {any} env - * @returns {LoadSource} + * @returns {LoadModule} */ -export function loadSourceWith (env) { - const logger = env.logger.child('loadSource') +export function loadModuleWith (env) { + const logger = env.logger.child('loadModule') env = { ...env, logger } - const getSourceBuffer = getSourceBufferWith(env) + const getModuleBuffer = getModuleBufferWith(env) return (ctx) => { return of(ctx.tags) - .chain(getSourceBuffer) + .chain(getModuleBuffer) .map(mergeRight(ctx)) .map(ctxSchema.parse) .map(ctx => { diff --git a/servers/cu/src/domain/lib/loadSource.test.js b/servers/cu/src/domain/lib/loadModule.test.js similarity index 50% rename from servers/cu/src/domain/lib/loadSource.test.js rename to servers/cu/src/domain/lib/loadModule.test.js index 00746f630..9cf3ae874 100644 --- a/servers/cu/src/domain/lib/loadSource.test.js +++ b/servers/cu/src/domain/lib/loadModule.test.js @@ -2,22 +2,22 @@ import { describe, test } from 'node:test' import * as assert from 'node:assert' import { createLogger } from '../logger.js' -import { loadSourceWith } from './loadSource.js' +import { loadModuleWith } from './loadModule.js' const PROCESS = 'contract-123-9HdeqeuYQOgMgWucro' const logger = createLogger('ao-cu:readState') -describe('loadSource', () => { - test('append process source and process source id', async () => { - const loadSource = loadSourceWith({ +describe('loadModule', () => { + test('append module and module id', async () => { + const loadModule = loadModuleWith({ loadTransactionData: async (_id) => new Response(JSON.stringify({ hello: 'world' })), logger }) - const result = await loadSource({ id: PROCESS, tags: [{ name: 'Contract-Src', value: 'foobar' }] }).toPromise() - assert.equal(result.src.byteLength, 17) - assert.equal(result.srcId, 'foobar') + const result = await loadModule({ id: PROCESS, tags: [{ name: 'Module', value: 'foobar' }] }).toPromise() + assert.equal(result.module.byteLength, 17) + assert.equal(result.moduleId, 'foobar') assert.equal(result.id, PROCESS) }) }) diff --git a/servers/cu/src/domain/lib/loadProcess.js b/servers/cu/src/domain/lib/loadProcess.js index 1b670ab84..1f4a06984 100644 --- a/servers/cu/src/domain/lib/loadProcess.js +++ b/servers/cu/src/domain/lib/loadProcess.js @@ -1,19 +1,19 @@ import { Rejected, Resolved, fromPromise, of } from 'hyper-async' -import { F, T, always, cond, equals, includes, is, isNotNil, mergeRight, omit } from 'ramda' +import { always, isNotNil, mergeRight, omit } from 'ramda' import { z } from 'zod' import { findLatestEvaluationSchema, findProcessSchema, loadProcessSchema, saveProcessSchema } from '../dal.js' import { rawBlockSchema, rawTagSchema } from '../model.js' -import { parseTags } from './utils.js' +import { eqOrIncludes, parseTags } from '../utils.js' function getProcessMetaWith ({ loadProcess, findProcess, saveProcess, logger }) { findProcess = fromPromise(findProcessSchema.implement(findProcess)) saveProcess = fromPromise(saveProcessSchema.implement(saveProcess)) loadProcess = fromPromise(loadProcessSchema.implement(loadProcess)) - const checkTag = (name, pred) => (tags) => pred(tags[name]) + const checkTag = (name, pred, err) => tags => pred(tags[name]) ? Resolved(tags) - : Rejected(`Tag '${name}' of value '${tags[name]}' was not valid on transaction`) + : Rejected(`Tag '${name}': ${err}`) /** * Load the process from the SU, extracting the metadata, @@ -27,17 +27,9 @@ function getProcessMetaWith ({ loadProcess, findProcess, saveProcess, logger }) .chain(ctx => of(ctx.tags) .map(parseTags) - /** - * The process could implement multiple Data-Protocols, - * so check in the case of a single value or an array of values - */ - .chain(checkTag('Data-Protocol', cond([ - [is(String), equals('ao')], - [is(Array), includes('ao')], - [T, F] - ]))) - .chain(checkTag('ao-type', equals('process'))) - .chain(checkTag('Contract-Src', isNotNil)) + .chain(checkTag('Data-Protocol', eqOrIncludes('ao'), 'value \'ao\' was not found on process')) + .chain(checkTag('Type', eqOrIncludes('Process'), 'value \'Process\' was not found on process')) + .chain(checkTag('Module', isNotNil, 'was not found on process')) .map(always({ id: processId, ...ctx })) .bimap( logger.tap('Verifying process failed: %s'), @@ -175,10 +167,10 @@ const ctxSchema = z.object({ /** * @typedef Args - * @property {string} id - the id of the contract + * @property {string} id - the id of the process * * @typedef Result - * @property {string} id - the id of the contract + * @property {string} id - the id of the process * @property {string} owner * @property {any} tags * @property {{ height: number, timestamp: number }} block diff --git a/servers/cu/src/domain/lib/loadProcess.test.js b/servers/cu/src/domain/lib/loadProcess.test.js index f3b3c58d1..9e404524e 100644 --- a/servers/cu/src/domain/lib/loadProcess.test.js +++ b/servers/cu/src/domain/lib/loadProcess.test.js @@ -12,9 +12,9 @@ const logger = createLogger('ao-cu:readState') describe('loadProcess', () => { test('appends process owner, tags, block, buffer as process tags parsed as JSON, result, from, and evaluatedAt to ctx', async () => { const tags = [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'process' }, + { name: 'Type', value: 'Process' }, { name: 'inbox', value: JSON.stringify([]) }, { name: 'balances', value: JSON.stringify({ 'myOVEwyX7QKFaPkXo3Wlib-Q80MOf5xyjL9ZyvYSVYc': 1000 }) } ] @@ -50,9 +50,9 @@ describe('loadProcess', () => { test('use process from db to set owner, tags, and block', async () => { const tags = [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'process' }, + { name: 'Type', value: 'Process' }, { name: 'Foo', value: 'Bar' } ] const loadProcess = loadProcessWith({ @@ -101,9 +101,9 @@ describe('loadProcess', () => { } const tags = [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'process' }, + { name: 'Type', value: 'Process' }, { name: 'Foo', value: 'Bar' } ] const loadProcess = loadProcessWith({ @@ -132,9 +132,9 @@ describe('loadProcess', () => { test('save process to db if fetched from chain', async () => { const tags = [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'process' }, + { name: 'Type', value: 'Process' }, { name: 'Foo', value: 'Bar' } ] const loadProcess = loadProcessWith({ @@ -162,9 +162,9 @@ describe('loadProcess', () => { test('gracefully handled failure to save to db', async () => { const tags = [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'process' }, + { name: 'Type', value: 'Process' }, { name: 'Foo', value: 'Bar' } ] const loadProcess = loadProcessWith({ @@ -186,7 +186,7 @@ describe('loadProcess', () => { assert.equal(res.id, PROCESS) }) - test('throw if the Contract-Src tag is not provided', async () => { + test('throw if the Module tag is not provided', async () => { const loadProcess = loadProcessWith({ findProcess: async () => { throw { status: 404 } }, saveProcess: async () => PROCESS, @@ -194,9 +194,9 @@ describe('loadProcess', () => { loadProcess: async (id) => ({ owner: 'woohoo', tags: [ - { name: 'Not-Contract-Src', value: 'foobar' }, + { name: 'Not_Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'process' } + { name: 'Type', value: 'Process' } ], block: { height: 123, timestamp: 1697574792000 } }), @@ -205,7 +205,7 @@ describe('loadProcess', () => { await loadProcess({ id: PROCESS }).toPromise() .then(() => assert.fail('unreachable. Should have thrown')) - .catch(err => assert.equal(err, "Tag 'Contract-Src' of value 'undefined' was not valid on transaction")) + .catch(err => assert.equal(err, "Tag 'Module': was not found on process")) }) test('throw if the Data-Protocol tag is not "ao"', async () => { @@ -216,9 +216,9 @@ describe('loadProcess', () => { loadProcess: async (id) => ({ owner: 'woohoo', tags: [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'not_ao' }, - { name: 'ao-type', value: 'process' } + { name: 'Type', value: 'Process' } ], block: { height: 123, timestamp: 1697574792000 } }), @@ -227,10 +227,10 @@ describe('loadProcess', () => { await loadProcess({ id: PROCESS }).toPromise() .then(() => assert.fail('unreachable. Should have thrown')) - .catch(err => assert.equal(err, "Tag 'Data-Protocol' of value 'not_ao' was not valid on transaction")) + .catch(err => assert.equal(err, "Tag 'Data-Protocol': value 'ao' was not found on process")) }) - test('throw if the ao-type tag is not "process"', async () => { + test('throw if the Type tag is not "Process"', async () => { const loadProcess = loadProcessWith({ findProcess: async () => { throw { status: 404 } }, saveProcess: async () => PROCESS, @@ -238,9 +238,9 @@ describe('loadProcess', () => { loadProcess: async (id) => ({ owner: 'woohoo', tags: [ - { name: 'Contract-Src', value: 'foobar' }, + { name: 'Module', value: 'foobar' }, { name: 'Data-Protocol', value: 'ao' }, - { name: 'ao-type', value: 'message' } + { name: 'Type', value: 'Not_process' } ], block: { height: 123, timestamp: 1697574792000 } }), @@ -249,6 +249,6 @@ describe('loadProcess', () => { await loadProcess({ id: PROCESS }).toPromise() .then(() => assert.fail('unreachable. Should have thrown')) - .catch(err => assert.equal(err, "Tag 'ao-type' of value 'message' was not valid on transaction")) + .catch(err => assert.equal(err, "Tag 'Type': value 'Process' was not found on process")) }) }) diff --git a/servers/cu/src/domain/lib/utils.js b/servers/cu/src/domain/lib/utils.js deleted file mode 100644 index dd0d77dea..000000000 --- a/servers/cu/src/domain/lib/utils.js +++ /dev/null @@ -1,60 +0,0 @@ -import { __, append, assoc, defaultTo, filter, head, map, pipe, propOr, reduce } from 'ramda' - -/** -* Parse tags into a object with key-value pairs of name -> values. -* -* If multiple tags with the same name exist, it's value will be the array of tag values -* in order of appearance -*/ -export function parseTags (rawTags) { - return pipe( - defaultTo([]), - reduce( - (map, tag) => pipe( - // [value, value, ...] || [] - propOr([], tag.name), - // [value] - append(tag.value), - // { [name]: [value, value, ...] } - assoc(tag.name, __, map) - )(map), - {} - ), - /** - * If the field is only a singly list, then extract the one value. - * - * Otherwise, keep the value as a list. - */ - map((values) => values.length > 1 ? values : values[0]) - )(rawTags) -} - -export function findRawTag (name, tags) { - return pipe( - defaultTo([]), - filter(tag => tag.name === name), - /** - * TODO: what if multiple tags with same name? - * For now, just grabbing the first one - */ - head - )(tags) -} - -/** -* Pad the block height portion of the sortKey to 12 characters -* -* This should work to increment and properly pad any sort key: -* - 000001257294,1694181441598,fb1ebd7d621d1398acc03e108b7a593c6960c6e522772c974cd21c2ba7ac11d5 (full Sequencer sort key) -* - 000001257294,fb1ebd7d621d1398acc03e108b7a593c6960c6e522772c974cd21c2ba7ac11d5 (Smartweave protocol sort key) -* - 1257294,1694181441598,fb1ebd7d621d1398acc03e108b7a593c6960c6e522772c974cd21c2ba7ac11d5 (missing padding) -* - 1257294 (just block height) -* -* @param {string} sortKey - the sortKey to be padded. If the sortKey is of sufficient length, then no padding -* is added. -*/ -export function padBlockHeight (sortKey) { - if (!sortKey) return sortKey - const [height, ...rest] = String(sortKey).split(',') - return [height.padStart(12, '0'), ...rest].join(',') -} diff --git a/servers/cu/src/domain/readState.js b/servers/cu/src/domain/readState.js index aa6ee4daf..b54dcd988 100644 --- a/servers/cu/src/domain/readState.js +++ b/servers/cu/src/domain/readState.js @@ -1,7 +1,7 @@ import { Resolved, of } from 'hyper-async' import { loadProcessWith } from './lib/loadProcess.js' -import { loadSourceWith } from './lib/loadSource.js' +import { loadModuleWith } from './lib/loadModule.js' import { loadMessagesWith } from './lib/loadMessages.js' import { evaluateWith } from './lib/evaluate.js' import { hydrateMessagesWith } from './lib/hydrateMessages.js' @@ -25,7 +25,7 @@ export function readStateWith (env) { const loadProcess = loadProcessWith(env) const loadMessages = loadMessagesWith(env) const hydrateMessages = hydrateMessagesWith(env) - const loadSource = loadSourceWith(env) + const loadModule = loadModuleWith(env) const evaluate = evaluateWith(env) return ({ processId, to }) => { @@ -44,7 +44,7 @@ export function readStateWith (env) { return of(res) .chain(loadMessages) .chain(hydrateMessages) - .chain(loadSource) + .chain(loadModule) .chain(evaluate) .map((ctx) => ctx.output) }) diff --git a/servers/cu/src/domain/utils.js b/servers/cu/src/domain/utils.js index b893697dd..9402ec47a 100644 --- a/servers/cu/src/domain/utils.js +++ b/servers/cu/src/domain/utils.js @@ -1,6 +1,6 @@ import { - T, always, chain, concat, cond, equals, - has, identity, is, join, pipe, prop, reduce + F, T, __, always, append, assoc, chain, concat, cond, defaultTo, equals, + filter, has, head, identity, includes, is, join, map, pipe, prop, propOr, reduce } from 'ramda' import { ZodError, ZodIssueCode } from 'zod' @@ -82,3 +82,70 @@ function mapZodErr (zodErr) { join(' | ') )(zodErr) } + +/** +* Parse tags into a object with key-value pairs of name -> values. +* +* If multiple tags with the same name exist, it's value will be the array of tag values +* in order of appearance +*/ +export function parseTags (rawTags) { + return pipe( + defaultTo([]), + reduce( + (map, tag) => pipe( + // [value, value, ...] || [] + propOr([], tag.name), + // [value] + append(tag.value), + // { [name]: [value, value, ...] } + assoc(tag.name, __, map) + )(map), + {} + ), + /** + * If the field is only a singly list, then extract the one value. + * + * Otherwise, keep the value as a list. + */ + map((values) => values.length > 1 ? values : values[0]) + )(rawTags) +} + +export function eqOrIncludes (val) { + return cond([ + [is(String), equals(val)], + [is(Array), includes(val)], + [T, F] + ]) +} + +export function findRawTag (name, tags) { + return pipe( + defaultTo([]), + filter(tag => tag.name === name), + /** + * TODO: what if multiple tags with same name? + * For now, just grabbing the first one + */ + head + )(tags) +} + +/** +* Pad the block height portion of the sortKey to 12 characters +* +* This should work to increment and properly pad any sort key: +* - 000001257294,1694181441598,fb1ebd7d621d1398acc03e108b7a593c6960c6e522772c974cd21c2ba7ac11d5 (full Sequencer sort key) +* - 000001257294,fb1ebd7d621d1398acc03e108b7a593c6960c6e522772c974cd21c2ba7ac11d5 (Smartweave protocol sort key) +* - 1257294,1694181441598,fb1ebd7d621d1398acc03e108b7a593c6960c6e522772c974cd21c2ba7ac11d5 (missing padding) +* - 1257294 (just block height) +* +* @param {string} sortKey - the sortKey to be padded. If the sortKey is of sufficient length, then no padding +* is added. +*/ +export function padBlockHeight (sortKey) { + if (!sortKey) return sortKey + const [height, ...rest] = String(sortKey).split(',') + return [height.padStart(12, '0'), ...rest].join(',') +}