Skip to content

Commit

Permalink
refactor: move encoding code into token.js
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Shaw committed Oct 15, 2021
1 parent d17b396 commit e39ae0c
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 156 deletions.
File renamed without changes.
152 changes: 38 additions & 114 deletions packages/client/src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@ import pRetry from 'p-retry'
import { TreewalkCarSplitter } from 'carbites/treewalk'
import { pack } from 'ipfs-car/pack'
import { CID } from 'multiformats/cid'
import * as Block from 'multiformats/block'
import { sha256 } from 'multiformats/hashes/sha2'
import * as dagCbor from '@ipld/dag-cbor'
import { BlockstoreCarReader } from './blockstore.js'
import * as API from './lib/interface.js'
import * as Token from './token.js'
import { fetch, File, Blob, FormData, Blockstore } from './platform.js'
import { toGatewayURL } from './gateway.js'
import { BlockstoreCarReader } from './bs-car-reader.js'

const MAX_STORE_RETRIES = 5
const MAX_CONCURRENT_UPLOADS = 3
Expand Down Expand Up @@ -132,33 +129,32 @@ class NFTStorage {
? await TreewalkCarSplitter.fromBlob(car, targetSize)
: new TreewalkCarSplitter(car, targetSize)

const upload = transform(
MAX_CONCURRENT_UPLOADS,
async function (/** @type {AsyncIterable<Uint8Array>} */ car) {
const carParts = []
for await (const part of car) {
carParts.push(part)
}
const carFile = new Blob(carParts, { type: 'application/car' })
const cid = await pRetry(
async () => {
const response = await fetch(url.toString(), {
method: 'POST',
headers: NFTStorage.auth(token),
body: carFile,
})
const result = await response.json()
if (!result.ok) {
throw new Error(result.error.message)
}
return result.value.cid
},
{ retries: maxRetries == null ? MAX_STORE_RETRIES : maxRetries }
)
onStoredChunk && onStoredChunk(carFile.size)
return cid
const upload = transform(MAX_CONCURRENT_UPLOADS, async function(
/** @type {AsyncIterable<Uint8Array>} */ car
) {
const carParts = []
for await (const part of car) {
carParts.push(part)
}
)
const carFile = new Blob(carParts, { type: 'application/car' })
const cid = await pRetry(
async () => {
const response = await fetch(url.toString(), {
method: 'POST',
headers: NFTStorage.auth(token),
body: carFile,
})
const result = await response.json()
if (!result.ok) {
throw new Error(result.error.message)
}
return result.value.cid
},
{ retries: maxRetries == null ? MAX_STORE_RETRIES : maxRetries }
)
onStoredChunk && onStoredChunk(carFile.size)
return cid
})

let root
for await (const cid of upload(splitter.cars())) {
Expand Down Expand Up @@ -226,61 +222,16 @@ class NFTStorage {
validateERC1155(input)
const blockstore = new Blockstore()
try {
const [blobs, meta] = Token.encode(input)
/** @type {API.Encoded<T, [[Blob, URL]]>} */
const data = JSON.parse(JSON.stringify(meta))
/** @type {API.Encoded<T, [[Blob, CID]]>} */
const dag = JSON.parse(JSON.stringify(meta))

for (const [dotPath, blob] of blobs.entries()) {
/** @type {string|undefined} */
// @ts-ignore blob may be a File!
const name = blob.name || 'blob'
const { root: cid } = await pack({
// @ts-ignore
input: [{ path: name, content: blob.stream() }],
blockstore,
wrapWithDirectory: true,
})

const href = new URL(`ipfs://${cid}/${name}`)
const path = dotPath.split('.')
setIn(data, path, href)
setIn(dag, path, cid)
}

const { root: metadataJsonCid } = await pack({
// @ts-ignore
input: [
{
path: 'metadata.json',
content: new Blob([JSON.stringify(data)]).stream(),
},
],
blockstore,
wrapWithDirectory: false,
})

const block = await Block.encode({
value: {
...dag,
'metadata.json': metadataJsonCid,
type: 'nft',
},
codec: dagCbor,
hasher: sha256,
})
await blockstore.put(block.cid, block.bytes)

onRootCidReady && onRootCidReady(block.cid.toString())
const car = new BlockstoreCarReader(1, [block.cid], blockstore)
await NFTStorage.storeCar(service, car, { onStoredChunk, maxRetries })

return new Token.Token(
block.cid.toString(),
`ipfs://${block.cid}/metadata.json`,
data
const token = await Token.encode(input, blockstore)
onRootCidReady && onRootCidReady(token.ipnft)
const car = new BlockstoreCarReader(
1,
[CID.parse(token.ipnft)],
blockstore
)
await NFTStorage.storeCar(service, car, { onStoredChunk, maxRetries })
// @ts-ignore
return token
} finally {
await blockstore.close()
}
Expand Down Expand Up @@ -556,8 +507,8 @@ For more context please see ERC-721 specification https://eips.ethereum.org/EIPS
* @param {API.Deal[]} deals
* @returns {API.Deal[]}
*/
const decodeDeals = (deals) =>
deals.map((deal) => {
const decodeDeals = deals =>
deals.map(deal => {
const { dealActivation, dealExpiration, lastChanged } = {
dealExpiration: null,
dealActivation: null,
Expand All @@ -576,34 +527,7 @@ const decodeDeals = (deals) =>
* @param {API.Pin} pin
* @returns {API.Pin}
*/
const decodePin = (pin) => ({ ...pin, created: new Date(pin.created) })

/**
* Sets a given `value` at the given `path` on a passed `object`.
*
* @example
* ```js
* const obj = { a: { b: { c: 1 }}}
* setIn(obj, ['a', 'b', 'c'], 5)
* obj.a.b.c //> 5
* ```
*
* @template V
* @param {any} object
* @param {string[]} path
* @param {V} value
*/
const setIn = (object, path, value) => {
const n = path.length - 1
let target = object
for (let [index, key] of path.entries()) {
if (index === n) {
target[key] = value
} else {
target = target[key]
}
}
}
const decodePin = pin => ({ ...pin, created: new Date(pin.created) })

const TokenModel = Token.Token
export { TokenModel as Token }
Expand Down
135 changes: 102 additions & 33 deletions packages/client/src/token.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { pack } from 'ipfs-car/pack'
import { CID } from 'multiformats/cid'
import * as Block from 'multiformats/block'
import { sha256 } from 'multiformats/hashes/sha2'
import * as dagCbor from '@ipld/dag-cbor'
import * as API from './lib/interface.js'
import { Blob } from './platform.js'
import { toGatewayURL, GATEWAY } from './gateway.js'

/** @typedef {import('./gateway.js').GatewayURLOptions} EmbedOptions */
/**
* @typedef {import('./gateway.js').GatewayURLOptions} EmbedOptions
* @typedef {import('ipfs-car/blockstore').Blockstore} Blockstore
*/

/**
* @template {API.TokenInput} T
Expand Down Expand Up @@ -67,7 +75,7 @@ export const decode = ({ ipnft, url, data }, paths) =>
* @param {any} value
* @returns {value is URL}
*/
const isURL = (value) => value instanceof URL
const isURL = value => value instanceof URL

/**
* @template State
Expand All @@ -88,7 +96,7 @@ const embedURL = (context, url) => [context, toGatewayURL(url, context)]
* @param {any} value
* @returns {value is object}
*/
const isObject = (value) => typeof value === 'object' && value != null
const isObject = value => typeof value === 'object' && value != null

/**
* @param {any} value
Expand All @@ -100,52 +108,86 @@ const isEncodedURL = (value, assetPaths, path) =>
typeof value === 'string' && assetPaths.has(path.join('.'))

/**
* Takes token input and encodes it into a
* [Map](https://developer.mozilla.org/en-US/docs/Web/API/Map)
* object where values are discovered `Blob` (or `File`) objects in
* the given token and field keys are `.` joined paths where they were discoverd
* in the token. Additionally a clone of the passed input `meta` with blobs and
* file values set to `null` (this allows backend to injest all of the files
* from `multipart/form-data` request and update provided "meta" data with
* corresponding file ipfs:// URLs)
* Takes token input and encodes it into a Token object. Where values are
* discovered `Blob` (or `File`) objects in the given input, they are replaced
* with IPFS URLs (an `ipfs://` prefixed CID with an optional path).
*
* The passed blockstore is used to store the DAG that is created. The root CID
* of which is `token.ipnft`.
*
* @example
* ```js
* const cat = new File([], 'cat.png')
* const kitty = new File([], 'kitty.png')
* const [map, meta] = encode({
* const token = await encode({
* name: 'hello'
* image: cat
* properties: {
* extra: {
* image: kitty
* }
* }
* })
* [...map.entries()] //>
* // [
* // ['image', cat],
* // ['properties.extra.image', kitty],
* // ['meta', '{"name":"hello",image:null,"properties":{"extra":{"kitty": null}}}']
* // ]
* meta //>
* // {
* // name: 'hello'
* // image: null
* // properties: {
* // extra: {
* // image: null
* // }
* // }
* // }
* }, blockstore)
* ```
*
* @template {API.TokenInput} T
* @param {API.Encoded<T, [[Blob, Blob]]>} input
* @returns {[Map<string, Blob>, API.MatchRecord<T, (input: Blob) => void>]}
* @param {Blockstore} blockstore
* @returns {Promise<API.Token<T>>}
*/
export const encode = (input) =>
mapValueWith(input, isBlob, encodeBlob, new Map(), [])
export const encode = async (input, blockstore) => {
const [blobs, meta] = mapValueWith(input, isBlob, encodeBlob, new Map(), [])
/** @type {API.Encoded<T, [[Blob, URL]]>} */
const data = JSON.parse(JSON.stringify(meta))
/** @type {API.Encoded<T, [[Blob, CID]]>} */
const dag = JSON.parse(JSON.stringify(meta))

for (const [dotPath, blob] of blobs.entries()) {
/** @type {string|undefined} */
// @ts-ignore blob may be a File!
const name = blob.name || 'blob'
const { root: cid } = await pack({
// @ts-ignore
input: [{ path: name, content: blob.stream() }],
blockstore,
wrapWithDirectory: true,
})

const href = new URL(`ipfs://${cid}/${name}`)
const path = dotPath.split('.')
setIn(data, path, href)
setIn(dag, path, cid)
}

const { root: metadataJsonCid } = await pack({
// @ts-ignore
input: [
{
path: 'metadata.json',
content: new Blob([JSON.stringify(data)]).stream(),
},
],
blockstore,
wrapWithDirectory: false,
})

const block = await Block.encode({
value: {
...dag,
'metadata.json': metadataJsonCid,
type: 'nft',
},
codec: dagCbor,
hasher: sha256,
})
await blockstore.put(block.cid, block.bytes)

return new Token(
block.cid.toString(),
`ipfs://${block.cid}/metadata.json`,
data
)
}

/**
* @param {Map<string, Blob>} data
Expand All @@ -162,7 +204,7 @@ const encodeBlob = (data, blob, path) => {
* @param {any} value
* @returns {value is Blob}
*/
const isBlob = (value) => value instanceof Blob
const isBlob = value => value instanceof Blob

/**
* Substitues values in the given `input` that match `p(value) == true` with
Expand Down Expand Up @@ -253,3 +295,30 @@ const mapArrayWith = (input, p, f, init, path) => {

return [state, /** @type {API.Encoded<T, [[I, O]]>} */ (output)]
}

/**
* Sets a given `value` at the given `path` on a passed `object`.
*
* @example
* ```js
* const obj = { a: { b: { c: 1 }}}
* setIn(obj, ['a', 'b', 'c'], 5)
* obj.a.b.c //> 5
* ```
*
* @template V
* @param {any} object
* @param {string[]} path
* @param {V} value
*/
const setIn = (object, path, value) => {
const n = path.length - 1
let target = object
for (let [index, key] of path.entries()) {
if (index === n) {
target[key] = value
} else {
target = target[key]
}
}
}
Loading

0 comments on commit e39ae0c

Please sign in to comment.