From d2816c8bf5ba71f2ff1c9120804ceca260d3c3ae Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 20 Sep 2023 17:47:07 +0100 Subject: [PATCH] refactor: filecoin client to use new capabilities docs: better docs fix: client tests --- packages/filecoin-client/README.md | 173 +++++++++++++++--- packages/filecoin-client/package.json | 1 + packages/filecoin-client/src/aggregator.js | 43 +++-- packages/filecoin-client/src/chain-tracker.js | 50 ----- packages/filecoin-client/src/deal-tracker.js | 57 ++++++ packages/filecoin-client/src/dealer.js | 52 +++--- packages/filecoin-client/src/index.js | 2 +- packages/filecoin-client/src/service.js | 10 +- packages/filecoin-client/src/storefront.js | 89 +++++++-- packages/filecoin-client/src/types.ts | 72 ++++---- .../filecoin-client/test/aggregator.test.js | 160 +++++----------- ...n-tracker.test.js => deal-tracker.test.js} | 42 ++--- packages/filecoin-client/test/dealer.test.js | 142 +++++++------- .../filecoin-client/test/helpers/mocks.js | 31 ++-- .../filecoin-client/test/storefront.test.js | 172 +++++++++++------ pnpm-lock.yaml | 25 +-- 16 files changed, 655 insertions(+), 466 deletions(-) delete mode 100644 packages/filecoin-client/src/chain-tracker.js create mode 100644 packages/filecoin-client/src/deal-tracker.js rename packages/filecoin-client/test/{chain-tracker.test.js => deal-tracker.test.js} (64%) diff --git a/packages/filecoin-client/README.md b/packages/filecoin-client/README.md index dc5f96533..86cb248bd 100644 --- a/packages/filecoin-client/README.md +++ b/packages/filecoin-client/README.md @@ -3,7 +3,7 @@ ## About -The `@web3-storage/filecoin-client` package provides the "low level" client API to make data uploaded with the w3up platform available in Filecoin Storage providers. It is based on [web3-storage/specs/w3-filecoin.md])https://github.com/web3-storage/specs/blob/feat/filecoin-spec/w3-filecoin.md) and is not intended for web3.storage end users. +The `@web3-storage/filecoin-client` package provides the "low level" client API to make data uploaded with the w3up platform available in Filecoin Storage providers. It is based on [web3-storage/specs/w3-filecoin.md](https://github.com/web3-storage/specs/blob/feat/filecoin-spec/w3-filecoin.md) and is not intended for web3.storage end users. ## Install @@ -15,38 +15,102 @@ npm install @web3-storage/filecoin-client ## Usage -### `Storefront.filecoinAdd` +### `Storefront.filecoinOffer` -Request a Storefront service to add computed filecoin piece into Filecoin Storage Providers. +The [`filecoin/offer`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#filecoinoffer) task can be executed to request storing a content piece in Filecoin. It issues a signed receipt of the execution result. + +A receipt for successful execution will contain an effect, linking to a `filecoin/submit` task that will complete asynchronously. + +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. ```js import { Storefront } from '@web3-storage/filecoin-client' -const add = await Storefront.filecoinQueue( +const res = await Storefront.filecoinOffer( invocationConfig, - piece, - content + content, + piece +) +``` + +```typescript +function filecoinOffer( + conf: InvocationConfig, + content: Link, // Content CID + piece: Piece, // Filecoin piece +): Promise +``` + +More information: [`InvocationConfig`](#invocationconfig) + +### `Storefront.filecoinSubmit` + +The [`filecoin/submit`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#filecoinsubmit) task is an _effect_ linked from successful execution of a `filecoin/offer` task, it is executed to issue a receipt for the success or failure of the task. + +A receipt for successful execution indicates that the offered piece has been submitted to the pipeline. In this case the receipt will contain an effect, linking to a `piece/offer` task that will complete asynchronously. + +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. + +```js +import { Storefront } from '@web3-storage/filecoin-client' + +const res = await Storefront.filecoinSubmit( + invocationConfig, + content, + piece ) ``` ```typescript -function filecoinQueue( +function filecoinSubmit( conf: InvocationConfig, + content: Link, // Content CID piece: Piece, // Filecoin piece +): Promise +``` + +More information: [`InvocationConfig`](#invocationconfig) + +### `Storefront.filecoinAccept` + +The [`filecoin/accept`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#filecoinsubmit) task is an _effect_ linked from successful execution of a `filecoin/offer` task, it is executed to issue a receipt for the success or failure of the task. + +A receipt for successful execution indicates that the offered piece has been accepted in a Filecoin deal. In this case the receipt will contain proofs that the piece was included in an aggregate and deal. + +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. + +```js +import { Storefront } from '@web3-storage/filecoin-client' + +const res = await Storefront.filecoinAccept( + invocationConfig, + content, + piece +) +``` + +```typescript +function filecoinAccept( + conf: InvocationConfig, content: Link, // Content CID + piece: Piece, // Filecoin piece ): Promise ``` More information: [`InvocationConfig`](#invocationconfig) -### `Aggregator.pieceAdd` +### `Aggregator.pieceOffer` + +The [`piece/offer`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#pieceoffer) task can be executed to request that a piece be aggregated for inclusion in an upcoming an Filecoin deal. It issues a signed receipt of the execution result. It is _also_ an effect linked from successful execution of a `filecoin/submit` task. + +A receipt for successful execution will contain an effect, linking to a `piece/accept` task that will complete asynchronously. -Request an Aggregator service to add a filecoin piece into an aggregate to be offered to Filecoin Storage Providers. +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. ```js import { Aggregator } from '@web3-storage/filecoin-client' -const add = await Aggregator.aggregateQueue( +const res = await Aggregator.pieceOffer( invocationConfig, piece, group @@ -54,7 +118,7 @@ const add = await Aggregator.aggregateQueue( ``` ```typescript -function aggregateQueue( +function pieceOffer( conf: InvocationConfig, piece: Piece, // Filecoin piece group: string, // Aggregate grouping with different criterium like storefront @@ -63,48 +127,109 @@ function aggregateQueue( More information: [`InvocationConfig`](#invocationconfig) -### `Dealer.aggregateAdd` +### `Aggregator.pieceAccept` -Request a Dealer service to offer a filecoin piece (larger aggregate of pieces) to Filecoin Storage Providers. +The [`piece/accept`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#pieceaccept) task is an _effect_ linked from successful execution of a `piece/offer` task, it is executed to issue a receipt for the success or failure of the task. + +A receipt for successful execution indicates that the offered piece was included in an aggregate. In this case the receipt will contain the aggregate piece CID and a proof that the piece was included in the aggregate. It also includes an effect, linking to an `aggregate/offer` task that will complete asynchronously. + +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. + +```js +import { Aggregator } from '@web3-storage/filecoin-client' + +const res = await Aggregator.pieceAccept( + invocationConfig, + piece, + group +) +``` + +```typescript +function pieceAccept( + conf: InvocationConfig, + piece: Piece, // Filecoin piece + group: string, // Aggregate grouping with different criterium like storefront +): Promise +``` + +More information: [`InvocationConfig`](#invocationconfig) + +### `Dealer.aggregateOffer` + +The [`aggregate/offer`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#aggregateoffer) task can be executed to request an aggregate be added to a deal with a Storage Provider. It issues a signed receipt of the execution result. It is _also_ an effect linked from successful execution of a `piece/accept` task. + +A receipt for successful execution will contain an effect, linking to an `aggregate/accept` task that will complete asynchronously. + +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. ```js import { Dealer } from '@web3-storage/filecoin-client' -const add = await Dealer.dealQueue( +const res = await Dealer.aggregateOffer( invocationConfig, aggregate, - pieces, - storefront, - label + pieces ) ``` ```typescript -function dealQueue( +function aggregateOffer( conf: InvocationConfig, aggregate: Piece, // Filecoin piece representing aggregate pieces: Piece[], // Filecoin pieces part of the aggregate (sorted) - label: string // optional label for deal ): Promise ``` More information: [`InvocationConfig`](#invocationconfig) -### `Chain.chainInfo` +### `Dealer.aggregateAccept` + +The [`aggregate/accept`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#aggregateaccept) task is an _effect_ linked from successful execution of a `aggregate/offer` task, it is executed to issue a receipt for the success or failure of the task. + +A receipt for successful execution indicates that an aggregate has been accepted for inclusion in a Filecoin deal. In this case the receipt will contain proofs that the piece was included in an aggregate and deal. + +Otherwise the task is failed and the receipt will contain details of the reason behind the failure, as well as multiple effects, linking to `piece/offer` tasks that will retry _valid_ pieces and complete asynchronously. + +```js +import { Dealer } from '@web3-storage/filecoin-client' + +const res = await Dealer.aggregateAccept( + invocationConfig, + aggregate, + pieces +) +``` + +```typescript +function aggregateAccept( + conf: InvocationConfig, + aggregate: Piece, // Filecoin piece representing aggregate + pieces: Piece[], // Filecoin pieces part of the aggregate (sorted) +): Promise +``` + +More information: [`InvocationConfig`](#invocationconfig) + +### `DealTracker.dealInfo` + +The [`deal/info`](https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#dealinfo) task can be executed to request deal information for a given piece. It issues a signed receipt of the execution result. + +A receipt for successful execution will contain details of deals the provided piece CID is currently active in. -Request a Chain service to find chain information of a given piece. It will return deals where given piece is present in Receipt. +Otherwise the task is failed and the receipt will contain details of the reason behind the failure. ```js -import { Chain } from '@web3-storage/filecoin-client' +import { DealTracker } from '@web3-storage/filecoin-client' -const add = await Chain.chainInfo( +const add = await DealTracker.dealInfo( invocationConfig, piece ) ``` ```typescript -function chainInfo( +function dealInfo( conf: InvocationConfig, piece: Piece, // Filecoin piece to check ): Promise diff --git a/packages/filecoin-client/package.json b/packages/filecoin-client/package.json index 5556350e1..3f11fccae 100644 --- a/packages/filecoin-client/package.json +++ b/packages/filecoin-client/package.json @@ -65,6 +65,7 @@ }, "devDependencies": { "@ipld/car": "^5.1.1", + "@ipld/dag-json": "^10.1.4", "@types/assert": "^1.5.6", "@types/mocha": "^10.0.1", "@ucanto/principal": "^8.0.0", diff --git a/packages/filecoin-client/src/aggregator.js b/packages/filecoin-client/src/aggregator.js index 22ff517af..d702dddf3 100644 --- a/packages/filecoin-client/src/aggregator.js +++ b/packages/filecoin-client/src/aggregator.js @@ -1,8 +1,6 @@ import { connect } from '@ucanto/client' import { CAR, HTTP } from '@ucanto/transport' - -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - +import * as Aggregator from '@web3-storage/capabilities/filecoin/aggregator' import { services } from './service.js' /** @@ -21,14 +19,25 @@ export const connection = connect({ }) /** - * Queues a piece to the aggregator system of the filecoin pipeline. + * The `piece/offer` task can be executed to request that a piece be aggregated + * for inclusion in an upcoming an Filecoin deal. It issues a signed receipt + * of the execution result. It is _also_ an effect linked from successful + * execution of a `filecoin/submit` task. + * + * A receipt for successful execution will contain an effect, linking to a + * `piece/accept` task that will complete asynchronously. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#pieceoffer * * @param {import('./types.js').InvocationConfig} conf - Configuration * @param {import('@web3-storage/data-segment').PieceLink} piece * @param {string} group * @param {import('./types.js').RequestOptions} [options] */ -export async function aggregateQueue( +export async function pieceOffer( { issuer, with: resource, proofs, audience }, piece, group, @@ -37,7 +46,7 @@ export async function aggregateQueue( /* c8 ignore next */ const conn = options.connection ?? connection - const invocation = FilecoinCapabilities.aggregateQueue.invoke({ + const invocation = Aggregator.pieceOffer.invoke({ issuer, /* c8 ignore next */ audience: audience ?? services.AGGREGATOR.principal, @@ -53,32 +62,42 @@ export async function aggregateQueue( } /** - * Add a piece to the aggregator system of the filecoin pipeline. + * The `piece/accept` task is an _effect_ linked from successful execution of a + * `piece/offer` task, it is executed to issue a receipt for the success or + * failure of the task. + * + * A receipt for successful execution indicates that the offered piece was + * included in an aggregate. In this case the receipt will contain the + * aggregate piece CID and a proof that the piece was included in the + * aggregate. It also includes an effect, linking to an `aggregate/offer` task + * that will complete asynchronously. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#pieceaccept * * @param {import('./types.js').InvocationConfig} conf - Configuration * @param {import('@web3-storage/data-segment').PieceLink} piece - * @param {string} storefront * @param {string} group * @param {import('./types.js').RequestOptions} [options] */ -export async function aggregateAdd( +export async function pieceAccept( { issuer, with: resource, proofs, audience }, piece, - storefront, group, options = {} ) { /* c8 ignore next */ const conn = options.connection ?? connection - const invocation = FilecoinCapabilities.aggregateAdd.invoke({ + const invocation = Aggregator.pieceAccept.invoke({ issuer, /* c8 ignore next */ audience: audience ?? services.AGGREGATOR.principal, with: resource, nb: { piece, - storefront, group, }, proofs, diff --git a/packages/filecoin-client/src/chain-tracker.js b/packages/filecoin-client/src/chain-tracker.js deleted file mode 100644 index d06dfb7b5..000000000 --- a/packages/filecoin-client/src/chain-tracker.js +++ /dev/null @@ -1,50 +0,0 @@ -import { connect } from '@ucanto/client' -import { CAR, HTTP } from '@ucanto/transport' - -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - -import { services } from './service.js' - -/** - * @typedef {import('./types.js').ChainTrackerService} ChainTrackerService - * @typedef {import('@ucanto/interface').ConnectionView} ConnectionView - */ - -/** @type {ConnectionView} */ -export const connection = connect({ - id: services.CHAIN_TRACKER.principal, - codec: CAR.outbound, - channel: HTTP.open({ - url: services.CHAIN_TRACKER.url, - method: 'POST', - }), -}) - -/** - * Get chain information for a given a piece.. - * - * @param {import('./types.js').InvocationConfig} conf - Configuration - * @param {import('@web3-storage/data-segment').PieceLink} piece - * @param {import('./types.js').RequestOptions} [options] - */ -export async function chainInfo( - { issuer, with: resource, proofs, audience }, - piece, - options = {} -) { - /* c8 ignore next */ - const conn = options.connection ?? connection - - const invocation = FilecoinCapabilities.chainTrackerInfo.invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? services.CHAIN_TRACKER.principal, - with: resource, - nb: { - piece, - }, - proofs, - }) - - return await invocation.execute(conn) -} diff --git a/packages/filecoin-client/src/deal-tracker.js b/packages/filecoin-client/src/deal-tracker.js new file mode 100644 index 000000000..4069ee689 --- /dev/null +++ b/packages/filecoin-client/src/deal-tracker.js @@ -0,0 +1,57 @@ +import { connect } from '@ucanto/client' +import { CAR, HTTP } from '@ucanto/transport' +import * as DealTracker from '@web3-storage/capabilities/filecoin/deal-tracker' +import { services } from './service.js' + +/** + * @typedef {import('./types.js').DealTrackerService} DealTrackerService + * @typedef {import('@ucanto/interface').ConnectionView} ConnectionView + */ + +/** @type {ConnectionView} */ +export const connection = connect({ + id: services.DEAL_TRACKER.principal, + codec: CAR.outbound, + channel: HTTP.open({ + url: services.DEAL_TRACKER.url, + method: 'POST', + }), +}) + +/** + * The `deal/info` task can be executed to request deal information for a given + * piece. It issues a signed receipt of the execution result. + * + * A receipt for successful execution will contain details of deals the + * provided piece CID is currently active in. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#dealinfo + * + * @param {import('./types.js').InvocationConfig} conf - Configuration + * @param {import('@web3-storage/data-segment').PieceLink} piece + * @param {import('./types.js').RequestOptions} [options] + */ +export async function dealInfo( + { issuer, with: resource, proofs, audience }, + piece, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + + const invocation = DealTracker.dealInfo.invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? services.DEAL_TRACKER.principal, + with: resource, + nb: { + piece, + }, + proofs, + }) + + return await invocation.execute(conn) +} diff --git a/packages/filecoin-client/src/dealer.js b/packages/filecoin-client/src/dealer.js index 4ff29852d..f8ad92cab 100644 --- a/packages/filecoin-client/src/dealer.js +++ b/packages/filecoin-client/src/dealer.js @@ -1,9 +1,7 @@ import { connect } from '@ucanto/client' import { CAR, HTTP } from '@ucanto/transport' import { CBOR } from '@ucanto/core' - -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - +import * as Dealer from '@web3-storage/capabilities/filecoin/dealer' import { services } from './service.js' /** @@ -22,28 +20,35 @@ export const connection = connect({ }) /** - * Queues a piece (aggregate) to the dealer system of the filecoin pipeline to offer to SPs. + * The `aggregate/offer` task can be executed to request an aggregate be added + * to a deal with a Storage Provider. It issues a signed receipt of the + * execution result. It is _also_ an effect linked from successful execution of + * a `piece/accept` task. + * + * A receipt for successful execution will contain an effect, linking to an + * `aggregate/accept` task that will complete asynchronously. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#aggregateoffer * * @param {import('./types.js').InvocationConfig} conf - Configuration * @param {import('@web3-storage/data-segment').PieceLink} aggregate * @param {import('@web3-storage/data-segment').PieceLink[]} pieces - * @param {string} storefront - * @param {string} label * @param {import('./types.js').RequestOptions} [options] */ -export async function dealQueue( +export async function aggregateOffer( { issuer, with: resource, proofs, audience }, aggregate, pieces, - storefront, - label, options = {} ) { /* c8 ignore next */ const conn = options.connection ?? connection const block = await CBOR.write(pieces) - const invocation = FilecoinCapabilities.dealQueue.invoke({ + const invocation = Dealer.aggregateOffer.invoke({ issuer, /* c8 ignore next */ audience: audience ?? services.AGGREGATOR.principal, @@ -51,8 +56,6 @@ export async function dealQueue( nb: { aggregate, pieces: block.cid, - storefront, - label, }, proofs, }) @@ -62,28 +65,37 @@ export async function dealQueue( } /** - * Add a piece (aggregate) to the dealer system of the filecoin pipeline to offer to SPs. + * The `aggregate/accept` task is an _effect_ linked from successful execution + * of a `aggregate/offer` task, it is executed to issue a receipt for the + * success or failure of the task. + * + * A receipt for successful execution indicates that an aggregate has been + * accepted for inclusion in a Filecoin deal. In this case the receipt will + * contain proofs that the piece was included in an aggregate and deal. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure, as well as multiple effects, linking to + * `piece/offer` tasks that will retry _valid_ pieces and complete + * asynchronously. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#aggregateaccept * * @param {import('./types.js').InvocationConfig} conf - Configuration * @param {import('@web3-storage/data-segment').PieceLink} aggregate * @param {import('@web3-storage/data-segment').PieceLink[]} pieces - * @param {string} storefront - * @param {string} label * @param {import('./types.js').RequestOptions} [options] */ -export async function dealAdd( +export async function aggregateAccept( { issuer, with: resource, proofs, audience }, aggregate, pieces, - storefront, - label, options = {} ) { /* c8 ignore next */ const conn = options.connection ?? connection const block = await CBOR.write(pieces) - const invocation = FilecoinCapabilities.dealAdd.invoke({ + const invocation = Dealer.aggregateAccept.invoke({ issuer, /* c8 ignore next */ audience: audience ?? services.AGGREGATOR.principal, @@ -91,8 +103,6 @@ export async function dealAdd( nb: { aggregate, pieces: block.cid, - storefront, - label, }, proofs, }) diff --git a/packages/filecoin-client/src/index.js b/packages/filecoin-client/src/index.js index 4ac9cae0b..dc85ebedb 100644 --- a/packages/filecoin-client/src/index.js +++ b/packages/filecoin-client/src/index.js @@ -1,4 +1,4 @@ export * as Storefront from './storefront.js' export * as Aggregator from './aggregator.js' export * as Dealer from './dealer.js' -export * as Chain from './chain-tracker.js' +export * as DealTracker from './deal-tracker.js' diff --git a/packages/filecoin-client/src/service.js b/packages/filecoin-client/src/service.js index c5e62c2ee..f4fdd62df 100644 --- a/packages/filecoin-client/src/service.js +++ b/packages/filecoin-client/src/service.js @@ -18,11 +18,11 @@ export const services = { principal: DID.parse('did:web:web3.storage'), }, DEALER: { - url: new URL('https://spade-proxy.web3.storage'), - principal: DID.parse('did:web:spade.web3.storage'), + url: new URL('https://dealer.web3.storage'), + principal: DID.parse('did:web:dealer.web3.storage'), }, - CHAIN_TRACKER: { - url: new URL('https://spade-proxy.web3.storage'), - principal: DID.parse('did:web:spade.web3.storage'), + DEAL_TRACKER: { + url: new URL('https://tracker.web3.storage'), + principal: DID.parse('did:web:tracker.web3.storage'), }, } diff --git a/packages/filecoin-client/src/storefront.js b/packages/filecoin-client/src/storefront.js index 4d6c05042..7eed402d8 100644 --- a/packages/filecoin-client/src/storefront.js +++ b/packages/filecoin-client/src/storefront.js @@ -1,8 +1,6 @@ import { connect } from '@ucanto/client' import { CAR, HTTP } from '@ucanto/transport' - -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - +import * as Storefront from '@web3-storage/capabilities/filecoin/storefront' import { services } from './service.js' /** @@ -21,29 +19,38 @@ export const connection = connect({ }) /** - * Queues a piece to the filecoin pipeline. + * The `filecoin/offer` task can be executed to request storing a content piece + * in Filecoin. It issues a signed receipt of the execution result. + * + * A receipt for successful execution will contain an effect, linking to a + * `filecoin/submit` task that will complete asynchronously. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#filecoinoffer * * @param {import('./types.js').InvocationConfig} conf - Configuration - * @param {import('@web3-storage/data-segment').PieceLink} piece * @param {import('multiformats').UnknownLink} content + * @param {import('@web3-storage/data-segment').PieceLink} piece * @param {import('./types.js').RequestOptions} [options] */ -export async function filecoinQueue( +export async function filecoinOffer( { issuer, with: resource, proofs, audience }, - piece, content, + piece, options = {} ) { /* c8 ignore next */ const conn = options.connection ?? connection - const invocation = FilecoinCapabilities.filecoinQueue.invoke({ + const invocation = Storefront.filecoinOffer.invoke({ issuer, /* c8 ignore next */ audience: audience ?? services.STOREFRONT.principal, with: resource, nb: { - content: content, + content, piece, }, proofs, @@ -53,29 +60,83 @@ export async function filecoinQueue( } /** - * Add a piece to the filecoin pipeline. + * The `filecoin/submit` task is an _effect_ linked from successful execution + * of a `filecoin/offer` task, it is executed to issue a receipt for the + * success or failure of the task. + * + * A receipt for successful execution indicates that the offered piece has been + * submitted to the pipeline. In this case the receipt will contain an effect, + * linking to a `piece/offer` task that will complete asynchronously. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#filecoinsubmit * * @param {import('./types.js').InvocationConfig} conf - Configuration - * @param {import('@web3-storage/data-segment').PieceLink} piece * @param {import('multiformats').UnknownLink} content + * @param {import('@web3-storage/data-segment').PieceLink} piece * @param {import('./types.js').RequestOptions} [options] */ -export async function filecoinAdd( +export async function filecoinSubmit( { issuer, with: resource, proofs, audience }, + content, piece, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + + const invocation = Storefront.filecoinSubmit.invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? services.STOREFRONT.principal, + with: resource, + nb: { + content, + piece, + }, + proofs, + }) + + return await invocation.execute(conn) +} + +/** + * The `filecoin/accept` task is an _effect_ linked from successful execution + * of a `filecoin/offer` task, it is executed to issue a receipt for the + * success or failure of the task. + * + * A receipt for successful execution indicates that the offered piece has been + * accepted in a Filecoin deal. In this case the receipt will contain proofs + * that the piece was included in an aggregate and deal. + * + * Otherwise the task is failed and the receipt will contain details of the + * reason behind the failure. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-filecoin.md#filecoinaccept + * + * @param {import('./types.js').InvocationConfig} conf - Configuration + * @param {import('multiformats').UnknownLink} content + * @param {import('@web3-storage/data-segment').PieceLink} piece + * @param {import('./types.js').RequestOptions} [options] + */ +export async function filecoinAccept( + { issuer, with: resource, proofs, audience }, content, + piece, options = {} ) { /* c8 ignore next */ const conn = options.connection ?? connection - const invocation = FilecoinCapabilities.filecoinAdd.invoke({ + const invocation = Storefront.filecoinAccept.invoke({ issuer, /* c8 ignore next */ audience: audience ?? services.STOREFRONT.principal, with: resource, nb: { - content: content, + content, piece, }, proofs, diff --git a/packages/filecoin-client/src/types.ts b/packages/filecoin-client/src/types.ts index 6fe79ee90..963212f0d 100644 --- a/packages/filecoin-client/src/types.ts +++ b/packages/filecoin-client/src/types.ts @@ -7,24 +7,33 @@ import { Principal, } from '@ucanto/interface' import { - FilecoinQueue, - FilecoinAdd, - FilecoinAddSuccess, - FilecoinAddFailure, - AggregateQueue, - AggregateAdd, - AggregateAddSuccess, - AggregateAddFailure, - DealQueue, - DealAdd, - DealAddSuccess, - DealAddFailure, - ChainTrackerInfo, - ChainTrackerInfoSuccess, - ChainTrackerInfoFailure, + FilecoinOffer, + FilecoinOfferSuccess, + FilecoinOfferFailure, + FilecoinSubmit, + FilecoinSubmitSuccess, + FilecoinSubmitFailure, + FilecoinAccept, + FilecoinAcceptSuccess, + FilecoinAcceptFailure, + PieceOffer, + PieceOfferSuccess, + PieceOfferFailure, + PieceAccept, + PieceAcceptSuccess, + PieceAcceptFailure, + AggregateOffer, + AggregateOfferSuccess, + AggregateOfferFailure, + AggregateAccept, + AggregateAcceptSuccess, + AggregateAcceptFailure, + DealInfo, + DealInfoSuccess, + DealInfoFailure } from '@web3-storage/capabilities/types' -export type SERVICE = 'STOREFRONT' | 'AGGREGATOR' | 'DEALER' | 'CHAIN_TRACKER' +export type SERVICE = 'STOREFRONT' | 'AGGREGATOR' | 'DEALER' | 'DEAL_TRACKER' export interface ServiceConfig { url: URL principal: Principal @@ -51,36 +60,29 @@ export interface InvocationConfig { export interface StorefrontService { filecoin: { - queue: ServiceMethod - add: ServiceMethod + offer: ServiceMethod + submit: ServiceMethod + accept: ServiceMethod } } export interface AggregatorService { - aggregate: { - queue: ServiceMethod< - AggregateQueue, - AggregateAddSuccess, - AggregateAddFailure - > - add: ServiceMethod + piece: { + offer: ServiceMethod + accept: ServiceMethod } } export interface DealerService { - deal: { - queue: ServiceMethod - add: ServiceMethod + aggregate: { + offer: ServiceMethod + accept: ServiceMethod } } -export interface ChainTrackerService { - 'chain-tracker': { - info: ServiceMethod< - ChainTrackerInfo, - ChainTrackerInfoSuccess, - ChainTrackerInfoFailure - > +export interface DealTrackerService { + deal: { + info: ServiceMethod } } diff --git a/packages/filecoin-client/test/aggregator.test.js b/packages/filecoin-client/test/aggregator.test.js index 558d0f82d..7d3796694 100644 --- a/packages/filecoin-client/test/aggregator.test.js +++ b/packages/filecoin-client/test/aggregator.test.js @@ -3,41 +3,35 @@ import * as Signer from '@ucanto/principal/ed25519' import * as Client from '@ucanto/client' import * as Server from '@ucanto/server' import * as CAR from '@ucanto/transport/car' -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - -import { aggregateQueue, aggregateAdd } from '../src/aggregator.js' - -import { randomCargo } from './helpers/random.js' +import * as AggregatorCaps from '@web3-storage/capabilities/filecoin/aggregator' +import { pieceOffer, pieceAccept } from '../src/aggregator.js' +import { randomAggregate, randomCargo } from './helpers/random.js' import { mockService } from './helpers/mocks.js' -import { OperationFailed, OperationErrorName } from './helpers/errors.js' import { serviceProvider as aggregatorService } from './fixtures.js' -describe('aggregate/add', () => { - it('storefront queues a filecoin piece for aggregator to handle', async () => { +describe('aggregator', () => { + it('storefront offers a filecoin piece', async () => { const { storefront } = await getContext() // Generate cargo to add const [cargo] = await randomCargo(1, 100) const group = 'did:web:free.web3.storage' - /** @type {import('@web3-storage/capabilities/types').AggregateAddSuccess} */ - const pieceAddResponse = { + /** @type {import('@web3-storage/capabilities/types').PieceOfferSuccess} */ + const pieceOfferResponse = { piece: cargo.link, } // Create Ucanto service const service = mockService({ - aggregate: { - queue: Server.provideAdvanced({ - capability: FilecoinCapabilities.aggregateQueue, + piece: { + offer: Server.provideAdvanced({ + capability: AggregatorCaps.pieceOffer, handler: async ({ invocation, context }) => { assert.strictEqual(invocation.issuer.did(), storefront.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual( - invCap.can, - FilecoinCapabilities.aggregateQueue.can - ) + assert.strictEqual(invCap.can, AggregatorCaps.pieceOffer.can) assert.equal(invCap.with, invocation.issuer.did()) // piece link assert.ok(invCap.nb?.piece.equals(cargo.link.link())) @@ -45,30 +39,25 @@ describe('aggregate/add', () => { assert.strictEqual(invCap.nb?.group, group) // Create effect for receipt with self signed queued operation - const fx = await FilecoinCapabilities.aggregateAdd + const fx = await AggregatorCaps.pieceAccept .invoke({ issuer: context.id, audience: context.id, with: context.id.did(), nb: { ...invCap.nb, - // add storefront - storefront: invCap.with, }, }) .delegate() - return Server.ok(pieceAddResponse).join(fx.link()) + return Server.ok(pieceOfferResponse).join(fx.link()) }, }), - add: () => { - throw new Error('not implemented') - }, }, }) - // invoke piece add from storefront - const res = await aggregateQueue( + // invoke piece offer from storefront + const res = await pieceOffer( { issuer: storefront, with: storefront.did(), @@ -80,130 +69,69 @@ describe('aggregate/add', () => { ) assert.ok(res.out.ok) - assert.ok(res.out.ok.piece.equals(pieceAddResponse.piece)) + assert.ok(res.out.ok.piece.equals(pieceOfferResponse.piece)) // includes effect fx in receipt assert.ok(res.fx.join) }) - it('aggregator self invokes add a filecoin piece to accept the piece queued', async () => { - const { storefront } = await getContext() - - // Generate cargo to add - const [cargo] = await randomCargo(1, 100) - const storefrontId = storefront.did() + it('aggregator accepts a filecoin piece', async () => { + const { pieces, aggregate } = await randomAggregate(100, 100) const group = 'did:web:free.web3.storage' - /** @type {import('@web3-storage/capabilities/types').AggregateAddSuccess} */ - const pieceAddResponse = { - piece: cargo.link, + /** @type {import('@web3-storage/capabilities/types').PieceAcceptSuccess} */ + const pieceAcceptResponse = { + piece: pieces[0].link, + aggregate: aggregate.link, + inclusion: { + subtree: { + path: [], + index: 0n, + }, + index: { + path: [], + index: 0n, + }, + } } // Create Ucanto service const service = mockService({ - aggregate: { - add: Server.provideAdvanced({ - capability: FilecoinCapabilities.aggregateAdd, + piece: { + accept: Server.provideAdvanced({ + capability: AggregatorCaps.pieceAccept, handler: async ({ invocation }) => { assert.strictEqual(invocation.issuer.did(), aggregatorService.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual( - invCap.can, - FilecoinCapabilities.aggregateAdd.can - ) + assert.strictEqual(invCap.can, AggregatorCaps.pieceAccept.can) assert.equal(invCap.with, invocation.issuer.did()) // piece link - assert.ok(invCap.nb?.piece.equals(cargo.link.link())) + assert.ok(invCap.nb?.piece.equals(pieces[0].link)) // group assert.strictEqual(invCap.nb?.group, group) - return Server.ok(pieceAddResponse) + return Server.ok(pieceAcceptResponse) }, }), - queue: () => { - throw new Error('not implemented') - }, }, }) - // self invoke piece/add from aggregator - const res = await aggregateAdd( + // self invoke piece/offer from aggregator + const res = await pieceAccept( { issuer: aggregatorService, with: aggregatorService.did(), audience: aggregatorService, }, - cargo.link.link(), - storefrontId, + pieces[0].link, group, { connection: getConnection(service).connection } ) assert.ok(res.out.ok) - assert.ok(res.out.ok.piece.equals(pieceAddResponse.piece)) - // does not include effect fx in receipt - assert.ok(!res.fx.join) - }) - - it('aggregator self invokes add a filecoin piece to reject the piece queued', async () => { - const { storefront } = await getContext() - - // Generate cargo to add - const [cargo] = await randomCargo(1, 100) - const storefrontId = storefront.did() - const group = 'did:web:free.web3.storage' - - /** @type {import('@web3-storage/capabilities/types').AggregateAddFailure} */ - const pieceAddResponse = new OperationFailed( - 'failed to add to aggregate', - cargo.link - ) - - // Create Ucanto service - const service = mockService({ - aggregate: { - add: Server.provideAdvanced({ - capability: FilecoinCapabilities.aggregateAdd, - handler: async ({ invocation }) => { - assert.strictEqual(invocation.issuer.did(), aggregatorService.did()) - assert.strictEqual(invocation.capabilities.length, 1) - const invCap = invocation.capabilities[0] - assert.strictEqual( - invCap.can, - FilecoinCapabilities.aggregateAdd.can - ) - assert.equal(invCap.with, invocation.issuer.did()) - // piece link - assert.ok(invCap.nb?.piece.equals(cargo.link.link())) - // group - assert.strictEqual(invCap.nb?.group, group) - - return { - error: pieceAddResponse, - } - }, - }), - queue: () => { - throw new Error('not implemented') - }, - }, - }) - - // self invoke piece add from aggregator - const res = await aggregateAdd( - { - issuer: aggregatorService, - with: aggregatorService.did(), - audience: aggregatorService, - }, - cargo.link.link(), - storefrontId, - group, - { connection: getConnection(service).connection } - ) - - assert.ok(res.out.error) - assert.deepEqual(res.out.error.name, OperationErrorName) + assert.ok(res.out.ok.piece.equals(pieceAcceptResponse.piece)) + assert.ok(res.out.ok.aggregate.equals(pieceAcceptResponse.aggregate)) + assert.deepEqual(res.out.ok.inclusion, pieceAcceptResponse.inclusion) // does not include effect fx in receipt assert.ok(!res.fx.join) }) diff --git a/packages/filecoin-client/test/chain-tracker.test.js b/packages/filecoin-client/test/deal-tracker.test.js similarity index 64% rename from packages/filecoin-client/test/chain-tracker.test.js rename to packages/filecoin-client/test/deal-tracker.test.js index f525afa80..ae12d202e 100644 --- a/packages/filecoin-client/test/chain-tracker.test.js +++ b/packages/filecoin-client/test/deal-tracker.test.js @@ -3,52 +3,49 @@ import * as Signer from '@ucanto/principal/ed25519' import * as Client from '@ucanto/client' import * as Server from '@ucanto/server' import * as CAR from '@ucanto/transport/car' -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - -import { chainInfo } from '../src/chain-tracker.js' - +import * as DealTrackerCaps from '@web3-storage/capabilities/filecoin/deal-tracker' +import { dealInfo } from '../src/deal-tracker.js' import { randomCargo } from './helpers/random.js' import { mockService } from './helpers/mocks.js' import { serviceProvider as chainService } from './fixtures.js' -describe('chain.info', () => { - it('storefront gets info of a filecoin piece from chain', async () => { +describe('deal tracker', () => { + it('storefront gets deal information', async () => { const { storefront } = await getContext() // Generate cargo to get info const [cargo] = await randomCargo(1, 100) - /** @type {import('@web3-storage/capabilities/types').ChainTrackerInfoSuccess} */ - const chainInfoResponse = { - piece: cargo.link, + /** @type {import('@web3-storage/capabilities/types').DealInfoSuccess} */ + const dealInfoResponse = { + deals: { + '12345': { + provider: 'f099' + } + } } // Create Ucanto service const service = mockService({ - 'chain-tracker': { + deal: { info: Server.provideAdvanced({ - capability: FilecoinCapabilities.chainTrackerInfo, + capability: DealTrackerCaps.dealInfo, handler: async ({ invocation, context }) => { assert.strictEqual(invocation.issuer.did(), storefront.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual( - invCap.can, - FilecoinCapabilities.chainTrackerInfo.can - ) + assert.strictEqual(invCap.can, DealTrackerCaps.dealInfo.can) assert.equal(invCap.with, invocation.issuer.did()) - // piece link - assert.ok(invCap.nb?.piece.equals(cargo.link.link())) - - return Server.ok(chainInfoResponse) + assert.ok(invCap.nb?.piece.equals(cargo.link)) + return Server.ok(dealInfoResponse) }, }), }, }) // invoke piece add from storefront - const res = await chainInfo( + const res = await dealInfo( { issuer: storefront, with: storefront.did(), @@ -59,8 +56,7 @@ describe('chain.info', () => { ) assert.ok(res.out.ok) - // @ts-expect-error todo check error - assert.ok(res.out.ok.piece.equals(chainInfoResponse.piece)) + assert.deepEqual(res.out.ok, dealInfoResponse) }) }) @@ -71,7 +67,7 @@ async function getContext() { } /** - * @param {import('../src/types.js').ChainTrackerService} service + * @param {import('../src/types.js').DealTrackerService} service */ function getConnection(service) { const server = Server.create({ diff --git a/packages/filecoin-client/test/dealer.test.js b/packages/filecoin-client/test/dealer.test.js index fbdc6269f..08ff14124 100644 --- a/packages/filecoin-client/test/dealer.test.js +++ b/packages/filecoin-client/test/dealer.test.js @@ -4,40 +4,34 @@ import * as Client from '@ucanto/client' import * as Server from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import { CBOR } from '@ucanto/core' -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - -import { dealQueue, dealAdd } from '../src/dealer.js' - +import * as DealerCaps from '@web3-storage/capabilities/filecoin/dealer' +import * as dagJSON from '@ipld/dag-json' +import { aggregateOffer, aggregateAccept } from '../src/dealer.js' import { randomAggregate } from './helpers/random.js' import { mockService } from './helpers/mocks.js' -import { OperationFailed, OperationErrorName } from './helpers/errors.js' import { serviceProvider as dealerService } from './fixtures.js' -describe('dealer.add', () => { - it('aggregator adds an aggregate piece to the dealer, getting the piece queued', async () => { - const { aggregator, storefront: storefrontSigner } = await getContext() - - // generate aggregate to add +describe('dealer', () => { + it('aggregator offers an aggregate', async () => { + const { aggregator } = await getContext() const { pieces, aggregate } = await randomAggregate(100, 100) const offer = pieces.map((p) => p.link) const piecesBlock = await CBOR.write(offer) - const storefront = storefrontSigner.did() - const label = 'label' - /** @type {import('@web3-storage/capabilities/types').DealAddSuccess} */ - const dealAddResponse = { + /** @type {import('@web3-storage/capabilities/types').AggregateOfferSuccess} */ + const aggregateOfferResponse = { aggregate: aggregate.link, } // Create Ucanto service const service = mockService({ - deal: { - queue: Server.provideAdvanced({ - capability: FilecoinCapabilities.dealQueue, + aggregate: { + offer: Server.provideAdvanced({ + capability: DealerCaps.aggregateOffer, handler: async ({ invocation, context }) => { assert.strictEqual(invocation.issuer.did(), aggregator.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual(invCap.can, FilecoinCapabilities.dealQueue.can) + assert.strictEqual(invCap.can, DealerCaps.aggregateOffer.can) assert.equal(invCap.with, invocation.issuer.did()) assert.ok(invCap.nb) @@ -51,7 +45,7 @@ describe('dealer.add', () => { ) // Create effect for receipt with self signed queued operation - const fx = await FilecoinCapabilities.dealAdd + const fx = await DealerCaps.aggregateAccept .invoke({ issuer: context.id, audience: context.id, @@ -60,60 +54,63 @@ describe('dealer.add', () => { }) .delegate() - return Server.ok(dealAddResponse).join(fx.link()) + return Server.ok(aggregateOfferResponse).join(fx.link()) }, }), - add: () => { - throw new Error('not implemented') - }, }, }) - // invoke piece add from storefront - const res = await dealQueue( + // invoke aggregate/offer from aggregator + const res = await aggregateOffer( { issuer: aggregator, with: aggregator.did(), audience: dealerService, }, - aggregate.link.link(), + aggregate.link, offer, - storefront, - label, { connection: getConnection(service).connection } ) assert.ok(res.out.ok) - assert.ok(res.out.ok.aggregate?.equals(dealAddResponse.aggregate)) + assert.ok(res.out.ok.aggregate?.equals(aggregateOfferResponse.aggregate)) // includes effect fx in receipt assert.ok(res.fx.join) }) - it('dealer self invokes add an aggregate piece to accept the piece queued', async () => { - const { storefront: storefrontSigner } = await getContext() - - // generate aggregate to add + it('dealer accepts an aggregate', async () => { const { pieces, aggregate } = await randomAggregate(100, 100) const offer = pieces.map((p) => p.link) const piecesBlock = await CBOR.write(offer) - const storefront = storefrontSigner.did() - const label = 'label' - /** @type {import('@web3-storage/capabilities/types').DealAddSuccess} */ - const dealAddResponse = { - aggregate: aggregate.link, + /** @type {import('@web3-storage/capabilities/types').AggregateAcceptSuccess} */ + const aggregateAcceptResponse = { + inclusion: { + subtree: { + path: [], + index: 0n, + }, + index: { + path: [], + index: 0n, + }, + }, + auxDataType: 0n, + auxDataSource: { + dealID: 1138n + } } // Create Ucanto service const service = mockService({ - deal: { - add: Server.provideAdvanced({ - capability: FilecoinCapabilities.dealAdd, + aggregate: { + accept: Server.provideAdvanced({ + capability: DealerCaps.aggregateAccept, handler: async ({ invocation }) => { assert.strictEqual(invocation.issuer.did(), dealerService.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual(invCap.can, FilecoinCapabilities.dealAdd.can) + assert.strictEqual(invCap.can, DealerCaps.aggregateAccept.can) assert.equal(invCap.with, invocation.issuer.did()) assert.ok(invCap.nb) @@ -126,17 +123,14 @@ describe('dealer.add', () => { invocationBlocks.find((b) => b.cid.equals(piecesBlock.cid)) ) - return Server.ok(dealAddResponse) + return Server.ok(aggregateAcceptResponse) }, }), - queue: () => { - throw new Error('not implemented') - }, }, }) - // invoke piece add from storefront - const res = await dealAdd( + // invoke aggregate accept from dealer + const res = await aggregateAccept( { issuer: dealerService, with: dealerService.did(), @@ -144,43 +138,41 @@ describe('dealer.add', () => { }, aggregate.link.link(), offer, - storefront, - label, { connection: getConnection(service).connection } ) assert.ok(res.out.ok) - assert.ok(res.out.ok.aggregate?.equals(dealAddResponse.aggregate)) + assert.deepEqual(res.out.ok, aggregateAcceptResponse) // does not include effect fx in receipt assert.ok(!res.fx.join) }) - it('dealer self invokes add an aggregate piece to reject the piece queued', async () => { - const { storefront: storefrontSigner } = await getContext() - - // generate aggregate to add + it('dealer rejects an aggregate', async () => { const { pieces, aggregate } = await randomAggregate(100, 100) const offer = pieces.map((p) => p.link) const piecesBlock = await CBOR.write(offer) - const storefront = storefrontSigner.did() - const label = 'label' - /** @type {import('@web3-storage/capabilities/types').DealAddFailure} */ - const dealAddResponse = new OperationFailed( - 'failed to add to aggregate', - aggregate.link - ) + /** @type {import('@web3-storage/capabilities/types').AggregateAcceptFailure} */ + const aggregateAcceptResponse = { + name: 'InvalidPiece', + message: 'Aggregate is not a valid piece.', + // piece 1 was a bad + cause: [{ + name: 'InvalidPieceCID', + piece: pieces[1].link + }] + } // Create Ucanto service const service = mockService({ - deal: { - add: Server.provideAdvanced({ - capability: FilecoinCapabilities.dealAdd, - handler: async ({ invocation, context }) => { + aggregate: { + accept: Server.provideAdvanced({ + capability: DealerCaps.aggregateAccept, + handler: async ({ invocation }) => { assert.strictEqual(invocation.issuer.did(), dealerService.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual(invCap.can, FilecoinCapabilities.dealAdd.can) + assert.strictEqual(invCap.can, DealerCaps.aggregateAccept.can) assert.equal(invCap.with, invocation.issuer.did()) assert.ok(invCap.nb) @@ -194,18 +186,15 @@ describe('dealer.add', () => { ) return { - error: dealAddResponse, + error: aggregateAcceptResponse, } }, }), - queue: () => { - throw new Error('not implemented') - }, }, }) - // invoke piece add from storefront - const res = await dealAdd( + // invoke aggregate accept from dealer + const res = await aggregateAccept( { issuer: dealerService, with: dealerService.did(), @@ -213,13 +202,11 @@ describe('dealer.add', () => { }, aggregate.link.link(), offer, - storefront, - label, { connection: getConnection(service).connection } ) assert.ok(res.out.error) - assert.deepEqual(res.out.error.name, OperationErrorName) + assert.equal(dagJSON.stringify(res.out.error), dagJSON.stringify(aggregateAcceptResponse)) // does not include effect fx in receipt assert.ok(!res.fx.join) }) @@ -227,9 +214,8 @@ describe('dealer.add', () => { async function getContext() { const aggregator = await Signer.generate() - const storefront = await Signer.generate() - return { aggregator, storefront } + return { aggregator } } /** diff --git a/packages/filecoin-client/test/helpers/mocks.js b/packages/filecoin-client/test/helpers/mocks.js index 5a71c785c..a0c652847 100644 --- a/packages/filecoin-client/test/helpers/mocks.js +++ b/packages/filecoin-client/test/helpers/mocks.js @@ -5,29 +5,30 @@ const notImplemented = () => { } /** - * @param {Partial< - * import('../../src/types').StorefrontService & - * import('../../src/types').AggregatorService & - * import('../../src/types').DealerService & - * import('../../src/types').ChainTrackerService - * >} impl + * @param {Partial<{ + * filecoin: Partial + * piece: Partial + * aggregate: Partial + * deal: Partial + * }>} impl */ export function mockService(impl) { return { filecoin: { - add: withCallCount(impl.filecoin?.add ?? notImplemented), - queue: withCallCount(impl.filecoin?.queue ?? notImplemented), + offer: withCallCount(impl.filecoin?.offer ?? notImplemented), + submit: withCallCount(impl.filecoin?.submit ?? notImplemented), + accept: withCallCount(impl.filecoin?.accept ?? notImplemented), + }, + piece: { + offer: withCallCount(impl.piece?.offer ?? notImplemented), + accept: withCallCount(impl.piece?.accept ?? notImplemented), }, aggregate: { - add: withCallCount(impl.aggregate?.add ?? notImplemented), - queue: withCallCount(impl.aggregate?.queue ?? notImplemented), + offer: withCallCount(impl.aggregate?.offer ?? notImplemented), + accept: withCallCount(impl.aggregate?.accept ?? notImplemented), }, deal: { - add: withCallCount(impl.deal?.add ?? notImplemented), - queue: withCallCount(impl.deal?.queue ?? notImplemented), - }, - 'chain-tracker': { - info: withCallCount(impl['chain-tracker']?.info ?? notImplemented), + info: withCallCount(impl.deal?.info ?? notImplemented), }, } } diff --git a/packages/filecoin-client/test/storefront.test.js b/packages/filecoin-client/test/storefront.test.js index eab3d8df9..4058cbdad 100644 --- a/packages/filecoin-client/test/storefront.test.js +++ b/packages/filecoin-client/test/storefront.test.js @@ -3,40 +3,33 @@ import * as Signer from '@ucanto/principal/ed25519' import * as Client from '@ucanto/client' import * as Server from '@ucanto/server' import * as CAR from '@ucanto/transport/car' -import { Filecoin as FilecoinCapabilities } from '@web3-storage/capabilities' - -import { filecoinQueue, filecoinAdd } from '../src/storefront.js' - +import * as StorefrontCaps from '@web3-storage/capabilities/filecoin/storefront' +import * as dagJSON from '@ipld/dag-json' +import { filecoinOffer, filecoinSubmit, filecoinAccept } from '../src/storefront.js' import { randomCargo } from './helpers/random.js' import { mockService } from './helpers/mocks.js' -import { OperationFailed, OperationErrorName } from './helpers/errors.js' import { serviceProvider as storefrontService } from './fixtures.js' -describe('filecoin/add', () => { - it('agent queues a filecoin piece for storefront to handle', async () => { +describe('storefront', () => { + it('agent offers a filecoin piece', async () => { const { agent } = await getContext() - - // Generate cargo to add const [cargo] = await randomCargo(1, 100) - /** @type {import('@web3-storage/capabilities/types').FilecoinAddSuccess} */ - const filecoinAddResponse = { + /** @type {import('@web3-storage/capabilities/types').FilecoinOfferSuccess} */ + const filecoinOfferResponse = { piece: cargo.link, } // Create Ucanto service const service = mockService({ filecoin: { - queue: Server.provideAdvanced({ - capability: FilecoinCapabilities.filecoinQueue, + offer: Server.provideAdvanced({ + capability: StorefrontCaps.filecoinOffer, handler: async ({ invocation, context }) => { assert.strictEqual(invocation.issuer.did(), agent.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual( - invCap.can, - FilecoinCapabilities.filecoinQueue.can - ) + assert.strictEqual(invCap.can, StorefrontCaps.filecoinOffer.can) assert.equal(invCap.with, invocation.issuer.did()) assert.ok(invCap.nb) // piece link @@ -45,7 +38,16 @@ describe('filecoin/add', () => { assert.ok(invCap.nb.content.equals(cargo.content.link())) // Create effect for receipt with self signed queued operation - const fx = await FilecoinCapabilities.filecoinAdd + const submitfx = await StorefrontCaps.filecoinSubmit + .invoke({ + issuer: context.id, + audience: context.id, + with: context.id.did(), + nb: invCap.nb, + }) + .delegate() + + const acceptfx = await StorefrontCaps.filecoinAccept .invoke({ issuer: context.id, audience: context.id, @@ -54,51 +56,105 @@ describe('filecoin/add', () => { }) .delegate() - return Server.ok(filecoinAddResponse).join(fx.link()) + return Server.ok(filecoinOfferResponse).fork(submitfx.link()).join(acceptfx.link()) }, }), - add: () => { - throw new Error('not implemented') - }, }, }) - const res = await filecoinQueue( + const res = await filecoinOffer( { issuer: agent, with: agent.did(), audience: storefrontService, }, - cargo.link.link(), - cargo.content.link(), + cargo.content, + cargo.link, { connection: getConnection(service).connection } ) assert.ok(res.out.ok) - assert.ok(res.out.ok.piece.equals(filecoinAddResponse.piece)) + assert.ok(res.out.ok.piece.equals(filecoinOfferResponse.piece)) // includes effect fx in receipt + assert.equal(res.fx.fork.length, 1) assert.ok(res.fx.join) }) - it('storefront self invokes add a filecoin piece to accept the piece queued', async () => { - // Generate cargo to add + it('storefront submits a filecoin piece', async () => { const [cargo] = await randomCargo(1, 100) - /** @type {import('@web3-storage/capabilities/types').FilecoinAddSuccess} */ - const filecoinAddResponse = { - piece: cargo.link, + /** @type {import('@web3-storage/capabilities/types').FilecoinSubmitSuccess} */ + const filecoinSubmitResponse = { + piece: cargo.link } // Create Ucanto service const service = mockService({ filecoin: { - add: Server.provideAdvanced({ - capability: FilecoinCapabilities.filecoinAdd, + submit: Server.provideAdvanced({ + capability: StorefrontCaps.filecoinSubmit, + handler: async ({ invocation, capability }) => { + assert.equal(invocation.issuer.did(), storefrontService.did()) + assert.equal(invocation.capabilities.length, 1) + assert.equal(capability.can, StorefrontCaps.filecoinSubmit.can) + assert.equal(capability.with, invocation.issuer.did()) + assert(capability.nb) + assert(capability.nb.piece.equals(cargo.link)) + assert(capability.nb.content.equals(cargo.content)) + return Server.ok(filecoinSubmitResponse) + }, + }), + }, + }) + + // self invoke filecoin/add from storefront + const res = await filecoinSubmit( + { + issuer: storefrontService, + with: storefrontService.did(), + audience: storefrontService, + }, + cargo.content, + cargo.link, + { connection: getConnection(service).connection } + ) + + assert.ok(res.out.ok) + assert(res.out.ok.piece.equals(filecoinSubmitResponse.piece)) + assert(!res.fx.join) // although IRL this would have a fx.join to piece/offer + }) + + it('storefront accepts a filecoin piece', async () => { + const [cargo] = await randomCargo(1, 100) + + /** @type {import('@web3-storage/capabilities/types').FilecoinAcceptSuccess} */ + const filecoinAcceptResponse = { + inclusion: { + subtree: { + path: [], + index: 0n, + }, + index: { + path: [], + index: 0n, + }, + }, + auxDataType: 0n, + auxDataSource: { + dealID: 1138n + } + } + + // Create Ucanto service + const service = mockService({ + filecoin: { + accept: Server.provideAdvanced({ + capability: StorefrontCaps.filecoinAccept, handler: async ({ invocation, context }) => { assert.strictEqual(invocation.issuer.did(), storefrontService.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual(invCap.can, FilecoinCapabilities.filecoinAdd.can) + assert.strictEqual(invCap.can, StorefrontCaps.filecoinAccept.can) assert.equal(invCap.with, invocation.issuer.did()) assert.ok(invCap.nb) // piece link @@ -106,53 +162,50 @@ describe('filecoin/add', () => { // content assert.ok(invCap.nb.content.equals(cargo.content.link())) - return Server.ok(filecoinAddResponse) + return Server.ok(filecoinAcceptResponse) }, }), - queue: () => { - throw new Error('not implemented') - }, }, }) // self invoke filecoin/add from storefront - const res = await filecoinAdd( + const res = await filecoinAccept( { issuer: storefrontService, with: storefrontService.did(), audience: storefrontService, }, - cargo.link.link(), - cargo.content.link(), + cargo.content, + cargo.link, { connection: getConnection(service).connection } ) assert.ok(res.out.ok) - assert.ok(res.out.ok.piece.equals(filecoinAddResponse.piece)) + assert.deepEqual(res.out.ok, filecoinAcceptResponse) // does not include effect fx in receipt assert.ok(!res.fx.join) }) - it('storefront self invokes add a filecoin piece to reject the piece queued', async () => { - // Generate cargo to add + it('storefront rejects a filecoin piece', async () => { const [cargo] = await randomCargo(1, 100) - /** @type {import('@web3-storage/capabilities/types').FilecoinAddFailure} */ - const filecoinAddResponse = new OperationFailed( - 'failed to verify piece', - cargo.link - ) + /** @type {import('@web3-storage/capabilities/types').FilecoinAcceptFailure} */ + const filecoinAcceptResponse = { + name: 'InvalidContentPiece', + message: 'Piece is a bad one.', + content: cargo.link + } // Create Ucanto service const service = mockService({ filecoin: { - add: Server.provideAdvanced({ - capability: FilecoinCapabilities.filecoinAdd, - handler: async ({ invocation, context }) => { + accept: Server.provideAdvanced({ + capability: StorefrontCaps.filecoinAccept, + handler: async ({ invocation }) => { assert.strictEqual(invocation.issuer.did(), storefrontService.did()) assert.strictEqual(invocation.capabilities.length, 1) const invCap = invocation.capabilities[0] - assert.strictEqual(invCap.can, FilecoinCapabilities.filecoinAdd.can) + assert.strictEqual(invCap.can, StorefrontCaps.filecoinAccept.can) assert.equal(invCap.with, invocation.issuer.did()) assert.ok(invCap.nb) // piece link @@ -161,30 +214,27 @@ describe('filecoin/add', () => { assert.ok(invCap.nb.content.equals(cargo.content.link())) return { - error: filecoinAddResponse, + error: filecoinAcceptResponse, } }, }), - queue: () => { - throw new Error('not implemented') - }, }, }) // self invoke filecoin/add from storefront - const res = await filecoinAdd( + const res = await filecoinAccept( { issuer: storefrontService, with: storefrontService.did(), audience: storefrontService, }, - cargo.link.link(), - cargo.content.link(), + cargo.content, + cargo.link, { connection: getConnection(service).connection } ) assert.ok(res.out.error) - assert.deepEqual(res.out.error.name, OperationErrorName) + assert.equal(dagJSON.stringify(res.out.error), dagJSON.stringify(filecoinAcceptResponse)) // does not include effect fx in receipt assert.ok(!res.fx.join) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d68b89e59..655db93fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,9 @@ importers: '@ipld/car': specifier: ^5.1.1 version: 5.1.1 + '@ipld/dag-json': + specifier: ^10.1.4 + version: 10.1.4 '@types/assert': specifier: ^1.5.6 version: 1.5.6 @@ -2600,11 +2603,11 @@ packages: cborg: 2.0.5 multiformats: 12.1.1 - /@ipld/dag-json@10.1.3: - resolution: {integrity: sha512-c0PJE+cDnsYfyuWMdTTwkzMvxjthEwWD+0u6ApQfXrs+GTcxeYh+Hefd0xE9L4fb2Trr1DGA1a/iia4PAS3Mpg==} + /@ipld/dag-json@10.1.4: + resolution: {integrity: sha512-Vgm739qPQ7P8cstna60oYx19tzJzep+Uy7yWi80dzIOygibfVaaRZ07M6qbHP+C9BJl81GNFaXy2Plr0y7poBA==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} dependencies: - cborg: 2.0.3 + cborg: 4.0.3 multiformats: 12.1.1 /@ipld/dag-pb@4.0.5: @@ -2617,7 +2620,7 @@ packages: resolution: {integrity: sha512-EhuOrAfnudsVYIbzEIgi3itHAEo3WZNOt1VNPsYhxKBhOzDMeoTXh6/IHc7ZKBW1T2vDQHdgj4m1r64z6MssGA==} dependencies: '@ipld/dag-cbor': 9.0.4 - '@ipld/dag-json': 10.1.3 + '@ipld/dag-json': 10.1.4 multiformats: 11.0.2 /@ipld/unixfs@2.1.1: @@ -4509,14 +4512,14 @@ packages: resolution: {integrity: sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==} hasBin: true - /cborg@2.0.3: - resolution: {integrity: sha512-f1IbyqgRLQK4ruNM+V3WikfYfXQg/f/zC1oneOw1P7F/Dn2OJX6MaXIdei3JMpz361IjY7OENBKcE53nkJFVCQ==} - hasBin: true - /cborg@2.0.5: resolution: {integrity: sha512-xVW1rSIw1ZXbkwl2XhJ7o/jAv0vnVoQv/QlfQxV8a7V5PlA4UU/AcIiXqmpyybwNWy/GPQU1m/aBVNIWr7/T0w==} hasBin: true + /cborg@4.0.3: + resolution: {integrity: sha512-poLvpK30KT5KI8gzDx3J/IuVCbsLqMT2fEbOrOuX0H7Hyj8yg5LezeWhRh9aLa5Z6MfPC5sriW3HVJF328M8LQ==} + hasBin: true + /ccount@1.1.0: resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} dev: true @@ -5793,9 +5796,9 @@ packages: eslint-plugin-promise: ^6.0.0 dependencies: eslint: 8.46.0 - eslint-plugin-import: 2.28.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0) - eslint-plugin-n: 15.7.0(eslint@8.46.0) - eslint-plugin-promise: 6.1.1(eslint@8.46.0) + eslint-plugin-import: 2.28.0(@typescript-eslint/parser@5.62.0)(eslint@8.49.0) + eslint-plugin-n: 15.7.0(eslint@8.49.0) + eslint-plugin-promise: 6.1.1(eslint@8.49.0) dev: true /eslint-config-standard@17.1.0(eslint-plugin-import@2.28.0)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0):