Skip to content

Commit

Permalink
feat: get receipt support in client (#1135)
Browse files Browse the repository at this point in the history
Adds `getReceipt(taskCid)` support in client after endpoint added into
w3filecoin earlier this week
storacha/w3infra#275

When you get a CID from an invocation, you can look for its receipt and
go through receipt chain if you like from the client :)
  • Loading branch information
vasco-santos authored Nov 15, 2023
1 parent aa4ba63 commit 660d088
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/w3up-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion packages/w3up-client/src/base.js
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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
Expand All @@ -27,6 +28,7 @@ export class Base {
url: this._serviceConf.access.channel.url,
connection: this._serviceConf.access,
})
this._receiptsEndpoint = options.receiptsEndpoint ?? receiptsEndpoint
}

/**
Expand Down
32 changes: 32 additions & 0 deletions packages/w3up-client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -37,6 +38,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)
Expand Down Expand Up @@ -144,6 +146,36 @@ 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)
)
/* 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()
)
// 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.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/w3up-client/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ export const serviceConf = {
upload: uploadServiceConnection,
filecoin: filecoinServiceConnection,
}

export const receiptsEndpoint = 'https://up.web3.storage/receipt/'
4 changes: 4 additions & 0 deletions packages/w3up-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export interface ClientFactoryOptions {
* here an error will be thrown.
*/
principal?: Signer<DID<'key'>>
/**
* URL configuration of endpoint where receipts from UCAN Log can be read from.
*/
receiptsEndpoint?: URL
}

export type ClientFactory = (options?: ClientFactoryOptions) => Promise<Client>
Expand Down
23 changes: 22 additions & 1 deletion packages/w3up-client/test/client.test.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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())
Expand Down
Binary file added packages/w3up-client/test/fixtures/workflow.car
Binary file not shown.
30 changes: 30 additions & 0 deletions packages/w3up-client/test/helpers/receipts-server.js
Original file line number Diff line number Diff line change
@@ -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}`))

0 comments on commit 660d088

Please sign in to comment.