From 357d453db713c4d8b5b14ecbc1f7c502e732cc7e Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 15 Nov 2023 14:56:03 +0300 Subject: [PATCH 1/4] feat: get receipt support in client --- packages/w3up-client/package.json | 1 + packages/w3up-client/src/base.js | 4 ++- packages/w3up-client/src/client.js | 25 +++++++++++++++ packages/w3up-client/src/service.js | 2 ++ packages/w3up-client/src/types.ts | 4 +++ packages/w3up-client/test/client.test.js | 23 +++++++++++++- .../w3up-client/test/fixtures/workflow.car | Bin 0 -> 2796 bytes .../test/helpers/receipts-server.js | 30 ++++++++++++++++++ 8 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/w3up-client/test/fixtures/workflow.car create mode 100644 packages/w3up-client/test/helpers/receipts-server.js diff --git a/packages/w3up-client/package.json b/packages/w3up-client/package.json index f59103abc..3fe0b96be 100644 --- a/packages/w3up-client/package.json +++ b/packages/w3up-client/package.json @@ -89,6 +89,7 @@ "test:browser": "playwright-test --runner mocha 'test/**/!(*.node).test.js'", "mock": "run-p mock:*", "mock:bucket-200": "PORT=9200 STATUS=200 node test/helpers/bucket-server.js", + "mock:receipts-server": "PORT=9201 node test/helpers/receipts-server.js", "rc": "npm version prerelease --preid rc", "docs": "npm run build && typedoc --out docs-generated", "docs:markdown": "npm run build && docusaurus generate-typedoc" diff --git a/packages/w3up-client/src/base.js b/packages/w3up-client/src/base.js index e49b0ffa4..3d820a8d2 100644 --- a/packages/w3up-client/src/base.js +++ b/packages/w3up-client/src/base.js @@ -1,5 +1,5 @@ import { Agent } from '@web3-storage/access/agent' -import { serviceConf } from './service.js' +import { serviceConf, receiptsEndpoint } from './service.js' export class Base { /** @@ -18,6 +18,7 @@ export class Base { * @param {import('@web3-storage/access').AgentData} agentData * @param {object} [options] * @param {import('./types.js').ServiceConf} [options.serviceConf] + * @param {URL} [options.receiptsEndpoint] */ constructor(agentData, options = {}) { this._serviceConf = options.serviceConf ?? serviceConf @@ -27,6 +28,7 @@ export class Base { url: this._serviceConf.access.channel.url, connection: this._serviceConf.access, }) + this._receiptsEndpoint = options.receiptsEndpoint ?? receiptsEndpoint } /** diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 577da1563..e7dcc25f7 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -7,6 +7,7 @@ import { Store as StoreCapabilities, Upload as UploadCapabilities, } from '@web3-storage/capabilities' +import { CAR } from '@ucanto/transport' import { Base } from './base.js' import * as Account from './account.js' import { Space } from './space.js' @@ -36,6 +37,7 @@ export class Client extends Base { * @param {import('@web3-storage/access').AgentData} agentData * @param {object} [options] * @param {import('./types.js').ServiceConf} [options.serviceConf] + * @param {URL} [options.receiptsEndpoint] */ constructor(agentData, options) { super(agentData, options) @@ -142,6 +144,29 @@ export class Client extends Base { return uploadCAR(conf, car, options) } + /** + * Get a receipt for an executed task by its CID. + * + * @param {import('multiformats').UnknownLink} taskCid + */ + async getReceipt(taskCid) { + // Fetch receipt from endpoint + const workflowResponse = await fetch( + new URL(taskCid.toString(), this._receiptsEndpoint) + ) + // Get receipt from Message Archive + const agentMessageBytes = new Uint8Array( + await workflowResponse.arrayBuffer() + ) + // Decode message + const agentMessage = await CAR.request.decode({ + body: agentMessageBytes, + headers: {}, + }) + // Get receipt from the potential multiple receipts in the message + return agentMessage.receipts.get(taskCid.toString()) + } + /** * Return the default provider. */ diff --git a/packages/w3up-client/src/service.js b/packages/w3up-client/src/service.js index 20bb09365..896579da9 100644 --- a/packages/w3up-client/src/service.js +++ b/packages/w3up-client/src/service.js @@ -44,3 +44,5 @@ export const serviceConf = { upload: uploadServiceConnection, filecoin: filecoinServiceConnection, } + +export const receiptsEndpoint = 'https://up.web3.storage/receipt/' diff --git a/packages/w3up-client/src/types.ts b/packages/w3up-client/src/types.ts index c2e8bbe28..650bd3585 100644 --- a/packages/w3up-client/src/types.ts +++ b/packages/w3up-client/src/types.ts @@ -47,6 +47,10 @@ export interface ClientFactoryOptions { * here an error will be thrown. */ principal?: Signer> + /** + * URL configuration of endpoint where receipts from UCAN Log can be read from. + */ + receiptsEndpoint?: URL } export type ClientFactory = (options?: ClientFactoryOptions) => Promise diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 6574049bd..a7b645a3d 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -1,5 +1,10 @@ import assert from 'assert' -import { Delegation, create as createServer, provide } from '@ucanto/server' +import { + Delegation, + create as createServer, + parseLink, + provide, +} from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import * as StoreCapabilities from '@web3-storage/capabilities/store' @@ -264,6 +269,22 @@ describe('Client', () => { }) }) + describe('getReceipt', () => { + it('should find a receipt', async () => { + const taskCid = parseLink( + 'bafyreibo6nqtvp67daj7dkmeb5c2n6bg5bunxdmxq3lghtp3pmjtzpzfma' + ) + const alice = new Client(await AgentData.create(), { + receiptsEndpoint: new URL('http://localhost:9201'), + }) + const receipt = await alice.getReceipt(taskCid) + // This is a real `piece/accept` receipt exported as fixture + assert(receipt) + assert.ok(receipt.ran.link().equals(taskCid)) + assert.ok(receipt.out.ok) + }) + }) + describe('currentSpace', () => { it('should return undefined or space', async () => { const alice = new Client(await AgentData.create()) diff --git a/packages/w3up-client/test/fixtures/workflow.car b/packages/w3up-client/test/fixtures/workflow.car new file mode 100644 index 0000000000000000000000000000000000000000..f571950a9220daffd3a54e18cdc01b5294ca1816 GIT binary patch literal 2796 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 H@=yN))phQX literal 0 HcmV?d00001 diff --git a/packages/w3up-client/test/helpers/receipts-server.js b/packages/w3up-client/test/helpers/receipts-server.js new file mode 100644 index 000000000..50608c6c7 --- /dev/null +++ b/packages/w3up-client/test/helpers/receipts-server.js @@ -0,0 +1,30 @@ +import { createServer } from 'http' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const port = process.env.PORT ?? 9201 +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const fixtureName = process.env.FIXTURE_NAME || 'workflow.car' + +const server = createServer((req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', '*') + res.setHeader('Access-Control-Allow-Headers', '*') + + fs.readFile( + path.resolve(`${__dirname}`, '..', 'fixtures', fixtureName), + (error, content) => { + if (error) { + res.writeHead(500) + res.end() + } + res.writeHead(200, { + 'Content-disposition': 'attachment; filename=' + fixtureName, + }) + res.end(content) + } + ) +}) + +server.listen(port, () => console.log(`Listening on :${port}`)) From 7d325ba9c7cb1c5f1a9ae38f8da54c9cce51dbec Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 15 Nov 2023 17:27:03 +0300 Subject: [PATCH 2/4] fix: handle error not found --- packages/w3up-client/src/client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index e7dcc25f7..298274b6a 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -154,6 +154,9 @@ export class Client extends Base { const workflowResponse = await fetch( new URL(taskCid.toString(), this._receiptsEndpoint) ) + if (!workflowResponse.ok) { + throw new Error(`no receipt available for requested task ${taskCid.toString()}`) + } // Get receipt from Message Archive const agentMessageBytes = new Uint8Array( await workflowResponse.arrayBuffer() From bf89b06b0e95a70545185da4587cf51a43dc91e0 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 15 Nov 2023 17:28:33 +0300 Subject: [PATCH 3/4] fix: format --- packages/w3up-client/src/client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 298274b6a..9239bf336 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -155,7 +155,9 @@ export class Client extends Base { new URL(taskCid.toString(), this._receiptsEndpoint) ) if (!workflowResponse.ok) { - throw new Error(`no receipt available for requested task ${taskCid.toString()}`) + throw new Error( + `no receipt available for requested task ${taskCid.toString()}` + ) } // Get receipt from Message Archive const agentMessageBytes = new Uint8Array( From 10f399dfac79ed5125edd611af1e4b77df299b24 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 15 Nov 2023 17:30:53 +0300 Subject: [PATCH 4/4] fix: format --- packages/w3up-client/src/client.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 9239bf336..bce7c2c38 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -154,11 +154,13 @@ export class Client extends Base { const workflowResponse = await fetch( new URL(taskCid.toString(), this._receiptsEndpoint) ) + /* c8 ignore start */ if (!workflowResponse.ok) { throw new Error( `no receipt available for requested task ${taskCid.toString()}` ) } + /* c8 ignore stop */ // Get receipt from Message Archive const agentMessageBytes = new Uint8Array( await workflowResponse.arrayBuffer()