From c8ad87d43bb8c25d8943ddf4c06f1610c169b6c2 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 10 Apr 2024 11:30:50 -0700 Subject: [PATCH 1/7] feat: in progress work on nft-get deals info --- packages/api/package.json | 1 + packages/api/src/routes/nfts-get.js | 8 ++- packages/api/src/utils/db-client.js | 6 +- packages/api/src/utils/w3up.js | 80 ++++++++++++++++++++++++++ packages/api/test/nfts-get.spec.js | 9 +++ packages/website/lib/api.js | 23 ++++++++ packages/website/pages/files.js | 3 + yarn.lock | 89 ++++++++++++++++++++++++++--- 8 files changed, 208 insertions(+), 11 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index 50c703aeb1..a74306a31d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -33,6 +33,7 @@ "@ucanto/server": "^9.0.1", "@web3-storage/access": "^18.2.0", "@web3-storage/car-block-validator": "^1.2.0", + "@web3-storage/content-claims": "^4.0.4", "@web3-storage/upload-client": "^13.2.0", "@web3-storage/w3up-client": "^12.5.0", "cardex": "^1.0.0", diff --git a/packages/api/src/routes/nfts-get.js b/packages/api/src/routes/nfts-get.js index fe4c6ad3ea..8b84a984bb 100644 --- a/packages/api/src/routes/nfts-get.js +++ b/packages/api/src/routes/nfts-get.js @@ -3,6 +3,7 @@ import { JSONResponse } from '../utils/json-response.js' import { checkAuth, validate } from '../utils/auth.js' import { parseCid } from '../utils/utils.js' import { toNFTResponse } from '../utils/db-transforms.js' +import { getW3upDeals } from '../utils/w3up.js' /** * @typedef {import('../bindings').Deal} Deal @@ -13,8 +14,13 @@ export const nftGet = async (event, ctx) => { const { params, db } = ctx const { user } = checkAuth(ctx) const cid = parseCid(params.cid) - const nft = await db.getUpload(cid.sourceCid, user.id) + const [nft, w3upDeals] = await Promise.all([ + db.getUpload(cid.sourceCid, user.id), + ctx.w3up ? getW3upDeals(ctx.w3up, cid.contentCid) : [], + ]) if (nft) { + // merge deals from dagcargo with deals from w3up + nft.deals = [...nft?.deals, ...(w3upDeals || [])] return new JSONResponse({ ok: true, value: toNFTResponse(nft, cid.sourceCid), diff --git a/packages/api/src/utils/db-client.js b/packages/api/src/utils/db-client.js index cd8159934f..e8988d61f6 100644 --- a/packages/api/src/utils/db-client.js +++ b/packages/api/src/utils/db-client.js @@ -429,7 +429,7 @@ export class DBClient { const cids = uploads?.map((u) => u.content_cid) - const deals = await this.getDealsForCids(cids) + const deals = await this.getDealsFromDagcargoFDW(cids) return uploads?.map((u) => { return { @@ -515,7 +515,7 @@ export class DBClient { * @returns {Promise} */ async getDeals(cid) { - const deals = await this.getDealsForCids([cid]) + const deals = await this.getDealsFromDagcargoFDW([cid]) return deals[cid] ? deals[cid] : [] } @@ -527,7 +527,7 @@ export class DBClient { * * @param {string[]} cids */ - async getDealsForCids(cids = []) { + async getDealsFromDagcargoFDW(cids = []) { try { const rsp = await this.client.rpc('find_deals_by_content_cids', { cids, diff --git a/packages/api/src/utils/w3up.js b/packages/api/src/utils/w3up.js index 8aff4a3758..851c324911 100644 --- a/packages/api/src/utils/w3up.js +++ b/packages/api/src/utils/w3up.js @@ -1,12 +1,14 @@ import * as W3UP from '@web3-storage/w3up-client' import * as ed25519 from '@ucanto/principal/ed25519' import { StoreMemory } from '@web3-storage/access/stores/store-memory' +import contentClaims from '@web3-storage/content-claims/client' import { CID } from 'multiformats/cid' import { base64 } from 'multiformats/bases/base64' import { identity } from 'multiformats/hashes/identity' import { CarReader } from '@ipld/car' import { importDAG } from '@ucanto/core/delegation' import * as W3upClient from '@web3-storage/w3up-client' +import { parseLink } from '@ucanto/core' import { connect } from '@ucanto/client' import { CAR, HTTP } from '@ucanto/transport' @@ -106,3 +108,81 @@ export async function createW3upClientFromConfig(options) { await w3up.addSpace(await parseW3Proof(options.proof)) return w3up } + +/** + * + * @param {W3upClient.Client} client + * @param {import('@web3-storage/upload-client/types').UploadListItem} upload + * @returns {Promise[]>} + */ +async function getFilecoinInfos(client, upload) { + return await Promise.all( + // for each shard of the upload + upload.shards + ? upload.shards.map(async (shard) => { + // find the equivalent piece link + const pieceClaims = await contentClaims.read(shard) + const pieceClaim = + /** @type {import('@web3-storage/content-claims/client/api').EqualsClaim} */ ( + pieceClaims.find((c) => c.type === 'assert/equals') + ) + if (pieceClaim) { + const pieceLink = pieceClaim.equals + // and get filecoin info for it + const filecoinInfo = await client.capability.filecoin.info( + /** @type {import('@web3-storage/access').PieceLink} */ ( + pieceLink + ) + ) + return filecoinInfo.out + } else { + return { + error: { + name: 'PieceLinkClaimNotFound', + message: `could not find piece link equivalent of ${shard}`, + }, + } + } + }) + : [] + ) +} + +/** + * + * @param {W3upClient.Client | undefined} client + * @param {string} contentCid + * @returns {Promise} + */ +export async function getW3upDeals(client, contentCid) { + if (client) { + const link = parseLink(contentCid) + // get the upload + const upload = await client.capability.upload.get(link) + const filecoinInfoResults = await getFilecoinInfos(client, upload) + /** + * @type {import('../bindings').Deal[]} + */ + const filecoinInfos = [] + for (const result of filecoinInfoResults) { + if (result.ok) { + const info = result.ok + for (const deal of info.deals) { + filecoinInfos.push({ + pieceCid: info.piece.toString(), + status: 'published', + // TODO: figure these two out + datamodelSelector: '', + batchRootCid: deal.aggregate, + }) + } + } else { + // @ts-expect-error - in practice this will just be undefined if message doesn't exist + console.warn(`error getting filecoininfo: ${result.error.message}`) + } + } + return filecoinInfos + } else { + return [] + } +} diff --git a/packages/api/test/nfts-get.spec.js b/packages/api/test/nfts-get.spec.js index 34c4f02eb2..cff8987da0 100644 --- a/packages/api/test/nfts-get.spec.js +++ b/packages/api/test/nfts-get.spec.js @@ -5,11 +5,20 @@ import { getMiniflareContext, setupMiniflareContext, } from './scripts/test-context.js' +import { read } from '@web3-storage/content-claims/client' +import { parseLink } from '@ucanto/core' test.before(async (t) => { await setupMiniflareContext(t) }) +test.only('should fetch deal details from w3up', async (t) => { + const testCid = 'bafybeiccy35oi3gajocq5bbg7pnaxb3kv5ibtdz3tc3kari53qhbjotzey' + const link = parseLink(testCid) + const claims = await read(link) + console.log('CLAIMS', claims) +}) + test.serial('should return proper response for cid v1', async (t) => { const cid = 'bafybeiaj5yqocsg5cxsuhtvclnh4ulmrgsmnfbhbrfxrc3u2kkh35mts4e' const client = await createClientWithUser(t) diff --git a/packages/website/lib/api.js b/packages/website/lib/api.js index 3c0f54e687..1ebe99e397 100644 --- a/packages/website/lib/api.js +++ b/packages/website/lib/api.js @@ -111,6 +111,29 @@ export async function getNfts({ limit, before }) { } } +/** + * Get NFTs + * + * @param {{cid: string }} query + */ +export async function getNft({ cid }) { + const res = await fetch(`${API}/cid/${cid}}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + (await getToken()), + }, + }) + + const body = await res.json() + + if (body.ok) { + return body.value + } else { + throw new Error(body.error.message) + } +} + export async function getUserTags() { const res = await fetch(`${API}/user/tags`, { method: 'GET', diff --git a/packages/website/pages/files.js b/packages/website/pages/files.js index cff3f4f853..0e9496a47f 100644 --- a/packages/website/pages/files.js +++ b/packages/website/pages/files.js @@ -165,6 +165,9 @@ export default function Files({ user }) { const dealsHidden = deals.splice(3) + const [w3upDeals, setW3upDeals] = useState([]) + async function loadW3upDeals() {} + if (!nft.deals.length) { deals.push( =2.2.1 <=2.3.0", prettier@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== -"prettier@>=2.2.1 <=2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" - integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== - pretty-error@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" @@ -21703,6 +21764,13 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.0.tgz#55bd6e9d19ce5eef0d5ad17cd1f587d85b180a85" integrity sha512-aTeNPVmgIMPpm1cxXr2Q/nEbvkmV8yq66F3om7X3P/cvOXQ0TMQ64Wk63iyT1gPlmdmGzjGpyLh1f3y8MZWXGg== +uint8arraylist@^2.4.3: + version "2.4.8" + resolved "https://registry.yarnpkg.com/uint8arraylist/-/uint8arraylist-2.4.8.tgz#5a4d17f4defd77799cb38e93fd5db0f0dceddc12" + integrity sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ== + dependencies: + uint8arrays "^5.0.1" + uint8arrays@^2.0.5, uint8arrays@^2.1.3, uint8arrays@^2.1.5: version "2.1.10" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" @@ -21731,6 +21799,13 @@ uint8arrays@^4.0.6: dependencies: multiformats "^12.0.1" +uint8arrays@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-5.0.3.tgz#92b894d9c4269ba97c51544d6e1f279fe6f80d1f" + integrity sha512-6LBuKji28kHjgPJMkQ6GDaBb1lRwIhyOYq6pDGwYMoDPfImE9SkuYENVmR0yu9yGgs2clHUSY9fKDukR+AXfqQ== + dependencies: + multiformats "^13.0.0" + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" From edf020cc061a8b6351e694f2324f00c4ca6c93f9 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 10 Apr 2024 14:01:03 -0700 Subject: [PATCH 2/7] fix: update esbuild this seems to fix the build errors I was seeing, and necessitates a small code change --- packages/api/package.json | 4 +- packages/api/src/utils/w3up.js | 2 +- yarn.lock | 255 +++++++++++++++++++-------------- 3 files changed, 152 insertions(+), 109 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index a74306a31d..fd0519cdb1 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -48,7 +48,7 @@ "regexparam": "^2.0.0", "toucan-js": "^2.7.0", "ucan-storage": "^1.3.0", - "uint8arrays": "^3.0.0" + "uint8arrays": "5.0.2" }, "devDependencies": { "@cloudflare/workers-types": "^3.17.0", @@ -68,7 +68,7 @@ "carbites": "^1.0.6", "delay": "^5.0.0", "dotenv": "^10.0.0", - "esbuild": "^0.13.13", + "esbuild": "^0.20.2", "execa": "^5.1.1", "git-rev-sync": "^3.0.1", "ipfs-unixfs-importer": "^9.0.3", diff --git a/packages/api/src/utils/w3up.js b/packages/api/src/utils/w3up.js index 851c324911..5e8a67abb2 100644 --- a/packages/api/src/utils/w3up.js +++ b/packages/api/src/utils/w3up.js @@ -1,7 +1,7 @@ import * as W3UP from '@web3-storage/w3up-client' import * as ed25519 from '@ucanto/principal/ed25519' import { StoreMemory } from '@web3-storage/access/stores/store-memory' -import contentClaims from '@web3-storage/content-claims/client' +import * as contentClaims from '@web3-storage/content-claims/client' import { CID } from 'multiformats/cid' import { base64 } from 'multiformats/bases/base64' import { identity } from 'multiformats/hashes/identity' diff --git a/yarn.lock b/yarn.lock index ba6f11fa23..51efdf4e8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2251,6 +2251,121 @@ escape-string-regexp "^4.0.0" rollup-plugin-node-polyfills "^0.2.1" +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + "@eslint/eslintrc@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.1.tgz#de0807bfeffc37b964a7d0400e0c348ce5a2543d" @@ -9871,11 +9986,6 @@ esbuild-android-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz#414a087cb0de8db1e347ecca6c8320513de433db" integrity sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ== -esbuild-android-arm64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" - integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg== - esbuild-android-arm64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz#e7d6430555e8e9c505fd87266bbc709f25f1825c" @@ -9886,11 +9996,6 @@ esbuild-android-arm64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz#55de3bce2aab72bcd2b606da4318ad00fb9c8151" integrity sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A== -esbuild-darwin-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72" - integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ== - esbuild-darwin-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz#4dc7484127564e89b4445c0a560a3cb50b3d68e1" @@ -9901,11 +10006,6 @@ esbuild-darwin-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz#4259f23ed6b4cea2ec8a28d87b7fb9801f093754" integrity sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA== -esbuild-darwin-arm64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a" - integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ== - esbuild-darwin-arm64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz#469e59c665f84a8ed323166624c5e7b9b2d22ac1" @@ -9916,11 +10016,6 @@ esbuild-darwin-arm64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz#d77b4366a71d84e530ba019d540b538b295d494a" integrity sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow== -esbuild-freebsd-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85" - integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA== - esbuild-freebsd-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz#895df03bf5f87094a56c9a5815bf92e591903d70" @@ -9931,11 +10026,6 @@ esbuild-freebsd-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz#27b6587b3639f10519c65e07219d249b01f2ad38" integrity sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g== -esbuild-freebsd-arm64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52" - integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ== - esbuild-freebsd-arm64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz#0b72a41a6b8655e9a8c5608f2ec1afdcf6958441" @@ -9946,11 +10036,6 @@ esbuild-freebsd-arm64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz#63c435917e566808c71fafddc600aca4d78be1ec" integrity sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg== -esbuild-linux-32@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69" - integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g== - esbuild-linux-32@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz#43b8ba3803b0bbe7f051869c6a8bf6de1e95de28" @@ -9961,11 +10046,6 @@ esbuild-linux-32@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz#c3da774143a37e7f11559b9369d98f11f997a5d9" integrity sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w== -esbuild-linux-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3" - integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA== - esbuild-linux-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz#dc8072097327ecfadba1735562824ce8c05dd0bd" @@ -9976,11 +10056,6 @@ esbuild-linux-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz#5d92b67f674e02ae0b4a9de9a757ba482115c4ae" integrity sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA== -esbuild-linux-arm64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1" - integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA== - esbuild-linux-arm64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz#c52b58cbe948426b1559910f521b0a3f396f10b8" @@ -9991,11 +10066,6 @@ esbuild-linux-arm64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz#dac84740516e859d8b14e1ecc478dd5241b10c93" integrity sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw== -esbuild-linux-arm@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe" - integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA== - esbuild-linux-arm@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz#df869dbd67d4ee3a04b3c7273b6bd2b233e78a18" @@ -10006,11 +10076,6 @@ esbuild-linux-arm@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz#b3ae7000696cd53ed95b2b458554ff543a60e106" integrity sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg== -esbuild-linux-mips64le@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7" - integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg== - esbuild-linux-mips64le@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz#a2b646d9df368b01aa970a7b8968be6dd6b01d19" @@ -10021,11 +10086,6 @@ esbuild-linux-mips64le@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz#dad10770fac94efa092b5a0643821c955a9dd385" integrity sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A== -esbuild-linux-ppc64le@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2" - integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ== - esbuild-linux-ppc64le@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz#9a21af766a0292578a3009c7408b8509cac7cefd" @@ -10056,11 +10116,6 @@ esbuild-linux-s390x@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz#c9e7791170a3295dba79b93aa452beb9838a8625" integrity sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw== -esbuild-netbsd-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038" - integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w== - esbuild-netbsd-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz#482a587cdbd18a6c264a05136596927deb46c30a" @@ -10071,11 +10126,6 @@ esbuild-netbsd-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz#0abd40b8c2e37fda6f5cc41a04cb2b690823d891" integrity sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A== -esbuild-openbsd-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7" - integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== - esbuild-openbsd-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz#e99f8cdc63f1628747b63edd124d53cf7796468d" @@ -10086,11 +10136,6 @@ esbuild-openbsd-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz#4adba0b7ea7eb1428bb00d8e94c199a949b130e8" integrity sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA== -esbuild-sunos-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" - integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== - esbuild-sunos-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz#8611d825bcb8239c78d57452e83253a71942f45c" @@ -10101,11 +10146,6 @@ esbuild-sunos-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz#4b8a6d97dfedda30a6e39607393c5c90ebf63891" integrity sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA== -esbuild-windows-32@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" - integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw== - esbuild-windows-32@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz#c06374206d4d92dd31d4fda299b09f51a35e82f6" @@ -10116,11 +10156,6 @@ esbuild-windows-32@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz#d31d8ca0c1d314fb1edea163685a423b62e9ac17" integrity sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg== -esbuild-windows-64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294" - integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ== - esbuild-windows-64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz#756631c1d301dfc0d1a887deed2459ce4079582f" @@ -10131,11 +10166,6 @@ esbuild-windows-64@0.14.51: resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz#7d3c09c8652d222925625637bdc7e6c223e0085d" integrity sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA== -esbuild-windows-arm64@0.13.15: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3" - integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA== - esbuild-windows-arm64@0.14.27: version "0.14.27" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz#ad7e187193dcd18768b16065a950f4441d7173f4" @@ -10198,28 +10228,34 @@ esbuild@0.14.51: esbuild-windows-64 "0.14.51" esbuild-windows-arm64 "0.14.51" -esbuild@^0.13.13: - version "0.13.15" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf" - integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw== +esbuild@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== optionalDependencies: - esbuild-android-arm64 "0.13.15" - esbuild-darwin-64 "0.13.15" - esbuild-darwin-arm64 "0.13.15" - esbuild-freebsd-64 "0.13.15" - esbuild-freebsd-arm64 "0.13.15" - esbuild-linux-32 "0.13.15" - esbuild-linux-64 "0.13.15" - esbuild-linux-arm "0.13.15" - esbuild-linux-arm64 "0.13.15" - esbuild-linux-mips64le "0.13.15" - esbuild-linux-ppc64le "0.13.15" - esbuild-netbsd-64 "0.13.15" - esbuild-openbsd-64 "0.13.15" - esbuild-sunos-64 "0.13.15" - esbuild-windows-32 "0.13.15" - esbuild-windows-64 "0.13.15" - esbuild-windows-arm64 "0.13.15" + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" escalade@^3.1.1: version "3.1.1" @@ -21771,6 +21807,13 @@ uint8arraylist@^2.4.3: dependencies: uint8arrays "^5.0.1" +uint8arrays@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-5.0.2.tgz#f05479bcd521d37c2e7710b24132a460b0ac80e3" + integrity sha512-S0GaeR+orZt7LaqzTRs4ZP8QqzAauJ+0d4xvP2lJTA99jIkKsE2FgDs4tGF/K/z5O9I/2W5Yvrh7IuqNeYH+0Q== + dependencies: + multiformats "^13.0.0" + uint8arrays@^2.0.5, uint8arrays@^2.1.3, uint8arrays@^2.1.5: version "2.1.10" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" From efca52f3319e42e77deba48452f1589e6de33313 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Thu, 11 Apr 2024 16:24:02 -0700 Subject: [PATCH 3/7] feat: add test for w3up deal fetch Add a test for fetching filecoin deal info from w3up. This is definitely not my best work - ideally we'd abstract this w3up mocking logic in a way that makes it less stateful, but I'm hesitant to do that when we will likely not be making many changes here in the future. --- packages/api/package.json | 2 +- packages/api/src/bindings.d.ts | 6 ++ packages/api/src/routes/nfts-get.js | 4 +- packages/api/src/routes/nfts-upload.js | 1 - packages/api/src/utils/context.js | 2 + packages/api/src/utils/w3up.js | 29 ++++-- packages/api/test/nfts-get.spec.js | 127 ++++++++++++++++++++++-- packages/api/test/utils/w3up-testing.js | 33 ++++++ yarn.lock | 30 +----- 9 files changed, 190 insertions(+), 44 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index fd0519cdb1..fdde6b9fa3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -35,7 +35,7 @@ "@web3-storage/car-block-validator": "^1.2.0", "@web3-storage/content-claims": "^4.0.4", "@web3-storage/upload-client": "^13.2.0", - "@web3-storage/w3up-client": "^12.5.0", + "@web3-storage/w3up-client": "^12.5.1", "cardex": "^1.0.0", "ipfs-car": "^0.6.1", "it-last": "^2.0.0", diff --git a/packages/api/src/bindings.d.ts b/packages/api/src/bindings.d.ts index 04da3d1ec3..31ba46662d 100644 --- a/packages/api/src/bindings.d.ts +++ b/packages/api/src/bindings.d.ts @@ -8,6 +8,7 @@ import { DBClient } from './utils/db-client.js' import { LinkdexApi } from './utils/linkdex.js' import { Logging } from './utils/logs.js' import { Client as W3upClient } from '@web3-storage/w3up-client' +import * as contentClaims from '@web3-storage/content-claims/client' export type RuntimeEnvironmentName = 'test' | 'dev' | 'staging' | 'production' @@ -142,6 +143,10 @@ export interface AuthOptions { checkHasPsaAccess?: boolean } +export interface ContentClaimsClient { + read: typeof contentClaims.read +} + export interface RouteContext { params: Record db: DBClient @@ -158,6 +163,7 @@ export interface RouteContext { W3_NFTSTORAGE_SPACE?: string W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS?: string w3up?: W3upClient + contentClaims?: ContentClaimsClient } export type Handler = ( diff --git a/packages/api/src/routes/nfts-get.js b/packages/api/src/routes/nfts-get.js index 8b84a984bb..beb7afaee3 100644 --- a/packages/api/src/routes/nfts-get.js +++ b/packages/api/src/routes/nfts-get.js @@ -16,7 +16,9 @@ export const nftGet = async (event, ctx) => { const cid = parseCid(params.cid) const [nft, w3upDeals] = await Promise.all([ db.getUpload(cid.sourceCid, user.id), - ctx.w3up ? getW3upDeals(ctx.w3up, cid.contentCid) : [], + ctx.w3up && ctx.contentClaims + ? getW3upDeals(ctx.w3up, ctx.contentClaims, cid.contentCid) + : [], ]) if (nft) { // merge deals from dagcargo with deals from w3up diff --git a/packages/api/src/routes/nfts-upload.js b/packages/api/src/routes/nfts-upload.js index d13f42021a..6f36eaea09 100644 --- a/packages/api/src/routes/nfts-upload.js +++ b/packages/api/src/routes/nfts-upload.js @@ -189,7 +189,6 @@ export async function uploadCarWithStat( if (stat.structure === 'Partial') { checkDagStructureTask = async () => { - // @ts-expect-error - I'm not sure why this started failing TODO debug further const info = await w3up.capability.upload.get(stat.rootCid) if (info.shards && info.shards.length > 1) { const structure = await ctx.linkdexApi.getDagStructureForCars( diff --git a/packages/api/src/utils/context.js b/packages/api/src/utils/context.js index 4b43242e54..bd4f4726ad 100644 --- a/packages/api/src/utils/context.js +++ b/packages/api/src/utils/context.js @@ -9,6 +9,7 @@ import { Service } from 'ucan-storage/service' import { LinkdexApi } from './linkdex.js' import { createW3upClientFromConfig } from './w3up.js' import { DID } from '@ucanto/core' +import * as contentClaims from '@web3-storage/content-claims/client' /** * Obtains a route context object. @@ -105,6 +106,7 @@ export async function getContext(event, params) { r2Uploader, log, ucanService, + contentClaims, w3up, } } diff --git a/packages/api/src/utils/w3up.js b/packages/api/src/utils/w3up.js index 5e8a67abb2..00bc17b32e 100644 --- a/packages/api/src/utils/w3up.js +++ b/packages/api/src/utils/w3up.js @@ -1,7 +1,6 @@ import * as W3UP from '@web3-storage/w3up-client' import * as ed25519 from '@ucanto/principal/ed25519' import { StoreMemory } from '@web3-storage/access/stores/store-memory' -import * as contentClaims from '@web3-storage/content-claims/client' import { CID } from 'multiformats/cid' import { base64 } from 'multiformats/bases/base64' import { identity } from 'multiformats/hashes/identity' @@ -112,16 +111,17 @@ export async function createW3upClientFromConfig(options) { /** * * @param {W3upClient.Client} client + * @param {{read: typeof import('@web3-storage/content-claims/client').read}} contentClaimsClient * @param {import('@web3-storage/upload-client/types').UploadListItem} upload * @returns {Promise[]>} */ -async function getFilecoinInfos(client, upload) { +async function getFilecoinInfos(client, contentClaimsClient, upload) { return await Promise.all( // for each shard of the upload upload.shards ? upload.shards.map(async (shard) => { // find the equivalent piece link - const pieceClaims = await contentClaims.read(shard) + const pieceClaims = await contentClaimsClient.read(shard) const pieceClaim = /** @type {import('@web3-storage/content-claims/client/api').EqualsClaim} */ ( pieceClaims.find((c) => c.type === 'assert/equals') @@ -151,15 +151,26 @@ async function getFilecoinInfos(client, upload) { /** * * @param {W3upClient.Client | undefined} client + * @param {{read: typeof import('@web3-storage/content-claims/client').read}} contentClaimsClient * @param {string} contentCid * @returns {Promise} */ -export async function getW3upDeals(client, contentCid) { +export async function getW3upDeals(client, contentClaimsClient, contentCid) { if (client) { const link = parseLink(contentCid) // get the upload - const upload = await client.capability.upload.get(link) - const filecoinInfoResults = await getFilecoinInfos(client, upload) + let upload + try { + upload = await client.capability.upload.get(link) + } catch (e) { + console.error('error getting upload', e) + return [] + } + const filecoinInfoResults = await getFilecoinInfos( + client, + contentClaimsClient, + upload + ) /** * @type {import('../bindings').Deal[]} */ @@ -171,9 +182,11 @@ export async function getW3upDeals(client, contentCid) { filecoinInfos.push({ pieceCid: info.piece.toString(), status: 'published', - // TODO: figure these two out + batchRootCid: deal.aggregate.toString(), + miner: deal.provider, + chainDealID: Number(deal.aux.dataSource.dealID), + // TODO: figure this out datamodelSelector: '', - batchRootCid: deal.aggregate, }) } } else { diff --git a/packages/api/test/nfts-get.spec.js b/packages/api/test/nfts-get.spec.js index cff8987da0..9e375176b0 100644 --- a/packages/api/test/nfts-get.spec.js +++ b/packages/api/test/nfts-get.spec.js @@ -1,22 +1,133 @@ import test from 'ava' +import { createServer } from 'node:http' +import { ed25519 } from '@ucanto/principal' +import { delegate, parseLink } from '@ucanto/core' +import { base64 } from 'multiformats/bases/base64' import { createClientWithUser } from './scripts/helpers.js' import { fixtures } from './scripts/fixtures.js' import { getMiniflareContext, setupMiniflareContext, } from './scripts/test-context.js' -import { read } from '@web3-storage/content-claims/client' -import { parseLink } from '@ucanto/core' +import { + createMockW3up, + locate, + encodeDelegationAsCid, +} from './utils/w3up-testing.js' + +const nftStorageSpace = ed25519.generate() +const nftStorageApiPrincipal = ed25519.generate() +const nftStorageAccountEmailAllowListedForW3up = 'test+w3up@dev.nft.storage' +const mockW3upDID = 'did:web:test.web3.storage' +/** + * @type {import('@web3-storage/access').PieceLink} + */ +const mockPieceLink = parseLink( + 'bafkzcibeslzwmewd4pugjanyiayot5m76a67dvdir25v6ms6kbuozy2sxotplrrrce' +) +/** + * @type {import('@web3-storage/access').FilecoinInfoAcceptedDeal[]} + */ +const mockDeals = [ + { + aggregate: parseLink( + 'bafkzcibcaapen7lfjgljzi523a5rau2l5pwpwseita6uunqy5otrlxa2l2pouca' + ), + aux: { + dataSource: { + dealID: BigInt(1), + }, + dataType: BigInt(1), + }, + provider: 'f01240', + }, +] +const mockW3up = Promise.resolve( + (async function () { + const server = createServer( + await createMockW3up({ + did: mockW3upDID, + // @ts-expect-error not returning a full upload get response for now + async onHandleUploadGet(cid) { + return { + // grabbed this shard CID from staging, it should correspond to a piece named bafkzcibeslzwmewd4pugjanyiayot5m76a67dvdir25v6ms6kbuozy2sxotplrrrce + shards: [ + parseLink( + 'bagbaieragf62xatg3bqrfafdy3lpk2fte7526kvxnltqsnhjr45cz6jjk7mq' + ), + ], + } + }, + async onHandleFilecoinInfo(invocation) { + return { + deals: mockDeals, + aggregates: [], + piece: mockPieceLink, + } + }, + }) + ) + server.listen(0) + await new Promise((resolve) => + server.addListener('listening', () => resolve(undefined)) + ) + return { + server, + } + })() +) test.before(async (t) => { - await setupMiniflareContext(t) + await setupMiniflareContext(t, { + overrides: { + W3UP_URL: locate((await mockW3up).server).url.toString(), + W3UP_DID: mockW3upDID, + W3_NFTSTORAGE_SPACE: (await nftStorageSpace).did(), + W3_NFTSTORAGE_PRINCIPAL: ed25519.format(await nftStorageApiPrincipal), + W3_NFTSTORAGE_PROOF: ( + await encodeDelegationAsCid( + await delegate({ + issuer: await nftStorageSpace, + audience: await nftStorageApiPrincipal, + capabilities: [ + { can: 'upload/get', with: (await nftStorageSpace).did() }, + { can: 'filecoin/info', with: (await nftStorageSpace).did() }, + ], + }) + ) + ).toString(base64), + W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS: JSON.stringify([ + nftStorageAccountEmailAllowListedForW3up, + ]), + }, + }) }) -test.only('should fetch deal details from w3up', async (t) => { - const testCid = 'bafybeiccy35oi3gajocq5bbg7pnaxb3kv5ibtdz3tc3kari53qhbjotzey' - const link = parseLink(testCid) - const claims = await read(link) - console.log('CLAIMS', claims) +test.serial('should fetch deal details from w3up', async (t) => { + const cid = 'bafybeiccy35oi3gajocq5bbg7pnaxb3kv5ibtdz3tc3kari53qhbjotzey' + const client = await createClientWithUser(t) + const mf = getMiniflareContext(t) + await client.addPin({ + cid, + name: 'test-filecoin-info', + }) + + const res = await mf.dispatchFetch(`http://miniflare.test/${cid}`, { + headers: { Authorization: `Bearer ${client.token}` }, + }) + const { ok, value } = await res.json() + t.assert(ok) + t.deepEqual( + value.deals, + mockDeals.map((deal) => ({ + pieceCid: mockPieceLink.toString(), + status: 'published', + datamodelSelector: '', + batchRootCid: deal.aggregate.toString(), + miner: deal.provider, + chainDealID: Number(deal.aux.dataSource.dealID), + })) + ) }) test.serial('should return proper response for cid v1', async (t) => { diff --git a/packages/api/test/utils/w3up-testing.js b/packages/api/test/utils/w3up-testing.js index 0df210ff6f..d0a347c026 100644 --- a/packages/api/test/utils/w3up-testing.js +++ b/packages/api/test/utils/w3up-testing.js @@ -42,6 +42,8 @@ export async function encodeDelegationAsCid(delegation) { * create a RequestListener that can be a mock up.web3.storage * @param {object} [options] - options * @param {string} options.did + * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleFilecoinInfo] - called in the filecoin/info handler and the result is returned + * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleUploadGet] - called in the upload/get handler and the result is returned * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleStoreAdd] - called at start of store/add handler * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleUploadAdd] - called at start of upload/add handler */ @@ -55,6 +57,21 @@ export async function createMockW3up( ok: {}, } }), + info: Server.provide(Filecoin.info, async (invocation) => { + const result = await options.onHandleFilecoinInfo?.(invocation) + if (result) { + return { + ok: result, + } + } else { + return { + error: { + name: 'UnexpectedError', + message: `onUploadGet was not defined or return ${result}`, + }, + } + } + }), }, store: { add: Server.provide(Store.add, async (invocation) => { @@ -82,6 +99,22 @@ export async function createMockW3up( ok: success, } }), + + get: Server.provide(Upload.get, async (invocation) => { + const result = await options.onHandleUploadGet?.(invocation) + if (result) { + return { + ok: result, + } + } else { + return { + error: { + name: 'UnexpectedError', + message: `onUploadGet was not defined or return ${result}`, + }, + } + } + }), }, } const serverId = (await ed25519.generate()).withDID( diff --git a/yarn.lock b/yarn.lock index 51efdf4e8f..e17259135e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5903,26 +5903,6 @@ resolved "https://registry.yarnpkg.com/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz#6b69dc2a32a5b207ba43e556c25cc136a56659c4" integrity sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw== -"@web3-storage/upload-client@^13.1.0": - version "13.1.0" - resolved "https://registry.yarnpkg.com/@web3-storage/upload-client/-/upload-client-13.1.0.tgz#e29beb5ab0991682c28bcfe8c318aca42e43041c" - integrity sha512-RK67hUFviFG7KdupTwbMJCPdIsGEBSzpllybIOzbip3FKVH4fKDq4Sb2kXLptXeeqQfPJ86uRTmFtHTAVGVbZw== - dependencies: - "@ipld/car" "^5.2.2" - "@ipld/dag-cbor" "^9.0.6" - "@ipld/dag-ucan" "^3.4.0" - "@ipld/unixfs" "^2.1.1" - "@ucanto/client" "^9.0.0" - "@ucanto/interface" "^9.0.0" - "@ucanto/transport" "^9.1.0" - "@web3-storage/capabilities" "^13.2.0" - "@web3-storage/data-segment" "^5.1.0" - "@web3-storage/filecoin-client" "^3.3.0" - ipfs-utils "^9.0.14" - multiformats "^12.1.2" - p-retry "^5.1.2" - varint "^6.0.0" - "@web3-storage/upload-client@^13.2.0": version "13.2.0" resolved "https://registry.yarnpkg.com/@web3-storage/upload-client/-/upload-client-13.2.0.tgz#b6781344f405d84a6575d4880c3abe73e20d8e67" @@ -5943,10 +5923,10 @@ p-retry "^5.1.2" varint "^6.0.0" -"@web3-storage/w3up-client@^12.5.0": - version "12.5.0" - resolved "https://registry.yarnpkg.com/@web3-storage/w3up-client/-/w3up-client-12.5.0.tgz#67663f6c024bb7d198b2030f7752b139e5d3f9ae" - integrity sha512-SLpXXgA0TZJNSGtLHeq2kF+uwaHYfsH5068utikeRccCXJRrKQnCN1y2FpCe01H4SjLMTIQK13vga6DgSfJiuA== +"@web3-storage/w3up-client@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@web3-storage/w3up-client/-/w3up-client-12.5.1.tgz#a5722c9b8ca0e1ea2c1a2b7c7749512938eda16d" + integrity sha512-fv53VEWOcDxNi2qsE5uHvOWDXbXstlYQ505uMN5vcpdetxo3FcxIkVqGBIIQDpsXLyloRNA8+ZXOy8+rKeOPVw== dependencies: "@ipld/dag-ucan" "^3.4.0" "@ucanto/client" "^9.0.0" @@ -5958,7 +5938,7 @@ "@web3-storage/capabilities" "^13.2.0" "@web3-storage/did-mailto" "^2.1.0" "@web3-storage/filecoin-client" "^3.3.0" - "@web3-storage/upload-client" "^13.1.0" + "@web3-storage/upload-client" "^13.2.0" "@webassemblyjs/ast@1.11.1": version "1.11.1" From 7b2f3e1d653150974b7ce36e2e2ddf9e4a8ce02c Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Fri, 12 Apr 2024 10:49:18 -0700 Subject: [PATCH 4/7] fix: don't return shards for anything but the one test url this broke existing tests because it was returning w3up deals in all situations --- packages/api/test/nfts-get.spec.js | 39 ++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/api/test/nfts-get.spec.js b/packages/api/test/nfts-get.spec.js index 9e375176b0..151ee62ad2 100644 --- a/packages/api/test/nfts-get.spec.js +++ b/packages/api/test/nfts-get.spec.js @@ -42,27 +42,40 @@ const mockDeals = [ provider: 'f01240', }, ] +const cidWithShards = parseLink( + 'bafybeiccy35oi3gajocq5bbg7pnaxb3kv5ibtdz3tc3kari53qhbjotzey' +) const mockW3up = Promise.resolve( (async function () { const server = createServer( await createMockW3up({ did: mockW3upDID, // @ts-expect-error not returning a full upload get response for now - async onHandleUploadGet(cid) { - return { - // grabbed this shard CID from staging, it should correspond to a piece named bafkzcibeslzwmewd4pugjanyiayot5m76a67dvdir25v6ms6kbuozy2sxotplrrrce - shards: [ - parseLink( - 'bagbaieragf62xatg3bqrfafdy3lpk2fte7526kvxnltqsnhjr45cz6jjk7mq' - ), - ], + async onHandleUploadGet(invocation) { + if (invocation.capability.nb.root?.equals(cidWithShards)) { + return { + // grabbed this shard CID from staging, it should correspond to a piece named bafkzcibeslzwmewd4pugjanyiayot5m76a67dvdir25v6ms6kbuozy2sxotplrrrce + shards: [ + parseLink( + 'bagbaieragf62xatg3bqrfafdy3lpk2fte7526kvxnltqsnhjr45cz6jjk7mq' + ), + ], + } + } else { + return { + shards: [], + } } }, async onHandleFilecoinInfo(invocation) { - return { - deals: mockDeals, - aggregates: [], - piece: mockPieceLink, + if (invocation.capability.nb.piece.equals(mockPieceLink)) { + return { + deals: mockDeals, + aggregates: [], + piece: mockPieceLink, + } + } else { + return undefined } }, }) @@ -104,7 +117,7 @@ test.before(async (t) => { }) test.serial('should fetch deal details from w3up', async (t) => { - const cid = 'bafybeiccy35oi3gajocq5bbg7pnaxb3kv5ibtdz3tc3kari53qhbjotzey' + const cid = cidWithShards.toString() const client = await createClientWithUser(t) const mf = getMiniflareContext(t) await client.addPin({ From 69af59d7e62ee145e84100be4a41606d0f18ae94 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Fri, 12 Apr 2024 11:55:59 -0700 Subject: [PATCH 5/7] feat: get frontend w3up deal loading working --- packages/website/lib/api.js | 2 +- packages/website/pages/files.js | 102 +++++++++++++++++++++++--------- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/packages/website/lib/api.js b/packages/website/lib/api.js index 1ebe99e397..110dea1d45 100644 --- a/packages/website/lib/api.js +++ b/packages/website/lib/api.js @@ -117,7 +117,7 @@ export async function getNfts({ limit, before }) { * @param {{cid: string }} query */ export async function getNft({ cid }) { - const res = await fetch(`${API}/cid/${cid}}`, { + const res = await fetch(`${API}/cid/${cid}`, { method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/packages/website/pages/files.js b/packages/website/pages/files.js index 0e9496a47f..660b3c49e3 100644 --- a/packages/website/pages/files.js +++ b/packages/website/pages/files.js @@ -1,7 +1,7 @@ -import { API, getNfts, getToken } from '../lib/api.js' +import { API, getNft, getNfts, getToken } from '../lib/api.js' import { useQuery, useQueryClient } from 'react-query' import { CID } from 'multiformats/cid' -import { VscQuestion } from 'react-icons/vsc' +import { VscLoading, VscQuestion } from 'react-icons/vsc' import Button from '../components/button.js' import Tooltip from '../components/tooltip.js' import Loading from '../components/loading' @@ -110,7 +110,19 @@ export default function Files({ user }) { */ const TableItem = ({ nft }) => { const [showAllDeals, setShowAllDeals] = useState(false) - const deals = nft.deals + const [loadW3upDeals, setLoadW3upDeals] = useState(false) + const { status: w3upDealsStatus, data: w3upDeals } = useQuery( + ['w3updeals', nft.cid], + async () => { + const fetchedNft = await getNft(nft) + return fetchedNft.deals + }, + { + enabled: loadW3upDeals, + refetchOnWindowFocus: false, + } + ) + const deals = [...nft.deals, ...(w3upDeals || [])] .filter((/** @type {any} */ d) => d.status !== 'queued') .map( ( @@ -165,33 +177,65 @@ export default function Files({ user }) { const dealsHidden = deals.splice(3) - const [w3upDeals, setW3upDeals] = useState([]) - async function loadW3upDeals() {} - - if (!nft.deals.length) { - deals.push( - - Queuing - - The content from this upload is being aggregated for redundant - storage on Filecoin. Filecoin deals will be active within 48 - hours of upload. While Queuing, data is still available on the - IPFS network. - - } - overlayClassName="ns-tooltip" - id="all-deals-queued-tooltip" + if (!deals.length) { + if (w3upDealsStatus === 'success' || w3upDealsStatus === 'error') { + deals.push( + - - - - ) + Queuing + + The content from this upload is being aggregated for redundant + storage on Filecoin. Filecoin deals will be active within 48 + hours of upload. While Queuing, data is still available on the + IPFS network. + + } + overlayClassName="ns-tooltip" + id="all-deals-queued-tooltip" + > + + + + ) + } else { + deals.push( + + {w3upDealsStatus === 'loading' ? ( +
+ +
+ ) : ( + Check for Filecoin deals for this upload.
} + overlayClassName="ns-tooltip" + id="load-w3up-deals-tooltip" + > + + + )} + + ) + } } return ( From cb4b333ccad792914cfd03d13a8b6547f369dafa Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Fri, 12 Apr 2024 12:55:25 -0700 Subject: [PATCH 6/7] chore: remove legacy upload codepath plus feature flag and related tests --- decisions/20240313-try-w3up.md | 8 - packages/api/src/bindings.d.ts | 8 - packages/api/src/config.js | 3 - packages/api/src/routes/nfts-upload.js | 51 +---- packages/api/src/utils/context.js | 2 - packages/api/test/nfts-get.spec.js | 3 - packages/api/test/nfts-upload.spec.js | 270 +------------------------ 7 files changed, 4 insertions(+), 341 deletions(-) diff --git a/decisions/20240313-try-w3up.md b/decisions/20240313-try-w3up.md index fdc1e3af3a..d055e5667b 100644 --- a/decisions/20240313-try-w3up.md +++ b/decisions/20240313-try-w3up.md @@ -86,14 +86,6 @@ configures how nft.storage will authenticate to web3.storage when sending invoca configures the capabilities that nft.storage has access to when interacting with web3.storage to store nfts. These capabilities will usually be UCAN delegations whose audience is the identifier of `W3_NFTSTORAGE_PRINCIPAL`. W3_NFTSTORAGE_PROOF needs to have proof rooted in W3_NFTSTORAGE_SPACE that authorize W3_NFTSTORAGE_PRINCIPAL to store in W3_NFTSTORAGE_SPACE. -##### `W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS` Environment Variable - -configures feature switch for which nftstorage accounts will have new uploads stored in web3.storage. - -Note: this environment variable may not be a permanent addition to the codebase. It's only meant to be used as a feature switch that decouples enabling the new functionality from deploying the new code. After testing, we may remove or change this feature switch when it is no longer useful. - -Format: JSON Array of email address strings. - #### UI Changes None. But the existing UI workflow of uploading via https://nft.storage/files/ and form should behave just like they do now. But after this change, there should be a new side effect, which is that the upload should appear in the listing of uploads for the configured `W3_NFTSTORAGE_SPACE` (e.g. via w3cli `w3 ls` or in console.web3.storage). diff --git a/packages/api/src/bindings.d.ts b/packages/api/src/bindings.d.ts index 31ba46662d..248ac684f9 100644 --- a/packages/api/src/bindings.d.ts +++ b/packages/api/src/bindings.d.ts @@ -113,13 +113,6 @@ export interface ServiceConfiguration { /** did:key of the w3up space in which to store NFTs */ W3_NFTSTORAGE_SPACE?: string - - /** - * JSON array of strings that are emails whose uploads should be uploaded via w3up. - * This is meant as a feature switch to test new functionality, - * and this configuration may be removed once the feature switch isn't needed to limit access. - */ - W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS?: string } export interface Ucan { @@ -161,7 +154,6 @@ export interface RouteContext { W3_NFTSTORAGE_PRINCIPAL?: string W3_NFTSTORAGE_PROOF?: string W3_NFTSTORAGE_SPACE?: string - W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS?: string w3up?: W3upClient contentClaims?: ContentClaimsClient } diff --git a/packages/api/src/config.js b/packages/api/src/config.js index a835b7afde..50603f5259 100644 --- a/packages/api/src/config.js +++ b/packages/api/src/config.js @@ -67,8 +67,6 @@ export function serviceConfigFromVariables(vars) { W3_NFTSTORAGE_PRINCIPAL: vars.W3_NFTSTORAGE_PRINCIPAL, W3_NFTSTORAGE_PROOF: vars.W3_NFTSTORAGE_PROOF, W3_NFTSTORAGE_SPACE: vars.W3_NFTSTORAGE_SPACE, - W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS: - vars.W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS, } } @@ -136,7 +134,6 @@ export function loadConfigVariables() { 'W3_NFTSTORAGE_SPACE', 'W3_NFTSTORAGE_PRINCIPAL', 'W3_NFTSTORAGE_PROOF', - 'W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS', ] for (const name of optional) { diff --git a/packages/api/src/routes/nfts-upload.js b/packages/api/src/routes/nfts-upload.js index 6f36eaea09..5c7de14930 100644 --- a/packages/api/src/routes/nfts-upload.js +++ b/packages/api/src/routes/nfts-upload.js @@ -108,24 +108,6 @@ export async function nftUpload(event, ctx) { return new JSONResponse({ ok: true, value: toNFTResponse(upload) }) } -/** - * returns whether w3up uploading feature is enabled given context + event - * @param {object} context - context of server operation, e.g. including configuration of feature switch - * @param {string} [context.W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS] - JSON array of allowed emails - * @param {object} event - specific event for which we should determine whether w3up feature is enabled - * @param {object} event.user - * @param {string} event.user.email - email address of user associated with event - */ -function w3upFeatureSwitchEnabled(context, event) { - // const { W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS = '[]' } = context - // const allowedEmails = JSON.parse(W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS) - // if (!Array.isArray(allowedEmails)) return false - // const eventHasAllowedEmail = allowedEmails.find( - // (allowed) => allowed === event.user.email - // ) - return true -} - /** * @typedef {{ * event: FetchEvent, @@ -170,10 +152,8 @@ export async function uploadCarWithStat( /** @type {(() => Promise)|undefined} */ let checkDagStructureTask const backupUrls = [] - // @ts-expect-error email is not expected in types - if (ctx.w3up && w3upFeatureSwitchEnabled(ctx, { user })) { - const { w3up } = ctx - + const { w3up } = ctx + if (w3up) { // we perform store/add and upload/add concurrently to save time. await Promise.all([ w3up.capability.store.add(car), @@ -204,32 +184,7 @@ export async function uploadCarWithStat( } } } else { - const carBytes = new Uint8Array(await car.arrayBuffer()) - const [s3Backup, r2Backup] = await Promise.all([ - ctx.s3Uploader.uploadCar(carBytes, stat.cid, user.id, metadata), - ctx.r2Uploader.uploadCar(carBytes, stat.cid, user.id, metadata), - ]) - backupUrls.push(s3Backup.url, r2Backup.url) - - // no need to ask linkdex if it's Complete or Unknown - if (stat.structure === 'Partial') { - // ask linkdex for the dag structure across the set of CARs in S3 for this upload. - checkDagStructureTask = async () => { - try { - const structure = await ctx.linkdexApi.getDagStructure(s3Backup.key) - if (structure === 'Complete') { - return ctx.db.updatePinStatus( - upload.content_cid, - elasticPin(structure) - ) - } - } catch (/** @type {any} */ err) { - if (err.code !== MissingApiUrlCode) { - throw err - } - } - } - } + throw new Error('w3up not defined, cannot upload') } const xName = event.request.headers.get('x-name') let name = xName && decodeURIComponent(xName) diff --git a/packages/api/src/utils/context.js b/packages/api/src/utils/context.js index bd4f4726ad..e387bf4240 100644 --- a/packages/api/src/utils/context.js +++ b/packages/api/src/utils/context.js @@ -73,8 +73,6 @@ export async function getContext(event, params) { W3_NFTSTORAGE_PRINCIPAL: config.W3_NFTSTORAGE_PRINCIPAL, W3_NFTSTORAGE_PROOF: config.W3_NFTSTORAGE_PROOF, W3_NFTSTORAGE_SPACE: config.W3_NFTSTORAGE_SPACE, - W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS: - config.W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS, } let w3up if ( diff --git a/packages/api/test/nfts-get.spec.js b/packages/api/test/nfts-get.spec.js index 151ee62ad2..9c0231cd9c 100644 --- a/packages/api/test/nfts-get.spec.js +++ b/packages/api/test/nfts-get.spec.js @@ -109,9 +109,6 @@ test.before(async (t) => { }) ) ).toString(base64), - W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS: JSON.stringify([ - nftStorageAccountEmailAllowListedForW3up, - ]), }, }) }) diff --git a/packages/api/test/nfts-upload.spec.js b/packages/api/test/nfts-upload.spec.js index a187266843..acebfc5504 100644 --- a/packages/api/test/nfts-upload.spec.js +++ b/packages/api/test/nfts-upload.spec.js @@ -88,9 +88,6 @@ test.before(async (t) => { }) ) ).toString(base64), - W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS: JSON.stringify([ - nftStorageAccountEmailAllowListedForW3up, - ]), }, }) }) @@ -131,10 +128,7 @@ test.serial('should upload a single file', async (t) => { test.serial('should forward uploads to W3UP_URL', async (t) => { const initialW3upStoreAddCount = mockW3upStoreAddCount const initialW3upUploadAddCount = mockW3upUploadAddCount - const client = await createClientWithUser(t, { - // note this email should be in W3_NFTSTORAGE_ENABLE_W3UP_FOR_EMAILS env var - email: nftStorageAccountEmailAllowListedForW3up, - }) + const client = await createClientWithUser(t) const mf = getMiniflareContext(t) const file = new Blob(['hello world!'], { type: 'application/text' }) const res = await mf.dispatchFetch('http://miniflare.test/upload', { @@ -330,92 +324,6 @@ test.serial('should upload a single CAR file', async (t) => { t.is(data.content.dag_size, 15, 'correct dag size') }) -// TODO verify with @alanshaw that we don't need to do this in the new upload flow -// TODO remove this once we remove legacy uploads -test.skip('should check dag completness with linkdex-api for partial CAR', async (t) => { - const client = await createClientWithUser(t) - const config = getTestServiceConfig(t) - const mf = getMiniflareContext(t) - - const leaf1 = await Block.encode({ - value: pb.prepare({ Data: 'leaf1' }), - codec: pb, - hasher: sha256, - }) - const leaf2 = await Block.encode({ - value: pb.prepare({ Data: 'leaf2' }), - codec: pb, - hasher: sha256, - }) - const parent = await Block.encode({ - value: pb.prepare({ Links: [leaf1.cid, leaf2.cid] }), - codec: pb, - hasher: sha256, - }) - const cid = parent.cid.toString() - const { writer, out } = CarWriter.create(parent.cid) - writer.put(parent) - writer.put(leaf1) - // leave out leaf2 to make patial car - writer.close() - const carBytes = [] - for await (const chunk of out) { - carBytes.push(chunk) - } - const body = new Blob(carBytes) - - if (!config.LINKDEX_URL) { - throw new Error('LINDEX_URL should be set in test config') - } - - const linkdexMock = getLinkdexMock(t) - mockLinkdexResponse(linkdexMock, 'Complete') - - const res = await mf.dispatchFetch('http://miniflare.test/upload', { - method: 'POST', - headers: { - Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', - }, - body, - }) - - t.truthy(res, 'Server responded') - t.true(res.ok, 'Server response ok') - const { ok, value } = await res.json() - t.truthy(ok, 'Server response payload has `ok` property') - t.is(value.cid, cid, 'Server responded with expected CID') - t.is(value.type, 'application/car', 'type should match blob mime-type') - - const db = getRawClient(config) - - const { data: upload } = await db - .from('upload') - .select('*') - .match({ source_cid: cid, user_id: client.userId }) - .single() - - // @ts-ignore - t.is(upload.source_cid, cid) - t.is(upload.deleted_at, null) - - // wait for the call to mock linkdex-api to complete - await res.waitUntil() - const { data: pin } = await db - .from('pin') - .select('*') - .match({ content_cid: cid, service: 'ElasticIpfs' }) - .single() - - t.is( - pin.status, - 'Pinned', - "Status should be pinned when linkdex-api returns 'Complete'" - ) - t.is(pin.service, 'ElasticIpfs') - t.is(pin.status, 'Pinned') -}) - test.serial('should allow a CAR with unsupported hash function', async (t) => { const client = await createClientWithUser(t) const mf = getMiniflareContext(t) @@ -599,111 +507,6 @@ test.serial('should upload to elastic ipfs', async (t) => { t.is(data.content.pin[0].service, 'ElasticIpfs') }) -// TODO: remove once we have fully removed legacy upload path -test.skip('should create S3 & R2 backups', async (t) => { - const client = await createClientWithUser(t) - const config = getTestServiceConfig(t) - const mf = getMiniflareContext(t) - const { root, car } = await packToBlob({ - input: [{ path: 'test.txt', content: 'S3 backup' }], - }) - - const res = await mf.dispatchFetch('http://miniflare.test/upload', { - method: 'POST', - headers: { Authorization: `Bearer ${client.token}` }, - body: car, - }) - - const { value } = await res.json() - t.is(root.toString(), value.cid) - - const upload = await client.client.getUpload(value.cid, client.userId) - t.truthy(upload) - t.truthy(upload?.backup_urls) - const backup_urls = upload?.backup_urls || [] - - // construct the expected backup URL - const carBuf = await car.arrayBuffer() - const carHash = await getHash(new Uint8Array(carBuf)) - const carCid = await getCarCid(new Uint8Array(carBuf)) - - t.is( - backup_urls[0], - expectedS3BackupUrl(config, root, client.userId, carHash) - ) - t.is(backup_urls[1], expectedR2BackupUrl(config, carCid)) -}) - -// TODO: remove once legacy codepath is fully removed -test.skip('should backup chunked uploads, preserving backup_urls for each chunk', async (t) => { - t.timeout(10_000) - const client = await createClientWithUser(t) - const config = getTestServiceConfig(t) - const mf = getMiniflareContext(t) - const chunkSize = 1024 - const nChunks = 5 - - const files = [] - for (let i = 0; i < nChunks; i++) { - files.push({ - path: `/dir/file-${i}.bin`, - content: getRandomBytes(chunkSize), - }) - } - - const { root, car } = await packToBlob({ - input: files, - maxChunkSize: chunkSize, - }) - const splitter = await TreewalkCarSplitter.fromBlob(car, chunkSize) - const linkdexMock = getLinkdexMock(t) - // respond with 'Partial' 5 times, then 'Complete' once. - mockLinkdexResponse(linkdexMock, 'Partial', 5) - mockLinkdexResponse(linkdexMock, 'Complete', 1) - - const backupUrls = [] - for await (const chunk of splitter.cars()) { - const carParts = [] - for await (const part of chunk) { - carParts.push(part) - } - const carFile = new Blob(carParts, { type: 'application/car' }) - const res = await mf.dispatchFetch('http://miniflare.test/upload', { - method: 'POST', - headers: { Authorization: `Bearer ${client.token}` }, - body: carFile, - }) - - const { value } = await res.json() - t.is(root.toString(), value.cid) - const carCid = await getCarCid(new Uint8Array(await carFile.arrayBuffer())) - const carHash = await getHash(new Uint8Array(await carFile.arrayBuffer())) - backupUrls.push(expectedS3BackupUrl(config, root, client.userId, carHash)) - backupUrls.push(expectedR2BackupUrl(config, carCid)) - } - - const upload = await client.client.getUpload(root.toString(), client.userId) - t.truthy(upload) - t.truthy(upload?.backup_urls) - const backup_urls = upload?.backup_urls || [] - t.truthy(backup_urls.length >= nChunks) // using >= to account for CAR / UnixFS overhead - t.is( - backup_urls.length, - backupUrls.length, - `expected ${backupUrls.length} backup urls, got: ${backup_urls.length}` - ) - - /** @type string[] */ - // @ts-expect-error upload.backup_urls has type unknown[], but it's really string[] - const resultUrls = upload.backup_urls - for (const url of resultUrls) { - t.true( - backupUrls.includes(url), - `upload is missing expected backup url ${url}` - ) - } -}) - test.serial('should upload a single file using ucan', async (t) => { const client = await createClientWithUser(t) const config = getTestServiceConfig(t) @@ -853,77 +656,6 @@ test.serial('should update a single file', async (t) => { t.is(uploadData.name, name) }) -// TODO: remove once legacy upload flow is fully removed -test.skip('should write satnav index', async (t) => { - const client = await createClientWithUser(t) - const config = getTestServiceConfig(t) - const mf = getMiniflareContext(t) - const { root, car: carBody } = await createCar('satnav') - const carBytes = new Uint8Array(await carBody.arrayBuffer()) - const carCid = await createCarCid(carBytes) - - const res = await mf.dispatchFetch('http://miniflare.test/upload', { - method: 'POST', - headers: { - Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', - }, - body: carBody, - }) - - const { ok, value } = await res.json() - t.truthy(ok, 'Server response payload has `ok` property') - t.is(value.cid, root.toString(), 'Server responded with expected CID') - t.is(value.type, 'application/car', 'type should match car mime-type') - - const r2Bucket = await mf.getR2Bucket('SATNAV') - const r2Object = await r2Bucket.get(`${carCid}/${carCid}.car.idx`) - if (!r2Object?.body) { - t.fail('repsonse stream must exist') - } - // @ts-expect-error - const reader = MultihashIndexSortedReader.fromIterable(r2Object?.body) - const entries = [] - for await (const entry of reader.entries()) { - entries.push(entry) - } - - t.is(entries.length, 1, 'Index contains a single entry') - t.true( - uint8ArrayEquals(entries[0].digest, root.multihash.digest), - 'Index entry is for root data CID' - ) -}) - -// TODO remove once legacy upload path is removed -test.skip('should write dudewhere index', async (t) => { - const client = await createClientWithUser(t) - const config = getTestServiceConfig(t) - const mf = getMiniflareContext(t) - const { root, car: carBody } = await createCar('dude') - const carBytes = new Uint8Array(await carBody.arrayBuffer()) - const carCid = await createCarCid(carBytes) - - const res = await mf.dispatchFetch('http://miniflare.test/upload', { - method: 'POST', - headers: { - Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', - }, - body: carBody, - }) - - const { ok, value } = await res.json() - t.truthy(ok, 'Server response payload has `ok` property') - t.is(value.cid, root.toString(), 'Server responded with expected CID') - t.is(value.type, 'application/car', 'type should match car mime-type') - - const r2Bucket = await mf.getR2Bucket('DUDEWHERE') - const r2Objects = await r2Bucket.list({ prefix: `${root}/` }) - t.is(r2Objects.objects.length, 1) - t.is(r2Objects.objects[0].key, `${root}/${carCid}`) -}) - test.serial('should fail upload for corrupt CAR', async (t) => { const client = await createClientWithUser(t) const mf = getMiniflareContext(t) From 389931dea2bd231aaf820eb3fb45f0c91bd7d9a8 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Fri, 12 Apr 2024 15:24:30 -0700 Subject: [PATCH 7/7] fix: fix tests by mocking out w3up in more places abstract w3up mocking a bit and use it in more places --- packages/api/test/bindings.d.ts | 26 +++++ packages/api/test/maintenance.spec.js | 10 +- packages/api/test/nfts-get.spec.js | 99 +++++++------------ .../api/test/nfts-metaplex-upload.spec.js | 10 +- packages/api/test/nfts-store.spec.js | 13 ++- packages/api/test/nfts-upload.spec.js | 73 ++++---------- packages/api/test/pin-list.spec.js | 13 ++- packages/api/test/utils/w3up-testing.js | 69 +++++++++++-- packages/api/tsconfig.json | 1 + 9 files changed, 182 insertions(+), 132 deletions(-) create mode 100644 packages/api/test/bindings.d.ts diff --git a/packages/api/test/bindings.d.ts b/packages/api/test/bindings.d.ts new file mode 100644 index 0000000000..13b05707bc --- /dev/null +++ b/packages/api/test/bindings.d.ts @@ -0,0 +1,26 @@ +import { ProviderInput } from '@ucanto/server' +import { InferInvokedCapability } from '@ucanto/client' +import { Store, Upload, Filecoin } from '@web3-storage/capabilities' +import { FilecoinInfoSuccess } from '@web3-storage/capabilities/types' +import { Server as HttpServer } from 'http' + +interface MockW3upOptions { + did?: string + onHandleFilecoinInfo?: ( + invocation: ProviderInput> + ) => Promise + onHandleUploadGet?: ( + invocation: ProviderInput> + ) => Promise + onHandleStoreAdd?: ( + invocation: ProviderInput> + ) => Promise + onHandleUploadAdd?: ( + invocation: ProviderInput> + ) => Promise +} + +interface MockW3up { + server: HttpServer + did: string +} diff --git a/packages/api/test/maintenance.spec.js b/packages/api/test/maintenance.spec.js index 5136f3ef4f..505f31ad6a 100644 --- a/packages/api/test/maintenance.spec.js +++ b/packages/api/test/maintenance.spec.js @@ -10,11 +10,19 @@ import { getMiniflareContext, setupMiniflareContext, } from './scripts/test-context.js' +import { + createMockW3upServer, + w3upMiniflareOverrides, +} from './utils/w3up-testing.js' /** @typedef {import('../src/middleware/maintenance.js').Mode} Mode */ test.before(async (t) => { - await setupMiniflareContext(t) + await setupMiniflareContext(t, { + overrides: { + ...(await w3upMiniflareOverrides(await createMockW3upServer())), + }, + }) }) /** diff --git a/packages/api/test/nfts-get.spec.js b/packages/api/test/nfts-get.spec.js index 9c0231cd9c..1d5dd37717 100644 --- a/packages/api/test/nfts-get.spec.js +++ b/packages/api/test/nfts-get.spec.js @@ -13,12 +13,10 @@ import { createMockW3up, locate, encodeDelegationAsCid, + createMockW3upServer, + w3upMiniflareOverrides, } from './utils/w3up-testing.js' -const nftStorageSpace = ed25519.generate() -const nftStorageApiPrincipal = ed25519.generate() -const nftStorageAccountEmailAllowListedForW3up = 'test+w3up@dev.nft.storage' -const mockW3upDID = 'did:web:test.web3.storage' /** * @type {import('@web3-storage/access').PieceLink} */ @@ -45,70 +43,43 @@ const mockDeals = [ const cidWithShards = parseLink( 'bafybeiccy35oi3gajocq5bbg7pnaxb3kv5ibtdz3tc3kari53qhbjotzey' ) -const mockW3up = Promise.resolve( - (async function () { - const server = createServer( - await createMockW3up({ - did: mockW3upDID, - // @ts-expect-error not returning a full upload get response for now - async onHandleUploadGet(invocation) { - if (invocation.capability.nb.root?.equals(cidWithShards)) { - return { - // grabbed this shard CID from staging, it should correspond to a piece named bafkzcibeslzwmewd4pugjanyiayot5m76a67dvdir25v6ms6kbuozy2sxotplrrrce - shards: [ - parseLink( - 'bagbaieragf62xatg3bqrfafdy3lpk2fte7526kvxnltqsnhjr45cz6jjk7mq' - ), - ], - } - } else { - return { - shards: [], - } - } - }, - async onHandleFilecoinInfo(invocation) { - if (invocation.capability.nb.piece.equals(mockPieceLink)) { - return { - deals: mockDeals, - aggregates: [], - piece: mockPieceLink, - } - } else { - return undefined - } - }, - }) - ) - server.listen(0) - await new Promise((resolve) => - server.addListener('listening', () => resolve(undefined)) - ) - return { - server, - } - })() -) test.before(async (t) => { + const mockW3up = await createMockW3upServer({ + async onHandleUploadGet(invocation) { + if (invocation.capability.nb.root?.equals(cidWithShards)) { + return { + // grabbed this shard CID from staging, it should correspond to a piece named bafkzcibeslzwmewd4pugjanyiayot5m76a67dvdir25v6ms6kbuozy2sxotplrrrce + shards: [ + parseLink( + 'bagbaieragf62xatg3bqrfafdy3lpk2fte7526kvxnltqsnhjr45cz6jjk7mq' + ), + ], + } + } else { + return { + shards: [], + } + } + }, + async onHandleFilecoinInfo(invocation) { + if (invocation.capability.nb.piece.equals(mockPieceLink)) { + return { + deals: mockDeals, + aggregates: [], + piece: mockPieceLink, + } + } else { + return undefined + } + }, + }) await setupMiniflareContext(t, { overrides: { - W3UP_URL: locate((await mockW3up).server).url.toString(), - W3UP_DID: mockW3upDID, - W3_NFTSTORAGE_SPACE: (await nftStorageSpace).did(), - W3_NFTSTORAGE_PRINCIPAL: ed25519.format(await nftStorageApiPrincipal), - W3_NFTSTORAGE_PROOF: ( - await encodeDelegationAsCid( - await delegate({ - issuer: await nftStorageSpace, - audience: await nftStorageApiPrincipal, - capabilities: [ - { can: 'upload/get', with: (await nftStorageSpace).did() }, - { can: 'filecoin/info', with: (await nftStorageSpace).did() }, - ], - }) - ) - ).toString(base64), + ...(await w3upMiniflareOverrides(mockW3up, [ + 'upload/get', + 'filecoin/info', + ])), }, }) }) diff --git a/packages/api/test/nfts-metaplex-upload.spec.js b/packages/api/test/nfts-metaplex-upload.spec.js index fb1b303a5f..4923c1a2f2 100644 --- a/packages/api/test/nfts-metaplex-upload.spec.js +++ b/packages/api/test/nfts-metaplex-upload.spec.js @@ -7,12 +7,20 @@ import { getTestServiceConfig, setupMiniflareContext, } from './scripts/test-context.js' +import { + createMockW3upServer, + w3upMiniflareOverrides, +} from './utils/w3up-testing.js' /** @type {number} */ let metaplexUserId test.before(async (t) => { - await setupMiniflareContext(t) + await setupMiniflareContext(t, { + overrides: { + ...(await w3upMiniflareOverrides(await createMockW3upServer())), + }, + }) const config = getTestServiceConfig(t) const rawClient = getRawClient(config) diff --git a/packages/api/test/nfts-store.spec.js b/packages/api/test/nfts-store.spec.js index f53daa25f6..7dfaf8aba7 100644 --- a/packages/api/test/nfts-store.spec.js +++ b/packages/api/test/nfts-store.spec.js @@ -10,9 +10,20 @@ import { } from './scripts/test-context.js' import { File, Blob } from 'nft.storage/src/platform.js' import { FormData } from 'undici' +import { + createMockW3upServer, + w3upMiniflareOverrides, +} from './utils/w3up-testing.js' + +const overrides = (async () => + await w3upMiniflareOverrides(await createMockW3upServer()))() test.beforeEach(async (t) => { - await setupMiniflareContext(t) + await setupMiniflareContext(t, { + overrides: { + ...(await overrides), + }, + }) }) test('should store image', async (t) => { diff --git a/packages/api/test/nfts-upload.spec.js b/packages/api/test/nfts-upload.spec.js index acebfc5504..a74692a3da 100644 --- a/packages/api/test/nfts-upload.spec.js +++ b/packages/api/test/nfts-upload.spec.js @@ -1,13 +1,8 @@ import test from 'ava' import { CID } from 'multiformats/cid' -import * as Block from 'multiformats/block' import { sha256, sha512 } from 'multiformats/hashes/sha2' import * as pb from '@ipld/dag-pb' import { CarWriter } from '@ipld/car' -import { packToBlob } from 'ipfs-car/pack/blob' -import { MultihashIndexSortedReader } from 'cardex' -import { TreewalkCarSplitter } from 'carbites/treewalk' -import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { createClientWithUser, getRawClient } from './scripts/helpers.js' import { createCar } from './scripts/car.js' @@ -25,75 +20,43 @@ import fs from 'node:fs' import { fileURLToPath } from 'node:url' import path from 'node:path' import { FormData } from 'undici' -import { createCarCid } from '../src/utils/car.js' import { createServer } from 'node:http' -import { ed25519 } from '@ucanto/principal' -import { delegate } from '@ucanto/core' -import { base64 } from 'multiformats/bases/base64' + import { - createMockW3up, - locate, - encodeDelegationAsCid, + createMockW3upServer, + w3upMiniflareOverrides, } from './utils/w3up-testing.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const nftStorageSpace = ed25519.generate() -const nftStorageApiPrincipal = ed25519.generate() -const nftStorageAccountEmailAllowListedForW3up = 'test+w3up@dev.nft.storage' -const mockW3upDID = 'did:web:test.web3.storage' let mockW3upStoreAddCount = 0 let mockW3upUploadAddCount = 0 -const mockW3up = Promise.resolve( - (async function () { - const server = createServer( - await createMockW3up({ - did: mockW3upDID, - async onHandleStoreAdd(invocation) { - mockW3upStoreAddCount++ - }, - async onHandleUploadAdd(invocation) { - mockW3upUploadAddCount++ - }, - }) - ) - server.listen(0) - await new Promise((resolve) => - server.addListener('listening', () => resolve(undefined)) - ) - return { - server, - } - })() -) +/** + * @type {import('./bindings.js').MockW3up} + */ +let mockW3up test.before(async (t) => { + mockW3up = await createMockW3upServer({ + async onHandleStoreAdd(invocation) { + mockW3upStoreAddCount++ + }, + async onHandleUploadAdd(invocation) { + mockW3upUploadAddCount++ + }, + }) + const linkdexUrl = 'http://fake.api.net' await setupMiniflareContext(t, { overrides: { LINKDEX_URL: linkdexUrl, - W3UP_URL: locate((await mockW3up).server).url.toString(), - W3UP_DID: mockW3upDID, - W3_NFTSTORAGE_SPACE: (await nftStorageSpace).did(), - W3_NFTSTORAGE_PRINCIPAL: ed25519.format(await nftStorageApiPrincipal), - W3_NFTSTORAGE_PROOF: ( - await encodeDelegationAsCid( - await delegate({ - issuer: await nftStorageSpace, - audience: await nftStorageApiPrincipal, - capabilities: [ - { can: 'store/add', with: (await nftStorageSpace).did() }, - { can: 'upload/add', with: (await nftStorageSpace).did() }, - ], - }) - ) - ).toString(base64), + ...(await w3upMiniflareOverrides(mockW3up)), }, }) }) test.after(async (t) => { - ;(await mockW3up).server.close() + mockW3up.server.close() }) test.serial('should upload a single file', async (t) => { diff --git a/packages/api/test/pin-list.spec.js b/packages/api/test/pin-list.spec.js index 0f5df347bd..0e883cbab6 100644 --- a/packages/api/test/pin-list.spec.js +++ b/packages/api/test/pin-list.spec.js @@ -5,9 +5,20 @@ import { getTestServiceConfig, setupMiniflareContext, } from './scripts/test-context.js' +import { + createMockW3upServer, + w3upMiniflareOverrides, +} from './utils/w3up-testing.js' + +const overrides = (async () => + await w3upMiniflareOverrides(await createMockW3upServer()))() test.beforeEach(async (t) => { - await setupMiniflareContext(t) + await setupMiniflareContext(t, { + overrides: { + ...(await overrides), + }, + }) }) test('should pin with just cid', async (t) => { diff --git a/packages/api/test/utils/w3up-testing.js b/packages/api/test/utils/w3up-testing.js index d0a347c026..eecf936fc1 100644 --- a/packages/api/test/utils/w3up-testing.js +++ b/packages/api/test/utils/w3up-testing.js @@ -1,12 +1,15 @@ +import { createServer } from 'node:http' import { Store, Upload, Filecoin } from '@web3-storage/capabilities' import * as Server from '@ucanto/server' import * as CAR from '@ucanto/transport/car' -import * as consumers from 'stream/consumers' import { ed25519 } from '@ucanto/principal' +import { delegate } from '@ucanto/core' +import * as consumers from 'stream/consumers' import { CarWriter } from '@ipld/car' import * as ucanto from '@ucanto/core' import { CID } from 'multiformats/cid' import { identity } from 'multiformats/hashes/identity' +import { base64 } from 'multiformats/bases/base64' /** * @param {import('@ucanto/interface').Delegation} delegation - delegation to encode @@ -38,18 +41,63 @@ export async function encodeDelegationAsCid(delegation) { return cid } +/** + * + * @param {import('../bindings').MockW3up} mockW3up + * @param {string[]} capabilities + */ +export async function w3upMiniflareOverrides( + mockW3up, + capabilities = ['store/add', 'upload/add'] +) { + const nftStorageSpace = await ed25519.generate() + const nftStorageSpaceDid = nftStorageSpace.did() + const nftStorageApiPrincipal = await ed25519.generate() + return { + W3UP_URL: locate(mockW3up.server).url.toString(), + W3UP_DID: mockW3up.did, + W3_NFTSTORAGE_SPACE: nftStorageSpaceDid, + W3_NFTSTORAGE_PRINCIPAL: ed25519.format(nftStorageApiPrincipal), + W3_NFTSTORAGE_PROOF: ( + await encodeDelegationAsCid( + await delegate({ + issuer: nftStorageSpace, + audience: nftStorageApiPrincipal, + // @ts-expect-error not sure why this is failing + capabilities: capabilities.map((can) => ({ + can, + with: nftStorageSpaceDid, + })), + }) + ) + ).toString(base64), + } +} + +/** + * @param {import('../bindings').MockW3upOptions} options + */ +export async function createMockW3upServer(options = {}) { + const { did, listener } = await createMockW3up(options) + const server = createServer(listener) + server.listen(0) + await new Promise((resolve) => + server.addListener('listening', () => resolve(undefined)) + ) + return { + did, + server, + } +} + /** * create a RequestListener that can be a mock up.web3.storage - * @param {object} [options] - options - * @param {string} options.did - * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleFilecoinInfo] - called in the filecoin/info handler and the result is returned - * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleUploadGet] - called in the upload/get handler and the result is returned - * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleStoreAdd] - called at start of store/add handler - * @param {(invocation: import('@ucanto/server').ProviderInput>) => Promise} [options.onHandleUploadAdd] - called at start of upload/add handler + * @param {import('../bindings').MockW3upOptions} [options] - options */ export async function createMockW3up( options = { did: 'did:web:test.web3.storage' } ) { + const did = options.did ?? 'did:web:test.web3.storage' const service = { filecoin: { offer: Server.provide(Filecoin.offer, async (invocation) => { @@ -118,7 +166,7 @@ export async function createMockW3up( }, } const serverId = (await ed25519.generate()).withDID( - ucanto.DID.parse(options.did).did() + ucanto.DID.parse(did).did() ) const server = Server.create({ id: serverId, @@ -145,7 +193,10 @@ export async function createMockW3up( res.end() } } - return listener + return { + did, + listener, + } } /** diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 1dc3f9ad3b..cb59580253 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -24,6 +24,7 @@ "src/bindings.d.ts", "node_modules/@cloudflare/workers-types/index.d.ts", "test", + "test/bindings.d.ts", "scripts" ], "exclude": ["node_modules/", "dist/"]