Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(billing): support blob capabilities #356

Merged
merged 2 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions billing/lib/ucan-stream.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as ServiceBlobCaps from '@web3-storage/capabilities/web3.storage/blob'
import * as BlobCaps from '@web3-storage/capabilities/blob'
import * as StoreCaps from '@web3-storage/capabilities/store'

/**
Expand All @@ -13,23 +15,35 @@ export const findSpaceUsageDeltas = messages => {
for (const message of messages) {
if (!isReceipt(message)) continue

/** @type {import('@ucanto/interface').DID|undefined} */
let resource
/** @type {number|undefined} */
let size
if (isReceiptForCapability(message, StoreCaps.add) && isStoreAddSuccess(message.out)) {
if (isReceiptForCapability(message, ServiceBlobCaps.allocate) && isServiceBlobAllocateSuccess(message.out)) {
resource = message.value.att[0].nb?.space
size = message.out.ok.size
} else if (isReceiptForCapability(message, BlobCaps.remove) && isBlobRemoveSuccess(message.out)) {
resource = /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with)
size = -message.out.ok.size
// TODO: remove me LEGACY store/add
} else if (isReceiptForCapability(message, StoreCaps.add) && isStoreAddSuccess(message.out)) {
resource = /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with)
size = message.out.ok.allocated
// TODO: remove me LEGACY store/remove
} else if (isReceiptForCapability(message, StoreCaps.remove) && isStoreRemoveSuccess(message.out)) {
resource = /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with)
size = -message.out.ok.size
}

// Is message is a repeat store/add for the same shard or not a valid
// store/add or store/remove receipt?
if (size == 0 || size == null) {
if (resource == null || size == 0 || size == null) {
continue
}

/** @type {import('./api.js').UsageDelta} */
const delta = {
resource: /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with),
resource,
cause: message.invocationCid,
delta: size,
// TODO: use receipt timestamp per https://github.com/web3-storage/w3up/issues/970
Expand Down Expand Up @@ -83,6 +97,28 @@ export const storeSpaceUsageDelta = async (delta, ctx) => {
*/
const isReceipt = m => m.type === 'receipt'

/**
* @param {import('@ucanto/interface').Result} r
* @returns {r is { ok: import('@web3-storage/capabilities/types').BlobAllocateSuccess }}
*/
const isServiceBlobAllocateSuccess = r =>
!r.error &&
r.ok != null &&
typeof r.ok === 'object' &&
'size' in r.ok &&
(typeof r.ok.size === 'number')

/**
* @param {import('@ucanto/interface').Result} r
* @returns {r is { ok: import('@web3-storage/capabilities/types').BlobRemoveSuccess }}
*/
const isBlobRemoveSuccess = r =>
!r.error &&
r.ok != null &&
typeof r.ok === 'object' &&
'size' in r.ok &&
(typeof r.ok.size === 'number')

/**
* @param {import('@ucanto/interface').Result} r
* @returns {r is { ok: import('@web3-storage/capabilities/types').StoreAddSuccess }}
Expand Down
2 changes: 1 addition & 1 deletion billing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@sentry/serverless": "^7.74.1",
"@ucanto/interface": "^10.0.1",
"@ucanto/server": "^10.0.0",
"@web3-storage/capabilities": "^13.3.1",
"@web3-storage/capabilities": "^14.0.0",
"big.js": "^6.2.1",
"multiformats": "^13.1.0",
"p-retry": "^6.2.0",
Expand Down
7 changes: 5 additions & 2 deletions billing/test/helpers/did.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const randomDomain = () =>
export const randomDIDMailto = () =>
`did:mailto:${randomDomain()}:${randomAlphas(randomInteger(1, 16))}`

/** @returns {Promise<import("@ucanto/interface").DID>} */
export const randomDID = async () => {
/** @returns {Promise<import("@ucanto/interface").DID>} */
export const randomDID = () => randomDIDKey()

/** @returns {Promise<import("@ucanto/interface").DID<'key'>>} */
export const randomDIDKey = async () => {
const signer = await Signer.generate()
return signer.did()
}
61 changes: 55 additions & 6 deletions billing/test/lib/ucan-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Schema } from '@ucanto/core'
import { findSpaceUsageDeltas, storeSpaceUsageDelta } from '../../lib/ucan-stream.js'
import { randomConsumer } from '../helpers/consumer.js'
import { randomLink } from '../helpers/dag.js'
import { randomDID } from '../helpers/did.js'
import { randomDID, randomDIDKey } from '../helpers/did.js'

/** @type {import('./api').TestSuite<import('./api').UCANStreamTestContext>} */
export const test = {
Expand All @@ -24,14 +24,60 @@ export const test = {

const shard = randomLink()

/** @type {import('../../lib/api.js').UcanReceiptMessage[]} */
/**
* @type {import('../../lib/api.js').UcanReceiptMessage<[
* | import('@web3-storage/capabilities/types').BlobAllocate
* | import('@web3-storage/capabilities/types').BlobRemove
* | import('@web3-storage/capabilities/types').StoreAdd
* | import('@web3-storage/capabilities/types').StoreRemove
* ]>[]}
*/
const receipts = [{
type: 'receipt',
carCid: randomLink(),
invocationCid: randomLink(),
value: {
att: [{
with: await randomDID(),
with: await randomDIDKey(),
can: 'web3.storage/blob/allocate',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, could we use exported .can from capabilities repo instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did reach for that first but then I realised I didn't do it for the rest of them in the other tests and it would be a bigger change. Worth doing in retrospect.

nb: {
blob: {
digest: randomLink().multihash.bytes,
size: 138
},
cause: randomLink(),
space: await randomDIDKey()
}
}],
aud: await randomDID(),
cid: randomLink()
},
out: { ok: { size: 138 } },
ts: new Date()
}, {
type: 'receipt',
carCid: randomLink(),
invocationCid: randomLink(),
value: {
att: [{
with: await randomDIDKey(),
can: 'blob/remove',
nb: {
digest: randomLink().multihash.bytes
}
}],
aud: await randomDID(),
cid: randomLink()
},
out: { ok: { size: 138 } },
ts: new Date()
}, {
type: 'receipt',
carCid: randomLink(),
invocationCid: randomLink(),
value: {
att: [{
with: await randomDIDKey(),
can: 'store/add',
nb: {
link: shard,
Expand All @@ -49,7 +95,7 @@ export const test = {
invocationCid: randomLink(),
value: {
att: [{
with: await randomDID(),
with: await randomDIDKey(),
can: 'store/remove',
nb: { link: shard }
}],
Expand All @@ -66,8 +112,11 @@ export const test = {
// ensure we have a delta for every receipt
for (const r of receipts) {
assert.ok(deltas.some(d => (
d.resource === r.value.att[0].with &&
d.cause.toString() === r.invocationCid.toString()
d.cause.toString() === r.invocationCid.toString() &&
// resource for blob allocate is found in the caveats
(r.value.att[0].can === 'web3.storage/blob/allocate'
? d.resource === r.value.att[0].nb.space
: d.resource === r.value.att[0].with)
)))
}
},
Expand Down
43 changes: 42 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading