From fc9f22e43f1da0a5f214bc5951f2babf25b30aef Mon Sep 17 00:00:00 2001 From: Tyler Hall Date: Tue, 12 Dec 2023 17:57:12 +0000 Subject: [PATCH] feat(sdk)!: verify presence of scheduler tag on spawn. remove id suffix from inputs #210 #212 --- sdk/README.md | 11 +++-- sdk/src/lib/message/index.js | 6 +-- sdk/src/lib/spawn/index.js | 7 +-- sdk/src/lib/spawn/upload-process.js | 27 +++++++++-- sdk/src/lib/spawn/upload-process.test.js | 30 +++++++++--- sdk/src/lib/spawn/verify-inputs.js | 39 +++++++++++---- sdk/src/lib/spawn/verify-inputs.test.js | 62 ++++++++++++++++++------ 7 files changed, 136 insertions(+), 46 deletions(-) diff --git a/sdk/README.md b/sdk/README.md index 105f9e084..8ce1a04b2 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -7,7 +7,7 @@ This sdk will run in a browser or server environment. - Read the result of an `ao` Message evaluation from a `ao` Compute Unit `cu` - Send a Message targeting an `ao` Process to an `ao` Message Unit `mu` -- Spawn an `ao` Process on an `ao` Sequencer Unit `su` +- Spawn an `ao` Process, assigning it to an `ao` Scheduler Unit `su` @@ -66,7 +66,7 @@ send a message to an `ao` Message Unit `mu` targeting an ao `process`. import { createDataItemSigner, message } from "@permaweb/ao-sdk"; const messageId = await message({ - processId, + process, signer: createDataItemSigner(wallet), anchor, tags, @@ -78,13 +78,14 @@ const messageId = await message({ #### `spawn` -Spawn an `ao` process +Spawn an `ao` process, assigning the `ao` Scheduler to schedule its messages ```js import { createDataItemSigner, spawn } from "@permaweb/ao-sdk"; const processId = await spawn({ - moduleId, + module, + scheduler, signer: createDataItemSigner(wallet), tags, }); @@ -98,7 +99,7 @@ specify those components by providing their urls to `connect`. You can currently - The GATEWAY_URL - The Messenger Unit URL - The Compute Unit URL -- The Sequencer Unit URL +- The Scheduler Unit URL ```js import { connect } from "@permaweb/ao-sdk"; diff --git a/sdk/src/lib/message/index.js b/sdk/src/lib/message/index.js index 85dfdfb8b..5d43539fc 100644 --- a/sdk/src/lib/message/index.js +++ b/sdk/src/lib/message/index.js @@ -9,7 +9,7 @@ import { uploadMessageWith } from './upload-message.js' * @typedef Env1 * * @typedef SendMessageArgs - * @property {string} processId + * @property {string} process * @property {string} [anchor] * @property {{ name: string, value: string }[]} [tags] * @property {any} signer @@ -25,8 +25,8 @@ export function messageWith (env) { const verifyProcess = verifyProcessWith(env) const uploadMessage = uploadMessageWith(env) - return ({ processId, tags, anchor, signer }) => { - return of({ id: processId, tags, anchor, signer }) + return ({ process, tags, anchor, signer }) => { + return of({ id: process, tags, anchor, signer }) .chain(verifyProcess) .chain(uploadMessage) .map((ctx) => ctx.messageId) // the id of the data item diff --git a/sdk/src/lib/spawn/index.js b/sdk/src/lib/spawn/index.js index c8705c78a..033de9442 100644 --- a/sdk/src/lib/spawn/index.js +++ b/sdk/src/lib/spawn/index.js @@ -9,7 +9,8 @@ import { uploadProcessWith } from './upload-process.js' * @typedef Env1 * * @typedef SpawnProcessArgs - * @property {string} moduleId + * @property {string} module + * @property {string} scheduler * @property {string} signer * @property {{ name: string, value: string }[]} [tags] * @property {string | ArrayBuffer} [data] @@ -25,8 +26,8 @@ export function spawnWith (env) { const verifyInputs = verifyInputsWith(env) const uploadProcess = uploadProcessWith(env) - return ({ moduleId, signer, tags, data }) => { - return of({ moduleId, signer, tags, data }) + return ({ module, scheduler, signer, tags, data }) => { + return of({ module, scheduler, signer, tags, data }) .chain(verifyInputs) .chain(uploadProcess) .map((ctx) => ctx.processId) diff --git a/sdk/src/lib/spawn/upload-process.js b/sdk/src/lib/spawn/upload-process.js index 6b80af253..87a744449 100644 --- a/sdk/src/lib/spawn/upload-process.js +++ b/sdk/src/lib/spawn/upload-process.js @@ -1,6 +1,6 @@ import { fromPromise, of, Resolved } from 'hyper-async' import { z } from 'zod' -import { __, always, assoc, concat, defaultTo, ifElse } from 'ramda' +import { __, always, append, assoc, concat, defaultTo, ifElse, pipe, prop, propEq, reject } from 'ramda' import { deployProcessSchema, signerSchema } from '../../dal.js' @@ -15,7 +15,7 @@ const tagSchema = z.array(z.object({ * @property {any} value * * @typedef Context3 - * @property {string} moduleId - the id of the transactions that contains the xontract source + * @property {string} module - the id of the transactions that contains the xontract source * @property {any} initialState -the initialState of the contract * @property {Tag[]} tags * @property {string | ArrayBuffer} [data] @@ -26,13 +26,14 @@ const tagSchema = z.array(z.object({ function buildTagsWith () { return (ctx) => { - return of(ctx.tags) + return of(ctx) + .map(prop('tags')) .map(defaultTo([])) .map(concat(__, [ { name: 'Data-Protocol', value: 'ao' }, { name: 'Type', value: 'Process' }, - { name: 'Module', value: ctx.moduleId }, - { name: 'Content-Type', value: 'text/plain' }, + { name: 'Module', value: ctx.module }, + { name: 'Scheduler', value: ctx.scheduler }, { name: 'SDK', value: 'ao' } ])) .map(tagSchema.parse) @@ -41,6 +42,10 @@ function buildTagsWith () { } function buildDataWith ({ logger }) { + function removeTagsByName (name) { + return (tags) => reject(propEq(name, 'name'), tags) + } + return (ctx) => { return of(ctx) .chain(ifElse( @@ -54,6 +59,18 @@ function buildDataWith ({ logger }) { */ () => Resolved(Math.random().toString().slice(-4)) .map(assoc('data', __, ctx)) + /** + * Since we generate the data value, we know it's Content-Type, + * so set it on the tags + */ + .map( + (ctx) => pipe( + prop('tags'), + removeTagsByName('Content-Type'), + append({ name: 'Content-Type', value: 'text/plain' }), + assoc('tags', __, ctx) + )(ctx) + ) .map(logger.tap('added pseudo-random data as payload for contract at "data"')) )) } diff --git a/sdk/src/lib/spawn/upload-process.test.js b/sdk/src/lib/spawn/upload-process.test.js index ccd77a7c7..98106bff4 100644 --- a/sdk/src/lib/spawn/upload-process.test.js +++ b/sdk/src/lib/spawn/upload-process.test.js @@ -16,8 +16,9 @@ describe('upload-process', () => { { name: 'Data-Protocol', value: 'ao' }, { name: 'Type', value: 'Process' }, { name: 'Module', value: 'module-id-123' }, - { name: 'Content-Type', value: 'text/plain' }, - { name: 'SDK', value: 'ao' } + { name: 'Scheduler', value: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro' }, + { name: 'SDK', value: 'ao' }, + { name: 'Content-Type', value: 'text/plain' } ]) assert.ok(signer) @@ -35,7 +36,8 @@ describe('upload-process', () => { }) await uploadProcess({ - moduleId: 'module-id-123', + module: 'module-id-123', + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', tags: [ { name: 'foo', value: 'bar' } ], @@ -51,8 +53,9 @@ describe('upload-process', () => { { name: 'Data-Protocol', value: 'ao' }, { name: 'Type', value: 'Process' }, { name: 'Module', value: 'module-id-123' }, - { name: 'Content-Type', value: 'text/plain' }, - { name: 'SDK', value: 'ao' } + { name: 'Scheduler', value: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro' }, + { name: 'SDK', value: 'ao' }, + { name: 'Content-Type', value: 'text/plain' } ]) return { res: 'foobar', processId: 'process-id-123' } @@ -62,7 +65,8 @@ describe('upload-process', () => { }) await uploadProcess({ - moduleId: 'module-id-123', + module: 'module-id-123', + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: async () => ({ id: 'process-id-123', raw: 'raw-buffer' }) }).toPromise() }) @@ -71,6 +75,17 @@ describe('upload-process', () => { const uploadProcess = uploadProcessWith({ deployProcess: async ({ data, tags, signer }) => { assert.equal(data, 'foobar') + /** + * Assert no Content-Type tag is added + */ + assert.deepStrictEqual(tags, [ + { name: 'foo', value: 'bar' }, + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Type', value: 'Process' }, + { name: 'Module', value: 'module-id-123' }, + { name: 'Scheduler', value: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro' }, + { name: 'SDK', value: 'ao' } + ]) return { res: 'foobar', processId: 'process-id-123', @@ -84,7 +99,8 @@ describe('upload-process', () => { }) await uploadProcess({ - moduleId: 'module-id-123', + module: 'module-id-123', + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', tags: [ { name: 'foo', value: 'bar' } ], diff --git a/sdk/src/lib/spawn/verify-inputs.js b/sdk/src/lib/spawn/verify-inputs.js index ef7565285..7781761d1 100644 --- a/sdk/src/lib/spawn/verify-inputs.js +++ b/sdk/src/lib/spawn/verify-inputs.js @@ -1,9 +1,13 @@ import { Rejected, Resolved, fromPromise, of } from 'hyper-async' -import { isNotNil, prop } from 'ramda' +import { ifElse, isNotNil, prop } from 'ramda' import { loadTransactionMetaSchema } from '../../dal.js' import { eqOrIncludes, parseTags } from '../utils.js' +const checkTag = (name, pred, err) => tags => pred(tags[name]) + ? Resolved(tags) + : Rejected(`Tag '${name}': ${err}`) + /** * @typedef Tag * @property {string} name @@ -19,16 +23,12 @@ import { eqOrIncludes, parseTags } from '../utils.js' */ function verifyModuleWith ({ loadTransactionMeta, logger }) { - const checkTag = (name, pred, err) => tags => pred(tags[name]) - ? Resolved(tags) - : Rejected(`Tag '${name}': ${err}`) - - return (moduleId) => of(moduleId) + return (module) => of(module) .chain(fromPromise(loadTransactionMetaSchema.implement(loadTransactionMeta))) .map(prop('tags')) .map(parseTags) /** - * Ensure all tags required by the specification are set + * Ensure all Module tags required by the specification are set */ .chain(checkTag('Data-Protocol', eqOrIncludes('ao'), 'value \'ao\' was not found on module')) .chain(checkTag('Type', eqOrIncludes('Module'), 'value \'Module\' was not found on module')) @@ -41,6 +41,25 @@ function verifyModuleWith ({ loadTransactionMeta, logger }) { ) } +function verifySchedulerWith ({ logger }) { + return (scheduler) => of(scheduler) + /** + * TODO: actually fetch Schedule-Location record + * by owner and confirm that it is valid + */ + .chain( + ifElse( + isNotNil, + Resolved, + () => Rejected('scheduler not found') + ) + ) + .bimap( + logger.tap('Verifying scheduler failed: %s'), + logger.tap('Verified scheduler') + ) +} + function verifySignerWith ({ logger }) { return (signer) => of(signer) .map(logger.tap('Checking for signer')) @@ -49,7 +68,7 @@ function verifySignerWith ({ logger }) { /** * @typedef Context - * @property {string} moduleId - the id of the module source + * @property {string} module - the id of the module source * @property {Function} sign - the signer used to sign the process * @property {Tag[]} tags - the additional tags to add to the process * @@ -70,11 +89,13 @@ export function verifyInputsWith (env) { env = { ...env, logger } const verifyModule = verifyModuleWith(env) + const verifyScheduler = verifySchedulerWith(env) const verifySigner = verifySignerWith(env) return (ctx) => { return of(ctx) - .chain(ctx => verifyModule(ctx.moduleId).map(() => ctx)) + .chain(ctx => verifyModule(ctx.module).map(() => ctx)) + .chain(ctx => verifyScheduler(ctx.scheduler)).map(() => ctx) .chain(ctx => verifySigner(ctx.signer).map(() => ctx)) .bimap( logger.tap('Error when verify input: %s'), diff --git a/sdk/src/lib/spawn/verify-inputs.test.js b/sdk/src/lib/spawn/verify-inputs.test.js index a09513c19..c7831a25c 100644 --- a/sdk/src/lib/spawn/verify-inputs.test.js +++ b/sdk/src/lib/spawn/verify-inputs.test.js @@ -26,8 +26,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: () => {}, tags: [ { name: 'foo', value: 'bar' } @@ -49,8 +49,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: () => {}, tags: [ { name: 'foo', value: 'bar' } @@ -78,8 +78,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: () => {}, tags: [ { name: 'foo', value: 'bar' } @@ -107,8 +107,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: () => {}, tags: [ { name: 'foo', value: 'bar' } @@ -137,8 +137,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: () => {}, tags: [ { name: 'foo', value: 'bar' } @@ -168,8 +168,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: () => {}, tags: [ { name: 'foo', value: 'bar' } @@ -202,8 +202,8 @@ describe('verify-input', () => { }) await verifyInput({ - moduleId: MODULE, - initialState: { balances: { foo: 1 } }, + module: MODULE, + scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro', signer: undefined, tags: [ { name: 'foo', value: 'bar' } @@ -218,4 +218,38 @@ describe('verify-input', () => { ) }) }) + + test('throw if scheduler is not found', async () => { + const verifyInput = verifyInputsWith({ + loadTransactionMeta: async (_id) => + ({ + tags: [ + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Data-Protocol', value: 'Data-Protocol' }, + { name: 'Type', value: 'Module' }, + { name: 'Module-Format', value: 'emscripten' }, + { name: 'Input-Encoding', value: 'JSON-1' }, + { name: 'Output-Encoding', value: 'JSON-1' } + ] + }), + logger + }) + + await verifyInput({ + module: MODULE, + scheduler: undefined, + signer: () => {}, + tags: [ + { name: 'foo', value: 'bar' } + ], + logger + }).toPromise() + .then(assert.fail) + .catch(err => { + assert.equal( + err, + 'scheduler not found' + ) + }) + }) })