Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sdk: set proper tags on spawn and verify module tags and input #215

Merged
merged 4 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<!-- toc -->

Expand Down Expand Up @@ -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,
Expand All @@ -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({
srcId,
module,
scheduler,
signer: createDataItemSigner(wallet),
tags,
});
Expand All @@ -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";
Expand Down
6 changes: 3 additions & 3 deletions sdk/src/lib/message/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions sdk/src/lib/spawn/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { uploadProcessWith } from './upload-process.js'
* @typedef Env1
*
* @typedef SpawnProcessArgs
* @property {string} srcId
* @property {string} module
* @property {string} scheduler
* @property {string} signer
* @property {{ name: string, value: string }[]} [tags]
* @property {string | ArrayBuffer} [data]
*
* @callback SpawnProcess
* @param {SpawnProcessArgs} args
Expand All @@ -24,8 +26,8 @@ export function spawnWith (env) {
const verifyInputs = verifyInputsWith(env)
const uploadProcess = uploadProcessWith(env)

return ({ srcId, signer, tags }) => {
return of({ srcId, signer, tags })
return ({ module, scheduler, signer, tags, data }) => {
return of({ module, scheduler, signer, tags, data })
.chain(verifyInputs)
.chain(uploadProcess)
.map((ctx) => ctx.processId)
Expand Down
61 changes: 38 additions & 23 deletions sdk/src/lib/spawn/upload-process.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fromPromise, of } from 'hyper-async'
import { fromPromise, of, Resolved } from 'hyper-async'
import { z } from 'zod'
import { __, assoc, concat, defaultTo, propEq, reject } from 'ramda'
import { __, always, append, assoc, concat, defaultTo, ifElse, pipe, prop, propEq, reject } from 'ramda'

import { deployProcessSchema, signerSchema } from '../../dal.js'

Expand All @@ -15,33 +15,25 @@ const tagSchema = z.array(z.object({
* @property {any} value
*
* @typedef Context3
* @property {string} srcId - 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]
*
* @typedef Env6
* @property {any} upload
*/

function buildTagsWith () {
function removeTagsByName (name) {
return (tags) => reject(propEq(name, 'name'), tags)
}

return (ctx) => {
return of(ctx.tags)
return of(ctx)
.map(prop('tags'))
.map(defaultTo([]))
/**
* Remove any reserved tags, so that the sdk
* can properly set them
*/
.map(removeTagsByName('ao-type'))
.map(removeTagsByName('Contract-Src'))
.map(concat(__, [
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'ao-type', value: 'process' },
{ name: 'Contract-Src', value: ctx.srcId },
{ name: 'Content-Type', value: 'text/plain' },
{ name: 'Type', value: 'Process' },
{ name: 'Module', value: ctx.module },
{ name: 'Scheduler', value: ctx.scheduler },
{ name: 'SDK', value: 'ao' }
]))
.map(tagSchema.parse)
Expand All @@ -50,14 +42,37 @@ function buildTagsWith () {
}

function buildDataWith ({ logger }) {
function removeTagsByName (name) {
return (tags) => reject(propEq(name, 'name'), tags)
}

return (ctx) => {
return of(ctx)
/**
* The data does not matter, so we just generate a random value
*/
.map(() => Math.random().toString().slice(-4))
.map(assoc('data', __, ctx))
.map(logger.tap('added pseudo-random data as payload for contract at "data"'))
.chain(ifElse(
always(ctx.data),
/**
* data is provided as input, so do nothing
*/
() => Resolved(ctx),
/**
* Just generate a random value for data
*/
() => 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"'))
))
}
}

Expand Down
58 changes: 36 additions & 22 deletions sdk/src/lib/spawn/upload-process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ describe('upload-process', () => {
assert.deepStrictEqual(tags, [
{ name: 'foo', value: 'bar' },
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'ao-type', value: 'process' },
{ name: 'Contract-Src', value: 'src-id-123' },
{ name: 'Content-Type', value: 'text/plain' },
{ name: 'SDK', value: 'ao' }
{ name: 'Type', value: 'Process' },
{ name: 'Module', value: 'module-id-123' },
{ name: 'Scheduler', value: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro' },
{ name: 'SDK', value: 'ao' },
{ name: 'Content-Type', value: 'text/plain' }
])
assert.ok(signer)

Expand All @@ -35,7 +36,8 @@ describe('upload-process', () => {
})

await uploadProcess({
srcId: 'src-id-123',
module: 'module-id-123',
scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro',
tags: [
{ name: 'foo', value: 'bar' }
],
Expand All @@ -49,10 +51,11 @@ describe('upload-process', () => {
deployProcess: async ({ tags }) => {
assert.deepStrictEqual(tags, [
{ name: 'Data-Protocol', value: 'ao' },
{ name: 'ao-type', value: 'process' },
{ name: 'Contract-Src', value: 'src-id-123' },
{ name: 'Content-Type', value: 'text/plain' },
{ name: 'SDK', value: 'ao' }
{ name: 'Type', value: 'Process' },
{ name: 'Module', value: 'module-id-123' },
{ name: 'Scheduler', value: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro' },
{ name: 'SDK', value: 'ao' },
{ name: 'Content-Type', value: 'text/plain' }
])

return { res: 'foobar', processId: 'process-id-123' }
Expand All @@ -62,35 +65,46 @@ describe('upload-process', () => {
})

await uploadProcess({
srcId: 'src-id-123',
module: 'module-id-123',
scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro',
signer: async () => ({ id: 'process-id-123', raw: 'raw-buffer' })
}).toPromise()
})

test('deduplicates identifying tags', async () => {
test('does not overwrite data', async () => {
const uploadProcess = uploadProcessWith({
deployProcess: async ({ tags }) => {
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: 'ao-type', value: 'process' },
{ name: 'Contract-Src', value: 'src-id-123' },
{ name: 'Content-Type', value: 'text/plain' },
{ 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' }
return {
res: 'foobar',
processId: 'process-id-123',
signedDataItem: { id: 'process-id-123', raw: 'raw-buffer' }
}
},
registerProcess: async () => {
return { foo: 'bar' }
},
registerProcess: async () => ({ foo: 'bar' }),
logger
})

await uploadProcess({
srcId: 'src-id-123',
module: 'module-id-123',
scheduler: 'zVkjFCALjk4xxuCilddKS8ShZ-9HdeqeuYQOgMgWucro',
tags: [
{ name: 'ao-type', value: 'process' },
{ name: 'ao-type', value: 'process' },
{ name: 'Contract-Src', value: 'oops-duplicate' }
{ name: 'foo', value: 'bar' }
],
data: 'foobar',
signer: async () => ({ id: 'process-id-123', raw: 'raw-buffer' })
}).toPromise()
})
Expand Down
57 changes: 42 additions & 15 deletions sdk/src/lib/spawn/verify-inputs.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Rejected, Resolved, fromPromise, of } from 'hyper-async'
import { equals, prop } from 'ramda'
import { ifElse, isNotNil, prop } from 'ramda'

import { loadTransactionMetaSchema } from '../../dal.js'
import { parseTags } from '../utils.js'
import { eqOrIncludes, parseTags } from '../utils.js'

const checkTag = (name, pred, err) => tags => pred(tags[name])
? Resolved(tags)
: Rejected(`Tag '${name}': ${err}`)

/**
* @typedef Tag
Expand All @@ -18,20 +22,41 @@ import { parseTags } from '../utils.js'
* @property {any} logger
*/

function verifySourceWith ({ loadTransactionMeta, logger }) {
const checkTag = (name, pred) => tags => pred(tags[name])
? Resolved(tags)
: Rejected(`Tag '${name}' of value '${tags[name]}' was not valid on contract source`)

return (srcId) => of(srcId)
function verifyModuleWith ({ loadTransactionMeta, logger }) {
return (module) => of(module)
.chain(fromPromise(loadTransactionMetaSchema.implement(loadTransactionMeta)))
.map(prop('tags'))
.map(parseTags)
.chain(checkTag('Content-Type', equals('application/wasm')))
.chain(checkTag('Contract-Type', equals('ao')))
/**
* 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'))
.chain(checkTag('Module-Format', isNotNil, 'was not found on module'))
.chain(checkTag('Input-Encoding', isNotNil, 'was not found on module'))
.chain(checkTag('Output-Encoding', isNotNil, 'was not found on module'))
.bimap(
logger.tap('Verifying module source failed: %s'),
logger.tap('Verified module source')
)
}

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 contract source failed: %s'),
logger.tap('Verified contract source')
logger.tap('Verifying scheduler failed: %s'),
logger.tap('Verified scheduler')
)
}

Expand All @@ -43,7 +68,7 @@ function verifySignerWith ({ logger }) {

/**
* @typedef Context
* @property {string} srcId - the id of the contract 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
*
Expand All @@ -63,12 +88,14 @@ export function verifyInputsWith (env) {
const logger = env.logger.child('verifyInput')
env = { ...env, logger }

const verifySource = verifySourceWith(env)
const verifyModule = verifyModuleWith(env)
const verifyScheduler = verifySchedulerWith(env)
const verifySigner = verifySignerWith(env)

return (ctx) => {
return of(ctx)
.chain(ctx => verifySource(ctx.srcId).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'),
Expand Down
Loading