Skip to content

Commit

Permalink
feat: filecoin info (#1091)
Browse files Browse the repository at this point in the history
Goal of the PR is to make filecoin deals information available for end
users via client (and later on CLI + console). For this:
- adds `filecoin/info` capability to `capabilities`, `filecoin-api` and
`filecoin-client`
- user can give the `piece` that was computed while uploading the file,
or that was published via content claims
- exposes via `w3up-client` some filecoin sugar `*.filecoin.offer` and
`*.filecoin.info`

Note that `filecoin.info` only returns effective deals. Alternatively,
user could ask directly dealTracker, even though user would need to know
the aggregate where their piece was included. So, better make storefront
facilitate user life with this shortcut

Would be great to actually give more information of the on going state,
but I am failing to find a nice way of giving this info here. However,
probably should be a separate capability (e.g. `track`, so that we avoid
reading from so many stores and follow the chain, unless user
specifically looks for it)

Note: I will add this to spec later on
  • Loading branch information
vasco-santos authored Nov 8, 2023
1 parent f9cc770 commit adb2442
Show file tree
Hide file tree
Showing 28 changed files with 844 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/capabilities/src/filecoin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export {
filecoinOffer as offer,
filecoinSubmit as submit,
filecoinAccept as accept,
filecoinInfo as info,
} from './storefront.js'
27 changes: 27 additions & 0 deletions packages/capabilities/src/filecoin/storefront.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,30 @@ export const filecoinAccept = capability({
)
},
})

/**
* Capability allowing an agent to _request_ info about a content piece in
* Filecoin deals.
*/
export const filecoinInfo = capability({
can: 'filecoin/info',
/**
* DID of the space the content is stored in.
*/
with: Schema.did(),
nb: Schema.struct({
/**
* CID of the piece.
*
* @see https://github.com/filecoin-project/FIPs/pull/758/files
*/
piece: PieceLink,
}),
derives: (claim, from) => {
return (
and(equalWith(claim, from)) ||
and(checkLink(claim.nb.piece, from.nb.piece, 'nb.piece')) ||
ok({})
)
},
})
1 change: 1 addition & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const abilitiesAsStrings = [
Storefront.filecoinOffer.can,
Storefront.filecoinSubmit.can,
Storefront.filecoinAccept.can,
Storefront.filecoinInfo.can,
Aggregator.pieceOffer.can,
Aggregator.pieceAccept.can,
Dealer.aggregateOffer.can,
Expand Down
19 changes: 19 additions & 0 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,21 @@ export interface ProofNotFound extends Ucanto.Failure {
name: 'ProofNotFound'
}

export interface FilecoinInfoSuccess {
piece: PieceLink
deals: FilecoinInfoAcceptedDeal[]
}
export interface FilecoinInfoAcceptedDeal
extends DataAggregationProof,
DealDetails {
aggregate: PieceLink
}

export type FilecoinInfoFailure =
| ContentNotFound
| InvalidContentPiece
| Ucanto.Failure

// filecoin aggregator
export interface PieceOfferSuccess {
/**
Expand Down Expand Up @@ -549,6 +564,9 @@ export type FilecoinSubmit = InferInvokedCapability<
export type FilecoinAccept = InferInvokedCapability<
typeof StorefrontCaps.filecoinAccept
>
export type FilecoinInfo = InferInvokedCapability<
typeof StorefrontCaps.filecoinInfo
>
export type PieceOffer = InferInvokedCapability<
typeof AggregatorCaps.pieceOffer
>
Expand Down Expand Up @@ -610,6 +628,7 @@ export type AbilitiesArray = [
FilecoinOffer['can'],
FilecoinSubmit['can'],
FilecoinAccept['can'],
FilecoinInfo['can'],
PieceOffer['can'],
PieceAccept['can'],
AggregateOffer['can'],
Expand Down
5 changes: 5 additions & 0 deletions packages/filecoin-api/src/storefront/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PieceLink } from '@web3-storage/data-segment'
import {
AggregatorService,
StorefrontService,
DealTrackerService,
} from '@web3-storage/filecoin-client/types'
import {
Store,
Expand Down Expand Up @@ -64,6 +65,10 @@ export interface ServiceContext {
* Stores receipts for tasks.
*/
receiptStore: ReceiptStore
/**
* Deal tracker connection to find out available deals for an aggregate.
*/
dealTrackerService: ServiceConfig<DealTrackerService>
/**
* Service options.
*/
Expand Down
99 changes: 98 additions & 1 deletion packages/filecoin-api/src/storefront/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import * as Client from '@ucanto/client'
import * as CAR from '@ucanto/transport/car'
import * as StorefrontCaps from '@web3-storage/capabilities/filecoin/storefront'
import * as AggregatorCaps from '@web3-storage/capabilities/filecoin/aggregator'
import { DealTracker } from '@web3-storage/filecoin-client'
// eslint-disable-next-line no-unused-vars
import * as API from '../types.js'
import { QueueOperationFailed, StoreOperationFailed } from '../errors.js'
import {
QueueOperationFailed,
StoreOperationFailed,
RecordNotFoundErrorName,
} from '../errors.js'

/**
* @param {API.Input<StorefrontCaps.filecoinOffer>} input
Expand Down Expand Up @@ -226,6 +231,94 @@ async function findDataAggregationProof({ taskStore, receiptStore }, task) {
}
}

/**
* @param {API.Input<StorefrontCaps.filecoinInfo>} input
* @param {import('./api.js').ServiceContext} context
* @returns {Promise<API.UcantoInterface.Result<API.FilecoinInfoSuccess, API.FilecoinInfoFailure> | API.UcantoInterface.JoinBuilder<API.FilecoinInfoSuccess>>}
*/
export const filecoinInfo = async ({ capability }, context) => {
const { piece } = capability.nb

// Get piece in store
const getPiece = await context.pieceStore.get({ piece })
if (getPiece.error && getPiece.error.name === RecordNotFoundErrorName) {
return {
error: getPiece.error,
}
} else if (getPiece.error) {
return { error: new StoreOperationFailed(getPiece.error.message) }
}

// Check if `piece/accept` receipt exists to get to know aggregate where it is included on a deal
const pieceAcceptInvocation = await StorefrontCaps.filecoinAccept
.invoke({
issuer: context.id,
audience: context.id,
with: context.id.did(),
nb: {
piece,
content: getPiece.ok.content,
},
expiration: Infinity,
})
.delegate()

const pieceAcceptReceiptGet = await context.receiptStore.get(
pieceAcceptInvocation.link()
)
if (pieceAcceptReceiptGet.error) {
/** @type {API.UcantoInterface.OkBuilder<API.FilecoinInfoSuccess, API.FilecoinInfoFailure>} */
const processingResult = Server.ok({
piece,
deals: [],
})
return processingResult
}

const pieceAcceptOut = /** @type {API.FilecoinAcceptSuccess} */ (
pieceAcceptReceiptGet.ok?.out.ok
)

// Query current info of aggregate from deal tracker
const info = await DealTracker.dealInfo(
context.dealTrackerService.invocationConfig,
pieceAcceptOut.aggregate,
{ connection: context.dealTrackerService.connection }
)

if (info.out.error) {
return {
error: info.out.error,
}
}
const deals = Object.entries(info.out.ok.deals || {})
if (!deals.length) {
// Should not happen if there is `piece/accept` receipt
return {
error: new Server.Failure(
`no deals were obtained for aggregate ${pieceAcceptOut.aggregate} where piece ${piece} is included`
),
}
}

/** @type {API.UcantoInterface.OkBuilder<API.FilecoinInfoSuccess, API.FilecoinInfoFailure>} */
const result = Server.ok({
piece,
deals: deals.map(([dealId, dealDetails]) => ({
aggregate: pieceAcceptOut.aggregate,
provider: dealDetails.provider,
inclusion: pieceAcceptOut.inclusion,
aux: {
dataType: 0n,
dataSource: {
dealID: BigInt(dealId),
},
},
})),
})
return result
}

export const ProofNotFoundName = /** @type {const} */ ('ProofNotFound')
export class ProofNotFound extends Server.Failure {
get reason() {
Expand Down Expand Up @@ -255,6 +348,10 @@ export function createService(context) {
capability: StorefrontCaps.filecoinAccept,
handler: (input) => filecoinAccept(input, context),
}),
info: Server.provideAdvanced({
capability: StorefrontCaps.filecoinInfo,
handler: (input) => filecoinInfo(input, context),
}),
},
}
}
Expand Down
94 changes: 94 additions & 0 deletions packages/filecoin-api/test/context/receipts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Receipt } from '@ucanto/core'
import * as StorefrontCaps from '@web3-storage/capabilities/filecoin/storefront'
import * as AggregatorCaps from '@web3-storage/capabilities/filecoin/aggregator'
import * as DealerCaps from '@web3-storage/capabilities/filecoin/dealer'

Expand All @@ -12,6 +13,7 @@ import * as API from '../../src/types.js'
* @param {API.PieceLink} context.aggregate
* @param {string} context.group
* @param {API.PieceLink} context.piece
* @param {API.CARLink} context.content
* @param {import('@ucanto/interface').Block} context.piecesBlock
* @param {API.InclusionProof} context.inclusionProof
* @param {API.AggregateAcceptSuccess} context.aggregateAcceptStatus
Expand All @@ -23,10 +25,47 @@ export async function createInvocationsAndReceiptsForDealDataProofChain({
aggregate,
group,
piece,
content,
piecesBlock,
inclusionProof,
aggregateAcceptStatus,
}) {
const filecoinOfferInvocation = await StorefrontCaps.filecoinOffer
.invoke({
issuer: storefront,
audience: storefront,
with: storefront.did(),
nb: {
piece,
content,
},
expiration: Infinity,
})
.delegate()
const filecoinSubmitInvocation = await StorefrontCaps.filecoinSubmit
.invoke({
issuer: storefront,
audience: storefront,
with: storefront.did(),
nb: {
piece,
content,
},
expiration: Infinity,
})
.delegate()
const filecoinAcceptInvocation = await StorefrontCaps.filecoinAccept
.invoke({
issuer: storefront,
audience: storefront,
with: storefront.did(),
nb: {
piece,
content,
},
expiration: Infinity,
})
.delegate()
const pieceOfferInvocation = await AggregatorCaps.pieceOffer
.invoke({
issuer: storefront,
Expand Down Expand Up @@ -76,6 +115,55 @@ export async function createInvocationsAndReceiptsForDealDataProofChain({
expiration: Infinity,
})
.delegate()

// Receipts
const filecoinOfferReceipt = await Receipt.issue({
issuer: storefront,
ran: filecoinOfferInvocation.cid,
result: {
ok: /** @type {API.FilecoinOfferSuccess} */ ({
piece,
}),
},
fx: {
join: filecoinAcceptInvocation.cid,
fork: [filecoinSubmitInvocation.cid],
},
})

const filecoinSubmitReceipt = await Receipt.issue({
issuer: storefront,
ran: filecoinSubmitInvocation.cid,
result: {
ok: /** @type {API.FilecoinSubmitSuccess} */ ({
piece,
}),
},
fx: {
join: pieceOfferInvocation.cid,
fork: [],
},
})

const filecoinAcceptReceipt = await Receipt.issue({
issuer: storefront,
ran: filecoinAcceptInvocation.cid,
result: {
ok: /** @type {API.FilecoinAcceptSuccess} */ ({
piece,
aggregate,
inclusion: inclusionProof,
aux: {
...aggregateAcceptStatus,
},
}),
},
fx: {
join: undefined,
fork: [],
},
})

const pieceOfferReceipt = await Receipt.issue({
issuer: aggregator,
ran: pieceOfferInvocation.cid,
Expand Down Expand Up @@ -130,12 +218,18 @@ export async function createInvocationsAndReceiptsForDealDataProofChain({

return {
invocations: {
filecoinOfferInvocation,
filecoinSubmitInvocation,
filecoinAcceptInvocation,
pieceOfferInvocation,
pieceAcceptInvocation,
aggregateOfferInvocation,
aggregateAcceptInvocation,
},
receipts: {
filecoinOfferReceipt,
filecoinSubmitReceipt,
filecoinAcceptReceipt,
pieceOfferReceipt,
pieceAcceptReceipt,
aggregateOfferReceipt,
Expand Down
8 changes: 4 additions & 4 deletions packages/filecoin-api/test/context/store.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as API from '../../src/types.js'
import { RecordNotFound, StoreOperationFailed } from '../../src/errors.js'
import { StoreOperationFailed, RecordNotFound } from '../../src/errors.js'

/**
* @typedef {import('../../src/types.js').StorePutError} StorePutError
Expand Down Expand Up @@ -47,7 +47,7 @@ export class Store {
const t = this.getFn(this.items, item)
if (!t) {
return {
error: new RecordNotFound(),
error: new RecordNotFound('not found'),
}
}
return {
Expand Down Expand Up @@ -85,7 +85,7 @@ export class Store {
const t = this.queryFn(this.items, search)
if (!t) {
return {
error: new RecordNotFound(),
error: new RecordNotFound('not found'),
}
}
return {
Expand Down Expand Up @@ -123,7 +123,7 @@ export class UpdatableStore extends Store {
const t = this.updateFn(this.items, key, item)
if (!t) {
return {
error: new RecordNotFound(),
error: new RecordNotFound('not found'),
}
}
return {
Expand Down
Loading

0 comments on commit adb2442

Please sign in to comment.